Skip to content

Commit dee631f

Browse files
authored
Merge pull request #750 from JiepengTan/pr_coro_env_check
Feat: Enhance SPX coroutine detection
2 parents 37b3569 + 6dfdd3e commit dee631f

5 files changed

Lines changed: 126 additions & 24 deletions

File tree

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ require (
99
golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd
1010
)
1111

12-
require golang.org/x/image v0.23.0 // indirect
12+
require (
13+
github.com/petermattis/goid v0.0.0-20250721140440-ea1c0173183e // indirect
14+
golang.org/x/image v0.23.0 // indirect
15+
)
1316

1417
replace (
1518
golang.org/x/image => golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
22
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
33
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
4+
github.com/petermattis/goid v0.0.0-20250721140440-ea1c0173183e h1:D0bJD+4O3G4izvrQUmzCL80zazlN7EwJ0PPDhpJWC/I=
5+
github.com/petermattis/goid v0.0.0-20250721140440-ea1c0173183e/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
46
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
57
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
68
github.com/realdream-ai/mathf v0.0.0-20250513071532-e55e1277a8c5 h1:KuC3mEHh8NPOaOR0acOW+6ISKL4S9iY3n102KE/tst0=

internal/coroutine/coro.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/goplus/spx/v2/internal/debug"
1515
"github.com/goplus/spx/v2/internal/engine/platform"
1616
"github.com/goplus/spx/v2/internal/time"
17+
"github.com/petermattis/goid"
1718
)
1819

