diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 930e95820b..78a842f2bc 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -337,6 +337,8 @@ add_executable(dolphin-emu TAS/StickWidget.h TAS/TASCheckBox.cpp TAS/TASCheckBox.h + TAS/TASControlState.cpp + TAS/TASControlState.h TAS/TASInputWindow.cpp TAS/TASInputWindow.h TAS/TASSlider.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index f776feceed..76b01c95d7 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -207,6 +207,7 @@ + @@ -242,6 +243,7 @@ + diff --git a/Source/Core/DolphinQt/TAS/TASCheckBox.cpp b/Source/Core/DolphinQt/TAS/TASCheckBox.cpp index 29a7171f47..32d1b3b0b0 100644 --- a/Source/Core/DolphinQt/TAS/TASCheckBox.cpp +++ b/Source/Core/DolphinQt/TAS/TASCheckBox.cpp @@ -6,23 +6,34 @@ #include #include "Core/Movie.h" +#include "DolphinQt/QtUtils/QueueOnObject.h" #include "DolphinQt/TAS/TASInputWindow.h" TASCheckBox::TASCheckBox(const QString& text, TASInputWindow* parent) : QCheckBox(text, parent), m_parent(parent) { setTristate(true); + + connect(this, &TASCheckBox::stateChanged, this, &TASCheckBox::OnUIValueChanged); } bool TASCheckBox::GetValue() const { - if (checkState() == Qt::PartiallyChecked) + Qt::CheckState check_state = static_cast(m_state.GetValue()); + + if (check_state == Qt::PartiallyChecked) { const u64 frames_elapsed = Movie::GetCurrentFrame() - m_frame_turbo_started; return static_cast(frames_elapsed % m_turbo_total_frames) < m_turbo_press_frames; } - return isChecked(); + return check_state != Qt::Unchecked; +} + +void TASCheckBox::OnControllerValueChanged(bool new_value) +{ + if (m_state.OnControllerValueChanged(new_value ? Qt::Checked : Qt::Unchecked)) + QueueOnObject(this, &TASCheckBox::ApplyControllerValueChange); } void TASCheckBox::mousePressEvent(QMouseEvent* event) @@ -44,3 +55,14 @@ void TASCheckBox::mousePressEvent(QMouseEvent* event) m_turbo_total_frames = m_turbo_press_frames + m_parent->GetTurboReleaseFrames(); setCheckState(Qt::PartiallyChecked); } + +void TASCheckBox::OnUIValueChanged(int new_value) +{ + m_state.OnUIValueChanged(static_cast(new_value)); +} + +void TASCheckBox::ApplyControllerValueChange() +{ + const QSignalBlocker blocker(this); + setCheckState(static_cast(m_state.ApplyControllerValueChange())); +} diff --git a/Source/Core/DolphinQt/TAS/TASCheckBox.h b/Source/Core/DolphinQt/TAS/TASCheckBox.h index afec671194..3af68d43b2 100644 --- a/Source/Core/DolphinQt/TAS/TASCheckBox.h +++ b/Source/Core/DolphinQt/TAS/TASCheckBox.h @@ -5,6 +5,8 @@ #include +#include "DolphinQt/TAS/TASControlState.h" + class QMouseEvent; class TASInputWindow; @@ -14,13 +16,21 @@ class TASCheckBox : public QCheckBox public: explicit TASCheckBox(const QString& text, TASInputWindow* parent); + // Can be called from the CPU thread bool GetValue() const; + // Must be called from the CPU thread + void OnControllerValueChanged(bool new_value); protected: void mousePressEvent(QMouseEvent* event) override; +private slots: + void OnUIValueChanged(int new_value); + void ApplyControllerValueChange(); + private: const TASInputWindow* m_parent; + TASControlState m_state; int m_frame_turbo_started = 0; int m_turbo_press_frames = 0; int m_turbo_total_frames = 0; diff --git a/Source/Core/DolphinQt/TAS/TASControlState.cpp b/Source/Core/DolphinQt/TAS/TASControlState.cpp new file mode 100644 index 0000000000..fd0c209aa6 --- /dev/null +++ b/Source/Core/DolphinQt/TAS/TASControlState.cpp @@ -0,0 +1,58 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/TAS/TASControlState.h" + +#include + +#include "Common/CommonTypes.h" + +u16 TASControlState::GetValue() const +{ + const State ui_thread_state = m_ui_thread_state.load(std::memory_order_relaxed); + const State cpu_thread_state = m_cpu_thread_state.load(std::memory_order_relaxed); + + return (ui_thread_state.version != cpu_thread_state.version ? cpu_thread_state : ui_thread_state) + .value; +} + +bool TASControlState::OnControllerValueChanged(u16 new_value) +{ + const State cpu_thread_state = m_cpu_thread_state.load(std::memory_order_relaxed); + + if (cpu_thread_state.value == new_value) + { + // The CPU thread state is already up to date with the controller. No need to do anything + return false; + } + + const State new_state{static_cast(cpu_thread_state.version + 1), new_value}; + m_cpu_thread_state.store(new_state, std::memory_order_relaxed); + + return true; +} + +void TASControlState::OnUIValueChanged(u16 new_value) +{ + const State ui_thread_state = m_ui_thread_state.load(std::memory_order_relaxed); + + const State new_state{ui_thread_state.version, new_value}; + m_ui_thread_state.store(new_state, std::memory_order_relaxed); +} + +u16 TASControlState::ApplyControllerValueChange() +{ + const State ui_thread_state = m_ui_thread_state.load(std::memory_order_relaxed); + const State cpu_thread_state = m_cpu_thread_state.load(std::memory_order_relaxed); + + if (ui_thread_state.version == cpu_thread_state.version) + { + // The UI thread state is already up to date with the CPU thread. No need to do anything + return ui_thread_state.value; + } + else + { + m_ui_thread_state.store(cpu_thread_state, std::memory_order_relaxed); + return cpu_thread_state.value; + } +} diff --git a/Source/Core/DolphinQt/TAS/TASControlState.h b/Source/Core/DolphinQt/TAS/TASControlState.h new file mode 100644 index 0000000000..b53c7f95f9 --- /dev/null +++ b/Source/Core/DolphinQt/TAS/TASControlState.h @@ -0,0 +1,43 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "Common/CommonTypes.h" + +class TASControlState +{ +public: + // Call this from the CPU thread to get the current value. (This function can also safely be + // called from the UI thread, but you're effectively just getting the value the UI control has.) + u16 GetValue() const; + // Call this from the CPU thread when the controller state changes. + // If the return value is true, queue up a call to ApplyControllerChangeValue on the UI thread. + bool OnControllerValueChanged(u16 new_value); + // Call this from the UI thread when the user changes the value using the UI. + void OnUIValueChanged(u16 new_value); + // Call this from the UI thread after OnControllerValueChanged returns true, + // and set the state of the UI control to the return value. + u16 ApplyControllerValueChange(); + +private: + // A description of how threading is handled: The UI thread can update its copy of the state + // whenever it wants to, and must *not* increment the version when doing so. The CPU thread can + // update its copy of the state whenever it wants to, and *must* increment the version when doing + // so. When the CPU thread updates its copy of the state, the UI thread should then (possibly + // after a delay) mirror the change by copying the CPU thread's state to the UI thread's state. + // This mirroring is the only way for the version number stored in the UI thread's state to + // change. The version numbers of the two copies can be compared to check if the UI thread's view + // of what has happened on the CPU thread is up to date. + + struct State + { + u16 version = 0; + u16 value = 0; + }; + + std::atomic m_ui_thread_state; + std::atomic m_cpu_thread_state; +}; diff --git a/Source/Core/DolphinQt/TAS/TASInputWindow.cpp b/Source/Core/DolphinQt/TAS/TASInputWindow.cpp index 0054043a8e..61e37e5238 100644 --- a/Source/Core/DolphinQt/TAS/TASInputWindow.cpp +++ b/Source/Core/DolphinQt/TAS/TASInputWindow.cpp @@ -239,18 +239,7 @@ std::optional TASInputWindow::GetButton(TASCheckBox* checkbox, { const bool pressed = std::llround(controller_state) > 0; if (m_use_controller->isChecked()) - { - if (pressed) - { - m_checkbox_set_by_controller[checkbox] = true; - QueueOnObjectBlocking(checkbox, [checkbox] { checkbox->setChecked(true); }); - } - else if (m_checkbox_set_by_controller.count(checkbox) && m_checkbox_set_by_controller[checkbox]) - { - m_checkbox_set_by_controller[checkbox] = false; - QueueOnObjectBlocking(checkbox, [checkbox] { checkbox->setChecked(false); }); - } - } + checkbox->OnControllerValueChanged(pressed); return checkbox->GetValue() ? 1.0 : 0.0; } diff --git a/Source/Core/DolphinQt/TAS/TASInputWindow.h b/Source/Core/DolphinQt/TAS/TASInputWindow.h index e1f56e0235..104cb28fe9 100644 --- a/Source/Core/DolphinQt/TAS/TASInputWindow.h +++ b/Source/Core/DolphinQt/TAS/TASInputWindow.h @@ -79,6 +79,5 @@ private: std::optional GetSpinBox(QSpinBox* spin, u16 zero, ControlState controller_state, ControlState scale); - std::map m_checkbox_set_by_controller; std::map m_spinbox_most_recent_values; };