rust-wasm: vec<u8> too large, cannot increase stack size

253 views Asked by At

I am trying to create a struct that has a large enough data buffer to hold HTML5 canvas ImageData larger than 64 x 64 pixels. The struct and implementation are defined here in rust:

// src/lib.rs
use wasm_bindgen::prelude::*;
extern crate fixedbitset;
extern crate web_sys;

#[wasm_bindgen]
pub struct CanvasSource {
    width: u32,
    height: u32,
    data: Vec<u8>,
}

#[wasm_bindgen]
impl CanvasSource {
    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    // returns pointer to canvas image data
    pub fn data(&self) -> *const u8 {
        self.data.as_ptr()
    }

    pub fn cover_in_blood(&mut self) {
        let blood: Vec<u8> = vec![252, 3, 27, 255];
        let mut new_data = blood.clone();
        for _ in 0..self.width {
            for _ in 0..self.height {
                let pixel = blood.clone();
                new_data = [new_data, pixel].concat()
            }
        }

        self.data = new_data;
    }

    pub fn new(width: u32, height: u32, initial_data: Vec<u8>) -> CanvasSource {
        let data_size = (width * height) as usize;
        let mut data = initial_data;
        data.resize(data_size, 0);

        CanvasSource {
            width,
            height,
            data,
        }
    }
}

...and called from here in Typescript:

import { useEffect, useRef, useState } from "react";
import { CanvasSource } from "rust-canvas-prototype";
import { memory } from "rust-canvas-prototype/rust_canvas_prototype_bg.wasm";
import styles from "./DirectCanvas.module.css";

const getRenderLoop = (
    source: CanvasSource,
    ctx: CanvasRenderingContext2D,
) => {
    if (source && ctx) {
        const loop = () => {
            const sourceDataPtr = source.data();
            
            const width = source.width();
            const height = source.height();
            const regionSize = width * height * 4;
            
            
            const pixelData = new Uint8ClampedArray(
                memory.buffer,
                sourceDataPtr,
                regionSize
            )

            const imageData = new ImageData(pixelData, width, height);

            ctx.putImageData(imageData, 0, 0)
        };

        return loop;
    }

    return null;
}


export function DirectCanvas() {
    const [source, setSource] = useState<CanvasSource>();
    const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);

    const [paused, setPaused] = useState<boolean>(false);

    // undefined on init, null when paused
    const [animationId, setAnimationId] = useState<number>(0);
    const canvasElement = useRef<HTMLCanvasElement>(null);

    const initialized = source && ctx;

    // initialization
    useEffect(() => {
        if (!source) {
            console.log("loading source");
            let [width, height] = [100, 100];
            // uncomment below to cause error
            // [width, height] = [358, 358]
            setSource(CanvasSource.new(width, height, new Uint8Array([])))
        }

        if (source && !ctx && canvasElement.current) {
            canvasElement.current.height = source.height();
            canvasElement.current.width = source.width();
            setCtx(canvasElement.current.getContext("2d"));
        }
    }, [source, ctx])


    useEffect(() => {
        if (initialized) {
            const renderLoop = getRenderLoop(source, ctx);
            if (renderLoop) {
                renderLoop();
                setTimeout(() => {
                    setAnimationId(prev => prev + 1);
                }, 10)
            }
        }
    }, [source, ctx, animationId]);

    return (
        <div className={styles.Container}>
            <span className={styles.Controls}>
                <button onClick={() => source?.cover_in_blood()}>Splatter</button>
            </span>
            <canvas ref={canvasElement}></canvas>
        </div>
    )
}

The function works correctly for sizes of ~100x100 or less, but once the total area begins to exceed that JS throws the following error:

Uncaught RangeError: attempting to construct out-of-bounds Uint8ClampedArray on ArrayBuffer
   loop DirectCanvas.tsx:23
   DirectCanvas DirectCanvas.tsx:77
   ...

Preliminary research suggests that it is a stack size problem on Rust's end, but attempts to increase the stack size in the config.toml throw errors of their own:

= note: rust-lld: error: unknown argument: -Wl,-zstack-size=29491200

How do I allocate a large enough stack size to paint to canvases larger than 100x100? (minimal reproducible example found here)

1

There are 1 answers

0
ajazz On

Welp, egg's on my face: the problem wasn't in fact on the rust side.

Uint8Array's default initialization size is just not large enough to hold ImageData larger than ~100x100. Ensuring the CanvasSource was initialized to the correct size did the trick:

// initialization
    useEffect(() => {
        if (!source) {
            console.log("loading source");
            [width, height] = [1000, 1000]
            // the fixed line: added third argument
            setSource(CanvasSource.new(width, height, new Uint8Array(width * height * 4)))
        }
         ...
    }, [source, ctx])

On the rust side, I did have a line of code that I thought would handle this:

// src/lib.rs
pub fn new(width: u32, height: u32, initial_data: Vec<u8>) -> CanvasSource {
        let data_size = (width * height) as usize;
        let mut data = initial_data;
        // here
        data.resize(data_size, 0);

        CanvasSource {
            width,
            height,
            data,
        }
    }

But, evidently, resizing it on the Rust end either (1) doesn't update the space that JS was looking for, or (2) I am misusing Vec.resize(). Both are possible. Thanks everyone for pointing stuff out.