diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 2a49e8b85..2cde7d7ff 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -328,4 +328,6 @@ Start File Update Notes This DuckStation update includes support for multiple controllers with vibration, and binding devices such as keyboards/volume buttons.\n\nYou must re-bind your controllers, otherwise they will no longer function. Do you want to do this now? + Show Controller Input + Shows the current controller state of the system in the bottom-left corner of the display. diff --git a/android/app/src/main/res/xml/display_preferences.xml b/android/app/src/main/res/xml/display_preferences.xml index d542c12c9..a195dbaae 100644 --- a/android/app/src/main/res/xml/display_preferences.xml +++ b/android/app/src/main/res/xml/display_preferences.xml @@ -98,5 +98,11 @@ app:defaultValue="false" app:summary="@string/settings_summary_osd_show_resolution" app:iconSpaceReserved="false" /> + diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp index ce14e4d7c..93c1702b0 100644 --- a/src/core/analog_controller.cpp +++ b/src/core/analog_controller.cpp @@ -104,6 +104,16 @@ std::optional AnalogController::GetButtonCodeByName(std::string_view button return StaticGetButtonCodeByName(button_name); } +float AnalogController::GetAxisState(s32 axis_code) const +{ + if (axis_code < 0 || axis_code >= static_cast(Axis::Count)) + return 0.0f; + + // 0..255 -> -1..1 + const float value = (((static_cast(m_axis_state[static_cast(axis_code)]) / 255.0f) * 2.0f) - 1.0f); + return std::clamp(value / m_axis_scale, -1.0f, 1.0f); +} + void AnalogController::SetAxisState(s32 axis_code, float value) { if (axis_code < 0 || axis_code >= static_cast(Axis::Count)) @@ -124,6 +134,15 @@ void AnalogController::SetAxisState(Axis axis, u8 value) m_axis_state[static_cast(axis)] = value; } +bool AnalogController::GetButtonState(s32 button_code) const +{ + if (button_code < 0 || button_code >= static_cast(Button::Analog)) + return false; + + const u16 bit = u16(1) << static_cast(button_code); + return ((m_button_state & bit) == 0); +} + void AnalogController::SetButtonState(Button button, bool pressed) { if (button == Button::Analog) diff --git a/src/core/analog_controller.h b/src/core/analog_controller.h index 96cb6a6b9..659516d1e 100644 --- a/src/core/analog_controller.h +++ b/src/core/analog_controller.h @@ -59,7 +59,9 @@ public: void Reset() override; bool DoState(StateWrapper& sw, bool ignore_input_state) override; + float GetAxisState(s32 axis_code) const override; void SetAxisState(s32 axis_code, float value) override; + bool GetButtonState(s32 button_code) const override; void SetButtonState(s32 button_code, bool pressed) override; u32 GetButtonStateBits() const override; std::optional GetAnalogInputBytes() const override; diff --git a/src/core/analog_joystick.cpp b/src/core/analog_joystick.cpp index b47e88760..dceb52af0 100644 --- a/src/core/analog_joystick.cpp +++ b/src/core/analog_joystick.cpp @@ -69,6 +69,16 @@ std::optional AnalogJoystick::GetButtonCodeByName(std::string_view button_n return StaticGetButtonCodeByName(button_name); } +float AnalogJoystick::GetAxisState(s32 axis_code) const +{ + if (axis_code < 0 || axis_code >= static_cast(Axis::Count)) + return 0.0f; + + // 0..255 -> -1..1 + const float value = (((static_cast(m_axis_state[static_cast(axis_code)]) / 255.0f) * 2.0f) - 1.0f); + return std::clamp(value / m_axis_scale, -1.0f, 1.0f); +} + void AnalogJoystick::SetAxisState(s32 axis_code, float value) { if (axis_code < 0 || axis_code >= static_cast(Axis::Count)) @@ -89,6 +99,15 @@ void AnalogJoystick::SetAxisState(Axis axis, u8 value) m_axis_state[static_cast(axis)] = value; } +bool AnalogJoystick::GetButtonState(s32 button_code) const +{ + if (button_code < 0 || button_code >= static_cast(Button::Count)) + return false; + + const u16 bit = u16(1) << static_cast(button_code); + return ((m_button_state & bit) == 0); +} + void AnalogJoystick::SetButtonState(Button button, bool pressed) { if (button == Button::Mode) diff --git a/src/core/analog_joystick.h b/src/core/analog_joystick.h index b5bf0d414..a833201ea 100644 --- a/src/core/analog_joystick.h +++ b/src/core/analog_joystick.h @@ -57,7 +57,9 @@ public: void Reset() override; bool DoState(StateWrapper& sw, bool apply_input_state) override; + float GetAxisState(s32 axis_code) const override; void SetAxisState(s32 axis_code, float value) override; + bool GetButtonState(s32 button_code) const override; void SetButtonState(s32 button_code, bool pressed) override; u32 GetButtonStateBits() const override; std::optional GetAnalogInputBytes() const override; diff --git a/src/core/controller.cpp b/src/core/controller.cpp index 4eaee7431..e4fd7b223 100644 --- a/src/core/controller.cpp +++ b/src/core/controller.cpp @@ -26,8 +26,18 @@ bool Controller::Transfer(const u8 data_in, u8* data_out) return false; } +float Controller::GetAxisState(s32 axis_code) const +{ + return 0.0f; +} + void Controller::SetAxisState(s32 axis_code, float value) {} +bool Controller::GetButtonState(s32 button_code) const +{ + return false; +} + void Controller::SetButtonState(s32 button_code, bool pressed) {} u32 Controller::GetButtonStateBits() const diff --git a/src/core/controller.h b/src/core/controller.h index de3d26da5..28dafd4ed 100644 --- a/src/core/controller.h +++ b/src/core/controller.h @@ -45,9 +45,15 @@ public: // Returns the value of ACK, as well as filling out_data. virtual bool Transfer(const u8 data_in, u8* data_out); + /// Changes the specified axis state. Values are normalized from -1..1. + virtual float GetAxisState(s32 axis_code) const; + /// Changes the specified axis state. Values are normalized from -1..1. virtual void SetAxisState(s32 axis_code, float value); + /// Returns the specified button state. + virtual bool GetButtonState(s32 button_code) const; + /// Changes the specified button state. virtual void SetButtonState(s32 button_code, bool pressed); diff --git a/src/core/digital_controller.cpp b/src/core/digital_controller.cpp index a302a069c..8b4af20c9 100644 --- a/src/core/digital_controller.cpp +++ b/src/core/digital_controller.cpp @@ -42,7 +42,14 @@ bool DigitalController::DoState(StateWrapper& sw, bool apply_input_state) return true; } -void DigitalController::SetAxisState(s32 axis_code, float value) {} +bool DigitalController::GetButtonState(s32 button_code) const +{ + if (button_code < 0 || button_code >= static_cast(Button::Count)) + return false; + + const u16 bit = u16(1) << static_cast(button_code); + return ((m_button_state & bit) == 0); +} void DigitalController::SetButtonState(Button button, bool pressed) { diff --git a/src/core/digital_controller.h b/src/core/digital_controller.h index d57b91331..8d3f1f29b 100644 --- a/src/core/digital_controller.h +++ b/src/core/digital_controller.h @@ -46,7 +46,7 @@ public: void Reset() override; bool DoState(StateWrapper& sw, bool apply_input_state) override; - void SetAxisState(s32 axis_code, float value) override; + bool GetButtonState(s32 button_code) const override; void SetButtonState(s32 button_code, bool pressed) override; u32 GetButtonStateBits() const override; diff --git a/src/core/namco_guncon.cpp b/src/core/namco_guncon.cpp index 43e63651f..8a3122d98 100644 --- a/src/core/namco_guncon.cpp +++ b/src/core/namco_guncon.cpp @@ -56,7 +56,14 @@ bool NamcoGunCon::DoState(StateWrapper& sw, bool apply_input_state) return true; } -void NamcoGunCon::SetAxisState(s32 axis_code, float value) {} +bool NamcoGunCon::GetButtonState(s32 button_code) const +{ + if (button_code < 0 || button_code > static_cast(Button::B)) + return false; + + const u16 bit = u16(1) << static_cast(button_code); + return ((m_button_state & bit) == 0); +} void NamcoGunCon::SetButtonState(Button button, bool pressed) { diff --git a/src/core/namco_guncon.h b/src/core/namco_guncon.h index eba8edf6f..cd99873fb 100644 --- a/src/core/namco_guncon.h +++ b/src/core/namco_guncon.h @@ -36,7 +36,7 @@ public: void LoadSettings(const char* section) override; bool GetSoftwareCursor(const Common::RGBA8Image** image, float* image_scale, bool* relative_mode) override; - void SetAxisState(s32 axis_code, float value) override; + bool GetButtonState(s32 button_code) const override; void SetButtonState(s32 button_code, bool pressed) override; void ResetTransferState() override; diff --git a/src/core/negcon.cpp b/src/core/negcon.cpp index 1d286dcfb..1c109df0b 100644 --- a/src/core/negcon.cpp +++ b/src/core/negcon.cpp @@ -48,6 +48,16 @@ bool NeGcon::DoState(StateWrapper& sw, bool apply_input_state) return true; } +float NeGcon::GetAxisState(s32 axis_code) const +{ + if (axis_code < 0 || axis_code >= static_cast(Axis::Count)) + return 0.0f; + + // 0..255 -> -1..1 + const float value = (((static_cast(m_axis_state[static_cast(axis_code)]) / 255.0f) * 2.0f) - 1.0f); + return std::clamp(value, -1.0f, 1.0f); +} + void NeGcon::SetAxisState(s32 axis_code, float value) { if (axis_code < 0 || axis_code >= static_cast(Axis::Count)) @@ -78,6 +88,15 @@ void NeGcon::SetAxisState(Axis axis, u8 value) m_axis_state[static_cast(axis)] = value; } +bool NeGcon::GetButtonState(s32 button_code) const +{ + if (button_code < 0 || button_code >= static_cast(Button::Count)) + return false; + + const u16 bit = u16(1) << static_cast(button_code); + return ((m_button_state & bit) == 0); +} + void NeGcon::SetButtonState(s32 button_code, bool pressed) { if (button_code < 0 || button_code >= static_cast(Button::Count)) diff --git a/src/core/negcon.h b/src/core/negcon.h index 6f5746173..8db6f1607 100644 --- a/src/core/negcon.h +++ b/src/core/negcon.h @@ -48,7 +48,10 @@ public: void Reset() override; bool DoState(StateWrapper& sw, bool apply_input_state) override; + float GetAxisState(s32 axis_code) const override; void SetAxisState(s32 axis_code, float value) override; + + bool GetButtonState(s32 button_code) const override; void SetButtonState(s32 button_code, bool pressed) override; void ResetTransferState() override; diff --git a/src/core/playstation_mouse.cpp b/src/core/playstation_mouse.cpp index b6ec365c7..ab57fe49f 100644 --- a/src/core/playstation_mouse.cpp +++ b/src/core/playstation_mouse.cpp @@ -59,7 +59,14 @@ bool PlayStationMouse::DoState(StateWrapper& sw, bool apply_input_state) return true; } -void PlayStationMouse::SetAxisState(s32 axis_code, float value) {} +bool PlayStationMouse::GetButtonState(s32 button_code) const +{ + if (button_code < 0 || button_code >= static_cast(Button::Count)) + return false; + + const u16 bit = u16(1) << static_cast(button_code); + return ((m_button_state & bit) == 0); +} void PlayStationMouse::SetButtonState(Button button, bool pressed) { diff --git a/src/core/playstation_mouse.h b/src/core/playstation_mouse.h index ecfc4abfd..8631bff45 100644 --- a/src/core/playstation_mouse.h +++ b/src/core/playstation_mouse.h @@ -32,7 +32,7 @@ public: void Reset() override; bool DoState(StateWrapper& sw, bool apply_input_state) override; - void SetAxisState(s32 axis_code, float value) override; + bool GetButtonState(s32 button_code) const override; void SetButtonState(s32 button_code, bool pressed) override; void ResetTransferState() override; diff --git a/src/duckstation-qt/displaysettingswidget.cpp b/src/duckstation-qt/displaysettingswidget.cpp index 7e31515fe..2b28f468f 100644 --- a/src/duckstation-qt/displaysettingswidget.cpp +++ b/src/duckstation-qt/displaysettingswidget.cpp @@ -51,6 +51,7 @@ DisplaySettingsWidget::DisplaySettingsWidget(QtHostInterface* host_interface, QW SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.showSpeed, "Display", "ShowSpeed", false); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.showResolution, "Display", "ShowResolution", false); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.showInput, "Display", "ShowInputs", false); connect(m_ui.renderer, QOverload::of(&QComboBox::currentIndexChanged), this, &DisplaySettingsWidget::populateGPUAdaptersAndResolutions); @@ -135,6 +136,9 @@ DisplaySettingsWidget::DisplaySettingsWidget(QtHostInterface* host_interface, QW tr("Shows the current emulation speed of the system in the top-right corner of the display as a percentage.")); dialog->registerWidgetHelp(m_ui.showResolution, tr("Show Resolution"), tr("Unchecked"), tr("Shows the resolution of the game in the top-right corner of the display.")); + dialog->registerWidgetHelp( + m_ui.showInput, tr("Show Controller Input"), tr("Unchecked"), + tr("Shows the current controller state of the system in the bottom-left corner of the display.")); #ifdef _WIN32 { diff --git a/src/duckstation-qt/displaysettingswidget.ui b/src/duckstation-qt/displaysettingswidget.ui index 68021d442..9ac7ca856 100644 --- a/src/duckstation-qt/displaysettingswidget.ui +++ b/src/duckstation-qt/displaysettingswidget.ui @@ -209,6 +209,13 @@ + + + + Show Controller Input + + + diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 46f17a6b0..621ecea4b 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -17,8 +17,10 @@ add_library(frontend-common icon.h ini_settings_interface.cpp ini_settings_interface.h - imgui_fullscreen.h + input_overlay_ui.cpp + input_overlay_ui.h imgui_fullscreen.cpp + imgui_fullscreen.h imgui_impl_opengl3.cpp imgui_impl_opengl3.h imgui_impl_vulkan.cpp diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index cf3df6b44..b03bc2816 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -28,6 +28,7 @@ #include "imgui_fullscreen.h" #include "imgui_styles.h" #include "ini_settings_interface.h" +#include "input_overlay_ui.h" #include "save_state_selector_ui.h" #include "scmversion/scmversion.h" #include @@ -57,6 +58,7 @@ Log_SetChannel(CommonHostInterface); static std::string s_settings_filename; +static std::unique_ptr s_input_overlay_ui; CommonHostInterface::CommonHostInterface() = default; @@ -104,6 +106,8 @@ bool CommonHostInterface::Initialize() void CommonHostInterface::Shutdown() { + s_input_overlay_ui.reset(); + HostInterface::Shutdown(); ImGui::DestroyContext(); @@ -1038,6 +1042,9 @@ void CommonHostInterface::DrawImGuiWindows() if (m_save_state_selector_ui->IsOpen()) m_save_state_selector_ui->Draw(); + if (s_input_overlay_ui) + s_input_overlay_ui->Draw(); + if (m_fullscreen_ui_enabled) { FullscreenUI::Render(); @@ -1210,11 +1217,6 @@ void CommonHostInterface::DrawOSDMessages() { AcquirePendingOSDMessages(); - constexpr ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav | - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing; - const float scale = ImGui::GetIO().DisplayFramebufferScale.x; const float spacing = 5.0f * scale; const float margin = 10.0f * scale; @@ -2639,6 +2641,13 @@ void CommonHostInterface::LoadSettings(SettingsInterface& si) } } } + + const bool input_display_enabled = si.GetBoolValue("Display", "ShowInputs", false); + const bool input_display_state = static_cast(s_input_overlay_ui); + if (input_display_enabled && !s_input_overlay_ui) + s_input_overlay_ui = std::make_unique(); + else if (!input_display_enabled && s_input_overlay_ui) + s_input_overlay_ui.reset(); } void CommonHostInterface::SaveSettings(SettingsInterface& si) diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index 5ea5b5e2a..eb376ea23 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -104,6 +104,7 @@ + @@ -135,6 +136,7 @@ + diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index 08b2c5c4e..54e7c39a0 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -30,6 +30,7 @@ + @@ -61,6 +62,7 @@ + diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 44537fb24..6069b1dd7 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -1980,6 +1980,10 @@ void DrawSettingsWindow() ToggleButton("Show Resolution", "Shows the current rendering resolution of the system in the top-right corner of the display.", &s_settings_copy.display_show_resolution); + settings_changed |= ToggleButtonForNonSetting( + "Show Controller Input", + "Shows the current controller state of the system in the bottom-left corner of the display.", "Display", + "ShowInputs", false); EndMenuButtons(); } diff --git a/src/frontend-common/input_overlay_ui.cpp b/src/frontend-common/input_overlay_ui.cpp new file mode 100644 index 000000000..a11ed19bf --- /dev/null +++ b/src/frontend-common/input_overlay_ui.cpp @@ -0,0 +1,118 @@ +#include "input_overlay_ui.h" +#include "common_host_interface.h" +#include "core/pad.h" +#include "core/settings.h" +#include "core/system.h" +#include "fullscreen_ui.h" +#include "imgui_fullscreen.h" + +static CommonHostInterface* GetHostInterface() +{ + return static_cast(g_host_interface); +} + +namespace FrontendCommon { + +InputOverlayUI::InputOverlayUI() = default; + +InputOverlayUI::~InputOverlayUI() = default; + +void InputOverlayUI::Draw() +{ + UpdateNames(); + + if (m_active_ports == 0) + return; + + ImFont* font; + float margin, spacing, shadow_offset; + + if (GetHostInterface()->IsFullscreenUIEnabled()) + { + font = ImGuiFullscreen::g_large_font; + margin = ImGuiFullscreen::LayoutScale(10.0f); + spacing = ImGuiFullscreen::LayoutScale(5.0f); + shadow_offset = ImGuiFullscreen::LayoutScale(1.0f); + } + else + { + font = ImGui::GetFont(); + margin = ImGuiFullscreen::DPIScale(10.0f); + spacing = ImGuiFullscreen::DPIScale(5.0f); + shadow_offset = ImGuiFullscreen::DPIScale(1.0f); + } + + static constexpr u32 text_color = IM_COL32(0xff, 0xff, 0xff, 255); + static constexpr u32 shadow_color = IM_COL32(0x00, 0x00, 0x00, 100); + + const ImVec2& display_size = ImGui::GetIO().DisplaySize; + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + + float current_x = margin; + float current_y = + display_size.y - margin - ((static_cast(m_active_ports) * (font->FontSize + spacing)) - spacing); + + const ImVec4 clip_rect(current_x, current_y, display_size.x - margin, display_size.y - margin); + + LargeString text; + + for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++) + { + if (m_types[port] == ControllerType::None) + continue; + + const Controller* controller = g_pad.GetController(port); + DebugAssert(controller); + + text.Format("P%u |", port + 1u); + + if (!m_axis_names[port].empty()) + { + for (const auto& [axis_name, axis_code, axis_type] : m_axis_names[port]) + { + const float value = controller->GetAxisState(axis_code); + text.AppendFormattedString(" %s: %.2f", axis_name.c_str(), value); + } + + text.AppendString(" |"); + } + + for (const auto& [button_name, button_code] : m_button_names[port]) + { + const bool pressed = controller->GetButtonState(button_code); + if (pressed) + text.AppendFormattedString(" %s", button_name.c_str()); + } + + dl->AddText(font, font->FontSize, ImVec2(current_x + shadow_offset, current_y + shadow_offset), shadow_color, + text.GetCharArray(), text.GetCharArray() + text.GetLength(), 0.0f, &clip_rect); + dl->AddText(font, font->FontSize, ImVec2(current_x, current_y), text_color, text.GetCharArray(), + text.GetCharArray() + text.GetLength(), 0.0f, &clip_rect); + + current_y += font->FontSize + spacing; + } +} + +void InputOverlayUI::UpdateNames() +{ + m_active_ports = 0; + if (!System::IsValid()) + return; + + for (u32 port = 0; port < NUM_CONTROLLER_AND_CARD_PORTS; port++) + { + const Controller* controller = g_pad.GetController(port); + const ControllerType type = (controller) ? controller->GetType() : ControllerType::None; + if (type != ControllerType::None) + m_active_ports++; + + if (type == m_types[port]) + continue; + + m_axis_names[port] = Controller::GetAxisNames(type); + m_button_names[port] = Controller::GetButtonNames(type); + m_types[port] = type; + } +} + +} // namespace FrontendCommon \ No newline at end of file diff --git a/src/frontend-common/input_overlay_ui.h b/src/frontend-common/input_overlay_ui.h new file mode 100644 index 000000000..3c794d395 --- /dev/null +++ b/src/frontend-common/input_overlay_ui.h @@ -0,0 +1,24 @@ +#pragma once +#include "core/controller.h" +#include + +namespace FrontendCommon { + +class InputOverlayUI +{ +public: + InputOverlayUI(); + ~InputOverlayUI(); + + void Draw(); + +private: + void UpdateNames(); + + std::array m_axis_names; + std::array m_button_names; + std::array m_types{}; + u32 m_active_ports = 0; +}; + +} // namespace FrontendCommon \ No newline at end of file