Skip to content

Instantly share code, notes, and snippets.

@mymmrac
Created February 19, 2024 10:34
Show Gist options
  • Save mymmrac/c2fc10b07289650d1e65c1a781299bb4 to your computer and use it in GitHub Desktop.
Save mymmrac/c2fc10b07289650d1e65c1a781299bb4 to your computer and use it in GitHub Desktop.
Go Mods [Ebiten + WASI]
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)
}
}
//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)
}
//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