use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen::Clamped; use js_sys::Float32Array; use web_sys::AudioContext; use web_sys::CanvasRenderingContext2d; use rustboyadvance_core::keypad as gba_keypad; use rustboyadvance_core::prelude::*; use rustboyadvance_utils::audio::AudioRingBuffer; use bit::BitIndex; #[wasm_bindgen] pub struct Emulator { gba: GameBoyAdvance, interface: Rc<RefCell<Interface>>, } struct Interface { frame: Vec<u8>, keyinput: u16, sample_rate: i32, audio_ctx: AudioContext, audio_ring_buffer: AudioRingBuffer, } impl Drop for Interface { fn drop(&mut self) { let _ = self.audio_ctx.clone(); } } impl Interface { fn new(audio_ctx: AudioContext) -> Result<Interface, JsValue> { Ok(Interface { frame: vec![0; 240 * 160 * 4], keyinput: gba_keypad::KEYINPUT_ALL_RELEASED, sample_rate: audio_ctx.sample_rate() as i32, audio_ctx: audio_ctx, audio_ring_buffer: Default::default(), }) } } impl VideoInterface for Interface { fn render(&mut self, buffer: &[u32]) { // TODO optimize for i in 0..buffer.len() { let color = buffer[i]; self.frame[4 * i + 0] = ((color >> 16) & 0xff) as u8; self.frame[4 * i + 1] = ((color >> 8) & 0xff) as u8; self.frame[4 * i + 2] = (color & 0xff) as u8; self.frame[4 * i + 3] = 255; } } } fn convert_sample(s: i16) -> f32 { (s as f32) / 32767_f32 } impl AudioInterface for Interface { fn get_sample_rate(&self) -> i32 { self.sample_rate } fn push_sample(&mut self, samples: &[i16]) { let prod = self.audio_ring_buffer.producer(); for s in samples.iter() { let _ = prod.push(*s); } } } impl InputInterface for Interface { fn poll(&mut self) -> u16 { self.keyinput } } #[wasm_bindgen] impl Emulator { #[wasm_bindgen(constructor)] pub fn new(bios: &[u8], rom: &[u8]) -> Result<Emulator, JsValue> { let audio_ctx = web_sys::AudioContext::new()?; let interface = Rc::new(RefCell::new(Interface::new(audio_ctx)?)); let gamepak = GamepakBuilder::new() .take_buffer(rom.to_vec().into_boxed_slice()) .without_backup_to_file() .build() .unwrap(); let gba = GameBoyAdvance::new( bios.to_vec().into_boxed_slice(), gamepak, interface.clone(), interface.clone(), interface.clone(), ); Ok(Emulator { gba, interface }) } pub fn skip_bios(&mut self) { self.gba.skip_bios(); } pub fn run_frame(&mut self, ctx: &CanvasRenderingContext2d) -> Result<(), JsValue> { self.gba.frame(); let mut frame_buffer = &mut self.interface.borrow_mut().frame; let data = web_sys::ImageData::new_with_u8_clamped_array_and_sh( Clamped(&mut frame_buffer), 240, 160, ) .unwrap(); ctx.put_image_data(&data, 0.0, 0.0) } fn map_key(event_key: &str) -> Option<gba_keypad::Keys> { match event_key { "Enter" => Some(gba_keypad::Keys::Start), "Backspace" => Some(gba_keypad::Keys::Select), "ArrowUp" => Some(gba_keypad::Keys::Up), "ArrowDown" => Some(gba_keypad::Keys::Down), "ArrowLeft" => Some(gba_keypad::Keys::Left), "ArrowRight" => Some(gba_keypad::Keys::Right), "z" => Some(gba_keypad::Keys::ButtonB), "x" => Some(gba_keypad::Keys::ButtonA), "a" => Some(gba_keypad::Keys::ButtonL), "s" => Some(gba_keypad::Keys::ButtonR), _ => None, } } pub fn key_down(&mut self, event_key: &str) { debug!("Key down: {}", event_key); let mut interface = self.interface.borrow_mut(); if let Some(key) = Emulator::map_key(event_key) { interface.keyinput.set_bit(key as usize, false); } } pub fn key_up(&mut self, event_key: &str) { debug!("Key up: {}", event_key); let mut interface = self.interface.borrow_mut(); if let Some(key) = Emulator::map_key(event_key) { interface.keyinput.set_bit(key as usize, true); } } pub fn test_fps(&mut self) { use rustboyadvance_utils::FpsCounter; let mut fps_counter = FpsCounter::default(); self.gba.skip_bios(); for _ in 0..6000 { self.gba.frame(); if let Some(fps) = fps_counter.tick() { info!("FPS: {}", fps); } } } pub fn collect_audio_samples(&self) -> Result<Float32Array, JsValue> { let mut interface = self.interface.borrow_mut(); let consumer = interface.audio_ring_buffer.consumer(); let mut samples = Vec::with_capacity(consumer.len()); while let Some(sample) = consumer.pop() { samples.push(convert_sample(sample)); } Ok(Float32Array::from(samples.as_slice())) } }