added coroutines library and multitasking example. Added sys.push_returnaddress().

This commit is contained in:
Irmen de Jong 2024-12-25 21:50:15 +01:00
parent fe011de934
commit 529ea5bf58
16 changed files with 494 additions and 155 deletions

View File

@ -367,6 +367,20 @@ save_SCRATCH_ZPWORD2 .word 0
}}
}
inline asmsub push_returnaddress(uword address @XY) {
%asm {{
; push like JSR would: address-1, MSB first then LSB
cpx #0
bne +
dey
+ dex
tya
pha
txa
pha
}}
}
inline asmsub pop() -> ubyte @A {
%asm {{
pla

View File

@ -918,6 +918,20 @@ _no_msb_size
}}
}
inline asmsub push_returnaddress(uword address @XY) {
%asm {{
; push like JSR would: address-1, MSB first then LSB
cpx #0
bne +
dey
+ dex
tya
pha
txa
pha
}}
}
inline asmsub pop() -> ubyte @A {
%asm {{
pla

View File

@ -934,6 +934,20 @@ _no_msb_size
}}
}
inline asmsub push_returnaddress(uword address @XY) {
%asm {{
; push like JSR would: address-1, MSB first then LSB
cpx #0
bne +
dey
+ dex
tya
pha
txa
pha
}}
}
inline asmsub pop() -> ubyte @A {
%asm {{
pla

View File

@ -0,0 +1,134 @@
; Cooperative multitasking / Coroutines
; EXPERIMENTAL LIBRARY: Api may change or it may be removed completely in a future version!
; Achieves cooperative multitasking among a list of tasks each calling yield() to pass control to the next.
; Uses cpu stack return address juggling to cycle between different tasks.
;
; Features:
; - can have a dynamic number of tasks (max 64), when tasks end they're automaticall removed from the task list.
; - you can add new tasks, even from IRQ handlers, while the rest is already running.
; - tasks are regular subroutines but have to call yield() to pass control to the next task (round-robin)
; - you can kill a task (if you know it's id...)
; - when all tasks are finished the run() call will also return.
; - tasks can't push anything on the cpu stack before calling yield() - that will cause chaos.
;
; Difference from IRQ handlers:
; - you can have many tasks instead of only 2 (main program + irq handler)
; - it's not tied to any IRQ setup, and will run as fast as the tasks themselves allow
; - tasks fully control the switch to the next task; there is no preemptive switching
;
; TODO to make it actually even more useful, we probably have to:
; - return a unique value (pointer that you had to provide when adding the task to the list?)
; from yield() that the subroutine could use to access unique state,
; because right now a single task == a single subroutine; right now you cannot re-use a subroutine to run
; the same task multiple times for different things.
;
; USAGE:
; - call add(taskaddress) to add a new task. It returns the task id.
; - call run() to start executing all tasks until none are left.
; - in tasks: call yield() to pass control to the next task.
; - call kill(tasknumber) to kill a task by id.
; - call killall() to kill all tasks.
coroutines {
const ubyte MAX_TASKS = 64
uword[MAX_TASKS] tasklist
uword[MAX_TASKS] returnaddresses
ubyte active_task
sub add(uword taskaddress) -> ubyte {
; find the next empty slot in the tasklist and stick it there
; returns the task id of the new task, or 255 if there was no space for more tasks. 0 is a valid task id!
; also returns the success in the Carry flag (carry set=success, carry clear = task was not added)
sys.irqsafe_set_irqd()
for cx16.r0L in 0 to len(tasklist)-1 {
if tasklist[cx16.r0L] == 0 {
tasklist[cx16.r0L] = taskaddress
returnaddresses[cx16.r0L] = 0
sys.irqsafe_clear_irqd()
sys.set_carry()
return cx16.r0L
}
}
sys.irqsafe_clear_irqd()
; no space for new task
sys.clear_carry()
return 255
}
sub killall() {
; kill all existing tasks
sys.irqsafe_set_irqd()
for cx16.r0L in 0 to len(tasklist)-1 {
kill(cx16.r0L)
}
sys.irqsafe_clear_irqd()
}
sub run() {
for active_task in 0 to len(tasklist)-1 {
if tasklist[active_task]!=0 {
; activate the termination handler and start the first task
; note: cannot use pushw() because JSR doesn't push the return address in the same way
sys.push_returnaddress(&termination)
goto tasklist[active_task]
}
}
}
sub yield() {
; store the return address of the yielding task,
; and continue with the next one instead (round-robin)
uword task_start, task_continue
returnaddresses[active_task] = sys.popw()
resume_with_next_task:
if not next_task() {
void sys.popw() ; remove return to the termination handler
return ; exiting here will now actually return from the start() call back to the calling program :)
}
if task_continue==0 {
; fetch start address of next task.
; address on the stack must be pushed in reverse byte order
; also, subtract 1 from the start address because JSR pushes returnaddress minus 1
; note: cannot use pushw() because JSR doesn't push the return address in the same way
sys.push_returnaddress(task_start)
} else
sys.pushw(task_continue)
; returning from yield then continues with the next coroutine
sub next_task() -> bool {
; search through the task list for the next active task
sys.irqsafe_set_irqd()
repeat len(tasklist) {
active_task++
if active_task==len(returnaddresses)
active_task=0
task_start = tasklist[active_task]
if task_start!=0 {
task_continue = returnaddresses[active_task]
sys.irqsafe_clear_irqd()
return true
}
}
sys.irqsafe_clear_irqd()
return false ; no task
}
}
sub kill(ubyte tasknum) {
tasklist[tasknum] = 0
returnaddresses[tasknum] = 0
}
sub termination() {
; a task has terminated. wipe it from the list.
; this is an internal routine
kill(active_task)
; reactivate this termination handler
; note: cannot use pushw() because JSR doesn't push the return address in the same way
sys.push_returnaddress(&termination)
goto coroutines.yield.resume_with_next_task
}
}

View File

@ -1848,6 +1848,18 @@ save_SCRATCH_ZPWORD2 .word 0
}}
}
inline asmsub push_returnaddress(uword address @XY) {
%asm {{
; push like JSR would: address-1, MSB first then LSB
cpx #0
bne +
dey
+ dex
phy
phx
}}
}
inline asmsub pop() -> ubyte @A {
%asm {{
pla

View File

@ -300,6 +300,20 @@ save_SCRATCH_ZPWORD2 .word 0
}}
}
inline asmsub push_returnaddress(uword address @XY) {
%asm {{
; push like JSR would: address-1, MSB first then LSB
cpx #0
bne +
dey
+ dex
tya
pha
txa
pha
}}
}
inline asmsub pop() -> ubyte @A {
%asm {{
pla

View File

@ -465,6 +465,20 @@ save_SCRATCH_ZPWORD2 .word 0
}}
}
inline asmsub push_returnaddress(uword address @XY) {
%asm {{
; push like JSR would: address-1, MSB first then LSB
cpx #0
bne +
dey
+ dex
tya
pha
txa
pha
}}
}
inline asmsub pop() -> ubyte @A {
%asm {{
pla

View File

@ -0,0 +1,3 @@
coroutines {
; the VM doesn't support coroutines because the flow of control is radically different than how the 6502 CPU does it.
}

View File

@ -190,6 +190,14 @@ sys {
}}
}
sub push_returnaddress(uword w) {
; note: this actually doesn't do anything useful on the VM because the code execution doesn't use the simulated cpu stack
%ir {{
loadm.w r65535,sys.pushw.w
push.w r65535
}}
}
sub pop() -> ubyte {
; note: this *should* be inlined, however since the VM has separate program counter and value stacks, this also works
%ir {{

View File

@ -172,7 +172,6 @@ class TestCompilerOnExamplesBothC64andCx16: FunSpec({
listOf(
"animals",
"balls",
"coroutines",
"cube3d",
"cube3d-float",
"cube3d-gfx",
@ -182,6 +181,7 @@ class TestCompilerOnExamplesBothC64andCx16: FunSpec({
"maze",
"mandelbrot",
"mandelbrot-gfx",
"multitasking",
"numbergame",
"primes",
"queens",

View File

@ -353,6 +353,7 @@ main {
sub start() {
sys.push(11)
sys.pushw(2222)
sys.push_returnaddress(3333)
cx16.r2++
cx16.r1 = sys.popw()
cx16.r0L = sys.pop()
@ -363,6 +364,7 @@ main {
val assembly = assemblyFile.readText()
assembly shouldContain "inlined routine follows: push"
assembly shouldContain "inlined routine follows: pushw"
assembly shouldContain "inlined routine follows: push_returnaddress"
assembly shouldContain "inlined routine follows: pop"
assembly shouldContain "inlined routine follows: popw"
}

View File

@ -369,6 +369,19 @@ Read the `conv source code <https://github.com/irmen/prog8/tree/master/compiler/
to see what's in there.
coroutines (experimental)
-------------------------
Provides a system to make cooperative multitasking programs via coroutines.
A 'coroutine' is a subroutine whose execution you can pause and resume.
This library handles the voodoo for you to switch between such coroutines transparently,
so it can seem that your program is executing many subroutines at the same time.
API is experimental and may change or disappear in a future version.
Read the `coroutines source code <https://github.com/irmen/prog8/tree/master/compiler/res/prog8lib/coroutines.p8>`_
to see what's in there. And look at the ``multitasking`` example to see how it can be used.
cx16
----
This is available on *all targets*, it is always imported as part of syslib.
@ -1079,13 +1092,19 @@ sys (part of syslib)
``pushw (value)``
pushes a 16-bit word value on the CPU hardware stack. Low-level function that should normally not be used.
Don't assume anything about the order in which the bytes are pushed - popw will make sense of them again.
``push_returnaddress (address)``
pushes a 16 bit memory address on the CPU hardware stack in the same byte order as a JSR instruction would,
which means the next RTS instruction will jump to that address instead.you
You cannot use pushw() for this because the bytes pushed by JSR are different
``pop ()``
pops a byte value off the CPU hardware stack and returns it.
Low-level function that should normally not be used.
``popw ()``
pops a 16-bit word value off the CPU hardware stack and returns it.
pops a 16-bit word value off the CPU hardware stack that was pushed before by pushw, and returns it.
Low-level function that should normally not be used.

View File

@ -12,7 +12,7 @@ Future Things and Ideas
- support &, &< and &> on array elements from split word arrays too not just the array as a whole (to get rid of the error "&< is only valid on array variables"
and "cannot take the adress of a word element that is in a split-word array" and the TODOS "address of element of a split word array")
- fix leftover asmgen split word array todo's
- after that: fix leftover asmgen split word array todo's
- Kotlin: can we use inline value classes in certain spots?
- Improve the SublimeText syntax file for prog8, you can also install this for 'bat': https://github.com/sharkdp/bat?tab=readme-ov-file#adding-new-syntaxes--language-definitions
@ -72,6 +72,7 @@ IR/VM
Libraries
---------
- coroutines: make yield() return a configured uword so that a task subroutine can get reused for multiple different things
- monogfx: flood fill should be able to fill stippled (it could do this in the past? vm version does it?)
- Sorting module gnomesort_uw could be optimized more, rewrite in asm? Shellshort seems consistently faster even if most of the words are already sorted.
- Add split-word array sorting routines to sorting module?

View File

@ -1,140 +0,0 @@
; Cooperative multitasking / Coroutines
;
; Uses cpu stack return address juggling to cycle between the different tasks when they call yield().
; Super simplistic implementation here to just show the core idea.
;
; To make it actually useful, we probably have to:
; - make the list of tasks dynamic;
; - allow tasks to finish, allow new tasks to be added
; - return a unique value (pointer that you had to provide when adding the task to the list?)
; from yield() that the subroutine could use to access unique state,
; because right now a single task == a single subroutine; right now you cannot re-use a subroutine to run
; the same task multiple times for different things.
%import textio
main {
sub start() {
txt.print("cooperative multitasking / coroutines\n\n")
txt.print("here are 4 subroutines that each run\nan endless loop bouncing a digit around.")
coroutines.start()
}
}
coroutines {
uword[] tasklist = [&task1, &task2, &task3, &task4, &vsynctask]
sub start() {
goto tasklist[0]
}
uword[len(tasklist)] returnaddresses
ubyte active_task
sub yield() {
; store the return address of the yielding task,
; and continue with the next one instead (round-robin)
returnaddresses[active_task] = sys.popw()
active_task++
if active_task==len(returnaddresses)
active_task=0
cx16.r0 = returnaddresses[active_task]
if cx16.r0==0 {
; fetch start address of next task.
; address on the stack must be pushed in reverse byte order
; also, subtract 1 from the start address because JSR pushes returnaddress minus 1
cx16.r0 = tasklist[active_task]-1
sys.push(cx16.r0H)
sys.push(cx16.r0L)
} else
sys.pushw(cx16.r0)
; returning from yield then continues with the next coroutine
}
sub task1() {
const ubyte x = 5
ubyte y
repeat {
for y in 10 to 24 {
txt.setchr(x, y-1, sc:' ')
txt.setchr(x, y, sc:'1')
yield()
}
for y in 24 downto 10 {
txt.setchr(x, y+1, sc:' ')
txt.setchr(x, y, sc:'1')
yield()
}
}
; need infinite loop
}
sub task2() {
const ubyte x = 10
ubyte y
repeat {
for y in 5 to 18 {
txt.setchr(x, y-1, sc:' ')
txt.setchr(x, y, sc:'2')
yield()
}
for y in 18 downto 5 {
txt.setchr(x, y+1, sc:' ')
txt.setchr(x, y, sc:'2')
yield()
}
}
; need infinite loop
}
sub task3() {
ubyte x
const ubyte y = 10
repeat {
for x in 14 to 38 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'3')
yield()
}
for x in 38 downto 14 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'3')
yield()
}
}
; need infinite loop
}
sub task4() {
ubyte x
const ubyte y = 14
repeat {
for x in 15 to 30 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'4')
yield()
}
for x in 30 downto 15 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'4')
yield()
}
}
; need infinite loop
}
sub vsynctask() {
repeat {
sys.waitvsync()
sys.waitvsync()
yield()
}
; need infinite loop
}
}

126
examples/multitasking.p8 Normal file
View File

@ -0,0 +1,126 @@
; Cooperative Multitasking example.
; Can be compiled for different targets (except virtual).
%import coroutines
%import textio
%zeropage basicsafe
main {
sub start() {
txt.print("cooperative multitasking / coroutines\n\n")
txt.print("here are couple of routines that each\nrun a few loops bouncing a digit around.\n")
coroutines.killall()
void coroutines.add(&task1)
void coroutines.add(&task2)
void coroutines.add(&task3)
void coroutines.add(&task4)
void coroutines.add(&delaytask)
coroutines.run()
txt.print("we're all done!\n")
}
sub task1() {
const ubyte x = 5
ubyte y
repeat 3 {
for y in 10 to 24 {
txt.setchr(x, y-1, sc:' ')
txt.setchr(x, y, sc:'1')
coroutines.yield()
}
for y in 24 downto 10 {
txt.setchr(x, y+1, sc:' ')
txt.setchr(x, y, sc:'1')
coroutines.yield()
}
}
txt.setchr(x, 10, sc:' ')
}
sub task2() {
const ubyte x = 10
ubyte y
repeat 2 {
for y in 5 to 18 {
txt.setchr(x, y-1, sc:' ')
txt.setchr(x, y, sc:'2')
coroutines.yield()
}
for y in 18 downto 5 {
txt.setchr(x, y+1, sc:' ')
txt.setchr(x, y, sc:'2')
coroutines.yield()
}
}
txt.setchr(x, 5, sc:' ')
; add a new task dynamically
void coroutines.add(&task5)
}
sub task3() {
ubyte x
const ubyte y = 10
repeat 4 {
for x in 14 to 38 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'3')
coroutines.yield()
}
for x in 38 downto 14 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'3')
coroutines.yield()
}
}
txt.setchr(14, y, sc:' ')
}
sub task4() {
ubyte x
const ubyte y = 14
repeat 4 {
for x in 15 to 30 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'4')
coroutines.yield()
}
for x in 30 downto 15 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'4')
coroutines.yield()
}
}
txt.setchr(15, y, sc:' ')
}
sub task5() {
ubyte x
const ubyte y = 16
repeat 4 {
for x in 15 to 30 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'5')
coroutines.yield()
}
for x in 30 downto 15 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'5')
coroutines.yield()
}
}
txt.setchr(15, y, sc:' ')
}
sub delaytask() {
repeat 200 {
sys.waitvsync()
sys.waitvsync()
coroutines.yield()
}
}
}

