Merge branch 'feat-fix/controller-overlay-std-badalloc' into 'main'

feat(ui)/(crash): Implement real-time Controller Overlay & Resolve std::bad_alloc

See merge request citron/emulator!122
This commit is contained in:
Zephyron
2025-10-29 15:28:37 +10:00
9 changed files with 263 additions and 30 deletions

View File

@@ -36,6 +36,8 @@ add_executable(citron
applets/qt_web_browser_scripts.h applets/qt_web_browser_scripts.h
bootmanager.cpp bootmanager.cpp
bootmanager.h bootmanager.h
controller_overlay.cpp
controller_overlay.h
compatdb.ui compatdb.ui
compatibility_list.cpp compatibility_list.cpp
compatibility_list.h compatibility_list.h
@@ -229,7 +231,6 @@ add_executable(citron
util/vram_overlay.h util/vram_overlay.h
util/sequence_dialog/sequence_dialog.cpp util/sequence_dialog/sequence_dialog.cpp
util/sequence_dialog/sequence_dialog.h util/sequence_dialog/sequence_dialog.h
util/title_ids.h
util/url_request_interceptor.cpp util/url_request_interceptor.cpp
util/url_request_interceptor.h util/url_request_interceptor.h
util/util.cpp util/util.cpp
@@ -242,6 +243,8 @@ add_executable(citron
citron.rc citron.rc
) )
set_source_files_properties(controller_overlay.cpp PROPERTIES SKIP_AUTOUIC ON)
if (CITRON_CRASH_DUMPS) if (CITRON_CRASH_DUMPS)
target_sources(citron PRIVATE target_sources(citron PRIVATE
breakpad.cpp breakpad.cpp

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu 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 <algorithm> #include <algorithm>
@@ -22,6 +23,10 @@ PlayerControlPreview::~PlayerControlPreview() {
UnloadController(); UnloadController();
}; };
void PlayerControlPreview::SetRawJoystickVisible(bool visible) {
raw_joystick_visible = visible;
}
void PlayerControlPreview::SetController(Core::HID::EmulatedController* controller_) { void PlayerControlPreview::SetController(Core::HID::EmulatedController* controller_) {
UnloadController(); UnloadController();
is_controller_set = true; is_controller_set = true;
@@ -226,7 +231,26 @@ void PlayerControlPreview::paintEvent(QPaintEvent* event) {
QFrame::paintEvent(event); QFrame::paintEvent(event);
QPainter p(this); QPainter p(this);
p.setRenderHint(QPainter::Antialiasing); p.setRenderHint(QPainter::Antialiasing);
// Define the base size that the original drawing coordinates were designed for.
// A Pro Controller is roughly 420x320 pixels in its drawing function.
constexpr QSizeF base_size(450.0, 350.0);
// Get the current size of the widget.
const QSize current_size = this->size();
// Calculate the scaling factor. We want to maintain aspect ratio,
// so we use the smaller of the width/height scaling factors.
const double scale_x = current_size.width() / base_size.width();
const double scale_y = current_size.height() / base_size.height();
const double scale = std::min(scale_x, scale_y);
// Save the painter's state, apply the scaling, and center the drawing.
p.save();
const QPointF center = rect().center(); const QPointF center = rect().center();
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
switch (controller_type) { switch (controller_type) {
case Core::HID::NpadStyleIndex::Handheld: case Core::HID::NpadStyleIndex::Handheld:
@@ -249,6 +273,8 @@ void PlayerControlPreview::paintEvent(QPaintEvent* event) {
DrawProController(p, center); DrawProController(p, center);
break; break;
} }
p.restore(); // Restore the painter's original state.
} }
void PlayerControlPreview::DrawLeftController(QPainter& p, const QPointF center) { void PlayerControlPreview::DrawLeftController(QPainter& p, const QPointF center) {
@@ -314,8 +340,10 @@ void PlayerControlPreview::DrawLeftController(QPainter& p, const QPointF center)
center + QPointF(9, -69) + center + QPointF(9, -69) +
(QPointF(stick_values[LStick].x.value, stick_values[LStick].y.value) * 8), (QPointF(stick_values[LStick].x.value, stick_values[LStick].y.value) * 8),
1.8f, button_values[Settings::NativeButton::LStick]); 1.8f, button_values[Settings::NativeButton::LStick]);
if (raw_joystick_visible) {
DrawRawJoystick(p, center + QPointF(-140, 90), QPointF(0, 0)); DrawRawJoystick(p, center + QPointF(-140, 90), QPointF(0, 0));
} }
}
{ {
// Draw motion cubes // Draw motion cubes
@@ -449,8 +477,10 @@ void PlayerControlPreview::DrawRightController(QPainter& p, const QPointF center
center + QPointF(-9, 11) + center + QPointF(-9, 11) +
(QPointF(stick_values[RStick].x.value, stick_values[RStick].y.value) * 8), (QPointF(stick_values[RStick].x.value, stick_values[RStick].y.value) * 8),
1.8f, button_values[Settings::NativeButton::RStick]); 1.8f, button_values[Settings::NativeButton::RStick]);
if (raw_joystick_visible) {
DrawRawJoystick(p, QPointF(0, 0), center + QPointF(140, 90)); DrawRawJoystick(p, QPointF(0, 0), center + QPointF(140, 90));
} }
}
{ {
// Draw motion cubes // Draw motion cubes
@@ -593,8 +623,10 @@ void PlayerControlPreview::DrawDualController(QPainter& p, const QPointF center)
DrawJoystick(p, center + QPointF(-65, -65) + (l_stick * 7), 1.62f, l_button); DrawJoystick(p, center + QPointF(-65, -65) + (l_stick * 7), 1.62f, l_button);
DrawJoystick(p, center + QPointF(65, 12) + (r_stick * 7), 1.62f, r_button); DrawJoystick(p, center + QPointF(65, 12) + (r_stick * 7), 1.62f, r_button);
if (raw_joystick_visible) {
DrawRawJoystick(p, center + QPointF(-180, 90), center + QPointF(180, 90)); DrawRawJoystick(p, center + QPointF(-180, 90), center + QPointF(180, 90));
} }
}
{ {
// Draw motion cubes // Draw motion cubes
@@ -696,8 +728,10 @@ void PlayerControlPreview::DrawHandheldController(QPainter& p, const QPointF cen
DrawJoystick(p, center + QPointF(-171, -41) + (l_stick * 4), 1.0f, l_button); DrawJoystick(p, center + QPointF(-171, -41) + (l_stick * 4), 1.0f, l_button);
DrawJoystick(p, center + QPointF(171, 8) + (r_stick * 4), 1.0f, r_button); DrawJoystick(p, center + QPointF(171, 8) + (r_stick * 4), 1.0f, r_button);
if (raw_joystick_visible) {
DrawRawJoystick(p, center + QPointF(-50, 0), center + QPointF(50, 0)); DrawRawJoystick(p, center + QPointF(-50, 0), center + QPointF(50, 0));
} }
}
{ {
// Draw motion cubes // Draw motion cubes
@@ -808,8 +842,10 @@ void PlayerControlPreview::DrawProController(QPainter& p, const QPointF center)
button_values[Settings::NativeButton::LStick]); button_values[Settings::NativeButton::LStick]);
DrawProJoystick(p, center + QPointF(51, 0), r_stick, 11, DrawProJoystick(p, center + QPointF(51, 0), r_stick, 11,
button_values[Settings::NativeButton::RStick]); button_values[Settings::NativeButton::RStick]);
if (raw_joystick_visible) {
DrawRawJoystick(p, center + QPointF(-50, 105), center + QPointF(50, 105)); DrawRawJoystick(p, center + QPointF(-50, 105), center + QPointF(50, 105));
} }
}
{ {
// Draw motion cubes // Draw motion cubes
@@ -910,8 +946,10 @@ void PlayerControlPreview::DrawGCController(QPainter& p, const QPointF center) {
p.setPen(colors.transparent); p.setPen(colors.transparent);
p.setBrush(colors.font); p.setBrush(colors.font);
DrawSymbol(p, center + QPointF(61, 37) + (r_stick * 9.5f), Symbol::C, 1.0f); DrawSymbol(p, center + QPointF(61, 37) + (r_stick * 9.5f), Symbol::C, 1.0f);
if (raw_joystick_visible) {
DrawRawJoystick(p, center + QPointF(-198, -125), center + QPointF(198, -125)); DrawRawJoystick(p, center + QPointF(-198, -125), center + QPointF(198, -125));
} }
}
using namespace Settings::NativeButton; using namespace Settings::NativeButton;

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu 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
#pragma once #pragma once
@@ -29,6 +30,8 @@ public:
// Sets the emulated controller to be displayed // Sets the emulated controller to be displayed
void SetController(Core::HID::EmulatedController* controller); void SetController(Core::HID::EmulatedController* controller);
void SetRawJoystickVisible(bool visible);
// Disables events from the emulated controller // Disables events from the emulated controller
void UnloadController(); void UnloadController();
@@ -207,6 +210,8 @@ private:
void SetTextFont(QPainter& p, float text_size, void SetTextFont(QPainter& p, float text_size,
const QString& font_family = QStringLiteral("sans-serif")); const QString& font_family = QStringLiteral("sans-serif"));
bool raw_joystick_visible = false;
bool is_controller_set{}; bool is_controller_set{};
bool is_connected{}; bool is_connected{};
bool needs_redraw{}; bool needs_redraw{};

View File

@@ -65,7 +65,7 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const std::st
std::vector<VkDeviceInfo::Record>& vk_device_records, std::vector<VkDeviceInfo::Record>& vk_device_records,
Core::System& system_) Core::System& system_)
: QDialog(parent), : QDialog(parent),
ui(std::make_unique<Ui::ConfigurePerGame>()), title_id{title_id_}, file_name{file_name}, system{system_}, ui(std::make_unique<Ui::ConfigurePerGame>()), title_id{title_id_}, file_name{file_name_}, system{system_},
builder{std::make_unique<ConfigurationShared::Builder>(this, !system_.IsPoweredOn())}, builder{std::make_unique<ConfigurationShared::Builder>(this, !system_.IsPoweredOn())},
tab_group{std::make_shared<std::vector<ConfigurationShared::Tab*>>()} , tab_group{std::make_shared<std::vector<ConfigurationShared::Tab*>>()} ,
rainbow_timer{new QTimer(this)} { rainbow_timer{new QTimer(this)} {

View File

@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/controller_overlay.h"
#include "citron/configuration/configure_input_player_widget.h"
#include "citron/main.h"
#include "core/core.h"
#include "hid_core/hid_core.h"
#include <QGridLayout>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QSizeGrip>
#include <QWindow> // Required for Wayland dragging
#include <QResizeEvent>
namespace {
// Helper to get the active controller for Player 1
Core::HID::EmulatedController* GetPlayer1Controller(Core::System* system) {
if (!system) return nullptr;
Core::HID::HIDCore& hid_core = system->HIDCore();
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
if (handheld && handheld->IsConnected()) {
return handheld;
}
return hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
}
}
ControllerOverlay::ControllerOverlay(GMainWindow* parent)
: QWidget(parent, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint),
main_window(parent) {
setAttribute(Qt::WA_TranslucentBackground);
auto* layout = new QGridLayout(this);
setLayout(layout);
// Set margins to 0 so the controller can go right to the edge of the resizable window
layout->setContentsMargins(0, 0, 0, 0);
// Create the widget that draws the controller and make it transparent
controller_widget = new PlayerControlPreview(this);
controller_widget->setAttribute(Qt::WA_TranslucentBackground);
// Disable the raw joystick (deadzone) visualization
controller_widget->SetRawJoystickVisible(false);
// Allow the widget to expand and shrink with the window
controller_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout->addWidget(controller_widget, 0, 0);
// Add a size grip for resizing
size_grip = new QSizeGrip(this);
layout->addWidget(size_grip, 0, 0, Qt::AlignBottom | Qt::AlignRight);
// Start the timer for continuous updates
connect(&update_timer, &QTimer::timeout, this, &ControllerOverlay::UpdateControllerState);
update_timer.start(16); // ~60 FPS
// Set a minimum size and a default starting size
setMinimumSize(225, 175);
resize(450, 350);
}
ControllerOverlay::~ControllerOverlay() = default;
void ControllerOverlay::UpdateControllerState() {
Core::System* system = main_window->GetSystem();
Core::HID::EmulatedController* controller = GetPlayer1Controller(system);
if (controller_widget && controller) {
controller_widget->SetController(controller);
controller_widget->UpdateInput();
}
}
// The paint event is now empty, which makes the background fully transparent.
void ControllerOverlay::paintEvent(QPaintEvent* event) {
Q_UNUSED(event);
// Intentionally left blank to achieve a fully transparent window background.
}
// These functions handle dragging the frameless window
void ControllerOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && !size_grip->geometry().contains(event->pos())) {
#if defined(Q_OS_LINUX)
// Use system move on Wayland/Linux for proper dragging
if (windowHandle()) {
windowHandle()->startSystemMove();
}
#else
// Original dragging implementation for other platforms (Windows, etc.)
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint() - this->pos();
#endif
event->accept();
}
}
void ControllerOverlay::mouseMoveEvent(QMouseEvent* event) {
#if !defined(Q_OS_LINUX)
if (is_dragging) {
move(event->globalPosition().toPoint() - drag_start_pos);
event->accept();
}
#else
// On Linux, the window manager handles the move, so we do nothing here.
Q_UNUSED(event);
#endif
}
void ControllerOverlay::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
is_dragging = false;
event->accept();
}
}
void ControllerOverlay::resizeEvent(QResizeEvent* event) {
QWidget::resizeEvent(event);
// This ensures the layout and its widgets (like the size grip) are correctly repositioned on resize.
layout()->update();
}

