DolphinQt: Rework TAS input threading, part 1 (buttons)
This gets rid of a blocking operation, improving performance and fixing https://bugs.dolphin-emu.org/issues/12893. This also makes us no longer directly access the state of certain UI elements from the CPU thread, which probably wasn't thread-safe but doesn't seem to have caused any observable issues so far.
This commit is contained in:
parent
95ce41ac56
commit
3eac1fc284
|
@ -337,6 +337,8 @@ add_executable(dolphin-emu
|
||||||
TAS/StickWidget.h
|
TAS/StickWidget.h
|
||||||
TAS/TASCheckBox.cpp
|
TAS/TASCheckBox.cpp
|
||||||
TAS/TASCheckBox.h
|
TAS/TASCheckBox.h
|
||||||
|
TAS/TASControlState.cpp
|
||||||
|
TAS/TASControlState.h
|
||||||
TAS/TASInputWindow.cpp
|
TAS/TASInputWindow.cpp
|
||||||
TAS/TASInputWindow.h
|
TAS/TASInputWindow.h
|
||||||
TAS/TASSlider.cpp
|
TAS/TASSlider.cpp
|
||||||
|
|
|
@ -207,6 +207,7 @@
|
||||||
<ClCompile Include="TAS\StickWidget.cpp" />
|
<ClCompile Include="TAS\StickWidget.cpp" />
|
||||||
<ClCompile Include="TAS\TASCheckBox.cpp" />
|
<ClCompile Include="TAS\TASCheckBox.cpp" />
|
||||||
<ClCompile Include="TAS\TASInputWindow.cpp" />
|
<ClCompile Include="TAS\TASInputWindow.cpp" />
|
||||||
|
<ClCompile Include="TAS\TASControlState.cpp" />
|
||||||
<ClCompile Include="TAS\TASSlider.cpp" />
|
<ClCompile Include="TAS\TASSlider.cpp" />
|
||||||
<ClCompile Include="TAS\WiiTASInputWindow.cpp" />
|
<ClCompile Include="TAS\WiiTASInputWindow.cpp" />
|
||||||
<ClCompile Include="ToolBar.cpp" />
|
<ClCompile Include="ToolBar.cpp" />
|
||||||
|
@ -242,6 +243,7 @@
|
||||||
<ClInclude Include="QtUtils\WrapInScrollArea.h" />
|
<ClInclude Include="QtUtils\WrapInScrollArea.h" />
|
||||||
<ClInclude Include="ResourcePackManager.h" />
|
<ClInclude Include="ResourcePackManager.h" />
|
||||||
<ClInclude Include="Resources.h" />
|
<ClInclude Include="Resources.h" />
|
||||||
|
<ClInclude Include="TAS\TASControlState.h" />
|
||||||
<ClInclude Include="TAS\TASSlider.h" />
|
<ClInclude Include="TAS\TASSlider.h" />
|
||||||
<ClInclude Include="Translation.h" />
|
<ClInclude Include="Translation.h" />
|
||||||
<ClInclude Include="WiiUpdate.h" />
|
<ClInclude Include="WiiUpdate.h" />
|
||||||
|
|
|
@ -6,23 +6,34 @@
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
|
||||||
#include "Core/Movie.h"
|
#include "Core/Movie.h"
|
||||||
|
#include "DolphinQt/QtUtils/QueueOnObject.h"
|
||||||
#include "DolphinQt/TAS/TASInputWindow.h"
|
#include "DolphinQt/TAS/TASInputWindow.h"
|
||||||
|
|
||||||
TASCheckBox::TASCheckBox(const QString& text, TASInputWindow* parent)
|
TASCheckBox::TASCheckBox(const QString& text, TASInputWindow* parent)
|
||||||
: QCheckBox(text, parent), m_parent(parent)
|
: QCheckBox(text, parent), m_parent(parent)
|
||||||
{
|
{
|
||||||
setTristate(true);
|
setTristate(true);
|
||||||
|
|
||||||
|
connect(this, &TASCheckBox::stateChanged, this, &TASCheckBox::OnUIValueChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TASCheckBox::GetValue() const
|
bool TASCheckBox::GetValue() const
|
||||||
{
|
{
|
||||||
if (checkState() == Qt::PartiallyChecked)
|
Qt::CheckState check_state = static_cast<Qt::CheckState>(m_state.GetValue());
|
||||||
|
|
||||||
|
if (check_state == Qt::PartiallyChecked)
|
||||||
{
|
{
|
||||||
const u64 frames_elapsed = Movie::GetCurrentFrame() - m_frame_turbo_started;
|
const u64 frames_elapsed = Movie::GetCurrentFrame() - m_frame_turbo_started;
|
||||||
return static_cast<int>(frames_elapsed % m_turbo_total_frames) < m_turbo_press_frames;
|
return static_cast<int>(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)
|
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();
|
m_turbo_total_frames = m_turbo_press_frames + m_parent->GetTurboReleaseFrames();
|
||||||
setCheckState(Qt::PartiallyChecked);
|
setCheckState(Qt::PartiallyChecked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TASCheckBox::OnUIValueChanged(int new_value)
|
||||||
|
{
|
||||||
|
m_state.OnUIValueChanged(static_cast<u16>(new_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TASCheckBox::ApplyControllerValueChange()
|
||||||
|
{
|
||||||
|
const QSignalBlocker blocker(this);
|
||||||
|
setCheckState(static_cast<Qt::CheckState>(m_state.ApplyControllerValueChange()));
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
|
||||||
|
#include "DolphinQt/TAS/TASControlState.h"
|
||||||
|
|
||||||
class QMouseEvent;
|
class QMouseEvent;
|
||||||
class TASInputWindow;
|
class TASInputWindow;
|
||||||
|
|
||||||
|
@ -14,13 +16,21 @@ class TASCheckBox : public QCheckBox
|
||||||
public:
|
public:
|
||||||
explicit TASCheckBox(const QString& text, TASInputWindow* parent);
|
explicit TASCheckBox(const QString& text, TASInputWindow* parent);
|
||||||
|
|
||||||
|
// Can be called from the CPU thread
|
||||||
bool GetValue() const;
|
bool GetValue() const;
|
||||||
|
// Must be called from the CPU thread
|
||||||
|
void OnControllerValueChanged(bool new_value);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void mousePressEvent(QMouseEvent* event) override;
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void OnUIValueChanged(int new_value);
|
||||||
|
void ApplyControllerValueChange();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const TASInputWindow* m_parent;
|
const TASInputWindow* m_parent;
|
||||||
|
TASControlState m_state;
|
||||||
int m_frame_turbo_started = 0;
|
int m_frame_turbo_started = 0;
|
||||||
int m_turbo_press_frames = 0;
|
int m_turbo_press_frames = 0;
|
||||||
int m_turbo_total_frames = 0;
|
int m_turbo_total_frames = 0;
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "DolphinQt/TAS/TASControlState.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#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<u16>(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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#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<State> m_ui_thread_state;
|
||||||
|
std::atomic<State> m_cpu_thread_state;
|
||||||
|
};
|
|
@ -239,18 +239,7 @@ std::optional<ControlState> TASInputWindow::GetButton(TASCheckBox* checkbox,
|
||||||
{
|
{
|
||||||
const bool pressed = std::llround(controller_state) > 0;
|
const bool pressed = std::llround(controller_state) > 0;
|
||||||
if (m_use_controller->isChecked())
|
if (m_use_controller->isChecked())
|
||||||
{
|
checkbox->OnControllerValueChanged(pressed);
|
||||||
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); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkbox->GetValue() ? 1.0 : 0.0;
|
return checkbox->GetValue() ? 1.0 : 0.0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,5 @@ private:
|
||||||
std::optional<ControlState> GetSpinBox(QSpinBox* spin, u16 zero, ControlState controller_state,
|
std::optional<ControlState> GetSpinBox(QSpinBox* spin, u16 zero, ControlState controller_state,
|
||||||
ControlState scale);
|
ControlState scale);
|
||||||
|
|
||||||
std::map<TASCheckBox*, bool> m_checkbox_set_by_controller;
|
|
||||||
std::map<QSpinBox*, u16> m_spinbox_most_recent_values;
|
std::map<QSpinBox*, u16> m_spinbox_most_recent_values;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue