#[allow(unused_imports)]
use anyhow::{anyhow, Result};
#[allow(unused_imports)]
use eframe::egui;
use eframe::{
wasm_bindgen::{closure::Closure, JsCast},
web_sys,
};
use log::{error, info};
use std::sync::mpsc;
use wasm_bindgen_futures::JsFuture;
use web_sys::js_sys::Uint8Array;
use crate::stage::{Event, Stage};
use crate::uxn::audio_setup;
use uxn::{Backend, Uxn, UxnRam};
use varvara::Varvara;
pub fn run() -> Result<()> {
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
let window = web_sys::window().ok_or_else(|| anyhow!("could not get window"))?;
let loc = window.location();
let hash = loc
.hash()
.map_err(|e| anyhow!("could not get location hash: {e:?}"))?;
let rom_name = hash.strip_prefix('#');
const ROMS: &[(&str, &[u8])] = &[
("controller", include_bytes!("../../roms/controller.rom")),
("screen", include_bytes!("../../roms/screen.rom")),
("drool", include_bytes!("../../roms/drool.rom")),
("audio", include_bytes!("../../roms/audio.rom")),
("mandelbrot", include_bytes!("../../roms/mandelbrot.rom")),
("bunnymark", include_bytes!("../../roms/bunnymark.rom")),
("piano", include_bytes!("../../roms/piano.rom")),
];
let rom = match ROMS
.iter()
.find(|(name, _data)| Some(*name) == rom_name)
.map(|(_name, data)| *data)
{
Some(data) => data,
None => {
log::warn!(
"ROM not found for hash {:?}, using default controller.rom",
rom_name
);
log::info!("[WASM] After ROM fallback, about to create UxnRam");
include_bytes!("../../roms/controller.rom")
}
};
log::info!("[WASM] Creating UxnRam");
let ram = UxnRam::new();
log::info!("[WASM] Creating Uxn VM");
let mut vm = Uxn::new(ram.leak(), Backend::Interpreter);
log::info!("[WASM] Creating Varvara");
let mut dev = Varvara::new();
log::info!("[WASM] Resetting VM with ROM");
let extra = vm.reset(rom);
log::info!("[WASM] Resetting Varvara with extra");
dev.reset(extra);
log::info!("[WASM] Running reset vector");
vm.run(&mut dev, 0x100);
log::info!("[WASM] Checking output");
dev.output(&vm).check()?;
let size @ (width, height) = dev.output(&vm).size;
log::info!("[WASM] Output size: {width}x{height}");
let options = eframe::WebOptions {
#[cfg(not(target_arch = "wasm32"))]
max_size_points: egui::Vec2::new(width as f32, height as f32),
..eframe::WebOptions::default()
};
info!("setting size to {width}, {height}");
let document = window
.document()
.ok_or_else(|| anyhow!("could not get document"))?;
let div = document
.get_element_by_id("box")
.ok_or_else(|| anyhow!("could not find box div"))?;
let div = div
.dyn_into::<web_sys::HtmlElement>()
.map_err(|e| anyhow!("could not cast to HtmlElement: {e:?}"))?;
div.style()
.set_css_text(&format!("width: {width}px; height: {height}px"));
let footer = document
.get_element_by_id("footer")
.ok_or_else(|| anyhow!("could not find footer div"))?;
let footer = footer
.dyn_into::<web_sys::HtmlElement>()
.map_err(|e| anyhow!("could not cast to HtmlElement: {e:?}"))?;
footer.style().set_css_text(&format!("width: {width}px"));
let sel = document
.get_element_by_id("example-selector")
.ok_or_else(|| anyhow!("could not find example-selector"))?
.dyn_into::<web_sys::Node>()
.map_err(|e| anyhow!("could not convert example-selector: {e:?}"))?;
for (r, _) in ROMS {
let opt = document
.create_element("option")
.map_err(|e| anyhow!("could not create option: {e:?}"))?
.dyn_into::<web_sys::HtmlOptionElement>()
.map_err(|e| anyhow!("could not convert example-selector: {e:?}"))?;
opt.set_text_content(Some(r));
sel.append_child(&opt.get_root_node())
.map_err(|e| anyhow!("could not append node: {e:?}"))?;
}
let sel = document
.get_element_by_id("example-selector")
.ok_or_else(|| anyhow!("could not find example-selector"))?
.dyn_into::<web_sys::HtmlSelectElement>()
.map_err(|e| anyhow!("could not convert example-selector: {e:?}"))?;
let (tx, rx) = mpsc::channel();
let tx_ = tx.clone();
let a = Closure::<dyn FnMut()>::new(move || match sel.selected_index() {
0 => (),
i => {
if let Some((name, r)) = ROMS.get(i as usize - 1) {
if tx_.send(Event::LoadRom(r.to_vec())).is_err() {
error!("error loading rom");
}
if let Err(e) = loc.set_hash(&format!("#{name}")) {
error!("could not update URL hash: {e:?}");
}
}
}
});
let sel = document
.get_element_by_id("example-selector")
.ok_or_else(|| anyhow!("could not find example-selector"))?
.dyn_into::<web_sys::HtmlSelectElement>()
.map_err(|e| anyhow!("could not convert example-selector: {e:?}"))?;
sel.set_onchange(Some(a.as_ref().unchecked_ref()));
std::mem::forget(a);
let file_load = document
.get_element_by_id("load-file")
.ok_or_else(|| anyhow!("could not find load-file"))?
.dyn_into::<web_sys::HtmlInputElement>()
.map_err(|e| anyhow!("could not convert load-file: {e:?}"))?;
let tx_ = tx.clone();
let a = Closure::<dyn FnMut(web_sys::Event)>::new(move |e: web_sys::Event| {
let Some(t) = e.target() else {
error!("could not get target from event");
return;
};
let t = match t.dyn_into::<web_sys::HtmlInputElement>() {
Ok(t) => t,
Err(e) => {
error!("could not cast target to HtmlInputElement: {e:?}");
return;
}
};
let Some(f) = t.files() else {
error!("could not get file list");
return;
};
let Some(f) = f.item(0) else {
error!("could not get file");
return;
};
log::info!("got files {f:?}");
let fut = JsFuture::from(f.array_buffer());
let tx_ = tx_.clone();
wasm_bindgen_futures::spawn_local(async move {
let v = fut.await;
let v = match v {
Ok(v) => v,
Err(e) => {
error!("could not wait for future: {e:?}");
return;
}
};
let Ok(buf) = v.dyn_into::<web_sys::js_sys::ArrayBuffer>() else {
error!("could not cast to ArrayBuffer");
return;
};
let buf = Uint8Array::new(&buf);
let mut dst = vec![0; buf.length() as usize];
buf.copy_to(&mut dst);
if tx_.send(Event::LoadRom(dst)).is_err() {
error!("error loading rom");
}
log::info!("got result {buf:?}");
});
});
file_load.set_onchange(Some(a.as_ref().unchecked_ref()));
std::mem::forget(a);
let mut _audio = None;
let mut audio_data = Some(dev.audio_streams());
let audio_check = document
.get_element_by_id("audio-check")
.ok_or_else(|| anyhow!("could not find audio-check"))?
.dyn_into::<web_sys::HtmlElement>()
.map_err(|e| anyhow!("could not cast to HtmlElement: {e:?}"))?;
let document_for_audio = document.clone();
let a = Closure::<dyn FnMut()>::new(move || {
if let Some(d) = audio_data.take() {
info!("setting up audio");
_audio = audio_setup(d);
}
let audio_check = match document_for_audio.get_element_by_id("audio-check") {
Some(el) => match el.dyn_into::<web_sys::HtmlInputElement>() {
Ok(html_el) => html_el,
Err(e) => {
error!("could not cast audio-check to HtmlInputElement: {e:?}");
return;
}
},
None => {
error!("could not find audio-check");
return;
}
};
if tx.send(Event::SetMuted(!audio_check.checked())).is_err() {
error!("error setting muted flag");
}
});
audio_check.set_onclick(Some(a.as_ref().unchecked_ref()));
std::mem::forget(a);
let resize_closure = Box::new(move |width: u16, height: u16| {
div.style()
.set_css_text(&format!("width: {width}px; height: {height}px"));
footer.style().set_css_text(&format!("width: {width}px"));
});
let canvas = document
.get_element_by_id("varvara")
.ok_or_else(|| anyhow!("could not find canvas element 'varvara'"))?
.dyn_into::<web_sys::HtmlCanvasElement>()
.map_err(|e| anyhow!("could not cast to HtmlCanvasElement: {e:?}"))?;
wasm_bindgen_futures::spawn_local(async move {
if let Err(e) = eframe::WebRunner::new()
.start(
canvas,
options,
Box::new(move |_cc| {
let mut s = Box::new(Stage::new(
vm,
dev,
rx,
crate::stage::StageConfig {
size,
scale: 1.0,
rom_title: String::new(),
transparent: None,
color_transform_name: "shift".to_string(),
color_params: vec![],
effects: crate::stage::EffectsConfig {
efxt: 0.0,
effect_mode: 0,
effect_order: (0..crate::effects::EFFECT_COUNT).collect(),
efx: None,
efxmode: Some("optimal".to_string()),
efx_ndx: 0,
last_effect_switch: 0.0,
},
fit_mode: "fit".to_string(),
mouse_mode: crate::stage::MouseDragMode::None,
mouse_resize: crate::stage::MouseResizeMode::None,
},
None,
));
s.set_resize_callback(resize_closure);
Ok(s)
}),
)
.await
{
error!("eframe start failed: {:?}", e);
}
});
Ok(())
}