Skip to main content

02. Applying the Concepts

About 5 minRustcrashcourserustrswasmassemblyassemblyscriptccpp

02. Applying the Concepts 관련


Examples that expand on the conceptual examples to show how these minimal examples could be used to build common features in larger applications.


Reading and Writing Graphics

Wasm By Example - Reading and Writing Graphics

Reading and Writing Graphics

Overview

As stated before, WebAssembly is a great fit for computationally intensive tasks. For example, Tasks that involve things like big data, heavy logic with conditionals, or nested looping. Thus, generating / rendering graphics can get a significant speedup by moving these mentioned parts into WebAssembly. In this example, we will be generating 20x20 colored checkerboard images once per second, and displaying them on a HTML5 Canvasopen in new window using Pixel Manipulation on the ImageData Objectopen in new window. In fancy graphics terms, this is a rasterizer.

Note

This example will continue to build on our simple buffer/pointer memory passing. This could be implemented using higher-level data structures, and these data structures will be covered in later examples.

So let's get into the example:

Implementation

As usual, let's get started with our src/lib.rs file. You will notice here we set up a buffer, similar to the WebAssembly Linear Memory exampleopen in new window. By doing this, Javascript can read the values placed into the buffer later. Please be sure to read the comments in the following code examples, and be sure to follow links or look at previous examples if something does not make sense. Let's get into it:

// The wasm-pack uses wasm-bindgen to build and generate JavaScript binding file.
// Import the wasm-bindgen crate.
use wasm_bindgen::prelude::*;

// Define the size of our "checkerboard"
const CHECKERBOARD_SIZE: usize = 20;

/*
 * 1. What is going on here?
 * Create a static mutable byte buffer.
 * We will use for putting the output of our graphics,
 * to pass the output to js.
 * NOTE: global `static mut` means we will have "unsafe" code
 * but for passing memory between js and wasm should be fine.
 *
 * 2. Why is the size CHECKERBOARD_SIZE * CHECKERBOARD_SIZE * 4?
 * We want to have 20 pixels by 20 pixels. And 4 colors per pixel (r,g,b,a)
 * Which, the Canvas API Supports.
 */
const OUTPUT_BUFFER_SIZE: usize = CHECKERBOARD_SIZE * CHECKERBOARD_SIZE * 4;
static mut OUTPUT_BUFFER: [u8; OUTPUT_BUFFER_SIZE] = [0; OUTPUT_BUFFER_SIZE];

// Function to return a pointer to our buffer
// in wasm memory
#[wasm_bindgen]
pub fn get_output_buffer_pointer() -> *const u8 {
  let pointer: *const u8;
  unsafe {
    pointer = OUTPUT_BUFFER.as_ptr();
  }

  return pointer;
}

// Function to generate our checkerboard, pixel by pixel
#[wasm_bindgen]
pub fn generate_checker_board(
    dark_value_red: u8,
    dark_value_green: u8,
    dark_value_blue: u8,
    light_value_red: u8,
    light_value_green: u8,
    light_value_blue: u8
    ) {


  // Since Linear memory is a 1 dimensional array, but we want a grid
  // we will be doing 2d to 1d mapping
  // https://softwareengineering.stackexchange.com/questions/212808/treating-a-1d-data-structure-as-2d-grid
  for y in 0..CHECKERBOARD_SIZE {
    for x in 0..CHECKERBOARD_SIZE {
      // Set our default case to be dark squares
      let mut is_dark_square: bool = true;

      // We should change our default case if
      // We are on an odd y
      if y % 2 == 0 {
        is_dark_square = false;
      }

      // Lastly, alternate on our x value
      if x % 2 == 0 {
        is_dark_square = !is_dark_square;
      }

      // Now that we determined if we are dark or light,
      // Let's set our square value
      let mut square_value_red: u8 = dark_value_red;
      let mut square_value_green: u8 = dark_value_green;
      let mut square_value_blue: u8 = dark_value_blue;
      if !is_dark_square {
        square_value_red = light_value_red;
        square_value_green = light_value_green;
        square_value_blue = light_value_blue;
      }

      // Let's calculate our index, using our 2d -> 1d mapping.
      // And then multiple by 4, for each pixel property (r,g,b,a).
      let square_number: usize = y * CHECKERBOARD_SIZE + x;
      let square_rgba_index: usize = square_number * 4;

      // Finally store the values.
      unsafe {
        OUTPUT_BUFFER[square_rgba_index + 0] = square_value_red; // Red
        OUTPUT_BUFFER[square_rgba_index + 1] = square_value_green; // Green
        OUTPUT_BUFFER[square_rgba_index + 2] = square_value_blue; // Blue
        OUTPUT_BUFFER[square_rgba_index + 3] = 255; // Alpha (Always Opaque)
      }
    }
  }
}