View File

@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QWidget>
#include <QTimer>
class GMainWindow;
class PlayerControlPreview;
class QSizeGrip;
class ControllerOverlay : public QWidget {
Q_OBJECT
public:
explicit ControllerOverlay(GMainWindow* parent);
~ControllerOverlay() override;
protected:
void paintEvent(QPaintEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
private slots:
void UpdateControllerState();
private:
GMainWindow* main_window;
QTimer update_timer;
PlayerControlPreview* controller_widget;
QSizeGrip* size_grip;
bool is_dragging = false;
QPoint drag_start_pos;
};

View File

@@ -156,6 +156,8 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "citron/about_dialog.h" #include "citron/about_dialog.h"
#include "citron/bootmanager.h" #include "citron/bootmanager.h"
#include "citron/compatdb.h" #include "citron/compatdb.h"
#include "citron/controller_overlay.h"
#include "core/core.h"
#include "citron/compatibility_list.h" #include "citron/compatibility_list.h"
#include "citron/configuration/configure_dialog.h" #include "citron/configuration/configure_dialog.h"
#include "citron/configuration/configure_input_per_game.h" #include "citron/configuration/configure_input_per_game.h"
@@ -181,7 +183,6 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "citron/util/clickable_label.h" #include "citron/util/clickable_label.h"
#include "citron/util/performance_overlay.h" #include "citron/util/performance_overlay.h"
#include "citron/util/multiplayer_room_overlay.h" #include "citron/util/multiplayer_room_overlay.h"
#include "citron/util/title_ids.h"
#include "citron/util/vram_overlay.h" #include "citron/util/vram_overlay.h"
#include "citron/vk_device_info.h" #include "citron/vk_device_info.h"
@@ -1668,6 +1669,9 @@ void GMainWindow::ConnectMenuEvents() {
connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys); connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys);
connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates);
connect_menu(ui->action_About, &GMainWindow::OnAbout); connect_menu(ui->action_About, &GMainWindow::OnAbout);
connect(ui->actionControllerOverlay, &QAction::triggered, this, &GMainWindow::OnToggleControllerOverlay);
} }
void GMainWindow::UpdateMenuState() { void GMainWindow::UpdateMenuState() {
@@ -2020,8 +2024,7 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP
system->ApplySettings(); system->ApplySettings();
// Final Fantasy Tactics requires single-core mode to boot properly // Final Fantasy Tactics requires single-core mode to boot properly
if (title_id == UICommon::TitleID::FinalFantasyTactics) { if (title_id == 0x010038B015560000ULL) {
LOG_INFO(Frontend, "Applying workaround: forcing single-core mode for Final Fantasy Tactics");
Settings::values.use_multi_core.SetValue(false); Settings::values.use_multi_core.SetValue(false);
} }
} }
@@ -4924,6 +4927,16 @@ void GMainWindow::OnAbout() {
aboutDialog.exec(); aboutDialog.exec();
} }
void GMainWindow::OnToggleControllerOverlay() {
const bool visible = ui->actionControllerOverlay->isChecked();
if (visible && !controller_overlay) {
controller_overlay = new ControllerOverlay(this);
}
if (controller_overlay) {
controller_overlay->setVisible(visible);
}
}
void GMainWindow::OnToggleFilterBar() { void GMainWindow::OnToggleFilterBar() {
game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
if (ui->action_Show_Filter_Bar->isChecked()) { if (ui->action_Show_Filter_Bar->isChecked()) {

View File

@@ -39,6 +39,7 @@ class OverlayDialog;
class PerformanceOverlay; class PerformanceOverlay;
class MultiplayerRoomOverlay; class MultiplayerRoomOverlay;
class VramOverlay; class VramOverlay;
class ControllerOverlay;
class ProfilerWidget; class ProfilerWidget;
class ControllerDialog; class ControllerDialog;
class QLabel; class QLabel;
@@ -109,6 +110,7 @@ public:
bool DropAction(QDropEvent* event); bool DropAction(QDropEvent* event);
void AcceptDropEvent(QDropEvent* event); void AcceptDropEvent(QDropEvent* event);
MultiplayerState* GetMultiplayerState() { return multiplayer_state; } MultiplayerState* GetMultiplayerState() { return multiplayer_state; }
Core::System* GetSystem() { return system.get(); }
bool IsEmulationRunning() const { return emulation_running; } bool IsEmulationRunning() const { return emulation_running; }
signals: signals:
void EmulationStarting(EmuThread* emu_thread); void EmulationStarting(EmuThread* emu_thread);
@@ -249,6 +251,7 @@ private slots:
void OnTogglePerformanceOverlay(); void OnTogglePerformanceOverlay();
void OnToggleMultiplayerRoomOverlay(); void OnToggleMultiplayerRoomOverlay();
void OnToggleVramOverlay(); void OnToggleVramOverlay();
void OnToggleControllerOverlay();
void OnDisplayTitleBars(bool); void OnDisplayTitleBars(bool);
double GetCurrentFPS() const; double GetCurrentFPS() const;
double GetCurrentFrameTime() const; double GetCurrentFrameTime() const;
@@ -335,6 +338,7 @@ private:
PerformanceOverlay* performance_overlay{}; PerformanceOverlay* performance_overlay{};
MultiplayerRoomOverlay* multiplayer_room_overlay{}; MultiplayerRoomOverlay* multiplayer_room_overlay{};
VramOverlay* vram_overlay{}; VramOverlay* vram_overlay{};
ControllerOverlay* controller_overlay{};
GameListPlaceholder* game_list_placeholder; GameListPlaceholder* game_list_placeholder;
std::vector<VkDeviceInfo::Record> vk_device_records; std::vector<VkDeviceInfo::Record> vk_device_records;
QLabel* message_label = nullptr; QLabel* message_label = nullptr;

View File

@@ -129,6 +129,7 @@
<addaction name="action_Show_Performance_Overlay"/> <addaction name="action_Show_Performance_Overlay"/>
<addaction name="action_Show_Vram_Overlay"/> <addaction name="action_Show_Vram_Overlay"/>
<addaction name="action_Show_Multiplayer_Room_Overlay"/> <addaction name="action_Show_Multiplayer_Room_Overlay"/>
<addaction name="actionControllerOverlay"/>
<addaction name="action_Toggle_Grid_View"/> <addaction name="action_Toggle_Grid_View"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="menu_Reset_Window_Size"/> <addaction name="menu_Reset_Window_Size"/>
@@ -347,6 +348,14 @@
<string>Show Multiplayer Room Overlay</string> <string>Show Multiplayer Room Overlay</string>
</property> </property>
</action> </action>
<action name="actionControllerOverlay">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Show Controller Overlay</string>
</property>
</action>
<action name="action_Toggle_Grid_View"> <action name="action_Toggle_Grid_View">
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>