Created
February 19, 2024 10:34
-
-
Save mymmrac/c2fc10b07289650d1e65c1a781299bb4 to your computer and use it in GitHub Desktop.
Go Mods [Ebiten + WASI]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
_ "embed" | |
"fmt" | |
"image/color" | |
"os" | |
"os/signal" | |
"sync" | |
"github.com/hajimehoshi/ebiten/v2" | |
"github.com/hajimehoshi/ebiten/v2/ebitenutil" | |
"github.com/hajimehoshi/ebiten/v2/inpututil" | |
"github.com/hajimehoshi/ebiten/v2/vector" | |
"github.com/tetratelabs/wazero" | |
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" | |
) | |
//go:embed mods/bin/m1.wasm | |
var mod1 []byte | |
//go:embed mods/bin/m2.wasm | |
var mod2 []byte | |
func main() { | |
game := &Game{} | |
if err := game.Init(); err != nil { | |
panic(err) | |
} | |
sigs := make(chan os.Signal, 1) | |
signal.Notify(sigs, os.Interrupt) | |
go func() { | |
if err := ebiten.RunGame(game); err != nil { | |
panic(err) | |
} | |
sigs <- os.Interrupt | |
}() | |
<-sigs | |
game.Shutdown() | |
} | |
type Mod struct { | |
updateReady chan struct{} | |
updateWaiter *sync.WaitGroup | |
updateCallWaiter *sync.WaitGroup | |
drawReady chan struct{} | |
drawWaiter *sync.WaitGroup | |
drawCallWaiter *sync.WaitGroup | |
modWaiter *sync.WaitGroup | |
} | |
type Game struct { | |
ctx context.Context | |
cancel context.CancelFunc | |
runtime wazero.Runtime | |
registerWaiter sync.WaitGroup | |
mods []Mod | |
updateLock sync.Mutex | |
drawLock sync.Mutex | |
screen *ebiten.Image | |
quit bool | |
x, y int32 | |
} | |
func (g *Game) Init() error { | |
ebiten.SetWindowClosingHandled(true) | |
g.ctx, g.cancel = context.WithCancel(context.Background()) | |
g.runtime = wazero.NewRuntimeWithConfig(g.ctx, wazero.NewRuntimeConfigInterpreter()) | |
wasi_snapshot_preview1.MustInstantiate(g.ctx, g.runtime) | |
_, err := g.runtime.NewHostModuleBuilder("env"). | |
NewFunctionBuilder().WithFunc(g.registerMod).Export("RegisterMod"). | |
NewFunctionBuilder().WithFunc(g.isClosed).Export("IsClosed"). | |
NewFunctionBuilder().WithFunc(g.beginUpdating).Export("BeginUpdating"). | |
NewFunctionBuilder().WithFunc(g.endUpdating).Export("EndUpdating"). | |
NewFunctionBuilder().WithFunc(g.isKeyJustPressed).Export("IsKeyJustPressed"). | |
NewFunctionBuilder().WithFunc(g.beginDrawing).Export("BeginDrawing"). | |
NewFunctionBuilder().WithFunc(g.endDrawing).Export("EndDrawing"). | |
NewFunctionBuilder().WithFunc(g.drawRect).Export("DrawRect"). | |
NewFunctionBuilder().WithFunc(g.drawCircle).Export("DrawCircle"). | |
Instantiate(g.ctx) | |
if err != nil { | |
return fmt.Errorf("env module: %w", err) | |
} | |
g.loadMod(mod1) | |
g.loadMod(mod2) | |
return nil | |
} | |
func (g *Game) loadMod(data []byte) { | |
mod := Mod{ | |
updateReady: make(chan struct{}, 1), | |
updateWaiter: &sync.WaitGroup{}, | |
updateCallWaiter: &sync.WaitGroup{}, | |
drawReady: make(chan struct{}, 1), | |
drawWaiter: &sync.WaitGroup{}, | |
drawCallWaiter: &sync.WaitGroup{}, | |
modWaiter: &sync.WaitGroup{}, | |
} | |
mod.modWaiter.Add(1) | |
g.registerWaiter.Add(1) | |
g.mods = append(g.mods, mod) | |
go func() { | |
defer mod.modWaiter.Done() | |
cfg := wazero.NewModuleConfig(). | |
WithStdout(os.Stdout). | |
WithStderr(os.Stderr). | |
WithSysWalltime(). | |
WithSysNanotime(). | |
WithSysNanosleep() | |
fmt.Println("Mod start") | |
m, modErr := g.runtime.InstantiateWithConfig(g.ctx, data, cfg) | |
if modErr != nil { | |
fmt.Println("Run mod:", modErr) | |
return | |
} | |
modCloseErr := m.Close(g.ctx) | |
if modCloseErr != nil { | |
fmt.Println("Stop mod:", modCloseErr) | |
} | |
fmt.Println("Mod end") | |
}() | |
g.registerWaiter.Wait() | |
} | |
func (g *Game) registerMod() int32 { | |
defer g.registerWaiter.Done() | |
return int32(len(g.mods)) - 1 | |
} | |
func (g *Game) isClosed() int32 { | |
if g.quit { | |
return 1 | |
} | |
return 0 | |
} | |
func (g *Game) beginUpdating(modIndex int32) int32 { | |
mod := g.mods[modIndex] | |
select { | |
case <-g.ctx.Done(): | |
return -1 | |
case <-mod.updateReady: | |
mod.updateCallWaiter.Done() | |
mod.updateWaiter.Add(1) | |
g.updateLock.Lock() | |
return 1 | |
default: | |
return 0 | |
} | |
} | |
func (g *Game) endUpdating(modIndex int32) { | |
mod := g.mods[modIndex] | |
g.updateLock.Unlock() | |
mod.updateWaiter.Done() | |
} | |
func (g *Game) isKeyJustPressed(key int32) int32 { | |
if inpututil.IsKeyJustPressed(ebiten.Key(key)) { | |
return 1 | |
} | |
return 0 | |
} | |
func (g *Game) beginDrawing(modIndex int32) int32 { | |
mod := g.mods[modIndex] | |
select { | |
case <-g.ctx.Done(): | |
return -1 | |
case <-mod.drawReady: | |
mod.drawCallWaiter.Done() | |
mod.drawWaiter.Add(1) | |
g.drawLock.Lock() | |
return 1 | |
default: | |
return 0 | |
} | |
} | |
func (g *Game) endDrawing(modIndex int32) { | |
mod := g.mods[modIndex] | |
g.drawLock.Unlock() | |
mod.drawWaiter.Done() | |
} | |
func (g *Game) drawRect(x, y, w, h int32, cr, cg, cb uint32) { | |
vector.DrawFilledRect( | |
g.screen, | |
float32(x), | |
float32(y), | |
float32(w), | |
float32(h), | |
color.RGBA{ | |
R: uint8(cr), | |
G: uint8(cg), | |
B: uint8(cb), | |
A: 0xFF, | |
}, | |
false, | |
) | |
} | |
func (g *Game) drawCircle(x, y, r int32, cr, cg, cb uint32) { | |
vector.DrawFilledCircle( | |
g.screen, | |
float32(x), | |
float32(y), | |
float32(r), | |
color.RGBA{ | |
R: uint8(cr), | |
G: uint8(cg), | |
B: uint8(cb), | |
A: 0xFF, | |
}, | |
false, | |
) | |
} | |
func (g *Game) Update() error { | |
if ebiten.IsWindowBeingClosed() || inpututil.IsKeyJustPressed(ebiten.KeyEscape) { | |
g.quit = true | |
} | |
select { | |
case <-g.ctx.Done(): | |
g.quit = true | |
default: | |
// Do nothing | |
} | |
if g.quit { | |
return ebiten.Termination | |
} | |
for _, mod := range g.mods { | |
mod.updateCallWaiter.Add(1) | |
mod.updateReady <- struct{}{} | |
mod.updateCallWaiter.Wait() | |
mod.updateWaiter.Wait() | |
} | |
g.x-- | |
g.y++ | |
if inpututil.IsKeyJustPressed(ebiten.KeyR) { | |
g.x = 0 | |
g.y = 0 | |
} | |
return nil | |
} | |
func (g *Game) Draw(screen *ebiten.Image) { | |
g.drawLock.Lock() | |
g.screen = screen | |
g.drawLock.Unlock() | |
for _, mod := range g.mods { | |
mod.drawCallWaiter.Add(1) | |
mod.drawReady <- struct{}{} | |
mod.drawCallWaiter.Wait() | |
mod.drawWaiter.Wait() | |
} | |
ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %.2f FPS: %.2f", ebiten.ActualTPS(), ebiten.ActualFPS())) | |
width := int32(screen.Bounds().Dx()) | |
g.drawRect(width+g.x-32, g.y, 32, 32, 0, 255, 0) | |
} | |
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { | |
return outsideWidth, outsideHeight | |
} | |
func (g *Game) Shutdown() { | |
g.cancel() | |
for _, mod := range g.mods { | |
mod.modWaiter.Wait() | |
} | |
if err := g.runtime.Close(g.ctx); err != nil { | |
fmt.Println("Close runtime:", err) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//go:generate env GOOS=wasip1 GOARCH=wasm go build -o ../bin/m1.wasm . | |
package main | |
import ( | |
"fmt" | |
"sync" | |
"time" | |
) | |
//go:wasmimport env RegisterMod | |
func RegisterMod() int32 | |
//go:wasmimport env IsClosed | |
func IsClosed() int32 | |
//go:wasmimport env BeginUpdating | |
func BeginUpdating(modIndex int32) int32 | |
//go:wasmimport env EndUpdating | |
func EndUpdating(modIndex int32) | |
//go:wasmimport env BeginDrawing | |
func BeginDrawing(modIndex int32) int32 | |
//go:wasmimport env EndDrawing | |
func EndDrawing(modIndex int32) | |
//go:wasmimport env DrawRect | |
func DrawRect(x, y, w, h int32, r, g, b uint32) | |
//go:wasmimport env IsKeyJustPressed | |
func IsKeyJustPressed(key int32) int32 | |
func main() { | |
modIndex := RegisterMod() | |
g := &Game{} | |
wg := sync.WaitGroup{} | |
wg.Add(1) | |
go func() { | |
defer wg.Done() | |
for IsClosed() != 1 { | |
var s int32 | |
for s == 0 { | |
s = BeginUpdating(modIndex) | |
if s == 0 { | |
time.Sleep(time.Millisecond) | |
} | |
} | |
if s != 1 { | |
return | |
} | |
g.Update() | |
EndUpdating(modIndex) | |
} | |
}() | |
wg.Add(1) | |
go func() { | |
defer wg.Done() | |
for IsClosed() != 1 { | |
var s int32 | |
for s == 0 { | |
s = BeginDrawing(modIndex) | |
if s == 0 { | |
time.Sleep(time.Millisecond) | |
} | |
} | |
if s != 1 { | |
return | |
} | |
g.Draw() | |
EndDrawing(modIndex) | |
} | |
}() | |
wg.Wait() | |
} | |
type Game struct { | |
ticks uint | |
tpsTime time.Time | |
frames uint | |
fpsTime time.Time | |
x, y int32 | |
} | |
func (g *Game) Update() { | |
now := time.Now() | |
if now.Sub(g.tpsTime) > time.Second { | |
g.tpsTime = now | |
fmt.Println("TPS:", g.ticks) | |
g.ticks = 0 | |
} | |
g.ticks++ | |
g.x++ | |
g.y++ | |
if IsKeyJustPressed(17) == 1 { | |
g.x = 0 | |
g.y = 0 | |
} | |
} | |
func (g *Game) Draw() { | |
now := time.Now() | |
if now.Sub(g.fpsTime) > time.Second { | |
g.fpsTime = now | |
fmt.Println("FPS:", g.frames) | |
g.frames = 0 | |
} | |
g.frames++ | |
DrawRect(g.x, g.y, 32, 32, 255, 0, 0) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//go:generate env GOOS=wasip1 GOARCH=wasm go build -o ../bin/m2.wasm . | |
package main | |
import ( | |
"sync" | |
"time" | |
) | |
//go:wasmimport env RegisterMod | |
func RegisterMod() int32 | |
//go:wasmimport env IsClosed | |
func IsClosed() int32 | |
//go:wasmimport env BeginUpdating | |
func BeginUpdating(modIndex int32) int32 | |
//go:wasmimport env EndUpdating | |
func EndUpdating(modIndex int32) | |
//go:wasmimport env BeginDrawing | |
func BeginDrawing(modIndex int32) int32 | |
//go:wasmimport env EndDrawing | |
func EndDrawing(modIndex int32) | |
//go:wasmimport env DrawRect | |
func DrawRect(x, y, w, h int32, cr, cg, cb uint32) | |
//go:wasmimport env DrawCircle | |
func DrawCircle(x, y, r int32, cr, cg, cb uint32) | |
//go:wasmimport env IsKeyJustPressed | |
func IsKeyJustPressed(key int32) int32 | |
func main() { | |
modIndex := RegisterMod() | |
g := &Game{ | |
r: 32, | |
} | |
wg := sync.WaitGroup{} | |
wg.Add(1) | |
go func() { | |
defer wg.Done() | |
for IsClosed() != 1 { | |
var s int32 | |
for s == 0 { | |
s = BeginUpdating(modIndex) | |
if s == 0 { | |
time.Sleep(time.Millisecond) | |
} | |
} | |
if s != 1 { | |
return | |
} | |
g.Update() | |
EndUpdating(modIndex) | |
} | |
}() | |
wg.Add(1) | |
go func() { | |
defer wg.Done() | |
for IsClosed() != 1 { | |
var s int32 | |
for s == 0 { | |
s = BeginDrawing(modIndex) | |
if s == 0 { | |
time.Sleep(time.Millisecond) | |
} | |
} | |
if s != 1 { | |
return | |
} | |
g.Draw() | |
EndDrawing(modIndex) | |
} | |
}() | |
wg.Wait() | |
} | |
type Game struct { | |
r int32 | |
} | |
func (g *Game) Update() { | |
if IsKeyJustPressed(85) == 1 { | |
g.r += 32 | |
g.r %= 256 | |
} | |
} | |
func (g *Game) Draw() { | |
DrawCircle(200, 200, g.r, 255, 255, 0) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment