Skip to content

Commit

Permalink
Canvas image blitting 60FPS on iPhone
Browse files Browse the repository at this point in the history
A Rust Vec<u8> controls the canvas at a full 60fps even on an iPhone
  • Loading branch information
bkase committed Mar 1, 2019
1 parent d718fb2 commit 94a05e0
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 18 deletions.
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ console_error_panic_hook = { version = "0.1.1", optional = true }
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
wee_alloc = { version = "0.4.2", optional = true }

[dependencies.web-sys]
version = "0.3.4"
features = [
'Document',
'Element',
'HtmlElement',
'HtmlCanvasElement',
'CanvasRenderingContext2d',
'ImageData',
'Node',
'Window',
]

[dev-dependencies]
wasm-bindgen-test = "0.2"

Expand Down
132 changes: 116 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,127 @@
extern crate cfg_if;
extern crate wasm_bindgen;
use std::cell::RefCell;
use std::rc::Rc;

mod utils;
extern crate wasm_bindgen;

use cfg_if::cfg_if;
use wasm_bindgen::prelude::*;
use wasm_bindgen::{JsCast, Clamped};

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

fn request_animation_frame(f: &Closure<FnMut()>) {
window()
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("should register `requestAnimationFrame` OK");
}

cfg_if! {
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
if #[cfg(feature = "wee_alloc")] {
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
fn document() -> web_sys::Document {
window()
.document()
.expect("should have a document on window")
}

fn draw(data: &mut Vec<u8>, width: u32, height: u32, i: u32) {
for row in 0..height {
for col in 0..width {
let idx : usize = (row*width*4 + col*4) as usize;
data[idx+0] = i as u8;
data[idx+1] = i as u8;
data[idx+2] = i as u8;
data[idx+3] = 255;
}
}
}


#[wasm_bindgen]
extern {
fn alert(s: &str);
extern "C" {
// Use `js_namespace` here to bind `console.log(..)` instead of just
// `log(..)`
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);

#[wasm_bindgen(js_namespace = performance, js_name = now)]
fn performance_now() -> f64;
}

#[wasm_bindgen]
pub fn greet() {
alert("Hello, gameboy!");
// This function is automatically invoked after the wasm module is instantiated.
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
let canvas = document().get_element_by_id("canvas").unwrap();
let canvas: web_sys::HtmlCanvasElement = canvas
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|_| ())
.unwrap();

let width = canvas.width() as u32;
let height = ((144.0 / 160.0) * width as f64).ceil() as u32;
canvas.set_height(height);

log(&format!("Height {} and width {}", height, width));

let ctx = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap();

// Here we want to call `requestAnimationFrame` in a loop, but only a fixed
// number of times. After it's done we want all our resources cleaned up. To
// achieve this we're using an `Rc`. The `Rc` will eventually store the
// closure we want to execute on each frame, but to start out it contains
// `None`.
//
// After the `Rc` is made we'll actually create the closure, and the closure
// will reference one of the `Rc` instances. The other `Rc` reference is
// used to store the closure, request the first frame, and then is dropped
// by this function.
//
// Inside the closure we've got a persistent `Rc` reference, which we use
// for all future iterations of the loop
let f = Rc::new(RefCell::new(None));
let g = f.clone();

let mut i = 0;
let mut data = Vec::new();
for _ in 0 .. height {
for _ in 0 .. width {
for _ in 0 .. 4 {
data.push(0);
}
}
}
let mut last = performance_now();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
if i > 500 {
// body().set_text_content(Some("All done!"));

// Drop our handle to this closure so that it will get cleaned
// up once we return.
let _ = f.borrow_mut().take();
return;
}

let now = performance_now();
let diff = now - last;
last = now;

draw(&mut data, width, height, i);
let data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(Clamped(&mut data), width, height).expect("u8 clamped array");
ctx.put_image_data(&data, 0.0, 0.0).expect("put_image_data");
ctx.fill_text(&format!("FPS {}", (1000.0 / diff).ceil()), 10.0, 50.0).expect("fill_text");

// Set the body's text content to how many times this
// requestAnimationFrame callback has fired.
i += 1;

// Schedule ourself for another requestAnimationFrame callback.
request_animation_frame(f.borrow().as_ref().unwrap());
}) as Box<FnMut()>));

request_animation_frame(g.borrow().as_ref().unwrap());
Ok(())
}

3 changes: 3 additions & 0 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<title>Hello wasm-pack!</title>
</head>
<body>
<div style="max-width: 960px; margin-left: 3rem; margin-right: 3rem;">
<canvas id="canvas" style="width: 100%;"></canvas>
</div>
<script src="./bootstrap.js"></script>
</body>
</html>
4 changes: 2 additions & 2 deletions www/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import * as wasm from "gameboy";
import('gameboy')
.catch(console.error);

wasm.greet();

0 comments on commit 94a05e0

Please sign in to comment.