# 生命游戏示例

¥Game of Life example

不断更新 康威的生命游戏 (opens new window) 的稍微修改的变体,并在画布上可视化其状态。

¥Continuously updates a slightly modified variant of Conway's Game of Life (opens new window) and visualizes its state on a canvas.

# 内容

¥Contents

  • 从 WebAssembly 模块导出函数。

    ¥Exporting functions from a WebAssembly module.

  • 调用从 WebAssembly 导出的函数。

    ¥Calling functions exported from WebAssembly.

  • 从 JavaScript 导入配置值。

    ¥Importing configuration values from JavaScript.

  • 在 JavaScript 中实例化模块的内存并使用 --importMemory 导入它。

    ¥Instantiating the module's memory in JavaScript and import it using --importMemory.

  • 通过强制热路径中的辅助函数始终为 @inline 来加速程序。

    ¥Speeding up a program by forcing helper functions in a hot path to always @inline.

  • 利用 JavaScript 的 Math 代替原生 libm,通过 --use Math=JSMath 减少模块大小。

    ¥Utilizing JavaScript's Math instead of native libm to reduce module size via --use Math=JSMath.

  • 通过直接修改输入图片缓冲区来响应用户输入。

    ¥Reacting to user input by directly modifying an input image buffer.

  • 了解 WebAssembly 不直观的字节顺序。

    ¥Finding out about WebAssembly's unintuitive byte order.

  • 最后:不断更新输出图片缓冲区的输入并渲染输出图片缓冲区。

    ¥And finally: Continuously updating an input to an output image buffer and rendering the output image buffer.

  • 特性:点击并绘制很多东西。

    ¥Featuring: Clicking and drawing lots of stuff.

# 示例

¥Example

#!optimize=speed&runtime=stub&importMemory&use=Math=JSMath
// Configuration imported from JS
declare const BGR_ALIVE: u32;
declare const BGR_DEAD: u32;
declare const BIT_ROT: u32;

var width: i32, height: i32, offset: i32;

/** Gets an input pixel in the range [0, s]. */
@inline
function get(x: u32, y: u32): u32 {
  return load<u32>((y * width + x) << 2);
}

/** Sets an output pixel in the range [s, 2*s]. */
@inline
function set(x: u32, y: u32, v: u32): void {
  store<u32>((offset + y * width + x) << 2, v);
}

/** Sets an output pixel in the range [s, 2*s] while fading it out. */
@inline
function rot(x: u32, y: u32, v: u32): void {
  var alpha = max<i32>((v >> 24) - BIT_ROT, 0);
  set(x, y, (alpha << 24) | (v & 0x00ffffff));
}

/** Initializes width and height. Called once from JS. */
export function init(w: i32, h: i32): void {
  width  = w;
  height = h;
  offset = w * h;

  // Start by filling output with random live cells.
  for (let y = 0; y < h; ++y) {
    for (let x = 0; x < w; ++x) {
      let c = Math.random() > 0.1
        ? BGR_DEAD  & 0x00ffffff
        : BGR_ALIVE | 0xff000000;
      set(x, y, c);
    }
  }
}

/** Performs one step. Called about 30 times a second from JS. */
export function step(): void {
  var w = width,
      h = height;

  var hm1 = h - 1, // h - 1
      wm1 = w - 1; // w - 1

  // The universe of the Game of Life is an infinite two-dimensional orthogonal grid of square
  // "cells", each of which is in one of two possible states, alive or dead.
  for (let y = 0; y < h; ++y) {
    let ym1 = y == 0 ? hm1 : y - 1,
        yp1 = y == hm1 ? 0 : y + 1;
    for (let x = 0; x < w; ++x) {
      let xm1 = x == 0 ? wm1 : x - 1,
          xp1 = x == wm1 ? 0 : x + 1;

      // Every cell interacts with its eight neighbours, which are the cells that are horizontally,
      // vertically, or diagonally adjacent. Least significant bit indicates alive or dead.
      let aliveNeighbors = (
        (get(xm1, ym1) & 1) + (get(x, ym1) & 1) + (get(xp1, ym1) & 1) +
        (get(xm1, y  ) & 1)                     + (get(xp1, y  ) & 1) +
        (get(xm1, yp1) & 1) + (get(x, yp1) & 1) + (get(xp1, yp1) & 1)
      );

      let self = get(x, y);
      if (self & 1) {
        // A live cell with 2 or 3 live neighbors rots on to the next generation.
        if ((aliveNeighbors & 0b1110) == 0b0010) rot(x, y, self);
        // A live cell with fewer than 2 or more than 3 live neighbors dies.
        else set(x, y, BGR_DEAD | 0xff000000);
      } else {
        // A dead cell with exactly 3 live neighbors becomes a live cell.
        if (aliveNeighbors == 3) set(x, y, BGR_ALIVE | 0xff000000);
        // A dead cell with fewer or more than 3 live neighbors just rots.
        else rot(x, y, self);
      }
    }
  }
}