1920
var (
@@ -85,6 +86,9 @@ type Coroutines struct {
8586
waitMutex sync.Mutex
8687
waitCond sync.Cond
8788
debug bool
89+
90+
// goroutineIDs tracks all goroutine IDs created by CreateAndStart
91+
goroutineIDs sync.Map // map[int64]bool
8892
}
8993

9094
const (
@@ -195,6 +199,10 @@ func (p *Coroutines) CreateAndStart(start bool, tobj ThreadObj, fn func(me Threa
195199

196200
id.cond = sync.NewCond(&id.mutex) // Initialize the thread's condition variable
197201
go func() {
202+
// Track this goroutine ID
203+
gid := goid.Get()
204+
p.goroutineIDs.Store(gid, true)
205+
198206
p.sema.Lock()
199207
p.setCurrent(id)
200208
defer func() {
@@ -203,6 +211,10 @@ func (p *Coroutines) CreateAndStart(start bool, tobj ThreadObj, fn func(me Threa
203211
p.mutex.Unlock()
204212
p.setWaitStatus(id, waitStatusDelete)
205213
p.sema.Unlock()
214+
215+
// Remove goroutine ID from tracking
216+
p.goroutineIDs.Delete(gid)
217+
206218
if e := recover(); e != nil {
207219
if e != ErrAbortThread {
208220
if p.onPanic != nil {
@@ -555,3 +567,11 @@ func (p *Coroutines) Update() {
555567
lastDebugUpdateStats = stats
556568

557569
}
570+
571+
// IsInCoroutine checks if the current execution environment is within
572+
// a coroutine created by (*Coroutines) CreateAndStart.
573+
func (p *Coroutines) IsInCoroutine() bool {
574+
currentGID := goid.Get()
575+
_, exists := p.goroutineIDs.Load(currentGID)
576+
return exists
577+
}

internal/engine/coro.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,18 @@ func GetGame() any {
1919
return pgame
2020
}
2121

22-
func IsSpxEnv() bool {
23-
return GetGame() != nil
22+
func IsInCoroutine() bool {
23+
if gco == nil {
24+
return false
25+
}
26+
return gco.IsInCoroutine()
27+
}
28+
29+
func GetCoroutineOwner() any {
30+
if IsInCoroutine() {
31+
return gco.Current().Obj
32+
}
33+
return nil
2434
}
2535

2636
func SetCoroutines(co *coroutine.Coroutines) {

pkg/spx/api.go

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,63 @@ import (
77
"github.com/goplus/spx/v2/internal/engine"
88
)
99

10-
// HasInit checks if the SPX engine has been initialized.
11-
func IsSpxEnv() bool {
12-
return engine.IsSpxEnv()
10+
func isSpxEnv() bool {
11+
return engine.GetGame() != nil
1312
}
1413

15-
// Executes the given function in a native Go goroutine from the current SPX coroutine context and waits for completion.
16-
// While waiting, it yields control via waitNextFrame to avoid blocking the SPX main thread.
17-
// Use this when you need to run potentially blocking Go operations (e.g., network requests, file I/O) from within SPX.
18-
func RunGoFromSpx(fn func()) {
14+
// IsInCoroutine checks whether the current execution context is within an SPX coroutine.
15+
// Returns true if running inside an SPX coroutine, false if running in a regular Go goroutine
16+
// or the main thread.
17+
//
18+
// This function is useful for determining the appropriate execution strategy when your code
19+
// needs to work in both SPX coroutine and regular Go contexts.
20+
//
21+
// Example:
22+
//
23+
// if spx.IsInCoroutine() {
24+
// // Use SPX-specific functions like Wait() or WaitNextFrame()
25+
// spx.Wait(1.0)
26+
// } else {
27+
// // Use regular Go functions
28+
// time.Sleep(time.Second)
29+
// }
30+
func IsInCoroutine() bool {
31+
return engine.IsInCoroutine()
32+
}
33+
34+
// ExecuteNative executes the given function in a native Go goroutine and waits for its completion.
35+
// While waiting, if it is in spx corotine, it yields control via WaitNextFrame to avoid blocking
36+
// the SPX main thread.
37+
//
38+
// This function is essential when you need to perform blocking Go operations (such as network requests,
39+
// file I/O, or system calls) from within an SPX coroutine without freezing the game engine.
40+
//
41+
// If called from outside an SPX coroutine context, the function executes synchronously.
42+
//
43+
// Example:
44+
//
45+
// spx.ExecuteNative(func() {
46+
// // Perform blocking network request
47+
// resp, err := http.Get("https://api.example.com/data")
48+
// if err != nil {
49+
// log.Printf("Error: %v", err)
50+
// return
51+
// }
52+
// defer resp.Body.Close()
53+
// // Process response...
54+
// })
55+
func ExecuteNative(fn func(owner any)) {
56+
// if not in spx coro, just run it
57+
if !engine.IsInCoroutine() {
58+
fn(nil)
59+
return
60+
}
61+
owner := engine.GetCoroutineOwner()
1962
done := &atomic.Bool{}
20-
// Run the actual logic in a go routine to avoid blocking
63+
// Execute the actual logic in a go routine to avoid blocking
2164
go func() {
2265
defer done.Store(true)
23-
fn()
66+
fn(owner)
2467
}()
2568
// Wait for completion while yielding control to SPX
2669
for !done.Load() {
@@ -31,11 +74,22 @@ func RunGoFromSpx(fn func()) {
3174
// Executes the given function in an SPX coroutine from the current Go goroutine context and waits for completion.
3275
// This function blocks until fn finishes execution.
3376
// Use this when you need to synchronously wait for the SPX coroutine to complete.
34-
func RunSpxFromGo(fn func()) {
77+
//
78+
// Parameters:
79+
//
80+
// owner - The SPX coroutine owner. When the owner is destroyed, all coroutines created by this owner will be properly stopped.
81+
// fn - The function to execute in the coroutine context.
82+
func Execute(owner any, fn func(owner any)) {
83+
// in spx coro, just run it
84+
if engine.IsInCoroutine() {
85+
fn(owner)
86+
return
87+
}
88+
3589
done := make(chan struct{}, 1)
36-
StartSpxCoro(func() {
90+
Go(owner, func(any) {
3791
defer close(done)
38-
fn()
92+
fn(owner)
3993
})
4094
<-done
4195
}
@@ -44,6 +98,12 @@ func RunSpxFromGo(fn func()) {
4498
// This is useful for running multiple operations in parallel without blocking
4599
// the main execution flow.
46100
//
101+
// Parameters:
102+
//
103+
// owner - The SPX coroutine owner. When the owner is destroyed, all coroutines created by this owner will be properly stopped.
104+
// If nil, the current coroutine's owner or the game instance will be used as the owner.
105+
// fn - The function to execute in the coroutine context.
106+
//
47107
// IMPORTANT: For long-running tasks, you MUST call Wait() or WaitNextFrame()
48108
// periodically to yield control back to the engine. Without these calls,
49109
// the main thread will wait indefinitely for the coroutine to complete,
@@ -56,7 +116,7 @@ func RunSpxFromGo(fn func()) {
56116
//
57117
// done := false
58118
// // ... do something
59-
// spx.GoAsync(func() {
119+
// spx.Go(owner, func(owner any) {
60120
// // ... do something
61121
// for !done {
62122
// // Do some work here
@@ -66,17 +126,24 @@ func RunSpxFromGo(fn func()) {
66126
//
67127
// Example of simple delayed execution:
68128
//
69-
// spx.GoAsync(func() {
129+
// spx.Go(owner, func(owner any) {
70130
// spx.Wait(2.0)
71131
// fmt.Println("Hello after 2 seconds")
72132
// })
73-
func StartSpxCoro(fn func()) {
74-
if IsSpxEnv() {
75-
engine.Go(engine.GetGame(), func() {
76-
fn()
133+
func Go(owner any, fn func(owner any)) {
134+
if isSpxEnv() {
135+
if owner == nil {
136+
if IsInCoroutine() {
137+
owner = engine.GetCoroutineOwner()
138+
} else {
139+
owner = engine.GetGame()
140+
}
141+
}
142+
engine.Go(owner, func() {
143+
fn(owner)
77144
})
78145
} else {
79-
go fn()
146+
go fn(owner)
80147
}
81148
}
82149

@@ -99,7 +166,7 @@ func StartSpxCoro(fn func()) {
99166
//
100167
// actualTime := spx.Wait(1.5) // Wait for 1.5 seconds
101168
func Wait(secs float64) float64 {
102-
if IsSpxEnv() {
169+
if engine.IsInCoroutine() {
103170
return engine.Wait(secs)
104171
} else {
105172
// Fallback to a regular wait
@@ -129,7 +196,7 @@ func Wait(secs float64) float64 {
129196
// }
130197
// }
131198
func WaitNextFrame() float64 {
132-
if IsSpxEnv() {
199+
if engine.IsInCoroutine() {
133200
return engine.WaitNextFrame()
134201
} else {
135202
// Fallback to a regular wait

0 commit comments

Comments
 (0)