Open s1gtrap opened 2 years ago
Just realized I could pass imageData.data
as &mut [u8]
😂
I assume there's nothing to be gained from calling ctx.putImageData
from Rust (and there's no copying when passed with &mut [u8]
), so I guess that sort of settles my problem?
I'm still wondering why the only way to access ImageData.data
is by copy, and why there isn't a 'mutable getter' though.
Can you clarify whether you got something like this to work, so you could modify the data? I don't follow your explanation from Apr 21 about what you realized you could do, or what settled your problem. It seems like you're saying you can't do this from Rust via ctx.put_image_data(). I've been trying similar things and so far my image data is effectively read-only.
Sure. I forgot what I ended up doing exactly, but what I meant by that was I realized it was possible to pass a Uint8Array
from JS-land to a #[wasm_bindgen]
fn taking a &mut [u8]
and mutating it that way. Then, back in JS, call imageData.data.set(newData)
to swap it and ctx.putImageData(imageData, 0, 0)
to copy it to the actual frame buffer.
I learned that there was no trickery to be had with the double buffering and that you had to copy the image data over, but it's been working okay for me so far.
Thanks for that. I've ended up for the moment with a different approach, where I'm repeatedly modifying a stored Vec<[u8]>, and then doing let imdata = ImageData::new_with_u8_clamped_array(Clamped(mydata.as_slice()), 0)
followed by ctx.put_image_data(&imdata, 0.0, 0.0)
in the Rust/wasm code, saving me a trip up to JavaScript. I have no idea yet if that's more costly than the other way, or cheaper, or a toss-up, though the performance appears quite acceptable for my case.
@alexcrichton @s1gtrap @peter9477
Hello, could you help me out with a vaguely related issue please? If not I can open a new question.
I have an HtmlCanvasElement, associated CanvasRenderingContext2d and ImageData. All created in rust.
I need to do some calculations with the pixel values after using the context to draw.
The problem is, calling methods such as .fill or .fill_rect using the context does not seem to modify my ImageData.
It seems get_image_data is returning a 'snapshot', how do I maintain a reference to the underlying vector? I'm really hoping even if it's not possible in JS there's a workaround on the rust side of things.
I have also attempted storing a Vec\<u8> (by calling .data().to_vec()) but had the same issue.
My current workaround is to keep replacing my ImageData with a fresh copy by calling context.get_image_data after drawing, but this is very costly (in a loop, part of a rendering engine I'm working on).
TLDR: how do I avoid having to constantly call get_image_data?
Here's a very simplified example with a 1x1 canvas:
let testCanvas = create_canvas();
resize_canvas(&testCanvas, 1, 1);
let testCtx = get_context(&testCanvas);
let testData: ImageData = testCtx.get_image_data(0.0, 0.0, 1.0, 1.0).unwrap();
console::log_1(&format!("Test data before : {:?}", &testData.data().to_vec()).into());
testCtx.set_fill_style(&JsValue::from("#fff"));
testCtx.fill_rect(0.0, 0.0, 1.0, 1.0);
// Shouldn't have to do this, I expect testCtx.fill_rect to have modified testData
// FIXME: how to avoid calling calling get_image_data?
testData = testCtx.get_image_data(0.0, 0.0, 1.0, 1.0).unwrap();
console::log_1(&format!("Test data after : {:?}", &testData.data().to_vec()).into());
fn create_canvas() -> HtmlCanvasElement {
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
document
.create_element("canvas")
.unwrap()
.dyn_into::<HtmlCanvasElement>()
.unwrap()
}
fn resize_canvas(canvas: &HtmlCanvasElement, w: u32, h: u32) {
canvas.set_width(w);
canvas.set_height(h);
}
fn get_context(canvas: &HtmlCanvasElement) -> CanvasRenderingContext2d {
let opts = js_sys::Object::new();
js_sys::Reflect::set(&opts, &"willReadFrequently".into(), &true.into()).unwrap(); // not sure if this works either
canvas
.get_context_with_context_options("2d", &opts)
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap()
}
Ouput:
Test data before : [0, 0, 0, 0]
Test data after : [0, 0, 0, 0] // without copying over
Test data after : [255, 255, 255, 255] // only works if I overwrite testData = testCtx.get_image_data
Is this because I need to be storing a &ImageData
instead of ImageData
?
If so, how to deal with the lifetime parameter?
#[wasm_bindgen()]
pub struct Engine<'a> { // error: structs with #[wasm_bindgen] cannot have lifetime or type parameters
...
testData: &'a ImageData,
}
Apologies if I'm missing something obvious, I'm new to rust.
If I understood it correctly you intend to edit what is on screen by directly editing the ImageData
as returned from getImageData
?
I'm afraid it wouldn't work like that as the ImageData
you get is not a live slice into the frame buffer: instead it's copied with getImageData
. If I recall correctly by reading up on current thread as well as my StackOverflow question you can't avoid copying back and forth either way as putImageData
also copies.
Do you need to use the CanvasRenderingContext2d
for rendering? I know your example is a simplification and all, but can you get away with modifying it in Rustland instead? Supposedly getImageData
is the 'slow one':
From your description you can indeed remove entirely the getImageData step, keep only one ImageData object, created either woth the new ImageData(w, h) constructor or ctx.createImageBitmap work on that and putImageData it on the canvas context every frame. And the good thing is that getImageData os really the slow one.
Not on screen but yes, I want to maintain a 'live' reference to the underlying buffer, and modify it by calling methods on the associated context.
I think you're right in that getImageData returns a snapshot (very unfortunate if there's really no way around that).
I'm really hoping that at least on the rust side of things there's a way (even unsafe would be fine) to get a reference to the underlying vector, is there a reason why that would not possible?
Implementing my own drawing logic and manipulating a Vec<u8>
directly could work in theory but would be a major pain (rendering overlapping semi-transparent polygons).
The result looks like this (the 'Generated' one in the middle):
Evolving new 'drawings' (was going to be a genetic algorithms project but ended up more like simulated annealing.).
Anyway, I'm generating mutations (randomly shifting points, colors, order of polygons etc), I have a fitness function to calculate 'distance'/error vs the reference image, in a canvas that is not visible (not even added to the DOM) and only update the visible canvas when I find a 'better' mutation (can be very rare, most mutations never get displayed on the UI).
In theory I should even be using an OffscreenCanvas and web workers but haven't gotten that far yet.
Open to suggestions on how to speed things up but the majority of the time is taken up by get_image_data currently, massive bottleneck.
I'm really hoping that at least on the rust side of things there's a way (even unsafe would be fine) to get a reference to the underlying vector, is there a reason why that would not possible?
Yea I highly doubt you'd get away with that for the time being (interesting proposal over at whatwg/html#5173). It's not even a rust thing, just a matter of limited memory access for obvious reasons.
Looks like you need to reconsider your need for canvas rendering along with rust data access as the two seemingly don't play nice together.
Yea I highly doubt you'd get away with that for the time being (interesting proposal over at whatwg/html#5173). It's not even a rust thing, just a matter of limited memory access for obvious reasons.
I see, that's unfortunate.
Looks like you need to reconsider your need for canvas rendering along with rust data access as the two seemingly don't play nice together.
Any good options other than implementing my own fill_rect? 😕
Any good options other than implementing my own fill_rect? 😕
Afraid not.. At least not from me at the moment. In the end I opted for copying from rust with putImageData
(and a few extra steps) and got distracted. I'm glad I'm not the only one mad enough to be dealing with this sort of issue though.
Just realized I could pass
imageData.data
as&mut [u8]
joyI assume there's nothing to be gained from calling
ctx.putImageData
from Rust (and there's no copying when passed with&mut [u8]
), so I guess that sort of settles my problem?I'm still wondering why the only way to access
ImageData.data
is by copy, and why there isn't a 'mutable getter' though.Sure. I forgot what I ended up doing exactly, but what I meant by that was I realized it was possible to pass a
Uint8Array
from JS-land to a#[wasm_bindgen]
fn taking a&mut [u8]
and mutating it that way. Then, back in JS, callimageData.data.set(newData)
to swap it andctx.putImageData(imageData, 0, 0)
to copy it to the actual frame buffer.
I think not all your steps are needed.
In JS, assigning imageData.data
to a variable will be a reference, if I am not mistaken.
Then, using your &mut [u8]
argument type idea, a Rust function can take that variable and modify it.
As a reference was modified, no need to "call imageData.data.set(newData)
to swap it", only call ctx.putImageData(imageData, 0, 0)
.
Here is some code snippets that I put together using these ideas. It worked on my machine (TM).
However, it feels to me that the documentation of Clam suggests that the input type of the rust function should not be &mut [u8]
, but &Clamped<&mut [u8]>
. But that will error with the trait bound
Clamped<&mut [u8]>: RefFromWasmAbiis not satisfied
. So I am gonna keep using the former, until I encounter issues.
All in all, thanks for your input. I can now finally progress with my project.
Summary
I'm really confused about the
ImageData
struct. Is it meant to be read-only?An example from MDN ought to also be possible with WASM, right?
But obviously, since
ImageData::data(&self)
produces aClamped<Vec<u8>>
the data is copied to a freshVec
from the underlyingUint8ClampedArray
and not writable like in the JS example above, so unless I misunderstood something entirely?Additional Details
I'm hoping to make calls to WASM for somewhat efficient rendering but having to allocate a new
ImageData
every frame sort of defeats the purpose. Knowing howImageData
worked on the JS side of things I was expecting to be able to modify one allocated beforehand like sowith something like
So is it possible to modify the underlying byte array? I know the
data
field is marked as read-only but modifying said data isn't.(I see a reference to
Clamped<&mut [u8]>
on the page forClamped
but as far as I can tell it's not possible to get from anImageData
)