/** Fills the row and column indicated by `x` and `y` with random live cells. */
export function fill(x: u32, y: u32, p: f64): void {
  for (let ix = 0; ix < width; ++ix) {
    if (Math.random() < p) set(ix, y, BGR_ALIVE | 0xff000000);
  }
  for (let iy = 0; iy < height; ++iy) {
    if (Math.random() < p) set(x, iy, BGR_ALIVE | 0xff000000);
  }
}

#!html
<canvas id="canvas" style="width: 100%; height: 100%; background: #000; cursor: crosshair"></canvas>
<script type="module">
// Configuration
const RGB_ALIVE = 0xD392E6;
const RGB_DEAD  = 0xA61B85;
const BIT_ROT   = 10;

// Set up the canvas with a 2D rendering context
const canvas = document.getElementById("canvas");
const boundingRect = canvas.getBoundingClientRect();
const ctx = canvas.getContext("2d");

// Compute the size of the universe (2 pixels per cell)
const width = boundingRect.width >>> 1;
const height = boundingRect.height >>> 1;
const size = width * height;
const byteSize = (size + size) << 2; // input & output (4 bytes per cell)

canvas.width = width;
canvas.height = height;
canvas.style.imageRendering = "pixelated";
ctx.imageSmoothingEnabled = false;

// Compute the size of and instantiate the module's memory
const memory = new WebAssembly.Memory({ initial: ((byteSize + 0xffff) & ~0xffff) >>> 16 });

// Compile and instantiate the module
const exports = await instantiate(await compile(), {
  env: {
    memory
  },
  module: {
    BGR_ALIVE : rgb2bgr(RGB_ALIVE) | 1, // LSB set indicates alive
    BGR_DEAD  : rgb2bgr(RGB_DEAD) & ~1, // LSB not set indicates dead
    BIT_ROT
  }
});

// Initialize the module with the universe's width and height
exports.init(width, height);

var buffer = new Uint32Array(memory.buffer);

// Update about 30 times a second
(function update() {
  setTimeout(update, 1000 / 30);
  buffer.copyWithin(0, size, size + size);   // copy output to input
  exports.step();                            // perform the next step
})();

// Keep rendering the output at [size, 2*size]
var imageData = ctx.createImageData(width, height);
var argb = new Uint32Array(imageData.data.buffer);
(function render() {
  requestAnimationFrame(render);
  argb.set(buffer.subarray(size, size + size)); // copy output to image buffer
  ctx.putImageData(imageData, 0, 0);            // apply image buffer
})();

// When clicked or dragged, fill the current row and column with random live cells
var down = false;
[ [canvas, "mousedown"],
  [canvas, "touchstart"]
].forEach(eh => eh[0].addEventListener(eh[1], e => down = true));
[ [document, "mouseup"],
  [document, "touchend"]
].forEach(eh => eh[0].addEventListener(eh[1], e => down = false));
[ [canvas, "mousemove"],
  [canvas, "touchmove"],
  [canvas, "mousedown"]
].forEach(eh => eh[0].addEventListener(eh[1], e => {
  if (!down) return;
  var loc;
  if (e.touches) {
    if (e.touches.length > 1) return;
    loc = e.touches[0];
  } else {
    loc = e;
  }
  const currentBoundingRect = canvas.getBoundingClientRect();
  exports.fill(
    ((loc.clientX - currentBoundingRect.left) / currentBoundingRect.width * boundingRect.width) >>> 1,
    ((loc.clientY - currentBoundingRect.top) / currentBoundingRect.height * boundingRect.height) >>> 1,
    0.5
  );
}));

/** Bitshifts an RGB color to BGR instead (WebAssembly is little endian). */
function rgb2bgr(rgb) {
  return ((rgb >>> 16) & 0xff) | (rgb & 0xff00) | (rgb & 0xff) << 16;
}
</script>

注意

该示例做出了一些假设。例如,像本例中那样使用程序的整个内存作为图片缓冲区是唯一可能的,因为我们知道不会创建干扰静态内存段,这是通过以下方式实现的

¥The example makes a couple assumptions. For instance, using the entire memory of the program as the image buffer as in this example is only possible because we know that no interferring static memory segments will be created, which is achieved by

  • 使用 JavaScript 的 Math 代替原生 libm(通常添加查找表),

    ¥using JavaScript's Math instead of native libm (usually adds lookup tables),

  • 不使用更复杂的运行时(通常会增加簿记)并且

    ¥not using a more sophisticated runtime (typically adds bookkeeping) and

  • 该示例的其余部分相对简单(即没有字符串或类似的内容)。

    ¥the rest of the example being relatively simple (i.e. no strings or similar).

一旦不再满足这些条件,人们就会通过指定合适的 --memoryBase 来保留一些空间,或者导出动态实例化的内存块(如 Uint32Array),并将其用作 WebAssembly 和 WebAssembly 中的输入和输出图片缓冲区。 JavaScript。

¥As soon as these conditions are no longer met, one would instead either reserve some space by specifying a suitable --memoryBase or export a dynamically instantiated chunk of memory, like an Uint32Array, and utilize it as the input and output image buffers both in WebAssembly and in JavaScript.

# 本地运行

¥Running locally

说明与 曼德尔布罗特例子中的那些 相同。

¥Instructions are identical to those of the Mandelbrot example.