Initial copy from life 1 post

Just committing so I can check diffs and describe it later
post/wasm-gol-2
Ashelyn Dawn 1 year ago
parent 12d2b39b0a
commit 394b3642a0

@ -0,0 +1,30 @@
---
title: "Pure Wasm Life 2: Optimizing Webassembly and Canvas"
subtitle: You know I couldn't leave it alone
resources:
- wasm-life-2/controller.js
- wasm-life-2/game.wat
---
This post is part 2 of my Webassembly Game of Life series, read part 1
[here](/wasm-game-of-life-1).
<noscript>[If you enable Javascript, you'll see a game board here]</noscript>
<canvas
id="game"
width="800"
height="600"
data-pixelsize="1"
style="width:100%; aspect-ratio: 4/3; image-rendering: pixelated;"
/>
<p style="display: flex; align-items: flex-start; margin-top: 4px; height: 64px">
<span style="flex: 1" id="frameTimes"/>
<button id="reset">Reset</button>
</p>
<ScriptLoader
src="/wasm-life-2/controller.js"
wasm="/wasm-life-2/game.wat"
canvas="#game"
/>

@ -0,0 +1,146 @@
let gameState = {
running: false,
pixelSize: 0,
lastReported: null,
frameTimes: [],
frames: 0,
width: 0,
height: 0,
canvas: null,
ctx: null,
gameExports: null
}
const initialMessage = "Click the board above to start simulation"
export async function setup(params, wasmModule) {
const { canvas: canvasSelector } = params;
gameState.gameExports = wasmModule.exports
const canvas = gameState.canvas = document.querySelector(canvasSelector)
gameState.ctx = gameState.canvas.getContext("2d")
gameState.ctx.fillStyle = getComputedStyle(gameState.canvas).getPropertyValue('--foreground')
const pixelSize = gameState.pixelSize = parseInt(canvas.getAttribute('data-pixelsize') || '2')
gameState.width = Math.floor(parseInt(canvas.width) / pixelSize)
gameState.height = Math.floor(parseInt(canvas.height) / pixelSize)
initialize()
drawBoard()
const frameTimesElem = document.getElementById('frameTimes')
const resetButton = document.getElementById('reset')
frameTimesElem.innerText = initialMessage
gameState.canvas.addEventListener('click', () => {
if (!gameState.running) {
if (frameTimesElem.innerText === initialMessage)
frameTimesElem.innerText = 'Starting...'
gameState.running = true
frameLoop()
} else {
gameState.running = false;
gameState.frameTimes = []
gameState.lastReported = null
}
})
resetButton.addEventListener('click', () => {
gameState.running = false
gameState.frameTimes = []
gameState.lastReported = null
frameTimesElem.innerText = initialMessage
initialize()
drawBoard()
})
}
export async function onThemeChange() {
drawBoard()
gameState.ctx.fillStyle = getComputedStyle(gameState.canvas).getPropertyValue('--foreground')
}
export async function cleanup() {
gameState.running = false
}
function initialize() {
const { gameExports, width, height } = gameState
gameExports.initializeBoard(width, height)
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const filled = Math.random() > .5;
gameExports.setValueAtPosition(row, column, filled ? 1 : 0)
}
}
}
function frameLoop() {
const { gameExports, running, frameTimes } = gameState
if (!gameState.lastReported)
gameState.lastReported = performance.now()
if (!running) return
const beforeTick = performance.now()
gameExports.tick()
const afterTick = performance.now()
drawBoard()
const afterDraw = performance.now()
// Push raw frame time
frameTimes.push(afterDraw - beforeTick)
if (frameTimes.length >= 10) {
const averageFrame = (frameTimes.reduce((a, b) => a + b) / frameTimes.length).toFixed(2);
const lastReported = gameState.lastReported
const current = gameState.lastReported = performance.now()
const tenFrameTime = (current - lastReported) / 1000
const fps = Math.floor(10 / tenFrameTime).toFixed(0)
gameState.frameTimes = []
const pagesUsed = gameExports.getPagesUsed()
document.getElementById('frameTimes').innerHTML =
`Frames per second: ${fps}<br/>
Milliseconds per frame: ${averageFrame}<br/>
Memory allocated: ${pagesUsed} pages (${pagesUsed * 64} KiB)`
}
const tickMS = (afterTick - beforeTick).toFixed(3).padStart(7, ' ')
const drawMS = (afterDraw - afterTick).toFixed(3).padStart(7, ' ')
console.log(`tick took ${tickMS}ms, draw took ${drawMS}ms`)
requestAnimationFrame(frameLoop)
}
function drawBoard() {
const { gameExports, width, height, pixelSize, ctx, canvas } = gameState
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.beginPath()
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const alive = gameExports.getValueAtPosition(row, column)
if (!alive) continue
const x = column * pixelSize
const y = row * pixelSize
ctx.moveTo(x, y)
ctx.lineTo(x + pixelSize, y)
ctx.lineTo(x + pixelSize, y + pixelSize)
ctx.lineTo(x, y + pixelSize)
ctx.lineTo(x, y)
}
}
ctx.fill()
}