View File

@ -1,21 +1,125 @@
%import coroutines
%import textio
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
uword[2] array1
cx16.r0--
array1[1] = $0122
txt.print_uwhex(array1[1], true)
txt.nl()
rol(array1[1])
txt.print_uwhex(array1[1], true)
txt.nl()
sys.set_carry()
ror(array1[1])
txt.print_uwhex(array1[1], true)
txt.nl()
txt.print("cooperative multitasking / coroutines\n\n")
txt.print("here are couple of routines that each\nrun a few loops bouncing a digit around.\n")
void coroutines.add(&task1)
void coroutines.add(&task2)
void coroutines.add(&task3)
void coroutines.add(&task4)
void coroutines.add(&vsynctask)
coroutines.run()
txt.print("we're all done!\n")
}
sub task1() {
const ubyte x = 5
ubyte y
repeat 3 {
for y in 10 to 24 {
txt.setchr(x, y-1, sc:' ')
txt.setchr(x, y, sc:'1')
coroutines.yield()
}
for y in 24 downto 10 {
txt.setchr(x, y+1, sc:' ')
txt.setchr(x, y, sc:'1')
coroutines.yield()
}
}
txt.setchr(x, 10, sc:' ')
}
sub task2() {
const ubyte x = 10
ubyte y
repeat 2 {
for y in 5 to 18 {
txt.setchr(x, y-1, sc:' ')
txt.setchr(x, y, sc:'2')
coroutines.yield()
}
for y in 18 downto 5 {
txt.setchr(x, y+1, sc:' ')
txt.setchr(x, y, sc:'2')
coroutines.yield()
}
}
txt.setchr(x, 5, sc:' ')
; add a new task dynamically
void coroutines.add(&task5)
}
sub task3() {
ubyte x
const ubyte y = 10
repeat 4 {
for x in 14 to 38 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'3')
coroutines.yield()
}
for x in 38 downto 14 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'3')
coroutines.yield()
}
}
txt.setchr(14, y, sc:' ')
}
sub task4() {
ubyte x
const ubyte y = 14
repeat 4 {
for x in 15 to 30 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'4')
coroutines.yield()
}
for x in 30 downto 15 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'4')
coroutines.yield()
}
}
txt.setchr(15, y, sc:' ')
}
sub task5() {
ubyte x
const ubyte y = 16
repeat 4 {
for x in 15 to 30 {
txt.setchr(x-1, y, sc:' ')
txt.setchr(x, y, sc:'5')
coroutines.yield()
}
for x in 30 downto 15 {
txt.setchr(x+1, y, sc:' ')
txt.setchr(x, y, sc:'5')
coroutines.yield()
}
}
txt.setchr(15, y, sc:' ')
}
sub vsynctask() {
repeat 200 {
sys.waitvsync()
sys.waitvsync()
coroutines.yield()
}
}
}