Skip to content

Commit 21d0ed1

Browse files
committed
Adding stackful coroutine support + example.
1 parent e7fca3d commit 21d0ed1

File tree

5 files changed

+275
-0
lines changed

5 files changed

+275
-0
lines changed

src/mips/psyqo/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ $(wildcard ../../../third_party/EASTL/source/*.cpp) \
1010
../common/crt0/memory-c.c \
1111
../common/crt0/memory-s.s \
1212
../common/hardware/flushcache.s \
13+
../common/psxlibc/ucontext.s \
1314
../common/syscalls/printf.s \
1415
$(wildcard src/hardware/*.cpp) \
1516
$(wildcard src/*.cpp) \

src/mips/psyqo/coroutine.hh

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ SOFTWARE.
2929
#include <coroutine>
3030
#include <type_traits>
3131

32+
#include "common/psxlibc/ucontext.h"
3233
#include "common/syscalls/syscalls.h"
3334

3435
namespace psyqo {
@@ -208,4 +209,97 @@ struct Coroutine {
208209
}
209210
};
210211

212+
class StackfulBase {
213+
protected:
214+
void initializeInternal(eastl::function<void()>&& func, void* ss_sp, unsigned ss_size);
215+
void resume();
216+
void yield();
217+
[[nodiscard]] bool isAlive() const { return m_isAlive; }
218+
219+
StackfulBase() = default;
220+
StackfulBase(const StackfulBase&) = delete;
221+
StackfulBase& operator=(const StackfulBase&) = delete;
222+
223+
private:
224+
static void trampoline(void* arg) {
225+
StackfulBase* self = static_cast<StackfulBase*>(arg);
226+
self->trampoline();
227+
}
228+
void trampoline();
229+
ucontext_t m_coroutine;
230+
ucontext_t m_return;
231+
eastl::function<void()> m_func;
232+
bool m_isAlive = false;
233+
};
234+
235+
/**
236+
* @brief Stackful coroutine class.
237+
*
238+
* @details This class provides a simple stackful coroutine implementation.
239+
* It allows you to create coroutines that can yield and resume execution.
240+
* While the Coroutine class above is a C++20 coroutine, it requires
241+
* that all of the code being run are coroutines or awaitables all the way down.
242+
* This class is a more traditional coroutine implementation that uses
243+
* a separate stack for each coroutine, allowing it to yield and resume
244+
* execution without requiring the entire call stack to be coroutine-aware.
245+
* It is suitable for use in scenarios where you need to yield execution
246+
* from legacy code without converting it to C++20 coroutines.
247+
*/
248+
template <unsigned StackSize = 0x10000>
249+
class Stackful : public StackfulBase {
250+
public:
251+
static constexpr unsigned c_stackSize = (StackSize + 7) & ~7;
252+
253+
Stackful() = default;
254+
Stackful(const Stackful&) = delete;
255+
Stackful& operator=(const Stackful&) = delete;
256+
257+
/**
258+
* @brief Initialize the coroutine with a function and an argument.
259+
*
260+
* @param func Function to be executed by the coroutine.
261+
* @param arg Argument to be passed to the function.
262+
*/
263+
void initialize(eastl::function<void()>&& func) {
264+
initializeInternal(eastl::move(func), m_stack.data, c_stackSize);
265+
}
266+
267+
/**
268+
* @brief Resume the coroutine.
269+
*
270+
* @details This will switch to the coroutine's context and execute it.
271+
* If the coroutine is not alive, this function does nothing. This
272+
* function should be called after the coroutine has been initialized,
273+
* and it will return to the point where the coroutine was last yielded.
274+
* It can only be called from the "main thread".
275+
*/
276+
void resume() { StackfulBase::resume(); }
277+
278+
/**
279+
* @brief Yield the coroutine.
280+
*
281+
* @details This will switch back to the main thread and save the
282+
* coroutine's context. The coroutine can be resumed later using
283+
* `resume()`. It can only be called from within the coroutine
284+
* to yield execution.
285+
*/
286+
void yield() { StackfulBase::yield(); }
287+
288+
/**
289+
* @brief Check if the coroutine is currently alive.
290+
* @details A coroutine is considered alive if it has been initialized
291+
* and has not yet completed its execution. It becomes not alive
292+
* when it returns from its function.
293+
*
294+
* @return true if the coroutine is alive, false otherwise.
295+
*/
296+
[[nodiscard]] bool isAlive() const { return StackfulBase::isAlive(); }
297+
298+
private:
299+
struct alignas(8) Stack {
300+
uint8_t data[c_stackSize];
301+
};
302+
Stack m_stack;
303+
};
304+
211305
} // namespace psyqo
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
TARGET = coroutine-demo-3
2+
TYPE = ps-exe
3+
4+
SRCS = \
5+
coroutine-demo-3.cpp \
6+
7+
ifeq ($(TEST),true)
8+
CPPFLAGS = -Werror
9+
endif
10+
CXXFLAGS = -std=c++20 -fcoroutines
11+
12+
include ../../psyqo.mk
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2025 PCSX-Redux authors
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
#include <EASTL/string.h>
28+
29+
#include "common/syscalls/syscalls.h"
30+
#include "psyqo/application.hh"
31+
#include "psyqo/coroutine.hh"
32+
#include "psyqo/font.hh"
33+
#include "psyqo/gpu.hh"
34+
#include "psyqo/kernel.hh"
35+
#include "psyqo/scene.hh"
36+
#include "psyqo/xprintf.h"
37+
38+
namespace {
39+
40+
class CoroutineDemo3 final : public psyqo::Application {
41+
void prepare() override;
42+
void createScene() override;
43+
44+
public:
45+
psyqo::Font<> m_font;
46+
};
47+
48+
class CoroutineDemoScene final : public psyqo::Scene {
49+
void frame() override;
50+
void start(StartReason reason) override;
51+
void coroutine();
52+
psyqo::Stackful<65536> m_coroutine;
53+
eastl::string m_text;
54+
};
55+
56+
CoroutineDemo3 coroutineDemo3;
57+
CoroutineDemoScene coroutineDemoScene;
58+
59+
} // namespace
60+
61+
void CoroutineDemo3::prepare() {
62+
psyqo::GPU::Configuration config;
63+
config.set(psyqo::GPU::Resolution::W320)
64+
.set(psyqo::GPU::VideoMode::AUTO)
65+
.set(psyqo::GPU::ColorMode::C15BITS)
66+
.set(psyqo::GPU::Interlace::PROGRESSIVE);
67+
gpu().initialize(config);
68+
}
69+
70+
void CoroutineDemo3::createScene() {
71+
m_font.uploadSystemFont(gpu());
72+
pushScene(&coroutineDemoScene);
73+
}
74+
75+
void CoroutineDemoScene::start(StartReason reason) {
76+
psyqo::Kernel::assert(reason == StartReason::Create, "Wrong internal state");
77+
m_coroutine.initialize([this]() { coroutine(); });
78+
m_coroutine.resume();
79+
}
80+
81+
void CoroutineDemoScene::coroutine() {
82+
using namespace psyqo::timer_literals;
83+
m_text = "Coroutine... sleeping for 2s";
84+
gpu().armTimer(gpu().now() + 2_s, [this](uint32_t) {
85+
m_text = "Coroutine... waking up";
86+
m_coroutine.resume();
87+
});
88+
m_coroutine.yield();
89+
m_text = "Waking up... sleeping for 1s";
90+
gpu().armTimer(gpu().now() + 1_s, [this](uint32_t) {
91+
m_text = "Waking up... sleeping for 5s this time";
92+
m_coroutine.resume();
93+
});
94+
m_coroutine.yield();
95+
m_text = "Waking up... sleeping again for 1s";
96+
gpu().armTimer(gpu().now() + 1_s, [this](uint32_t) {
97+
m_text = "Waking up... sleeping for 5s this time";
98+
m_coroutine.resume();
99+
});
100+
m_coroutine.yield();
101+
m_text = "Waking up... sleeping for 5s this time";
102+
gpu().armTimer(gpu().now() + 5_s, [this](uint32_t) {
103+
m_text = "All done.";
104+
m_coroutine.resume();
105+
});
106+
m_coroutine.yield();
107+
}
108+
109+
void CoroutineDemoScene::frame() {
110+
psyqo::Color bg{{.r = 0, .g = 64, .b = 91}};
111+
coroutineDemo3.gpu().clear(bg);
112+
auto c = psyqo::Color{{.r = 255, .g = 255, .b = 255}};
113+
coroutineDemo3.m_font.print(coroutineDemo3.gpu(), m_text, {{.x = 4, .y = 32}}, c);
114+
}
115+
116+
int main() { return coroutineDemo3.run(); }

src/mips/psyqo/src/coroutine.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2025 PCSX-Redux authors
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
#include "psyqo/coroutine.hh"
28+
29+
void psyqo::StackfulBase::initializeInternal(eastl::function<void()>&& func, void* ss_sp, unsigned ss_size) {
30+
m_isAlive = true;
31+
m_func = eastl::move(func);
32+
getcontext(&m_coroutine);
33+
m_coroutine.uc_stack.ss_sp = ss_sp;
34+
m_coroutine.uc_stack.ss_size = ss_size;
35+
m_coroutine.uc_link = &m_return;
36+
makecontext(&m_coroutine, trampoline, this);
37+
}
38+
39+
void psyqo::StackfulBase::trampoline() {
40+
m_func();
41+
m_isAlive = false;
42+
}
43+
44+
void psyqo::StackfulBase::resume() {
45+
if (!m_isAlive) return;
46+
swapcontext(&m_return, &m_coroutine);
47+
}
48+
49+
void psyqo::StackfulBase::yield() {
50+
if (!m_isAlive) return;
51+
swapcontext(&m_coroutine, &m_return);
52+
}

0 commit comments

Comments
 (0)