@ -0,0 +1,337 @@
(module
(memory (export "shared_memory") 1)
(global $boardWidth (mut i32) (i32.const 0))
(global $boardHeight (mut i32) (i32.const 0))
(global $boardBufferLength (mut i32) (i32.const 0))
(global $buffer0ptr (mut i32) (i32.const -1))
(global $buffer1ptr (mut i32) (i32.const -1))
(global $currentBuffer (mut i32) (i32.const -1))
(func (export "initializeBoard") (param $width i32) (param $height i32)
;; Store width and height for later
(global.set $boardWidth (local.get $width))
(global.set $boardHeight (local.get $height))
;; Compute total cells per board
local.get $width
local.get $height
i32.mul
global.set $boardBufferLength
;; Request enough memory for both boards
global.get $boardBufferLength
i32.const 2
i32.mul
call $growMemoryForBoards
;; Set pointer locations for our two boards
(global.set $buffer0ptr (i32.const 0))
(global.set $buffer1ptr (global.get $boardBufferLength))
;; Set current board
(global.set $currentBuffer (i32.const 0))
)
(func (export "getPagesUsed") (result i32)
memory.size
)
(func $growMemoryForBoards (param $totalBytes i32)
(local $targetPages i32)
;; figure out target page size
local.get $totalBytes
i32.const 1
i32.sub
i32.const 65536 ;; size of a page
i32.div_u
i32.const 1
i32.add
;; get difference
memory.size
i32.sub
;; grow
memory.grow
drop ;; ignore result, we're gonna crash anyways
;; perhaps we should have a way to report errors back to JS next time
)
(func $getBoardPtr (result i32)
global.get $currentBuffer
i32.eqz
if (result i32)
global.get $buffer0ptr
else
global.get $buffer1ptr
end
)
(func $swapBoards
global.get $currentBuffer
i32.eqz
if (result i32)
i32.const 1
else
i32.const 0
end
global.set $currentBuffer
)
(func $positionInRange (param $position i32) (param $min i32) (param $max i32) (result i32)
local.get $position
local.get $min
i32.lt_s
if
i32.const 0
return
end
local.get $position
local.get $max
i32.ge_s
if
i32.const 0
return
end
i32.const 1
return
)
(func $getIndexForPosition (param $row i32) (param $column i32) (result i32)
local.get $row
i32.const 0
global.get $boardHeight
call $positionInRange
local.get $column
i32.const 0
global.get $boardWidth
call $positionInRange
i32.and
i32.eqz
if
i32.const -1
return
end
global.get $boardWidth
local.get $row
i32.mul
local.get $column
i32.add
)
(func $getValueAtPosition (export "getValueAtPosition") (param $row i32) (param $column i32) (result i32)
(local $position i32)
local.get $row
local.get $column
call $getIndexForPosition
local.tee $position
i32.const 0
i32.lt_s
if
i32.const 0
return
end
local.get $position
call $getBoardPtr
i32.add
i32.load8_u
)
(func $setValueAtPosition (export "setValueAtPosition") (param $row i32) (param $column i32) (param $value i32)
(local $position i32)
local.get $row
local.get $column
call $getIndexForPosition
local.tee $position
i32.const 0
i32.lt_s
if
return
end
local.get $position
call $getBoardPtr
i32.add
local.get $value
i32.store8
)
(func $getNewValueAtPosition (param $row i32) (param $column i32) (result i32)
(local $count i32)
local.get $row
i32.const 1
i32.sub
local.get $column
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
call $getValueAtPosition
local.get $row
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
local.get $row
i32.const 1
i32.sub
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
i32.const 1
i32.sub
call $getValueAtPosition
local.get $row
i32.const 1
i32.sub
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
local.get $row
i32.const 1
i32.add
local.get $column
i32.const 1
i32.add
call $getValueAtPosition
i32.add
i32.add
i32.add
i32.add
i32.add
i32.add
i32.add
;; Exactly 3 neighbors
local.tee $count
i32.const 3
i32.eq
if
;; becomes or stays alive
i32.const 1
return
end
;; If currently dead
local.get $row
local.get $column
call $getValueAtPosition
i32.eqz
if
;; Stay dead
i32.const 0
return
end
;; 2 neighbors
local.get $count
i32.const 2
i32.eq
if
i32.const 1
return
end
i32.const 0
return
)
(func $tick (export "tick")
(local $row i32)
(local $column i32)
(local $value i32)
i32.const 0
local.set $row
loop $rows
;; start at the beginning of a row
i32.const 0
local.set $column
;; for every column in the row
loop $columns
;; compute new value
local.get $row
local.get $column
call $getNewValueAtPosition
local.set $value
;; place in next board
call $swapBoards
local.get $row
local.get $column
local.get $value
call $setValueAtPosition
call $swapBoards
;; increment column
local.get $column
i32.const 1
i32.add
local.tee $column
;; loop back if less than width
global.get $boardWidth
i32.lt_s
br_if $columns
end
;;increment row
local.get $row
i32.const 1
i32.add
local.tee $row
;; loop back if less than height
global.get $boardHeight
i32.lt_s
br_if $rows
end
;; swap to the new board
call $swapBoards
)
)
Loading…
Cancel
Save