Skip to content

Commit 2d3f1a3

Browse files
committed
Minor refactor of controllers.
1. Separate out a public controller-state function that properly goes through the command queue. 2. Split the lock inside ProControllerWithScheduler to remove some async uncertainties on the cancellation paths. 3. Make the controller implementations directly implement "push_state()".
1 parent 8c68c10 commit 2d3f1a3

15 files changed

+530
-308
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* Reverse Lock Guard
2+
*
3+
* From: https://github.com/PokemonAutomation/Arduino-Source
4+
*
5+
*/
6+
7+
#ifndef PokemonAutomation_ReverseLockGuard_H
8+
#define PokemonAutomation_ReverseLockGuard_H
9+
10+
#include "Common/Cpp/Exceptions.h"
11+
12+
namespace PokemonAutomation{
13+
14+
15+
template <class Mutex>
16+
class ReverseLockGuard{
17+
public:
18+
ReverseLockGuard(Mutex& lock)
19+
: m_lock(lock)
20+
{
21+
if (lock.try_lock()){
22+
throw InternalProgramError(
23+
nullptr, PA_CURRENT_FUNCTION,
24+
"Attempted to unlock an already unlocked lock."
25+
);
26+
}
27+
lock.unlock();
28+
}
29+
~ReverseLockGuard(){
30+
m_lock.lock();
31+
}
32+
33+
34+
private:
35+
Mutex& m_lock;
36+
};
37+
38+
39+
}
40+
#endif