Next, we can compile the module following the Hello Worldopen in new window examples compilation process, replacing the appropriate file names.

Next, lets create an index.js file to load and run our wasm output. Let's import the wasm initialization module from pkg/graphics.js that was generated by wasm-pack. Then, let's call the module passing in the path to our wasm file at pkg/graphics_bg.wasm that was generated by wasm-pack. A lot of the logic here is expanding on the WebAssembly Linear Memory Exampleopen in new window, but applying the learnings to a DOM API. The most important thing here is probably how we are copying out memory from Wasm, using .slice calls. Please see the reference links if things get confusing. Here is the index.js below!

Note

In this example, we are using the exported function from the wasm module directly to help highlight the WebAssembly API. wasm-bindgen generates JavaScript bindings code that can be imported as an ES6 import, and is the reccomended way to work with your Rust Wasm modules. These JavaScript bindings are shown in the "Passing High Level Data Types with wasm-bindgen" example.

import wasmInit from "./pkg/graphics.js";

const runWasm = async () => {
  // Instantiate our wasm module
  const rustWasm = await wasmInit("./pkg/graphics_bg.wasm");

  // Create a Uint8Array to give us access to Wasm Memory
  const wasmByteMemoryArray = new Uint8Array(rustWasm.memory.buffer);

  // Get our canvas element from our index.html
  const canvasElement = document.querySelector("canvas");

  // Set up Context and ImageData on the canvas
  const canvasContext = canvasElement.getContext("2d");
  const canvasImageData = canvasContext.createImageData(
    canvasElement.width,
    canvasElement.height
  );

  // Clear the canvas
  canvasContext.clearRect(0, 0, canvasElement.width, canvasElement.height);

  const getDarkValue = () => {
    return Math.floor(Math.random() * 100);
  };

  const getLightValue = () => {
    return Math.floor(Math.random() * 127) + 127;
  };

  const drawCheckerBoard = () => {
    const checkerBoardSize = 20;

    // Generate a new checkboard in wasm
    rustWasm.generate_checker_board(
      getDarkValue(),
      getDarkValue(),
      getDarkValue(),
      getLightValue(),
      getLightValue(),
      getLightValue()
    );

    // Pull out the RGBA values from Wasm memory
    // Starting at the memory index of out output buffer (given by our pointer)
    // 20 * 20 * 4 = checkboard max X * checkerboard max Y * number of pixel properties (R,G.B,A)
    const outputPointer = rustWasm.get_output_buffer_pointer();
    const imageDataArray = wasmByteMemoryArray.slice(
      outputPointer,
      outputPointer + checkerBoardSize * checkerBoardSize * 4
    );

    // Set the values to the canvas image data
    canvasImageData.data.set(imageDataArray);

    // Clear the canvas
    canvasContext.clearRect(0, 0, canvasElement.width, canvasElement.height);

    // Place the new generated checkerboard onto the canvas
    canvasContext.putImageData(canvasImageData, 0, 0);
  };

  drawCheckerBoard();
  setInterval(() => {
    drawCheckerBoard();
  }, 1000);
};
runWasm();

Lastly, lets load our ES6 Module, index.js Javascript file in our index.html. And let's be sure to add a canvas element as well!

Random tip

use the image-renderingopen in new window property to display pixel art, and other "sharp" images correctly.

<body>
  <canvas
    width="20"
    height="20"
    style="image-rendering: pixelated; image-rendering: crisp-edges; width: 100%;"
  >
  </canvas>
</body>

<!-- Other HTML here. -->

And you should get something similar to the demo (Source Codeopen in new window) below!

Demo

Demo

Next, lets took a look at an example of implementing Reading and Writing Audio with WebAssembly.


Reading and Writing Audio

Wasm By Example - Reading and Writing Audio

Reading and Writing Audio

이찬희 (MarkiiimarK)
Never Stop Learning.