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)
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
CanvasSourcewas initialized to the correct size did the trick:On the rust side, I did have a line of code that I thought would handle this:
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.