SerialPrograms/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ file(GLOB MAIN_SOURCES
7474
../Common/Cpp/Concurrency/ParallelTaskRunner.h
7575
../Common/Cpp/Concurrency/PeriodicScheduler.cpp
7676
../Common/Cpp/Concurrency/PeriodicScheduler.h
77+
../Common/Cpp/Concurrency/ReverseLockGuard.h
7778
../Common/Cpp/Concurrency/ScheduledTaskRunner.cpp
7879
../Common/Cpp/Concurrency/ScheduledTaskRunner.h
7980
../Common/Cpp/Concurrency/SpinLock.cpp

SerialPrograms/SerialPrograms.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,7 @@ HEADERS += \
11481148
../Common/Cpp/Concurrency/FireForgetDispatcher.h \
11491149
../Common/Cpp/Concurrency/ParallelTaskRunner.h \
11501150
../Common/Cpp/Concurrency/PeriodicScheduler.h \
1151+
../Common/Cpp/Concurrency/ReverseLockGuard.h \
11511152
../Common/Cpp/Concurrency/ScheduledTaskRunner.h \
11521153
../Common/Cpp/Concurrency/SpinLock.h \
11531154
../Common/Cpp/Concurrency/SpinPause.h \

SerialPrograms/Source/Controllers/SuperscalarScheduler.cpp

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ SuperscalarScheduler::SuperscalarScheduler(
3030

3131
void SuperscalarScheduler::clear() noexcept{
3232
// SpinLockGuard lg(m_lock);
33-
m_logger.log("Clearing schedule...");
33+
// m_logger.log("Clearing schedule...");
3434
WallClock now = current_time();
3535
m_local_start = now;
3636
m_local_last_activity = now;
@@ -77,13 +77,17 @@ bool SuperscalarScheduler::iterate_schedule(const Cancellable* cancellable){
7777

7878
// Things get complicated if we overshoot the issue time.
7979
if (next_state_change > m_device_issue_time){
80-
// m_logger.log("SuperscalarScheduler: Overshoot due to unfinished early return.", COLOR_RED);
80+
// m_logger.log("SuperscalarScheduler: Overshoot due to unfinished early return.", COLOR_RED);
8181
next_state_change = m_device_issue_time;
8282
}
8383

8484
WallDuration duration = next_state_change - m_device_sent_time;
8585
if (duration == WallDuration::zero()){
86-
// m_logger.log("SuperscalarScheduler: Scheduler is stalled.", COLOR_RED);
86+
// This happens when the command has a delay of zero. A delay of zero
87+
// is almost always immediately followed by another command that is
88+
// intended to execute simultaneously. Thus we do not attempt to
89+
// execute the schedule any further.
90+
// m_logger.log("SuperscalarScheduler: Scheduler is stalled.", COLOR_RED);
8791
return false;
8892
}
8993

@@ -104,22 +108,35 @@ bool SuperscalarScheduler::iterate_schedule(const Cancellable* cancellable){
104108
WallClock now = current_time();
105109
m_local_last_activity = now;
106110

107-
#if 1
108-
// If it's been long enough since the issue, we can assume a gap and
109-
// fast forward whatever is scheduled into the future.
110-
if (m_device_sent_time < m_device_issue_time){
111-
WallDuration time_since_last_activity = now - m_local_last_activity;
112-
if (time_since_last_activity > m_flush_threshold){
113-
m_logger.log("SuperscalarScheduler: A dangling early-return issue has gapped.", COLOR_RED);
114-
m_device_issue_time += time_since_last_activity;
115-
116-
// Since we're gapping, we might as well gap a bit more to we don't
117-
// re-enter here so quickly.
118-
m_device_issue_time += m_flush_threshold;
111+
// If we are not dangling anything, we can return now.
112+
if (m_device_sent_time >= m_device_issue_time){
113+
return true;
114+
}
119115

120-
m_max_free_time = std::max(m_max_free_time, m_device_issue_time);
121-
}
116+
#if 1
117+
//
118+
// We are currently dangling a command. We don't know whether the current
119+
// ongoing commands will run out, or if something new will join them in the
120+
// near future.
121+
//
122+
// If it's been long enough since the last issue, we can assume that
123+
// nothing will join and thus can gap. (We do require that dangling
124+
// commands be resolved promptly or they may run out.)
125+
//
126+
// Thus we can fast forward whatever is scheduled into the future.
127+
//
128+
WallDuration time_since_last_activity = now - m_local_last_activity;
129+
if (time_since_last_activity > m_flush_threshold){
130+
m_logger.log("SuperscalarScheduler: A dangling early-return issue has gapped.", COLOR_RED);
131+
m_device_issue_time += time_since_last_activity;
132+
133+
// Since we're gapping, we might as well gap a bit more to we don't
134+
// re-enter here so quickly.
135+
m_device_issue_time += m_flush_threshold;
136+
137+
m_max_free_time = std::max(m_max_free_time, m_device_issue_time);
122138
}
139+
123140
#endif
124141

125142
return true;
@@ -140,6 +157,7 @@ void SuperscalarScheduler::issue_wait_for_all(const Cancellable* cancellable){
140157
clear();
141158
return;
142159
}
160+
// m_logger.log("issue_wait_for_all(): states = " + std::to_string(m_state_changes.size()), COLOR_DARKGREEN);
143161
// cout << "issue_wait_for_all(): " << m_state_changes.size() << endl;
144162
// cout << "issue_time = " << std::chrono::duration_cast<Milliseconds>((m_device_issue_time - m_local_start)).count()
145163
// << ", sent_time = " << std::chrono::duration_cast<Milliseconds>((m_device_sent_time - m_local_start)).count()
@@ -168,7 +186,7 @@ void SuperscalarScheduler::issue_nop(const Cancellable* cancellable, WallDuratio
168186
m_local_last_activity = current_time();
169187
process_schedule(cancellable);
170188
}
171-
void SuperscalarScheduler::wait_for_resource(const Cancellable* cancellable, ExecutionResource& resource){
189+
void SuperscalarScheduler::issue_wait_for_resource(const Cancellable* cancellable, ExecutionResource& resource){
172190
if (m_pending_clear.load(std::memory_order_acquire)){
173191
clear();
174192
return;

SerialPrograms/Source/Controllers/SuperscalarScheduler.h

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ class SuperscalarScheduler{
6464

6565

6666
public:
67-
// These are the standard commands.
68-
// They are not thread-safe with each other.
67+
// These are the standard "issue" commands.
68+
//
69+
// These functions may or may not block.
70+
// These are not thread-safe with each other.
71+
//
6972

7073
// Wait until the pipeline has completely cleared and all resources have
7174
// returned to the ready state.
@@ -77,7 +80,7 @@ class SuperscalarScheduler{
7780

7881
// Wait until the specified resource is ready to be used.
7982
// This will advance the issue timestamp until the resource is ready.
80-
void wait_for_resource(const Cancellable* cancellable, ExecutionResource& resource);
83+
void issue_wait_for_resource(const Cancellable* cancellable, ExecutionResource& resource);
8184

8285
// Issue a resource with the specified timing parameters.
8386
// The resource must be ready to be used.
@@ -88,6 +91,17 @@ class SuperscalarScheduler{
8891

8992

9093
protected:
94+
//
95+
// This class will automatically call "push_state()" when a state is ready.
96+
// The child class should send/queue this state to the device/Switch.
97+
//
98+
// This function is allowed to block. In other words, it can wait until
99+
// there is space in the queue before returning.
100+
//
101+
// This will always be called inside one of the above "issue_XXX()"
102+
// functions. Implementations of this method should be aware of this
103+
// re-entrancy when this gets called on them with respect to locking.
104+
//
91105
virtual void push_state(const Cancellable* cancellable, WallDuration duration) = 0;
92106

93107

SerialPrograms/Source/NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ void pbf_controller_state(
9191
uint8_t right_x, uint8_t right_y,
9292
uint16_t ticks
9393
){
94-
context->issue_controller_state(
94+
context->issue_full_controller_state(
9595
&context,
9696
button, position,
9797
left_x, left_y,
@@ -107,7 +107,7 @@ void pbf_controller_state(
107107
uint8_t right_x, uint8_t right_y,
108108
Milliseconds duration
109109
){
110-
context->issue_controller_state(
110+
context->issue_full_controller_state(
111111
&context,
112112
button, position,
113113
left_x, left_y,

SerialPrograms/Source/NintendoSwitch/Controllers/NintendoSwitch_ProController.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class ProController::KeyboardManager : public KeyboardInputController{
7272
}
7373
virtual void send_state(const ControllerState& state) override{
7474
const SwitchControllerState& switch_state = static_cast<const SwitchControllerState&>(state);
75+
#if 0
7576
m_controller.logger().log(
7677
"VirtualController: (" + button_to_string(switch_state.buttons) +
7778
"), dpad(" + dpad_to_string(switch_state.dpad) +
@@ -80,7 +81,8 @@ class ProController::KeyboardManager : public KeyboardInputController{
8081
")",
8182
COLOR_DARKGREEN
8283
);
83-
m_controller.issue_controller_state(
84+
#endif
85+
m_controller.issue_full_controller_state(
8486
nullptr,
8587
switch_state.buttons,
8688
switch_state.dpad,

SerialPrograms/Source/NintendoSwitch/Controllers/NintendoSwitch_ProController.h

Lines changed: 59 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,10 @@ class ProController : public AbstractController{
8888

8989

9090
public:
91-
// General Control
92-
93-
// Wait for all unfinished commands to finish.
94-
virtual void wait_for_all(const Cancellable* cancellable) = 0;
91+
//
92+
// Cancellation
93+
// These are thread-safe with everything.
94+
//
9595

9696
// Cancel all commands. This returns the controller to the neutral button
9797
// state and clears the command queue.
@@ -103,7 +103,21 @@ class ProController : public AbstractController{
103103

104104

105105
public:
106+
//
106107
// Basic Commands
108+
//
109+
// Commands not thread-safe with other commands. But they are thread-safe
110+
// with the cancellation functions above.
111+
//
112+
// As of this writing, all implementations are thread-safe enough that
113+
// they will neither crash nor enter an invalid state with concurrent
114+
// commands. But the implied queuing semantics means that parallelizing
115+
// commands will not do what you want it to do.
116+
//
117+
118+
// Wait for all unfinished commands to finish. This will also wait out
119+
// hanging commands including their cooldown periods.
120+
virtual void wait_for_all(const Cancellable* cancellable) = 0;
107121

108122
//
109123
// All commands are enqueued into a FIFO that the controller will execute
@@ -125,36 +139,6 @@ class ProController : public AbstractController{
125139
// if the command can be enqueued into the FIFO. Otherwise, they will block
126140
// until there is space in the FIFO.
127141

128-
//
129-
// Press all the following buttons/joysticks simultaneously for the
130-
// specified duration. No wait is added at the end. Thus you can issue
131-
// these back-to-back to simulate buttons being pressed and released
132-
// concurrently with other buttons being held down the whole time.
133-
//
134-
// The behavior of this function is undefined if there are any unfinished
135-
// asynchronous commands in the controller's queue (including unfinished
136-
// cooldowns). It is the responsibility of the caller to ensure the
137-
// controller is idle by calling "this->wait_for_all_requests()".
138-
//
139-
// The sole purpose of this function is for keyboard commands.
140-
// While it's technically possible to implement any button overlapping
141-
// sequence with this, doing so this way can lead to very inefficient
142-
// serial bandwidth usage if buttons are being rapidly pressed and released
143-
// in an arbitrary manner that leads to constant state changes.
144-
//
145-
// If we need to support new Switch controller functionality
146-
// (such as Joycon gyro or new stuff in Switch 2), we can simply add
147-
// overloads to this and gate them behind features.
148-
//
149-
virtual void issue_controller_state(
150-
const Cancellable* cancellable,
151-
Button button,
152-
DpadPosition position,
153-
uint8_t left_x, uint8_t left_y,
154-
uint8_t right_x, uint8_t right_y,
155-
Milliseconds milliseconds
156-
) = 0;
157-
158142
// Temporary for refactor: Send custom requests for PABotBase's advanced
159143
// RPCs.
160144
virtual void send_botbase_request(
@@ -168,16 +152,9 @@ class ProController : public AbstractController{
168152

169153

170154
public:
155+
//
171156
// Superscalar Commands (the "ssf" framework)
172-
173-
// Tell the scheduler to wait for all pending commands to finish
174-
// (including cooldowns) before executing further instructions.
175-
// This is used to prevent hanging commands from overlapping with new
176-
// commands issued after this barrier.
177-
virtual void issue_barrier(const Cancellable* cancellable) = 0;
178-
179-
// Do nothing for this much time.
180-
virtual void issue_nop(const Cancellable* cancellable, Milliseconds duration) = 0;
157+
//
181158

182159
//
183160
// delay Time to wait before moving onto the next command.
@@ -207,6 +184,15 @@ class ProController : public AbstractController{
207184
// managing the timeline/scheduling.
208185
//
209186

187+
// Tell the scheduler to wait for all pending commands to finish
188+
// (including cooldowns) before executing further instructions.
189+
// This is used to prevent hanging commands from overlapping with new
190+
// commands issued after this barrier.
191+
virtual void issue_barrier(const Cancellable* cancellable) = 0;
192+
193+
// Do nothing for this much time.
194+
virtual void issue_nop(const Cancellable* cancellable, Milliseconds duration) = 0;
195+
210196
// Press all the buttons set in the bitfield simultaneously.
211197
// This command will wait until all the selected buttons are ready to
212198
// ensure that they are all dispatched simultaneously.
@@ -235,10 +221,39 @@ class ProController : public AbstractController{
235221
Milliseconds delay, Milliseconds hold, Milliseconds cooldown
236222
) = 0;
237223

224+
//
225+
// Press all the following buttons/joysticks simultaneously for the
226+
// specified duration. No wait is added at the end. Thus you can issue
227+
// these back-to-back to simulate buttons being pressed and released
228+
// concurrently with other buttons being held down the whole time.
229+
//
230+
// This command will wait until the controller is fully idle (including
231+
// cooldowns) before it starts. This ensures that everything is issued
232+
// simultaneously.
233+
//
234+
// The sole purpose of this function is for keyboard commands.
235+
// While it's technically possible to implement any button overlapping
236+
// sequence with this, doing so this way can lead to very inefficient
237+
// serial bandwidth usage if buttons are being rapidly pressed and released
238+
// in an arbitrary manner that leads to constant state changes.
239+
//
240+
// If we need to support new Switch controller functionality
241+
// (such as Joycon gyro or new stuff in Switch 2), we can simply add
242+
// overloads to this and gate them behind features.
243+
//
244+
virtual void issue_full_controller_state(
245+
const Cancellable* cancellable,
246+
Button button,
247+
DpadPosition position,
248+
uint8_t left_x, uint8_t left_y,
249+
uint8_t right_x, uint8_t right_y,
250+
Milliseconds hold
251+
) = 0;
252+
238253

239254
public:
255+
//
240256
// High speed Macros
241-
242257
//
243258
// It is currently unclear if these can be properly executed over wireless.
244259
// If they can't, then it remains to be decided if we should gate these

0 commit comments

Comments
 (0)