Skip to content

Commit 7f045f9

Browse files
committed
WASM: Avoid frame copy, pause when not in focus
1 parent dd7f6e9 commit 7f045f9

File tree

2 files changed

+38
-22
lines changed

2 files changed

+38
-22
lines changed

cmd/dendy-wasm/main.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const (
2121
//go:embed nestest.nes
2222
var bootROM []byte
2323

24-
func createSystem(joy *input.Joystick, romData []byte) (*system.System, error) {
24+
func create(joy *input.Joystick, romData []byte) (*system.System, error) {
2525
rom, err := ines.NewFromBuffer(romData)
2626
if err != nil {
2727
return nil, fmt.Errorf("failed to load ROM: %v", err)
@@ -38,10 +38,10 @@ func createSystem(joy *input.Joystick, romData []byte) (*system.System, error) {
3838
}
3939

4040
func main() {
41-
log.SetFlags(0)
41+
log.SetFlags(0) // disable timestamps
4242
joystick := input.NewJoystick()
4343

44-
nes, err := createSystem(joystick, bootROM)
44+
nes, err := create(joystick, bootROM)
4545
if err != nil {
4646
log.Fatalf("[ERROR] failed to initialize: %v", err)
4747
}
@@ -53,43 +53,43 @@ func main() {
5353
)
5454

5555
js.Global().Set("runFrame", js.FuncOf(func(this js.Value, args []js.Value) any {
56+
buttons := args[0].Int()
5657
start := time.Now()
57-
frameBuf := args[0]
58-
buttons := args[1].Int()
58+
var framePtr uintptr
5959

6060
for {
6161
nes.Tick()
6262
if nes.FrameReady() {
6363
frame := nes.Frame()
64-
frameBytes := unsafe.Slice((*byte)(unsafe.Pointer(&frame[0])), len(frame)*4)
65-
js.CopyBytesToJS(frameBuf, frameBytes) // TODO: can we avoid copying here?
6664
joystick.SetButtons(uint8(buttons))
65+
framePtr = uintptr(unsafe.Pointer(&frame[0]))
6766
break
6867
}
6968
}
7069

7170
if debugFrameTime {
72-
frameTimeSum += time.Since(start)
71+
frameTime := time.Since(start)
72+
frameTimeSum += frameTime
7373
frameCount++
7474

7575
if frameCount%120 == 0 {
7676
runtime.ReadMemStats(&mem)
77-
elapsed := frameTimeSum / time.Duration(frameCount)
78-
log.Printf("[DEBUG] frame time: %s, memory: %d", elapsed, mem.Alloc)
77+
avgFrameTime := frameTimeSum / time.Duration(frameCount)
78+
log.Printf("[INFO] frame time: %v, memory: %d", avgFrameTime, mem.HeapAlloc)
7979
frameTimeSum = 0
8080
frameCount = 0
8181
}
8282
}
8383

84-
return nil
84+
return framePtr
8585
}))
8686

8787
js.Global().Set("uploadROM", js.FuncOf(func(this js.Value, args []js.Value) any {
8888
data := js.Global().Get("Uint8Array").New(args[0])
8989
romData := make([]byte, data.Length())
9090
js.CopyBytesToGo(romData, data)
9191

92-
nes2, err := createSystem(joystick, romData)
92+
nes2, err := create(joystick, romData)
9393
if err != nil {
9494
log.Printf("[ERROR] failed to initialize: %v", err)
9595
return false

web/main.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ const documentReady = new Promise((resolve) => {
66
}
77
});
88

9-
let wasm = null;
109
const go = new Go();
1110
const wasmReady = WebAssembly.instantiateStreaming(fetch("dendy.wasm"), go.importObject).then((result) => {
12-
wasm = result.instance;
13-
go.run(wasm);
11+
go.run(result.instance);
1412
});
1513

1614
Promise.all([wasmReady, documentReady]).then(() => {
@@ -28,8 +26,6 @@ Promise.all([wasmReady, documentReady]).then(() => {
2826

2927
let ctx = canvas.getContext("2d");
3028
ctx.imageSmoothingEnabled = false;
31-
32-
let imageData = ctx.createImageData(width, height);
3329
let buttonsPressed = 0;
3430

3531
const BUTTON_A = 1 << 0;
@@ -90,8 +86,28 @@ Promise.all([wasmReady, documentReady]).then(() => {
9086
});
9187
}
9288

93-
setInterval(() => {
94-
runFrame(imageData.data, buttonsPressed);
95-
ctx.putImageData(imageData, 0, 0);
96-
}, 1000 / targetFPS);
97-
});
89+
function isInFocus() {
90+
return document.hasFocus() && document.visibilityState === "visible";
91+
}
92+
93+
function gameLoop() {
94+
let nextFrame = () => {
95+
let start = performance.now();
96+
97+
if (isInFocus()) {
98+
let framePtr = runFrame(buttonsPressed);
99+
let memPtr = go._inst.exports.mem?.buffer || go._inst.exports.memory.buffer; // latter is for TinyGo
100+
let image = new ImageData(new Uint8ClampedArray(memPtr, framePtr, width * height * 4), width, height);
101+
ctx.putImageData(image, 0, 0);
102+
}
103+
104+
let elapsed = performance.now() - start;
105+
let nextTimeout = Math.max(0, (1000 / targetFPS) - elapsed);
106+
setTimeout(nextFrame, nextTimeout);
107+
};
108+
109+
nextFrame();
110+
}
111+
112+
gameLoop();
113+
});

0 commit comments

Comments
 (0)