Last active
February 6, 2025 10:57
-
-
Save peterhellberg/b165f24d4392de8519df2d2606549351 to your computer and use it in GitHub Desktop.
Rudimentary emulator for Firefly Zero written in Go
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" | |
"flag" | |
"fmt" | |
"image" | |
"image/color" | |
"image/draw" | |
"io" | |
"math/rand" | |
"os" | |
"github.com/hajimehoshi/ebiten/v2" | |
"github.com/hajimehoshi/ebiten/v2/ebitenutil" | |
"github.com/peterhellberg/gfx" | |
"github.com/tetratelabs/wazero" | |
"github.com/tetratelabs/wazero/api" | |
) | |
const ( | |
width = 240 | |
height = 160 | |
) | |
var palette = [17]color.RGBA{ | |
{0x00, 0x00, 0x00, 0x00}, | |
{0x1A, 0x1C, 0x2C, 0xFF}, | |
{0x5D, 0x27, 0x5D, 0xFF}, | |
{0xB1, 0x3E, 0x53, 0xFF}, | |
{0xEF, 0x7D, 0x57, 0xFF}, | |
{0xFF, 0xCD, 0x75, 0xFF}, | |
{0xA7, 0xF0, 0x70, 0xFF}, | |
{0x38, 0xB7, 0x64, 0xFF}, | |
{0x25, 0x71, 0x79, 0xFF}, | |
{0x29, 0x36, 0x6F, 0xFF}, | |
{0x3B, 0x5D, 0xC9, 0xFF}, | |
{0x41, 0xA6, 0xF6, 0xFF}, | |
{0x73, 0xEF, 0xF7, 0xFF}, | |
{0xF4, 0xF4, 0xF4, 0xFF}, | |
{0x94, 0xB0, 0xC2, 0xFF}, | |
{0x56, 0x6C, 0x86, 0xFF}, | |
{0x33, 0x3C, 0x57, 0xFF}, | |
} | |
func main() { | |
if err := run(context.Background(), os.Args); err != nil { | |
fmt.Fprintf(os.Stderr, "Error: %v\n", err) | |
os.Exit(1) | |
} | |
} | |
func run(ctx context.Context, args []string) error { | |
cfg, err := parse(args) | |
if err != nil { | |
return err | |
} | |
ebiten.SetWindowSize(width*cfg.scale, height*cfg.scale) | |
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) | |
ebiten.SetWindowTitle(cfg.title) | |
emu, err := newGame(ctx, cfg) | |
if err != nil { | |
return err | |
} | |
defer emu.runtime.Close(ctx) | |
return ebiten.RunGame(emu) | |
} | |
func newGame(ctx context.Context, cfg config) (*Game, error) { | |
g := &Game{ | |
config: cfg, | |
frame: newFrame(), | |
} | |
r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig()) | |
if _, err := r. | |
NewHostModuleBuilder("fs"). | |
NewFunctionBuilder().WithFunc(g.fsLoadfile).Export("load_file"). | |
NewFunctionBuilder().WithFunc(g.fsGetFileSize).Export("get_file_size"). | |
Instantiate(ctx); err != nil { | |
return nil, err | |
} | |
if _, err := r. | |
NewHostModuleBuilder("input"). | |
NewFunctionBuilder().WithFunc(g.inputReadPad).Export("read_pad"). | |
NewFunctionBuilder().WithFunc(g.inputReadButtons).Export("read_buttons"). | |
Instantiate(ctx); err != nil { | |
return nil, err | |
} | |
if _, err := r. | |
NewHostModuleBuilder("misc"). | |
NewFunctionBuilder().WithFunc(g.miscGetRandom).Export("get_random"). | |
NewFunctionBuilder().WithFunc(g.miscSetSeed).Export("set_seed"). | |
Instantiate(ctx); err != nil { | |
return nil, err | |
} | |
if _, err := r. | |
NewHostModuleBuilder("graphics"). | |
NewFunctionBuilder().WithFunc(g.graphicsClearScreen).Export("clear_screen"). | |
NewFunctionBuilder().WithFunc(g.graphicsSetColor).Export("set_color"). | |
NewFunctionBuilder().WithFunc(g.graphicsDrawLine).Export("draw_line"). | |
NewFunctionBuilder().WithFunc(g.graphicsDrawPoint).Export("draw_point"). | |
NewFunctionBuilder().WithFunc(g.graphicsDrawCircle).Export("draw_circle"). | |
NewFunctionBuilder().WithFunc(g.graphicsDrawText).Export("draw_text"). | |
NewFunctionBuilder().WithFunc(g.graphicsDrawImage).Export("draw_image"). | |
NewFunctionBuilder().WithFunc(g.graphicsDrawSubImage).Export("draw_sub_image"). | |
Instantiate(ctx); err != nil { | |
return nil, err | |
} | |
if _, err := r. | |
NewHostModuleBuilder("net"). | |
NewFunctionBuilder().WithFunc(g.netGetPeers).Export("get_peers"). | |
NewFunctionBuilder().WithFunc(g.netGetMe).Export("get_me"). | |
Instantiate(ctx); err != nil { | |
return nil, err | |
} | |
if _, err := r. | |
NewHostModuleBuilder("stats"). | |
NewFunctionBuilder().WithFunc(g.statsAddProgress).Export("add_progress"). | |
Instantiate(ctx); err != nil { | |
return nil, err | |
} | |
if _, err := r. | |
NewHostModuleBuilder("audio"). | |
NewFunctionBuilder().WithFunc(g.audioClear).Export("clear"). | |
NewFunctionBuilder().WithFunc(g.audioAddFile).Export("add_file"). | |
NewFunctionBuilder().WithFunc(g.audioAddGain).Export("add_gain"). | |
Instantiate(ctx); err != nil { | |
return nil, err | |
} | |
if _, err := r. | |
NewHostModuleBuilder("menu"). | |
NewFunctionBuilder().WithFunc(g.menuAddMenuItem).Export("add_menu_item"). | |
Instantiate(ctx); err != nil { | |
return nil, err | |
} | |
g.runtime = r | |
mod, err := g.runtime.InstantiateWithConfig(ctx, cfg.source, wazero. | |
NewModuleConfig(). | |
WithStdin(os.Stdin), | |
) | |
if err != nil { | |
return nil, err | |
} | |
g.module = mod | |
g.memory = g.module.Memory() | |
if boot := g.module.ExportedFunction("boot"); boot != nil { | |
if _, err := boot.Call(ctx); err != nil { | |
return g, err | |
} | |
} | |
return g, nil | |
} | |
type Game struct { | |
config config | |
frame *image.RGBA | |
runtime wazero.Runtime | |
module api.Module | |
memory api.Memory | |
buttons buttons | |
dpad dpad | |
texts []text | |
} | |
type dpad struct { | |
up bool | |
down bool | |
left bool | |
right bool | |
} | |
func newDPad() dpad { | |
return dpad{ | |
up: ebiten.IsKeyPressed(ebiten.KeyW), | |
down: ebiten.IsKeyPressed(ebiten.KeyS), | |
left: ebiten.IsKeyPressed(ebiten.KeyA), | |
right: ebiten.IsKeyPressed(ebiten.KeyD), | |
} | |
} | |
func (d dpad) state() int32 { | |
var v int32 | |
if d.up { | |
v += 800 & 0xFFFF | |
} | |
if d.down { | |
v -= 800 & 0xFFFF | |
} | |
if d.left { | |
v -= (800 << 16) & -0xFFFF | |
} | |
if d.right { | |
v += (800 << 16) & -0xFFFF | |
} | |
return v | |
} | |
type buttons struct { | |
a bool | |
b bool | |
x bool | |
y bool | |
m bool | |
} | |
func newButtons() buttons { | |
return buttons{ | |
a: ebiten.IsKeyPressed(ebiten.KeyH), | |
b: ebiten.IsKeyPressed(ebiten.KeyJ), | |
x: ebiten.IsKeyPressed(ebiten.KeyK), | |
y: ebiten.IsKeyPressed(ebiten.KeyL), | |
m: ebiten.IsKeyPressed(ebiten.KeyEnter), | |
} | |
} | |
func (b buttons) state() uint32 { | |
var v uint32 | |
if b.a { | |
v += 0b1 | |
} | |
if b.b { | |
v += 0b10 | |
} | |
if b.x { | |
v += 0b100 | |
} | |
if b.y { | |
v += 0b1000 | |
} | |
if b.m { | |
v += 0b10000 | |
} | |
return v | |
} | |
type text struct { | |
str string | |
x int | |
y int | |
} | |
func (g *Game) Update() error { | |
ctx := context.Background() | |
g.texts = []text{} | |
g.buttons = newButtons() | |
g.dpad = newDPad() | |
if update := g.module.ExportedFunction("update"); update != nil { | |
if _, err := update.Call(ctx); err != nil { | |
return err | |
} | |
} | |
return nil | |
} | |
func (g *Game) Draw(screen *ebiten.Image) { | |
ctx := context.Background() | |
if render := g.module.ExportedFunction("render"); render != nil { | |
render.Call(ctx) | |
} | |
screen.WritePixels(g.frame.Pix) | |
for _, t := range g.texts { | |
ebitenutil.DebugPrintAt(screen, t.str, t.x, t.y) | |
} | |
} | |
func (g *Game) Layout(_, _ int) (int, int) { | |
return width, height | |
} | |
func newFrame() *image.RGBA { | |
return image.NewRGBA(image.Rect(0, 0, width, height)) | |
} | |
type config struct { | |
scale int | |
title string | |
file string | |
source []byte | |
} | |
func parse(args []string) (config, error) { | |
var cfg config | |
if len(args) == 0 { | |
return cfg, fmt.Errorf("no arguments") | |
} | |
flags := flag.NewFlagSet(args[0], flag.ExitOnError) | |
flags.IntVar(&cfg.scale, "scale", 3, "Scale to render in [1-5]") | |
if err := flags.Parse(args[1:]); err != nil { | |
return cfg, err | |
} | |
if cfg.scale < 1 || cfg.scale > 5 { | |
return cfg, fmt.Errorf("scale must be a positive number between 1-5") | |
} | |
if rest := flags.Args(); len(rest) > 0 { | |
cfg.file = rest[0] | |
} | |
if cfg.file == "" { | |
return cfg, fmt.Errorf("no .wasm file specified") | |
} | |
source, err := os.ReadFile(cfg.file) | |
if err != nil { | |
return cfg, err | |
} | |
cfg.source = source | |
cfg.title = fmt.Sprintf("Firefly Zero Emulator in Go: %s", cfg.file) | |
return cfg, nil | |
} | |
func pal(c int32) color.RGBA { | |
return palette[c] | |
} | |
func fill(m draw.Image, c color.Color) { | |
draw.Draw(m, m.Bounds(), &image.Uniform{c}, image.Point{}, draw.Src) | |
} | |
// -- GRAPHICS -- // | |
// DONE: func clearScreen(c int32) | |
func (g *Game) graphicsClearScreen(c int32) { | |
fmt.Fprintln(io.Discard, "graphics.clear_screen", c) | |
fill(g.frame, pal(c)) | |
} | |
// MOCK: func setColor(c, r, g, b int32) | |
func (g *Game) graphicsSetColor(c, R, G, B int32) { | |
if c > 0 && c < 17 { | |
palette[c] = color.RGBA{ | |
uint8(R), uint8(G), uint8(B), 0xFF, | |
} | |
} | |
} | |
// DONE: func drawPoint(x, y, c int32) | |
func (g *Game) graphicsDrawPoint(x, y, c int32) { | |
fmt.Fprintln(os.Stderr, "graphics.draw_point", x, y, c) | |
gfx.Set(g.frame, int(x), int(y), pal(c)) | |
} | |
func (g *Game) graphicsDrawLine(x1, y1, x2, y2, c, sw int32) { | |
fmt.Fprintln(io.Discard, "graphics.draw_line", x1, y1, x2, y1, c, sw) | |
if sw == 0 { | |
sw = 1 | |
} | |
polyline := gfx.NewPolyline( | |
gfx.Polygon{ | |
gfx.IV(int(x1), int(y1)), | |
gfx.IV(int(x2), int(y2)), | |
}, 0) | |
gfx.DrawPolyline(g.frame, polyline, float64(sw/2), pal(c)) | |
} | |
// TODO: func drawRect(x, y, w, h, fc, sc, sw int32) | |
// TODO: func drawRoundedRect(x, y, w, h, cw, ch, fc, sc, sw int32) | |
// MOCK: func drawCircle(x, y, d, fc, sc, sw int32) | |
func (g *Game) graphicsDrawCircle(x, y, d, fc, sc, sw int32) { | |
fmt.Fprintln(io.Discard, "graphics.draw_circle", x, y, d, fc, sc, sw) | |
r := d / 2 | |
ix := int(x + r) | |
iy := int(y + r) | |
ir := int(r) | |
if fc != 0 { | |
gfx.DrawIntFilledCircle(g.frame, ix, iy, ir, pal(fc)) | |
} | |
if sc != 0 { | |
gfx.DrawIntCircle(g.frame, ix, iy, ir, pal(sc)) | |
} | |
} | |
// TODO: func drawEllipse(x, y, w, h, fc, sc, sw int32) | |
// TODO: func drawTriangle(x1, y1, x2, y2, x3, y3, fc, sc, sw int32) | |
// TODO: func drawArc(x, y, d int32, ast, asw float32, fc, sc, sw int32) | |
// TODO: func drawSector(x, y, d int32, ast, asw float32, fc, sc, sw int32) | |
// MOCK: func drawText(textPtr unsafe.Pointer, textLen uint32, fontPtr unsafe.Pointer, fontLen uint32, x, y, color int32) | |
func (g *Game) graphicsDrawText(textPtr, textLen, fontPtr, fontLen uint32, x, y, c int32) { | |
fmt.Fprintln(io.Discard, "graphics.draw_text", textPtr, textLen, fontPtr, fontLen, x, y, c) | |
if data, ok := g.memory.Read(textPtr, textLen); ok { | |
g.texts = append(g.texts, text{str: string(data), x: int(x), y: int(y)}) | |
} | |
} | |
// MOCK: func drawImage(ptr unsafe.Pointer, len uint32, x, y int32) | |
func (g *Game) graphicsDrawImage(ptr, ptrLen uint32, x, y int32) { | |
fmt.Fprintln(io.Discard, "graphics.draw_image", ptr, ptrLen, x, y) | |
gfx.DrawIntRectangle(g.frame, int(x), int(y), 10, 10, pal(1)) | |
} | |
// MOCK: func drawSubImage(ptr unsafe.Pointer, len uint32, x, y, subX, subY int32, subWidth, subHeight uint32) | |
func (g *Game) graphicsDrawSubImage(ptr, ptrLen uint32, x, y, subX, subY int32, subWidth, subHeight uint32) { | |
fmt.Fprintln(os.Stderr, "graphics.draw_sub_image", ptr, ptrLen, x, y, subX, subY, subWidth, subHeight) | |
} | |
// TODO: func setCanvas(ptr unsafe.Pointer, len uint32) | |
// TODO: func unsetCanvas() | |
// -- INPUT -- // | |
// MOCK: func readPad(player uint32) int32 | |
func (g *Game) inputReadPad(player uint32) int32 { | |
fmt.Fprintln(io.Discard, "input.read_pad", player) | |
return g.dpad.state() | |
} | |
// MOCK: func readButtons(player uint32) uint32 | |
func (g *Game) inputReadButtons(player uint32) uint32 { | |
fmt.Fprintln(io.Discard, "input.read_buttons", player) | |
return g.buttons.state() | |
} | |
// -- FS -- // | |
// MOCK: func getFileSize(pathPtr unsafe.Pointer, pathLen uint32) uint32 | |
func (g *Game) fsGetFileSize(pathPtr, pathLen uint32) uint32 { | |
fmt.Fprintln(io.Discard, "fs.get_file_size", pathPtr, pathLen) | |
return 0 | |
} | |
// MOCK: func loadFile(pathPtr unsafe.Pointer, pathLen uint32, bufPtr unsafe.Pointer, bufLen uint32) uint32 | |
func (g *Game) fsLoadfile(pathPtr, pathLen, bufPtr, bufLen uint32) uint32 { | |
fmt.Fprintln(io.Discard, "fs.load_file", pathPtr, pathLen, bufPtr, bufLen) | |
return 0 | |
} | |
// TODO: func dumpFile(pathPtr unsafe.Pointer, pathLen uint32, bufPtr unsafe.Pointer, bufLen uint32) uint32 | |
// TODO: func removeFile(pathPtr unsafe.Pointer, pathLen uint32) uint32 | |
// -- NET -- // | |
// MOCK: func getMe() uint32 | |
func (g *Game) netGetMe() uint32 { | |
fmt.Fprintln(os.Stderr, "net.get_me") | |
return 1 | |
} | |
// MOCK: func getPeers() uint32 | |
func (g *Game) netGetPeers() uint32 { | |
fmt.Fprintln(io.Discard, "net.get_peers") | |
return 2 | |
} | |
// TODO: func saveStash(peerID uint32, bufPtr unsafe.Pointer, bufLen uint32) | |
// TODO: func loadStash(peerID uint32, bufPtr unsafe.Pointer, bufLen uint32) uint32 | |
// -- STATS -- // | |
// MOCK: func addProgress(peerID, badgeID uint32, val int32) uint32 | |
func (g *Game) statsAddProgress(peerID, badgeID uint32, val int32) uint32 { | |
fmt.Fprintln(io.Discard, "stats.add_progress", peerID, badgeID, val) | |
return 0 | |
} | |
// TODO: func addScore(peerID, boardID uint32, val int32) int32 | |
// -- MISC -- // | |
// TODO: func logDebug(ptr unsafe.Pointer, len uint32) | |
// TODO: func logError(ptr unsafe.Pointer, len uint32) | |
// DONE: func setSeed(seed uint32) | |
func (g *Game) miscSetSeed(seed uint32) { | |
fmt.Fprintln(os.Stderr, "misc.set_seed", seed) | |
rand.Seed(int64(seed)) | |
} | |
// DONE: func getRandom() uint32 | |
func (g *Game) miscGetRandom() uint32 { | |
fmt.Fprintln(io.Discard, "misc.get_random") | |
return rand.Uint32() | |
} | |
// TODO: func restart() | |
// TODO: func quit() | |
// -- AUDIO -- // | |
// TODO: func addSine(parentID uint32, freq float32, phase float32) uint32 | |
// TODO: func addSquare(parentID uint32, freq float32, phase float32) uint32 | |
// TODO: func addSawtooth(parentID uint32, freq float32, phase float32) uint32 | |
// TODO: func addTriangle(parentID uint32, freq float32, phase float32) uint32 | |
// TODO: func addNoise(parentID uint32, seed int32) uint32 | |
// TODO: func addEmpty(parentID uint32) uint32 | |
// TODO: func addZero(parentID uint32) uint32 | |
// MOCK: func addFile(parentID uint32, ptr unsafe.Pointer, len uint32) uint32 | |
func (g *Game) audioAddFile(parentID, ptr, ptrLen uint32) uint32 { | |
fmt.Fprintln(os.Stderr, "audio.add_file", parentID, ptr, ptrLen) | |
return 7 | |
} | |
// TODO: func addMix(parentID uint32) uint32 | |
// TODO: func addAllForOne(parentID uint32) uint32 | |
// MOCK: func addGain(parentID uint32, lvl float32) uint32 | |
func (g *Game) audioAddGain(parentID uint32, lvl float32) uint32 { | |
fmt.Fprintln(os.Stderr, "audio.add_gain", parentID, lvl) | |
return 0 | |
} | |
// TODO: func addLoop(parentID uint32) uint32 | |
// TODO: func addConcat(parentID uint32) uint32 | |
// TODO: func addPan(parentID uint32, lvl float32) uint32 | |
// TODO: func addMute(parentID uint32) uint32 | |
// TODO: func addPause(parentID uint32) uint32 | |
// TODO: func addTrackPosition(parentID uint32) uint32 | |
// TODO: func addLowPass(parentID uint32, freq float32, q float32) uint32 | |
// TODO: func addHighPass(parentID uint32, freq float32, q float32) uint32 | |
// TODO: func addTakeLeft(parentID uint32) uint32 | |
// TODO: func addTakeRight(parentID uint32) uint32 | |
// TODO: func addSwap(parentID uint32) uint32 | |
// TODO: func addClip(parentID uint32, low float32, high float32) uint32 | |
// TODO: func modLinear(nodeID uint32, param uint32, start float32, end float32, startAt uint32, endAt uint32) | |
// TODO: func modHold(nodeID uint32, param uint32, v1 float32, v2 float32, time uint32) | |
// TODO: func modSine(nodeID uint32, param uint32, freq float32, low float32, high float32) | |
// TODO: func reset(nodeID uint32) | |
// TODO: func resetAll(nodeID uint32) | |
// MOCK: func clear(nodeID uint32) | |
func (g *Game) audioClear(nodeID uint32) { | |
fmt.Fprintln(os.Stderr, "audio.clear", nodeID) | |
} | |
// -- MENU -- // | |
func (g *Game) menuAddMenuItem(a, b, c int32) { | |
fmt.Fprintln(os.Stderr, "menu.add_menu_item", a, b, c) | |
} | |
// -- SUDO -- // | |
// TODO: func listDirsBufSize(pathPtr unsafe.Pointer, pathLen uint32) uint32 | |
// TODO: func listDirs(pathPtr unsafe.Pointer, pathLen uint32, bufPtr unsafe.Pointer, bufLen uint32) uint32 | |
// TODO: func runApp(authorPtr unsafe.Pointer, authorLen uint32, appPtr unsafe.Pointer, appLen uint32) | |
// TODO: func loadFile(pathPtr unsafe.Pointer, pathLen uint32, bufPtr unsafe.Pointer, bufLen uint32) uint32 | |
// TODO: func getFileSize(pathPtr unsafe.Pointer, pathLen uint32) uint32 |
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
module experiments/ff-emu | |
go 1.23.5 | |
require ( | |
github.com/hajimehoshi/ebiten/v2 v2.8.6 | |
github.com/peterhellberg/gfx v0.0.0-20240717094052-4fa835cea5a4 | |
github.com/tetratelabs/wazero v1.8.2 | |
) | |
require ( | |
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee // indirect | |
github.com/ebitengine/hideconsole v1.0.0 // indirect | |
github.com/ebitengine/purego v0.8.2 // indirect | |
github.com/jezek/xgb v1.1.1 // indirect | |
golang.org/x/image v0.24.0 // indirect | |
golang.org/x/sync v0.11.0 // indirect | |
golang.org/x/sys v0.30.0 // indirect | |
) |
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
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee h1:YoNt0DHeZ92kjR78SfyUn1yEf7KnBypOFlFZO14cJ6w= | |
github.com/ebitengine/gomobile v0.0.0-20241016134836-cc2e38a7c0ee/go.mod h1:ZDIonJlTRW7gahIn5dEXZtN4cM8Qwtlduob8cOCflmg= | |
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= | |
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= | |
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= | |
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= | |
github.com/hajimehoshi/ebiten/v2 v2.8.6 h1:Dkd/sYI0TYyZRCE7GVxV59XC+WCi2BbGAbIBjXeVC1U= | |
github.com/hajimehoshi/ebiten/v2 v2.8.6/go.mod h1:cCQ3np7rdmaJa1ZnvslraVlpxNb3wCjEnAP1LHNyXNA= | |
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= | |
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= | |
github.com/peterhellberg/gfx v0.0.0-20240717094052-4fa835cea5a4 h1:Iv17i2mukPLwXTftiwj1bzOamqw1UVXm4vHGxKxi+1U= | |
github.com/peterhellberg/gfx v0.0.0-20240717094052-4fa835cea5a4/go.mod h1:ipo3f7y1+RHNR32fj+4TETou6OF4VXvJBhb9rwncKuw= | |
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= | |
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= | |
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= | |
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= | |
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= | |
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | |
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= | |
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Progress of the emulation