use std::cell::{Cell, RefCell};
use std::rc::Rc;
use js_sys::{Function, Object, Reflect, WebAssembly};
use wasm_bindgen::prelude::*;
use wasm_bindgen::{Clamped, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
use super::dom;
use super::templates;
const FB_W: u32 = 256;
const FB_H: u32 = 144;
const FB_BYTES: usize = (FB_W * FB_H * 4) as usize;
type Framebuffer = Rc<RefCell<Vec<u8>>>;
thread_local! {
static FRAME_GEN: Cell<u32> = const { Cell::new(0) };
static RUNTIME: RefCell<Option<CartridgeRuntime>> = const { RefCell::new(None) };
static POINTER: Cell<(i32, i32)> = const { Cell::new((0, 0)) };
static POINTER_DOWN: Cell<i32> = const { Cell::new(0) };
static STATE: RefCell<[i32; 64]> = const { RefCell::new([0; 64]) };
}
#[allow(dead_code)]
struct CartridgeRuntime {
clear: Closure<dyn FnMut(i32)>,
set_pixel: Closure<dyn FnMut(i32, i32, i32)>,
fill_rect: Closure<dyn FnMut(i32, i32, i32, i32, i32)>,
draw_char: Closure<dyn FnMut(i32, i32, i32, i32, i32)>,
draw_number: Closure<dyn FnMut(i32, i32, i32, i32, i32)>,
present: Closure<dyn FnMut()>,
width: Closure<dyn FnMut() -> i32>,
height: Closure<dyn FnMut() -> i32>,
pointer_x: Closure<dyn FnMut() -> i32>,
pointer_y: Closure<dyn FnMut() -> i32>,
pointer_down: Closure<dyn FnMut() -> i32>,
state_get: Closure<dyn FnMut(i32) -> i32>,
state_set: Closure<dyn FnMut(i32, i32)>,
}
pub(crate) async fn run_wasm(wasm_bytes: &[u8]) -> Result<(), JsValue> {
let ctx = mount_canvas()?;
run_with_ctx(wasm_bytes, ctx).await
}
pub(crate) async fn run_in_root_canvas(wasm_bytes: &[u8]) -> Result<(), JsValue> {
let ctx = size_and_get_ctx()?;
run_with_ctx(wasm_bytes, ctx).await
}
async fn run_with_ctx(
wasm_bytes: &[u8],
ctx: CanvasRenderingContext2d,
) -> Result<(), JsValue> {
let generation = FRAME_GEN.with(|g| {
let n = g.get().wrapping_add(1);
g.set(n);
n
});
let fb: Framebuffer = Rc::new(RefCell::new(black_framebuffer()));
POINTER_DOWN.with(|d| d.set(0));
STATE.with(|s| *s.borrow_mut() = [0; 64]);
let (imports, runtime) = build_host_display(&fb, &ctx)?;
RUNTIME.with(|cell| *cell.borrow_mut() = Some(runtime));
let result = JsFuture::from(WebAssembly::instantiate_buffer(wasm_bytes, &imports)).await?;
let instance = Reflect::get(&result, &JsValue::from_str("instance"))?;
let exports = Reflect::get(&instance, &JsValue::from_str("exports"))?;
if let Some(frame) = export_fn(&exports, "frame") {
start_frame_loop(frame, generation);
} else if let Some(render) = export_fn(&exports, "render") {
render.call0(&JsValue::NULL)?;
} else {
return Err(JsValue::from_str("cartridge exports neither frame nor render"));
}
Ok(())
}
fn build_host_display(
fb: &Framebuffer,
ctx: &CanvasRenderingContext2d,
) -> Result<(Object, CartridgeRuntime), JsValue> {
let clear = {
let fb = fb.clone();
Closure::<dyn FnMut(i32)>::new(move |rgb: i32| {
let (r, g, b) = rgb_components(rgb);
let mut buf = fb.borrow_mut();
let mut i = 0;
while i < buf.len() {
buf[i] = r;
buf[i + 1] = g;
buf[i + 2] = b;
buf[i + 3] = 255;
i += 4;
}
})
};
let set_pixel = {
let fb = fb.clone();
Closure::<dyn FnMut(i32, i32, i32)>::new(move |x: i32, y: i32, rgb: i32| {
if x < 0 || y < 0 || x >= FB_W as i32 || y >= FB_H as i32 {
return;
}
let (r, g, b) = rgb_components(rgb);
let mut buf = fb.borrow_mut();
let idx = ((y as usize) * (FB_W as usize) + (x as usize)) * 4;
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = 255;
})
};
let fill_rect = {
let fb = fb.clone();
Closure::<dyn FnMut(i32, i32, i32, i32, i32)>::new(
move |x: i32, y: i32, w: i32, h: i32, rgb: i32| {
let (r, g, b) = rgb_components(rgb);
let x0 = x.max(0);
let y0 = y.max(0);
let x1 = x.saturating_add(w).min(FB_W as i32);
let y1 = y.saturating_add(h).min(FB_H as i32);
let mut buf = fb.borrow_mut();
let mut yy = y0;
while yy < y1 {
let mut xx = x0;
while xx < x1 {
let idx = ((yy as usize) * (FB_W as usize) + (xx as usize)) * 4;
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = 255;
xx += 1;
}
yy += 1;
}
},
)
};
let present = {
let fb = fb.clone();
let ctx = ctx.clone();
Closure::<dyn FnMut()>::new(move || {
let buf = fb.borrow();
if let Ok(img) =
ImageData::new_with_u8_clamped_array_and_sh(Clamped(&buf[..]), FB_W, FB_H)
{
let _ = ctx.put_image_data(&img, 0.0, 0.0);
}
})
};
let draw_char = {
let fb = fb.clone();
Closure::<dyn FnMut(i32, i32, i32, i32, i32)>::new(
move |x: i32, y: i32, code: i32, rgb: i32, scale: i32| {
let mut buf = fb.borrow_mut();
blit_glyph(&mut buf, x, y, code as u32, rgb_components(rgb), scale.max(1));
},
)
};
let draw_number = {
let fb = fb.clone();
Closure::<dyn FnMut(i32, i32, i32, i32, i32)>::new(
move |x: i32, y: i32, value: i32, rgb: i32, scale: i32| {
let color = rgb_components(rgb);
let s = scale.max(1);
let advance = 6 * s; let mut buf = fb.borrow_mut();
let mut cx = x;
let mut n = (value as i64).unsigned_abs();
if value < 0 {
blit_glyph(&mut buf, cx, y, '-' as u32, color, s);
cx += advance;
}
let mut digits = [0u8; 20];
let mut count = 0;
if n == 0 {
digits[0] = b'0';
count = 1;
} else {
while n > 0 {
digits[count] = b'0' + (n % 10) as u8;
n /= 10;
count += 1;
}
}
for i in (0..count).rev() {
blit_glyph(&mut buf, cx, y, digits[i] as u32, color, s);
cx += advance;
}
},
)
};
let width = Closure::<dyn FnMut() -> i32>::new(move || FB_W as i32);
let height = Closure::<dyn FnMut() -> i32>::new(move || FB_H as i32);
let pointer_x = Closure::<dyn FnMut() -> i32>::new(move || POINTER.with(|p| p.get().0));
let pointer_y = Closure::<dyn FnMut() -> i32>::new(move || POINTER.with(|p| p.get().1));
let pointer_down = Closure::<dyn FnMut() -> i32>::new(move || POINTER_DOWN.with(|d| d.get()));
let state_get = Closure::<dyn FnMut(i32) -> i32>::new(move |slot: i32| {
if slot < 0 || slot >= 64 {
return 0;
}
STATE.with(|s| s.borrow()[slot as usize])
});
let state_set = Closure::<dyn FnMut(i32, i32)>::new(move |slot: i32, value: i32| {
if slot < 0 || slot >= 64 {
return;
}
STATE.with(|s| s.borrow_mut()[slot as usize] = value);
});
let host_display = Object::new();
set_fn(&host_display, "clear", &clear)?;
set_fn(&host_display, "set_pixel", &set_pixel)?;
set_fn(&host_display, "fill_rect", &fill_rect)?;
set_fn(&host_display, "draw_char", &draw_char)?;
set_fn(&host_display, "draw_number", &draw_number)?;
set_fn(&host_display, "present", &present)?;
set_fn(&host_display, "width", &width)?;
set_fn(&host_display, "height", &height)?;
set_fn(&host_display, "pointer_x", &pointer_x)?;
set_fn(&host_display, "pointer_y", &pointer_y)?;
set_fn(&host_display, "pointer_down", &pointer_down)?;
set_fn(&host_display, "state_get", &state_get)?;
set_fn(&host_display, "state_set", &state_set)?;
let imports = Object::new();
Reflect::set(&imports, &JsValue::from_str("host_display"), &host_display)?;
Ok((
imports,
CartridgeRuntime {
clear, set_pixel, fill_rect, draw_char, draw_number, present, width, height,
pointer_x, pointer_y, pointer_down, state_get, state_set,
},
))
}
fn blit_glyph(buf: &mut [u8], x: i32, y: i32, code: u32, color: (u8, u8, u8), scale: i32) {
let glyph = glyph_5x7(code);
let (r, g, b) = color;
for (row, bits) in glyph.iter().enumerate() {
for col in 0..5 {
if (bits >> (4 - col)) & 1 == 0 {
continue;
}
for dy in 0..scale {
for dx in 0..scale {
let px = x + col * scale + dx;
let py = y + row as i32 * scale + dy;
if px < 0 || py < 0 || px >= FB_W as i32 || py >= FB_H as i32 {
continue;
}
let idx = ((py as usize) * (FB_W as usize) + (px as usize)) * 4;
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = 255;
}
}
}
}
}
fn glyph_5x7(c: u32) -> [u8; 7] {
match c {
0x20 => [0, 0, 0, 0, 0, 0, 0], 0x30 => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E], 0x31 => [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E], 0x32 => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F], 0x33 => [0x1E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x1E], 0x34 => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02], 0x35 => [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E], 0x36 => [0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E], 0x37 => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08], 0x38 => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E], 0x39 => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E], 0x2B => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00], 0x2D => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00], 0x2A => [0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00], 0x2F => [0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10], 0x3D => [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00], 0x2E => [0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06], 0x28 => [0x04, 0x08, 0x10, 0x10, 0x10, 0x08, 0x04], 0x29 => [0x04, 0x02, 0x01, 0x01, 0x01, 0x02, 0x04], 0x41 => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 0x42 => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E], 0x43 => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E], 0x44 => [0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E], 0x45 => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F], 0x46 => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10], 0x47 => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E], 0x48 => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 0x49 => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], 0x4A => [0x07, 0x02, 0x02, 0x02, 0x12, 0x12, 0x0C], 0x4B => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11], 0x4C => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F], 0x4D => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11], 0x4E => [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11], 0x4F => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 0x50 => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10], 0x51 => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D], 0x52 => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11], 0x53 => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E], 0x54 => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04], 0x55 => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 0x56 => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04], 0x57 => [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11], 0x58 => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11], 0x59 => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04], 0x5A => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F], _ => [0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F], }
}
pub(crate) fn set_pointer_down(down: bool) {
POINTER_DOWN.with(|d| d.set(if down { 1 } else { 0 }));
}
pub(crate) fn set_pointer(client_x: f64, client_y: f64) {
let Some(el) = dom::by_id("display-canvas") else { return };
let Ok(canvas) = el.dyn_into::<HtmlCanvasElement>() else { return };
let rect = canvas.get_bounding_client_rect();
let (w, h) = (rect.width(), rect.height());
if w <= 0.0 || h <= 0.0 {
return;
}
let fx = (((client_x - rect.left()) / w) * FB_W as f64).clamp(0.0, (FB_W - 1) as f64) as i32;
let fy = (((client_y - rect.top()) / h) * FB_H as f64).clamp(0.0, (FB_H - 1) as f64) as i32;
POINTER.with(|p| p.set((fx, fy)));
}
fn set_fn<T: ?Sized>(obj: &Object, name: &str, closure: &Closure<T>) -> Result<(), JsValue> {
Reflect::set(obj, &JsValue::from_str(name), closure.as_ref().unchecked_ref())?;
Ok(())
}
fn rgb_components(c: i32) -> (u8, u8, u8) {
let u = c as u32;
(((u >> 16) & 0xff) as u8, ((u >> 8) & 0xff) as u8, (u & 0xff) as u8)
}
fn black_framebuffer() -> Vec<u8> {
let mut buf = vec![0u8; FB_BYTES];
let mut i = 3;
while i < buf.len() {
buf[i] = 255;
i += 4;
}
buf
}
fn export_fn(exports: &JsValue, name: &str) -> Option<Function> {
Reflect::get(exports, &JsValue::from_str(name))
.ok()
.and_then(|v| v.dyn_into::<Function>().ok())
}
fn start_frame_loop(frame: Function, generation: u32) {
let start = js_sys::Date::now();
let holder: Rc<RefCell<Option<Closure<dyn FnMut()>>>> = Rc::new(RefCell::new(None));
let holder2 = holder.clone();
*holder.borrow_mut() = Some(Closure::wrap(Box::new(move || {
if FRAME_GEN.with(|g| g.get()) != generation {
let _ = holder2.borrow_mut().take();
return;
}
let t = (js_sys::Date::now() - start) as i32;
let _ = frame.call1(&JsValue::NULL, &JsValue::from(t));
if let Some(cb) = holder2.borrow().as_ref() {
let _ = request_af(cb);
}
}) as Box<dyn FnMut()>));
if let Some(cb) = holder.borrow().as_ref() {
let _ = request_af(cb);
}
}
fn request_af(cb: &Closure<dyn FnMut()>) -> Result<i32, JsValue> {
dom::window()?.request_animation_frame(cb.as_ref().unchecked_ref())
}
pub(crate) fn stop() {
FRAME_GEN.with(|g| g.set(g.get().wrapping_add(1)));
RUNTIME.with(|cell| *cell.borrow_mut() = None);
}
fn mount_canvas() -> Result<CanvasRenderingContext2d, JsValue> {
dom::swap_inner("view-content", &templates::display_surface().into_string());
super::opfs::set_view_collapsed(false);
size_and_get_ctx()
}
fn size_and_get_ctx() -> Result<CanvasRenderingContext2d, JsValue> {
let canvas = dom::by_id("display-canvas")
.ok_or_else(|| JsValue::from_str("display-canvas missing"))?
.dyn_into::<HtmlCanvasElement>()?;
canvas.set_width(FB_W);
canvas.set_height(FB_H);
let ctx = canvas
.get_context("2d")?
.ok_or_else(|| JsValue::from_str("no 2d context"))?
.dyn_into::<CanvasRenderingContext2d>()?;
Ok(ctx)
}