Eccentric Developments


Light WASM Baseline

This is a quick post to test light-wasm integration.

With this simple implentation, I want to compare the wasm implementation against the JavaScript code performance. I also want this to act as a baseline with future iterations of the Light project.

async function loadWasm(wasmFile) {
  const {
    instance: { exports: wasm },
  } = await WebAssembly.instantiate(wasmFile, {});
  return wasm;
}

async function renderAndPresent(canvas, frameCount, framesAcc, wasm, bmpPtr, finalBitmap) {
  const ctx = canvas.getContext('2d');
  const width = canvas.width;
  const renderStart = performance.now();

  // render here
  await wasm.render(bmpPtr);

  const bitmap = new Uint8Array(wasm.memory.buffer, bmpPtr, framesAcc.length);

  for (let i = 0; i < bitmap.length; i += 3) {
    const r = bitmap[i] + (framesAcc[i] || 0);
    const g = bitmap[i + 1] + (framesAcc[i + 1] || 0);
    const b = bitmap[i + 2] + (framesAcc[i + 2] || 0);
    framesAcc[i] = r;
    framesAcc[i + 1] = g;
    framesAcc[i + 2] = b;
    finalBitmap[i / 3] =
      (255 << 24) |
      (Math.min(b / frameCount, 255) << 16) |
      (Math.min(g / frameCount, 255) << 8) |
      Math.min(r / frameCount, 255);
  }
  const imageData = new ImageData(new Uint8ClampedArray(finalBitmap.buffer), width);
  ctx.putImageData(imageData, 0, 0);
  const elapsed = Math.floor(performance.now() - renderStart);
  const elapsedMs = `${elapsed}ms|${(Math.round(10000 / elapsed) / 10).toFixed(1)}fps`;
  ctx.font = '20px monospace';
  ctx.textBaseline = 'top';
  const measure = ctx.measureText(elapsedMs);
  ctx.fillStyle = '#000000';
  ctx.fillRect(0, 0, measure.width, measure.fontBoundingBoxDescent);
  ctx.fillStyle = '#999999';
  ctx.fillText(elapsedMs, 0, 0);
}

window.running = false;
(async () => {
  const canvas = document.getElementById('canvas-1');
  const width = canvas.width;
  const height = canvas.height;
  const wasmFile = await (await fetch('wasm/00086-light_wasm.wasm')).arrayBuffer();
  const wasm = await loadWasm(wasmFile);
  const bmpPtr = wasm.init(width, height);
  let frameCount = 0;
  const framesAcc = new Array(width * height * 3);
  const finalBitmap = new Uint32Array(width * height);
  window.running = true;

  const animation = async () => {
    frameCount++;
    await renderAndPresent(canvas, frameCount, framesAcc, wasm, bmpPtr, finalBitmap);

    window.running && window.requestAnimationFrame(animation);
  };
  window.requestAnimationFrame(animation);
})();

One thing you will notice is that the lighing doesn't look exactly the same as the JavaScript path tracer, the reason for this is that Light uses a different algorithm to generate a random ray from an intersection point. This algorithm is biased to generate rays with an angle of incidence with higher potential for illumination.

Summary

Some performance numbers

The following numbers were taken using Safari 18.0, in a M1 MBA running macOS Sequoia (15.0) on Low Power Mode:

Implementation Time Per Frame
JavaScript 1230ms
Light Wasm 202ms

Light, as you can see, is significantly faster than the JavaScript path tracer. Much of this is thanks to the Rust language, the LLVM compiler, and the low amount of data moved between Wasm and JavaScript. Another point is that I didn't enable SIMD support during compilation, and looking at the WAT decompilation there are no traces of vectorization, this means that there is more room to improvement.

Now to figure out what the next step is going to be.

Enrique CR - 2024-09-25