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;
};