When compiling Rust to wasm (web assembly), how can I sleep for 10 milliseconds?
Asked Answered
C

2

10

My rust program is managing memory for a 2d html canvas context, and I'm trying to hit ~60fps. I can calculate the delta between each frame easily, and it turns out to be roughly ~5ms.

I'm unclear on how to put my Rust webassembly program to sleep for the remaining 11ms. One option would be to have JavaScript call into Rust on every requestAnimationFrame and use that as the driver, but I'm curious to keep it all in Rust if possible.

I'm effectively looking for the Rust equivalent of JavaScript's setTimeout(renderNext, 11) when compiling out to the wasm target.

Coordinate answered 3/9, 2019 at 6:18 Comment(0)
G
3

I'm effectively looking for the Rust equivalent of JavaScript's setTimeout(renderNext, 11) when compiling out to the wasm target.

There are several Rust crates that have bindings to the JavaScript web API, most notably web-sys. Take a look at the documentation for one of the setTimeout overloads.

This is not really a Rust equivalent though, as it pretty directly calls the JS function. But you won't be able to get around that: sleeping or getting the current time are both functions that the host environment has to offer. They cannot be implemented in the raw language alone.

One option would be to have JavaScript call into Rust on every requestAnimationFrame and use that as the driver, but I'm curious to keep it all in Rust if possible.

Yes, you should use requestAnimationFrame (link to web-sys docs). This is much preferred over timing it yourself. In particular, this method will also pause calling your code when the tab is not active and stuff like that. In a desktop environment you would do the same: ask the host environment (i.e. the operating system, often via OpenGL or so) to synchronize your program to screen refreshes.

Gaga answered 3/9, 2019 at 7:25 Comment(0)
W
5

In your requestAnimationFrame callback, call setTimeout, and have that in turn make the next call to requestAnimationFrame. You can see the JS version of this here.

Based on the example in the wasm-bindgen book, here's how I do this in Rust:

fn animate_limited(mut draw_frame: impl FnMut() + 'static, max_fps: i32) {
    // Based on:
    // https://rustwasm.github.io/docs/wasm-bindgen/examples/request-animation-frame.html#srclibrs

    // https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
    let animate_cb = Rc::new(RefCell::new(None));
    let animate_cb2 = animate_cb.clone();

    let timeout_cb = Rc::new(RefCell::new(None));
    let timeout_cb2 = timeout_cb.clone();

    let w = window();
    *timeout_cb2.borrow_mut() = Some(Closure::wrap(Box::new(move || {
        request_animation_frame(&w, animate_cb.borrow().as_ref().unwrap());
    }) as Box<dyn FnMut()>));

    let w2 = window();
    *animate_cb2.borrow_mut() = Some(Closure::wrap(Box::new(move || {
        draw_frame();

        set_timeout(&w2, timeout_cb.borrow().as_ref().unwrap(), 1000 / max_fps);
    }) as Box<dyn FnMut()>));

    request_animation_frame(&window(), animate_cb2.borrow().as_ref().unwrap());
}

fn window() -> web_sys::Window {
    web_sys::window().expect("no global `window` exists")
}

fn request_animation_frame(window: &web_sys::Window, f: &Closure<dyn FnMut()>) -> i32 {
    window
        .request_animation_frame(f.as_ref().unchecked_ref())
        .expect("should register `requestAnimationFrame` OK")
}

fn set_timeout(window: &web_sys::Window, f: &Closure<dyn FnMut()>, timeout_ms: i32) -> i32 {
    window
        .set_timeout_with_callback_and_timeout_and_arguments_0(
            f.as_ref().unchecked_ref(),
            timeout_ms,
        )
        .expect("should register `setTimeout` OK")
}

Then you simply pass animate_limited a function to do your drawing (a closure like move || { /* drawing logic here */ } will do the trick), and the maximum framerate you want.

There are almost certainly improvements to be made, there. I'm very new to Rust and just spent far too long figuring out how to make this work. Hopefully this makes it quicker for someone else in the future.

Walsingham answered 30/8, 2020 at 5:5 Comment(2)
What were your imports for this, apart from std::cell:RefCell and std::rc::Rc? Trying to recreate this, the compiler throws no method named `unchecked_ref` found for reference `&wasm_bindgen::JsValue` in the current scopePhoebe
That comes from use wasm_bindgen::JsCast; You may also want to use wasm_bindgen::prelude::*;.Walsingham
G
3

I'm effectively looking for the Rust equivalent of JavaScript's setTimeout(renderNext, 11) when compiling out to the wasm target.

There are several Rust crates that have bindings to the JavaScript web API, most notably web-sys. Take a look at the documentation for one of the setTimeout overloads.

This is not really a Rust equivalent though, as it pretty directly calls the JS function. But you won't be able to get around that: sleeping or getting the current time are both functions that the host environment has to offer. They cannot be implemented in the raw language alone.

One option would be to have JavaScript call into Rust on every requestAnimationFrame and use that as the driver, but I'm curious to keep it all in Rust if possible.

Yes, you should use requestAnimationFrame (link to web-sys docs). This is much preferred over timing it yourself. In particular, this method will also pause calling your code when the tab is not active and stuff like that. In a desktop environment you would do the same: ask the host environment (i.e. the operating system, often via OpenGL or so) to synchronize your program to screen refreshes.

Gaga answered 3/9, 2019 at 7:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.