Add game of life post
parent
c42e58a54d
commit
fb972bc222
@ -0,0 +1,459 @@
|
|||||||
|
---
|
||||||
|
title: Pure Wasm Game of Life
|
||||||
|
subtitle: Because I'm truly a pedant at heart
|
||||||
|
script: wasm-life-1/controller.js
|
||||||
|
wasm: wasm-life-1/game.wat
|
||||||
|
unlisted: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Lately in doing research on WebAssembly I've been lookig around for examples
|
||||||
|
of things implemented in it, and I've come across several blog posts based on
|
||||||
|
the fantastic *Rust And WebAssembly* tutorial for
|
||||||
|
[Conway's Game of Life](https://rustwasm.github.io/docs/book/game-of-life/introduction.html).
|
||||||
|
|
||||||
|
And I mean no shade towards the folks who wrote those, but I feel it's mildly
|
||||||
|
disingenuous to say that those are "Conway's Game of Life in WebAssembly" when
|
||||||
|
the actual game code was written entirely in Rust. WebAssembly is in that case
|
||||||
|
really no more than a compilation target, not actually the language used.
|
||||||
|
|
||||||
|
So of course I knew what I had to do . . .
|
||||||
|
|
||||||
|
Welcome to Conway's Game of Life, *actually implemented* in WebAssembly:
|
||||||
|
|
||||||
|
<canvas
|
||||||
|
id="game"
|
||||||
|
width="800"
|
||||||
|
height="600"
|
||||||
|
data-pixelsize="3"
|
||||||
|
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>
|
||||||
|
|
||||||
|
What you're looking at here is Conway's Game of Life where ***all of the game code***
|
||||||
|
is written in pure, raw, not-a-compiler-in-sight WebAssembly. Its performance is
|
||||||
|
pretty comparable to the Rust versions, and I'm glad to say the code for it isn't
|
||||||
|
even that much of a mess!
|
||||||
|
|
||||||
|
Let's dive into how it works together, shall we?
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
As with most things in wasm we need to decide ahead of time what we are going
|
||||||
|
to implement directly in it, and what things are going to stay in the Javascript
|
||||||
|
part. I went with implementing the core game logic in wasm, leaving the initialization
|
||||||
|
and display code in JS.
|
||||||
|
|
||||||
|
I chose the first because I didn't want to have to deal with getting a float
|
||||||
|
value back from `Math.random()` in wasm, and the second because DOM manipulation,
|
||||||
|
canvas, and WebGL are all a bit of a pain to do manually from WebAssembly.
|
||||||
|
|
||||||
|
For convenience in this implementation I am using a byte per cell. Packing a bit
|
||||||
|
per cell into less memory space would be great, but I'm trying to keep it
|
||||||
|
*relatively* simple at first. I'm planning to revisit that in a later post though.
|
||||||
|
|
||||||
|
## Code samples
|
||||||
|
|
||||||
|
For now, let's look at a few selections from the code (links to full source will be
|
||||||
|
at the [bottom of the page](#resources)).
|
||||||
|
|
||||||
|
### Globals and Initialization
|
||||||
|
|
||||||
|
```wasm
|
||||||
|
(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))
|
||||||
|
```
|
||||||
|
|
||||||
|
I start the wasm module out with 1 page of memory (64 KiB), and define global
|
||||||
|
variables for the board dimensions, the length of each board buffer (in bytes),
|
||||||
|
the locations of each buffer, and which one is currently selected.
|
||||||
|
|
||||||
|
You can see that each of these gets initialized in the next function:
|
||||||
|
|
||||||
|
```wasm
|
||||||
|
(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))
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In the case that `$growMemoryForBoards` fails it will crash the WebAssembly
|
||||||
|
module, but considering I don't have a backup plan for how to make do with
|
||||||
|
less memory, that's acceptable to me.
|
||||||
|
|
||||||
|
### Manipulating the board
|
||||||
|
|
||||||
|
Next let's check out some of the basic board manipulation functions that our
|
||||||
|
Javascript code calls during initialization and display:
|
||||||
|
|
||||||
|
```wasm
|
||||||
|
(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
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see both rely on another function called `$getIndexForPosition`, check
|
||||||
|
its return value to make sure it didn't give -1, and then add that position to
|
||||||
|
the current board pointer. Not too bad so far!
|
||||||
|
|
||||||
|
That helper function `$getIndexForPosition` is also relatively simple:
|
||||||
|
|
||||||
|
```wasm
|
||||||
|
(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
|
||||||
|
)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
It again does some basic bounds checking, then some math with the board with,
|
||||||
|
row and column. Generally this all matches so far to how you might implement
|
||||||
|
this in any other language.
|
||||||
|
|
||||||
|
### Updating the board
|
||||||
|
|
||||||
|
Okay so this is where stuff starts to get a bit messy. WebAssembly *ostensibly*
|
||||||
|
has loops, but they're really more just a conditional jump. So the main function
|
||||||
|
for updating the board (which has to iterate through every position) gets to
|
||||||
|
be a bit verbose:
|
||||||
|
|
||||||
|
```wasm
|
||||||
|
(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 heeight
|
||||||
|
global.get $boardHeight
|
||||||
|
i32.lt_s
|
||||||
|
br_if $rows
|
||||||
|
end
|
||||||
|
|
||||||
|
;; swap to the new board
|
||||||
|
call $swapBoards
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `$swapBoards` function here is not that important to look at, it just
|
||||||
|
changes the current board flag so that `$getBoardPtr` returns the correct one.
|
||||||
|
I am kind of annoyed that I have to swap the board back and forth all the time,
|
||||||
|
but we'll see if that becomes an issue later.
|
||||||
|
|
||||||
|
But what's this `$getNewValueAtPosition` function? Let's have a look at that!
|
||||||
|
|
||||||
|
. . . prepare yourself, this one's a doozy.
|
||||||
|
|
||||||
|
```wasm
|
||||||
|
(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
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is (effectively) an unrolled loop. I could make this code shorter
|
||||||
|
but un-unrolling my loop, but I couldn't find a way to do that which didn't
|
||||||
|
immediately result in more instructions being run overall, so *for the moment*
|
||||||
|
I'm leaving it like this.
|
||||||
|
|
||||||
|
But once you know what each chunk is doing, yeah it's pretty simple! Each of the
|
||||||
|
`$getValueAtPosition` calls adds either a 0 or a 1 to the stack, and then we add
|
||||||
|
all of those up, store it in a variable, and check it against our various possible
|
||||||
|
outcomes.
|
||||||
|
|
||||||
|
Not that bad really, it's just rather verbose.
|
||||||
|
|
||||||
|
### And the glue
|
||||||
|
|
||||||
|
Lastly let's look at some of the JS that ties this together. I'm not going to
|
||||||
|
look that closely at the bit that loads the WebAssembly and initializes the
|
||||||
|
module - I assume most ~~(sane)~~ folks are using a bindings generator or bundler
|
||||||
|
or something else that does that for them. But let's look at the board initialization
|
||||||
|
and drawing code.
|
||||||
|
|
||||||
|
Starting with the board initialization, you can see it's rather short:
|
||||||
|
|
||||||
|
```js
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Oh the pleasures of a high-level language - the conciseness is just lovely isn't it?
|
||||||
|
And hopefully you can see why I didn't want to import `Math.random()` into my
|
||||||
|
WebAssembly - then I'd have to deal with floats, and more iteration, and it'd just
|
||||||
|
not be fun.
|
||||||
|
|
||||||
|
Okay now for the drawing code:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function drawBoard() {
|
||||||
|
const { gameExports, width, height, pixelSize, ctx, canvas } = gameState
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
ctx.fillStyle = 'currentColor'
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a pretty simple use of the `<canvas>` element, I think maybe in the
|
||||||
|
future if I want to optimize this I'd probably look into only updating the
|
||||||
|
changed cells or something like that, so that it doesn't need to redraw
|
||||||
|
the *entire* board each time. But on anything up to about a 400x300 grid
|
||||||
|
this was staying at roughly 5ms per frame on my machine, which should be
|
||||||
|
suitable for keeping about 60 frames per second - particularly if I can
|
||||||
|
optimize the board update function a bit as well.
|
@ -0,0 +1,142 @@
|
|||||||
|
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(wasmModule) {
|
||||||
|
gameState.gameExports = wasmModule.exports
|
||||||
|
|
||||||
|
const canvas = gameState.canvas = document.querySelector('#game')
|
||||||
|
gameState.ctx = gameState.canvas.getContext("2d")
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)`
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`tick took ${afterTick - beforeTick}ms`)
|
||||||
|
console.log(`draw took ${afterDraw - afterTick}ms`)
|
||||||
|
|
||||||
|
requestAnimationFrame(frameLoop)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBoard() {
|
||||||
|
const { gameExports, width, height, pixelSize, ctx, canvas } = gameState
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
ctx.fillStyle = 'currentColor'
|
||||||
|
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 heeight
|
||||||
|
global.get $boardHeight
|
||||||
|
i32.lt_s
|
||||||
|
br_if $rows
|
||||||
|
end
|
||||||
|
|
||||||
|
;; swap to the new board
|
||||||
|
call $swapBoards
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue