Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Editing canvas pixel data in WebAssembly/Rust

I'm toying around with WebAssembly and Rust trying to create canvas pixel data. As an initial experiment I'm trying to make Rust write to its linear memory and then I'll use that to create an ImageData object which I can write to the canvas.

Underlying ImageData is an Uint8Array where each pixel is 4 numbers for rgba. I'm representing that in rust using the following Struct:

struct Pixel {
    r: u8,
    g: u8,
    b: u8,
    a: u8,
}

I've exported a function to JavaScript which will attempt to color all 250,000 pixels in a 500 x 500 pixel canvas:

#[no_mangle]
pub fn color(width: u32, height: u32) {
    for i in 0..width * height {
        let ptr = (i * 4) as u64 as *mut Pixel;
        let mut pixel = unsafe { &mut *ptr };
        pixel.r = 10;
        pixel.g = 10;
        pixel.b = 10;
        pixel.a = 255;
    }
}

Here's the corresponding HTML/JS for the frontend

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>

    <style>
        canvas {
            border: 1px solid red;
        }
    </style>
</head>

<body>
    <canvas id="canvas" height="500" width="500"></canvas>

    <script>

        const WIDTH = 500;
        const HEIGHT = 500;

        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');

        fetch('/rotate.wasm')
            .then((res) => res.arrayBuffer())
            .then((ab) => WebAssembly.instantiate(ab))
            .then(({ instance }) => {

                instance.exports.memory.grow(100);              // make memory big enough
                instance.exports.color(WIDTH, HEIGHT);

                const data = new Uint8ClampedArray(instance.exports.memory.buffer, 0, WIDTH * HEIGHT * 4)
                const imageData = new ImageData(data, 500, 500);

                ctx.putImageData(imageData, 0, 0);
            });
    </script>
</body>

</html>

The result is that not all pixels are colored. Only a section at the top:

enter image description here

When I inspect the WebAssembly memory I can see it looks like it gives up coloring after around 42k pixels.

enter image description here

like image 840
Matt Harrison Avatar asked Apr 20 '18 06:04

Matt Harrison


2 Answers

I think I discovered the answer. There's no guarantee that the start of the linear memory is available to use by JavaScript. The runtime that gets included in the wasm binary by Rust could be freely writing to that memory location. I solved my issue by instead statically allocating a chunk of memory in my program and returning a pointer to JavaScript so it knew where was safe to write to.

// Statically allocate space for 1m pixels

static mut PIXELS: [Pixel; 1_000_000] = [Pixel {
    r: 255,
    g: 0,
    b: 0,
    a: 255,
}; 1_000_000];

// return pointer to JavaScript

#[no_mangle]
pub fn get_memory_offset() -> i32 {
    return unsafe { &PIXELS as *const _ as i32 };
}

It would be nice to also dynamically allocate the memory but I'm not sure how to do that yet.

like image 193
Matt Harrison Avatar answered Sep 22 '22 00:09

Matt Harrison


Your code writes image data to linear memory starting at location 0, are you sure it is safe to do so? Most languages, when compiled to WebAssembly, use linear memory for their own runtime.

A safer option is to create a struct that represents your image, then obtain a reference to this from your JavaScript code so that you can ensure your JS and Rust code are aligned:

https://github.com/ColinEberhardt/wasm-rust-chip8/blob/master/web/chip8.js#L124

like image 43
ColinE Avatar answered Sep 20 '22 00:09

ColinE