# 生命游戏示例

持续更新 康威的生命游戏 (在新窗口中打开) 的一个略微修改后的变体,并在画布上可视化其状态。

# 内容

  • 从 WebAssembly 模块导出函数。
  • 调用从 WebAssembly 导出的函数。
  • 从 JavaScript 导入配置值。
  • 在 JavaScript 中实例化模块的内存,并使用 --importMemory 导入它。
  • 通过强制热路径中的辅助函数始终 @inline 来加速程序。
  • 使用 JavaScript 的 Math 而不是原生 libm 来通过 --use Math=JSMath 减少模块大小。
  • 通过直接修改输入图像缓冲区来响应用户输入。
  • 了解 WebAssembly 不直观的字节顺序。
  • 最后:持续更新输入到输出图像缓冲区,并渲染输出图像缓冲区。
  • 特色:点击和绘制大量的 *东西*。

# 示例

#!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>

注意

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

  • 使用 JavaScript 的 Math 而不是原生 libm(通常会添加查找表),
  • 不使用更复杂的运行时(通常会添加簿记),以及
  • 示例的其余部分相对简单(即没有字符串或类似内容)。

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

# 在本地运行

说明与 曼德布罗特集示例的说明 相同。