use crate::gb::console::gameboy::GameBoy;
use crate::platform::app_context::{AppContext, SharedAppContext};
use crate::platform::frontend_toasts::cartridge_load_toast_message;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct WasmGb {
gb: GameBoy,
audio_muted: bool,
rom_loaded: bool,
pending_toasts: Vec<String>,
}
impl Default for WasmGb {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl WasmGb {
fn rgb_to_rgba(rgb: &[u8]) -> Vec<u8> {
rgb.chunks_exact(3)
.flat_map(|p| [p[0], p[1], p[2], 0xFF])
.collect()
}
fn opaque_black_rgba_frame() -> Vec<u8> {
let pixel_count = (GameBoy::SCREEN_WIDTH * GameBoy::SCREEN_HEIGHT) as usize;
let mut rgba = vec![0u8; pixel_count * 4];
for alpha in rgba.iter_mut().skip(3).step_by(4) {
*alpha = 0xFF;
}
rgba
}
fn run_until_frame_ready(&mut self) {
while !self.gb.is_frame_ready() {
self.gb.run_tick();
}
self.gb.clear_frame_ready();
}
#[wasm_bindgen(constructor)]
pub fn new() -> WasmGb {
console_error_panic_hook::set_once();
let app_context: SharedAppContext = Rc::new(RefCell::new(AppContext::new_with_config(
Default::default(),
)));
WasmGb {
gb: GameBoy::new(app_context),
audio_muted: false,
rom_loaded: false,
pending_toasts: Vec::new(),
}
}
#[wasm_bindgen]
pub fn load_rom(&mut self, rom: &[u8], rom_name: &str) -> Result<(), JsValue> {
self.rom_loaded = false;
match self.gb.load_rom(rom, rom_name) {
Ok(()) => {
self.rom_loaded = true;
self.gb.set_audio_sample_rate(44100.0);
self.pending_toasts
.push(cartridge_load_toast_message(rom_name, true));
web_sys::console::log_1(&JsValue::from_str("GB ROM loaded successfully"));
Ok(())
}
Err(err) => {
self.pending_toasts
.push(cartridge_load_toast_message(rom_name, false));
Err(JsValue::from_str(&err))
}
}
}
#[wasm_bindgen]
pub fn drain_toasts(&mut self) -> Vec<JsValue> {
self.pending_toasts.drain(..).map(JsValue::from).collect()
}
#[wasm_bindgen]
pub fn render_frame_rgba(&mut self) -> Vec<u8> {
if !self.rom_loaded {
return Self::opaque_black_rgba_frame();
}
self.run_until_frame_ready();
let rgb = self.gb.screen_snapshot();
Self::rgb_to_rgba(&rgb)
}
#[wasm_bindgen]
pub fn screen_width(&self) -> u32 {
GameBoy::SCREEN_WIDTH
}
#[wasm_bindgen]
pub fn screen_height(&self) -> u32 {
GameBoy::SCREEN_HEIGHT
}
#[wasm_bindgen]
pub fn frame_rate_hz(&self) -> f64 {
4_194_304.0 / 70_224.0
}
fn drain_audio_buffer(&mut self) {
while self.gb.get_sample().is_some() {}
}
#[wasm_bindgen]
pub fn get_audio_samples(&mut self) -> Vec<f32> {
if self.audio_muted {
self.drain_audio_buffer();
return Vec::new();
}
let mut samples = Vec::new();
while let Some(s) = self.gb.get_sample() {
samples.push(s);
}
samples
}
#[wasm_bindgen]
pub fn set_audio_sample_rate(&mut self, sample_rate: f32) {
self.gb.set_audio_sample_rate(sample_rate);
}
#[wasm_bindgen]
pub fn set_audio_muted(&mut self, muted: bool) {
self.audio_muted = muted;
if muted {
self.drain_audio_buffer();
}
}
#[wasm_bindgen]
pub fn is_audio_muted(&self) -> bool {
self.audio_muted
}
#[wasm_bindgen]
pub fn set_button(&mut self, controller: u8, button: u8, pressed: bool) {
if controller == 1 {
self.gb.set_button(button, pressed);
}
}
#[wasm_bindgen]
pub fn reset(&mut self, soft_reset: bool) {
self.gb.reset(soft_reset);
}
#[wasm_bindgen]
pub fn save_state_bytes(&self) -> Vec<u8> {
self.gb.save_state_bytes().unwrap_or_default()
}
#[wasm_bindgen]
pub fn load_state_bytes(&mut self, bytes: &[u8]) -> Result<(), JsValue> {
self.gb
.load_state_bytes(bytes)
.map_err(|e| JsValue::from_str(&e))
}
}