1#![cfg(target_arch = "wasm32")] 2 3mod audio; 4mod config; 5mod js; 6mod save; 7 8use crate::audio::AudioQueue; 9use crate::config::{EmulatorChannel, EmulatorCommand, InputConfig, WebConfig, WebConfigRef}; 10use bincode::{Decode, Encode}; 11use cdrom::reader::CdRom; 12use gba_config::GbaInputs; 13use gba_core::api::GameBoyAdvanceEmulator; 14use genesis_core::api::GenesisHardware; 15use genesis_core::{GenesisEmulator, GenesisInputs}; 16use jgenesis_common::audio::DynamicResamplingRate; 17use jgenesis_common::frontend::{ 18 AudioOutput, Color, ConstantInputPoller, EmulatorTrait, FrameSize, RenderFrameOptions, 19 Renderer, SaveWriter, TickEffect, 20}; 21use jgenesis_renderer::renderer::{WgpuRenderer, WindowSize}; 22use js_sys::Uint8Array; 23use rfd::AsyncFileDialog; 24use smsgg_config::SmsGgInputs; 25use smsgg_core::{SmsGgEmulator, SmsGgHardware}; 26use snes_core::api::{CoprocessorRoms, SnesEmulator}; 27use snes_core::input::SnesInputs; 28use std::collections::HashMap; 29use std::error::Error; 30use std::fmt::{Debug, Display, Formatter}; 31use std::mem; 32use std::path::Path; 33use std::rc::Rc; 34use std::time::Duration; 35use wasm_bindgen::prelude::*; 36use wasm_bindgen_futures::JsFuture; 37use web_sys::{AudioContext, AudioContextOptions, Performance}; 38use web_time::Instant; 39use winit::event::{ElementState, Event, KeyEvent, WindowEvent}; 40use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy}; 41use winit::keyboard::{KeyCode, PhysicalKey}; 42use winit::platform::web::WindowExtWebSys; 43use winit::window::{Fullscreen, Window, WindowAttributes}; 44 45struct WebAudioOutput { 46 audio_ctx: AudioContext, 47 audio_queue: AudioQueue, 48 audio_started: bool, 49} 50 51impl WebAudioOutput { 52 fn new(audio_ctx: AudioContext) -> Self { 53 Self { audio_ctx, audio_queue: AudioQueue::new(), audio_started: false } 54 } 55 56 fn suspend(&mut self) { 57 // Suspending the AudioContext while loading/resetting is necessary to avoid audio delay 58 // in Chrome 59 let _ = self.audio_ctx.suspend(); 60 self.audio_started = false; 61 } 62} 63 64impl AudioOutput for WebAudioOutput { 65 type Err = String; 66 67 fn push_sample(&mut self, sample_l: f64, sample_r: f64) -> Result<(), Self::Err> { 68 if !self.audio_started { 69 self.audio_started = true; 70 let _ = self.audio_ctx.resume(); 71 } 72 73 self.audio_queue 74 .push_if_space((sample_l as f32, sample_r as f32)) 75 .map_err(|err| format!("{err:?}"))?; 76 Ok(()) 77 } 78}
1MB should be big enough for any save file
81const SERIALIZATION_BUFFER_LEN: usize = 1024 * 1024;
83macro_rules! bincode_config { 84 () => { 85 bincode::config::standard() 86 .with_little_endian() 87 .with_fixed_int_encoding() 88 .with_limit::<{ 10 * 1024 * 1024 }>() 89 }; 90} 91 92struct IndexedDbSaveWriter { 93 file_name: Rc<str>, 94 extension_to_bytes: HashMap<String, Vec<u8>>, 95 serialization_buffer: Box<[u8]>, 96} 97 98impl IndexedDbSaveWriter { 99 fn new() -> Self { 100 let serialization_buffer = vec![0; SERIALIZATION_BUFFER_LEN].into_boxed_slice(); 101 Self { 102 file_name: String::new().into(), 103 extension_to_bytes: HashMap::new(), 104 serialization_buffer, 105 } 106 } 107 108 fn update_file_name( 109 &mut self, 110 file_name: String, 111 extension_to_bytes: HashMap<String, Vec<u8>>, 112 ) { 113 self.file_name = file_name.into(); 114 self.extension_to_bytes = extension_to_bytes; 115 } 116} 117 118impl SaveWriter for IndexedDbSaveWriter { 119 type Err = String; 120 121 fn load_bytes(&mut self, extension: &str) -> Result<Vec<u8>, Self::Err> { 122 match self.extension_to_bytes.get(extension) { 123 Some(bytes) => Ok(bytes.clone()), 124 None => Err(format!("No save file found for extension {extension}")), 125 } 126 } 127 128 fn persist_bytes(&mut self, extension: &str, bytes: &[u8]) -> Result<(), Self::Err> { 129 self.extension_to_bytes.insert(extension.into(), bytes.to_vec()); 130 131 let file_name = Rc::clone(&self.file_name); 132 let extension = extension.to_string(); 133 let bytes = bytes.to_vec(); 134 wasm_bindgen_futures::spawn_local(async move { 135 save::write(&file_name, &extension, &bytes).await; 136 }); 137 138 Ok(()) 139 } 140 141 fn load_serialized<D: Decode<()>>(&mut self, extension: &str) -> Result<D, Self::Err> { 142 match self.extension_to_bytes.get(extension) { 143 Some(bytes) => { 144 let (value, _) = 145 bincode::decode_from_slice(bytes, bincode_config!()).map_err(|err| { 146 format!("Error deserializing value for {}: {err}", self.file_name) 147 })?; 148 Ok(value) 149 } 150 None => Err(format!("No save file found for extension {extension}")), 151 } 152 } 153 154 fn persist_serialized<E: Encode>(&mut self, extension: &str, data: E) -> Result<(), Self::Err> { 155 let bytes_len = 156 bincode::encode_into_slice(data, &mut self.serialization_buffer, bincode_config!()) 157 .map_err(|err| format!("Error serializing value: {err}"))?; 158 159 let bytes = self.serialization_buffer[..bytes_len].to_vec(); 160 self.extension_to_bytes.insert(extension.into(), bytes.clone()); 161 162 let file_name = Rc::clone(&self.file_name); 163 let extension = extension.to_string(); 164 wasm_bindgen_futures::spawn_local(async move { 165 save::write(&file_name, &extension, &bytes).await; 166 }); 167 168 Ok(()) 169 } 170}
Panics
This function will panic if it cannot initialize the console logger.
181const STATIC_FRAME_SIZE: FrameSize = FrameSize { width: 878 / 4, height: 672 / 4 }; 182const STATIC_FRAME_LEN: usize = (STATIC_FRAME_SIZE.width * STATIC_FRAME_SIZE.height) as usize; 183 184struct RandomNoiseGenerator { 185 buffer: Vec<Color>, 186} 187 188impl RandomNoiseGenerator { 189 fn new() -> Self { 190 Self { buffer: vec![Color::default(); STATIC_FRAME_LEN] } 191 } 192 193 fn randomize(&mut self) { 194 for color in &mut self.buffer { 195 *color = Color::rgb(rand::random(), rand::random(), rand::random()); 196 } 197 } 198 199 fn render<R: Renderer>(&self, renderer: &mut R) -> Result<(), R::Err> { 200 renderer.render_frame(&self.buffer, STATIC_FRAME_SIZE, 60.0, RenderFrameOptions::default()) 201 } 202} 203 204struct QueuedFrame { 205 buffer: Vec<Color>, 206 size: FrameSize, 207 target_fps: f64, 208 options: RenderFrameOptions, 209 queued: bool, 210} 211 212impl QueuedFrame { 213 fn new() -> Self { 214 Self { 215 buffer: Vec::with_capacity(320 * 224), 216 size: FrameSize { width: 320, height: 224 }, 217 target_fps: 60.0, 218 options: RenderFrameOptions::default(), 219 queued: false, 220 } 221 } 222} 223 224impl Renderer for QueuedFrame { 225 type Err = String; 226 227 fn render_frame( 228 &mut self, 229 frame_buffer: &[Color], 230 frame_size: FrameSize, 231 target_fps: f64, 232 options: RenderFrameOptions, 233 ) -> Result<(), Self::Err> { 234 self.buffer.clear(); 235 self.buffer.extend(&frame_buffer[..(frame_size.width * frame_size.height) as usize]); 236 237 self.size = frame_size; 238 self.target_fps = target_fps; 239 self.options = options; 240 self.queued = true; 241 242 Ok(()) 243 } 244} 245 246#[allow(clippy::large_enum_variant)] 247enum Emulator { 248 None(RandomNoiseGenerator), 249 SmsGg(SmsGgEmulator, SmsGgInputs), 250 Genesis(GenesisEmulator, GenesisInputs), 251 Snes(SnesEmulator, SnesInputs), 252 Gba(GameBoyAdvanceEmulator, GbaInputs), 253} 254 255impl Emulator { 256 fn render_frame<R: Renderer, A: AudioOutput, S: SaveWriter>( 257 &mut self, 258 renderer: &mut R, 259 audio_output: &mut A, 260 save_writer: &mut S, 261 ) { 262 macro_rules! run_emulator { 263 ($emulator:expr, $inputs:expr) => { 264 while $emulator 265 .tick(renderer, audio_output, &mut ConstantInputPoller($inputs), save_writer) 266 .expect("Emulator error") 267 != TickEffect::FrameRendered 268 {} 269 }; 270 } 271 272 match self { 273 Self::None(noise_generator) => { 274 noise_generator.randomize(); 275 noise_generator.render(renderer).expect("Failed to render random noise"); 276 } 277 Self::SmsGg(emulator, inputs) => run_emulator!(emulator, inputs), 278 Self::Genesis(emulator, inputs) => run_emulator!(emulator, inputs), 279 Self::Snes(emulator, inputs) => run_emulator!(emulator, inputs), 280 Self::Gba(emulator, inputs) => run_emulator!(emulator, inputs), 281 } 282 } 283 284 fn reset(&mut self, save_writer: &mut IndexedDbSaveWriter) { 285 match self { 286 Self::None(..) => {} 287 Self::SmsGg(emulator, ..) => { 288 emulator.hard_reset(save_writer); 289 } 290 Self::Genesis(emulator, ..) => { 291 emulator.hard_reset(save_writer); 292 } 293 Self::Snes(emulator, ..) => { 294 emulator.hard_reset(save_writer); 295 } 296 Self::Gba(emulator, ..) => { 297 emulator.hard_reset(save_writer); 298 } 299 } 300 } 301 302 fn target_fps(&self) -> f64 { 303 match self { 304 Self::None(..) => 30.0, 305 Self::SmsGg(emulator, ..) => emulator.target_fps(), 306 Self::Genesis(emulator, ..) => emulator.target_fps(), 307 Self::Snes(emulator, ..) => emulator.target_fps(), 308 Self::Gba(emulator, ..) => emulator.target_fps(), 309 } 310 } 311 312 fn handle_window_event(&mut self, input_config: &InputConfig, event: &WindowEvent) { 313 let WindowEvent::KeyboardInput { 314 event: KeyEvent { physical_key: PhysicalKey::Code(keycode), state, .. }, 315 .. 316 } = event 317 else { 318 return; 319 }; 320 let pressed = *state == ElementState::Pressed; 321 322 match self { 323 Self::None(..) => {} 324 Self::SmsGg(_, inputs) => { 325 input_config.smsgg.handle_input(*keycode, pressed, inputs); 326 } 327 Self::Genesis(_, inputs) => { 328 input_config.genesis.handle_input(*keycode, pressed, inputs); 329 } 330 Self::Snes(_, inputs) => { 331 input_config.snes.handle_input(*keycode, pressed, inputs); 332 } 333 Self::Gba(_, inputs) => { 334 input_config.gba.handle_input(*keycode, pressed, inputs); 335 } 336 } 337 } 338 339 fn reload_config(&mut self, config: &WebConfig) { 340 match self { 341 Self::None(..) => {} 342 Self::SmsGg(emulator, ..) => { 343 emulator.reload_config(&config.smsgg.to_emulator_config()); 344 } 345 Self::Genesis(emulator, ..) => { 346 emulator.reload_config(&config.to_genesis_config()); 347 } 348 Self::Snes(emulator, ..) => { 349 emulator.reload_config(&config.snes.to_emulator_config()); 350 } 351 Self::Gba(emulator, ..) => { 352 emulator.reload_config(&config.gba.to_emulator_config()); 353 } 354 } 355 } 356 357 fn rom_title(&mut self, current_file_name: &str) -> String { 358 match self { 359 Self::None(..) => "(No ROM loaded)".into(), 360 Self::SmsGg(..) | Self::Gba(..) => current_file_name.into(), 361 Self::Genesis(emulator, ..) => emulator.game_title().unwrap_or("(No title)".into()), 362 Self::Snes(emulator, ..) => emulator.cartridge_title(), 363 } 364 } 365 366 fn has_persistent_save(&self) -> bool { 367 match self { 368 Self::None(..) => false, 369 Self::SmsGg(emulator, ..) => emulator.has_sram(), 370 Self::Genesis(emulator, ..) => emulator.has_sram(), 371 Self::Snes(emulator, ..) => emulator.has_sram(), 372 Self::Gba(emulator, ..) => emulator.has_save_memory(), 373 } 374 } 375 376 fn update_audio_output_frequency(&mut self, output_frequency: u64) { 377 match self { 378 Self::None(..) => {} 379 Self::SmsGg(emulator, ..) => emulator.update_audio_output_frequency(output_frequency), 380 Self::Genesis(emulator, ..) => emulator.update_audio_output_frequency(output_frequency), 381 Self::Snes(emulator, ..) => emulator.update_audio_output_frequency(output_frequency), 382 Self::Gba(emulator, ..) => emulator.update_audio_output_frequency(output_frequency), 383 } 384 } 385} 386 387#[derive(Debug, Clone)] 388enum JgenesisUserEvent { 389 FileOpen { 390 rom: Vec<u8>, 391 bios_rom: Option<Vec<u8>>, 392 rom_file_name: String, 393 save_files: HashMap<String, Vec<u8>>, 394 }, 395 UploadSaveFile { 396 contents: Vec<u8>, 397 }, 398} 399 400const CANVAS_WIDTH: u32 = 878; 401const CANVAS_HEIGHT: u32 = 672; 402const CANVAS_SIZE: WindowSize = 403 WindowSize { width: CANVAS_WIDTH, height: CANVAS_HEIGHT, pixel_density: 1.0 };
Panics
406#[wasm_bindgen] 407pub async fn run_emulator(config_ref: WebConfigRef, emulator_channel: EmulatorChannel) { 408 if !wgpu::util::is_browser_webgpu_supported().await { 409 js::showWebGpuUnsupportedMessage(); 410 return; 411 } 412 413 let event_loop = EventLoop::<JgenesisUserEvent>::with_user_event() 414 .build() 415 .expect("Failed to create winit event loop"); 416 417 #[allow(deprecated)] 418 let window = 419 event_loop.create_window(WindowAttributes::default()).expect("Unable to create window"); 420 421 web_sys::window() 422 .and_then(|window| window.document()) 423 .and_then(|document| { 424 let dst = document.get_element_by_id("jgenesis-wasm")?; 425 let canvas = web_sys::Element::from(window.canvas()?); 426 dst.append_child(&canvas).ok()?; 427 Some(()) 428 }) 429 .expect("Unable to append canvas to document"); 430 431 let renderer_config = config_ref.borrow().to_renderer_config(); 432 let mut renderer = WgpuRenderer::new(window, CANVAS_SIZE, renderer_config) 433 .await 434 .expect("Unable to create wgpu renderer"); 435 436 // Render a blank gray frame 437 renderer 438 .render_frame( 439 &[Color::rgb(128, 128, 128)], 440 FrameSize { width: 1, height: 1 }, 441 60.0, 442 RenderFrameOptions::default(), 443 ) 444 .expect("Unable to render blank frame"); 445 446 let audio_ctx_options = AudioContextOptions::new(); 447 audio_ctx_options.set_sample_rate(audio::SAMPLE_RATE as f32); 448 449 let audio_ctx = AudioContext::new_with_context_options(&audio_ctx_options) 450 .expect("Unable to create audio context"); 451 let audio_output = WebAudioOutput::new(audio_ctx); 452 let _audio_worklet = 453 audio::initialize_audio_worklet(&audio_output.audio_ctx, &audio_output.audio_queue) 454 .await 455 .expect("Unable to initialize audio worklet"); 456 457 let save_writer = IndexedDbSaveWriter::new(); 458 459 js::showUi(); 460 461 run_event_loop( 462 event_loop, 463 AppState::new(renderer, audio_output, save_writer, config_ref, emulator_channel), 464 ); 465}
467struct AppState { 468 renderer: WgpuRenderer<Window>, 469 audio_output: WebAudioOutput, 470 save_writer: IndexedDbSaveWriter, 471 config_ref: WebConfigRef, 472 current_config: WebConfig, 473 emulator_channel: EmulatorChannel, 474 emulator: Emulator, 475 dynamic_resampling_rate: DynamicResamplingRate, 476 queued_frame: QueuedFrame, 477 performance: Performance, 478 next_frame_time_ms: f64, 479 waiting_for_input: Option<String>, 480} 481 482impl AppState { 483 fn new( 484 renderer: WgpuRenderer<Window>, 485 audio_output: WebAudioOutput, 486 save_writer: IndexedDbSaveWriter, 487 config_ref: WebConfigRef, 488 emulator_channel: EmulatorChannel, 489 ) -> Self { 490 let current_config = config_ref.borrow().clone(); 491 let emulator = Emulator::None(RandomNoiseGenerator::new()); 492 let dynamic_resampling_rate = 493 DynamicResamplingRate::new(audio::SAMPLE_RATE, audio::BUFFER_LEN_SAMPLES / 2); 494 let queued_frame = QueuedFrame::new(); 495 let performance = web_sys::window() 496 .and_then(|window| window.performance()) 497 .expect("Unable to get window.performance"); 498 let next_frame_time_ms = performance.now(); 499 500 Self { 501 renderer, 502 audio_output, 503 save_writer, 504 config_ref, 505 current_config, 506 emulator_channel, 507 emulator, 508 dynamic_resampling_rate, 509 queued_frame, 510 performance, 511 next_frame_time_ms, 512 waiting_for_input: None, 513 } 514 } 515} 516 517impl AppState { 518 fn handle_file_open( 519 &mut self, 520 rom: Vec<u8>, 521 bios_rom: Option<Vec<u8>>, 522 save_files: HashMap<String, Vec<u8>>, 523 rom_file_name: String, 524 ) { 525 self.audio_output.suspend(); 526 self.dynamic_resampling_rate = 527 DynamicResamplingRate::new(audio::SAMPLE_RATE, audio::BUFFER_LEN_SAMPLES / 2); 528 529 let prev_file_name = Rc::clone(&self.save_writer.file_name); 530 let prev_save_files = mem::take(&mut self.save_writer.extension_to_bytes); 531 self.save_writer.update_file_name(rom_file_name.clone(), save_files); 532 self.emulator = match open_emulator( 533 rom, 534 bios_rom, 535 &rom_file_name, 536 &self.config_ref, 537 &mut self.save_writer, 538 ) { 539 Ok(emulator) => emulator, 540 Err(err) => { 541 js::alert(&format!("Error opening ROM file: {err}")); 542 self.save_writer.update_file_name(prev_file_name.to_string(), prev_save_files); 543 return; 544 } 545 }; 546 547 self.emulator_channel.set_current_file_name(rom_file_name.clone()); 548 549 self.renderer.reload_config(self.config_ref.borrow().to_renderer_config()); 550 551 js::setRomTitle(&self.emulator.rom_title(&rom_file_name)); 552 js::setSaveUiEnabled(self.emulator.has_persistent_save()); 553 554 js::focusCanvas(); 555 } 556 557 fn handle_upload_save_file(&mut self, contents: Vec<u8>) { 558 if matches!(self.emulator, Emulator::None(..)) { 559 return; 560 } 561 562 self.audio_output.suspend(); 563 564 self.save_writer.extension_to_bytes.insert("sav".into(), contents.clone()); 565 566 // Immediately persist save file because it won't get written again until the game writes to SRAM 567 let file_name = self.emulator_channel.current_file_name(); 568 wasm_bindgen_futures::spawn_local(async move { 569 save::write(&file_name, "sav", &contents).await; 570 }); 571 572 self.emulator.reset(&mut self.save_writer); 573 574 js::focusCanvas(); 575 } 576 577 fn handle_about_to_wait( 578 &mut self, 579 event_loop_proxy: &EventLoopProxy<JgenesisUserEvent>, 580 elwt: &ActiveEventLoop, 581 ) { 582 if self.waiting_for_input.is_some() { 583 elwt.set_control_flow(ControlFlow::WaitUntil( 584 Instant::now() + Duration::from_millis(1), 585 )); 586 return; 587 } 588 589 if !self.queued_frame.queued { 590 // No frame queued; run emulator until it renders the next frame 591 self.emulator.render_frame( 592 &mut self.queued_frame, 593 &mut self.audio_output, 594 &mut self.save_writer, 595 ); 596 } 597 598 let now = self.performance.now(); 599 if now < self.next_frame_time_ms { 600 let wait_until_duration = 601 Duration::from_micros((1000.0 * (self.next_frame_time_ms - now)).ceil() as u64); 602 elwt.set_control_flow(ControlFlow::WaitUntil(Instant::now() + wait_until_duration)); 603 return; 604 } 605 606 let fps = self.emulator.target_fps(); 607 let frame_interval_ms = 1000.0 / fps; 608 if self.next_frame_time_ms < now - 5.0 * frame_interval_ms { 609 // Too far behind; catch up 610 self.next_frame_time_ms = now + frame_interval_ms; 611 } else { 612 self.next_frame_time_ms += frame_interval_ms; 613 } 614 615 self.renderer 616 .render_frame( 617 &self.queued_frame.buffer, 618 self.queued_frame.size, 619 self.queued_frame.target_fps, 620 self.queued_frame.options, 621 ) 622 .expect("Frame render error"); 623 self.queued_frame.queued = false; 624 625 // GBA may not detect persistent memory until the emulator has been running for a bit, so 626 // call this after every frame 627 js::setSaveUiEnabled(self.emulator.has_persistent_save()); 628 629 self.dynamic_resampling_rate.adjust(self.audio_output.audio_queue.len().unwrap()); 630 self.emulator.update_audio_output_frequency( 631 self.dynamic_resampling_rate.current_output_frequency().into(), 632 ); 633 634 let config = self.config_ref.borrow().clone(); 635 if config != self.current_config { 636 config.save_to_local_storage(); 637 638 self.renderer.reload_config(config.to_renderer_config()); 639 self.emulator.reload_config(&config); 640 self.current_config = config; 641 } 642 643 self.drain_emulator_channel(event_loop_proxy); 644 } 645 646 fn drain_emulator_channel(&mut self, event_loop_proxy: &EventLoopProxy<JgenesisUserEvent>) { 647 while let Some(command) = self.emulator_channel.pop_command() { 648 match command { 649 EmulatorCommand::OpenFile => { 650 wasm_bindgen_futures::spawn_local(open_file(event_loop_proxy.clone())); 651 } 652 EmulatorCommand::OpenSegaCdBios => { 653 wasm_bindgen_futures::spawn_local(open_bios(SCD_BIOS_KEY, &["bin"])); 654 } 655 EmulatorCommand::OpenGbaBios => { 656 wasm_bindgen_futures::spawn_local(open_bios(GBA_BIOS_KEY, &["bin"])); 657 } 658 EmulatorCommand::UploadSaveFile => { 659 wasm_bindgen_futures::spawn_local(upload_save_file(event_loop_proxy.clone())); 660 } 661 EmulatorCommand::Reset => { 662 self.audio_output.suspend(); 663 664 self.emulator.reset(&mut self.save_writer); 665 666 js::focusCanvas(); 667 } 668 EmulatorCommand::ConfigureInput { name } => { 669 self.waiting_for_input = Some(name); 670 671 js::beforeInputConfigure(); 672 js::focusCanvas(); 673 } 674 } 675 } 676 } 677 678 fn handle_window_event(&mut self, window_event: WindowEvent, elwt: &ActiveEventLoop) { 679 match &self.waiting_for_input { 680 Some(button) => { 681 if let WindowEvent::KeyboardInput { 682 event: 683 KeyEvent { 684 physical_key: PhysicalKey::Code(keycode), 685 state: ElementState::Pressed, 686 .. 687 }, 688 .. 689 } = &window_event 690 { 691 let input_config = &mut self.config_ref.borrow_mut().inputs; 692 match &self.emulator { 693 Emulator::None(..) => {} 694 Emulator::SmsGg(..) => input_config.smsgg.update_field(button, *keycode), 695 Emulator::Genesis(..) => { 696 input_config.genesis.update_field(button, *keycode); 697 } 698 Emulator::Snes(..) => input_config.snes.update_field(button, *keycode), 699 Emulator::Gba(..) => input_config.gba.update_field(button, *keycode), 700 } 701 702 js::afterInputConfigure(button, &format!("{keycode:?}")); 703 self.waiting_for_input = None; 704 } 705 } 706 None => { 707 self.emulator.handle_window_event(&self.config_ref.borrow().inputs, &window_event); 708 } 709 } 710 711 match window_event { 712 WindowEvent::CloseRequested => { 713 elwt.exit(); 714 } 715 WindowEvent::Resized(_) => { 716 self.renderer.handle_resize(CANVAS_SIZE); 717 718 // Show cursor only when not fullscreen 719 js::setCursorVisible(self.renderer.window().fullscreen().is_none()); 720 721 js::setFullscreen(self.renderer.window().fullscreen().is_some()); 722 } 723 WindowEvent::KeyboardInput { 724 event: 725 KeyEvent { 726 physical_key: PhysicalKey::Code(KeyCode::F8), 727 state: ElementState::Pressed, 728 .. 729 }, 730 .. 731 } => { 732 // Toggle fullscreen 733 let new_fullscreen = match self.renderer.window().fullscreen() { 734 None => Some(Fullscreen::Borderless(None)), 735 Some(_) => None, 736 }; 737 // SAFETY: Not reassigning the window 738 unsafe { 739 self.renderer.window_mut().set_fullscreen(new_fullscreen); 740 } 741 } 742 _ => {} 743 } 744 } 745} 746 747fn run_event_loop(event_loop: EventLoop<JgenesisUserEvent>, mut state: AppState) { 748 event_loop.set_control_flow(ControlFlow::Poll); 749 750 let event_loop_proxy = event_loop.create_proxy(); 751 752 #[allow(deprecated)] 753 event_loop 754 .run(move |event, elwt| match event { 755 Event::UserEvent(user_event) => match user_event { 756 JgenesisUserEvent::FileOpen { rom, bios_rom, rom_file_name, save_files } => { 757 state.handle_file_open(rom, bios_rom, save_files, rom_file_name); 758 } 759 JgenesisUserEvent::UploadSaveFile { contents } => { 760 state.handle_upload_save_file(contents); 761 } 762 }, 763 Event::AboutToWait => { 764 state.handle_about_to_wait(&event_loop_proxy, elwt); 765 } 766 Event::WindowEvent { event: window_event, window_id } 767 if window_id == state.renderer.window().id() => 768 { 769 state.handle_window_event(window_event, elwt); 770 } 771 _ => {} 772 }) 773 .unwrap(); 774} 775 776async fn open_file(event_loop_proxy: EventLoopProxy<JgenesisUserEvent>) { 777 let file = AsyncFileDialog::new() 778 .add_filter( 779 "Supported Files", 780 &["sms", "gg", "gen", "md", "bin", "smd", "chd", "32x", "sfc", "smc", "gba"], 781 ) 782 .add_filter("All Types", &["*"]) 783 .pick_file() 784 .await; 785 let Some(file) = file else { return }; 786 787 let file_name = file.file_name(); 788 let extension = 789 Path::new(&file_name).extension().map(|ext| ext.to_string_lossy().to_ascii_lowercase()); 790 791 let bios_rom = match extension.as_deref() { 792 Some("chd") => { 793 let Some(bios_rom) = read_bios_from_idb(SCD_BIOS_KEY).await else { 794 js::alert("Sega CD emulation requires a Sega CD BIOS ROM to be configured"); 795 return; 796 }; 797 Some(bios_rom) 798 } 799 Some("gba") => { 800 let Some(bios_rom) = read_bios_from_idb(GBA_BIOS_KEY).await else { 801 js::alert("GBA emulation requires a GBA BIOS ROM to be configured"); 802 return; 803 }; 804 Some(bios_rom) 805 } 806 _ => None, 807 }; 808 809 let contents = file.read().await; 810 811 let save_files = save::load_all(&file_name).await; 812 813 event_loop_proxy 814 .send_event(JgenesisUserEvent::FileOpen { 815 rom: contents, 816 bios_rom, 817 rom_file_name: file_name, 818 save_files, 819 }) 820 .expect("Unable to send file opened event"); 821} 822 823async fn open_bios(key: &str, supported_extensions: &[&str]) { 824 let file = AsyncFileDialog::new() 825 .add_filter("Supported Files", supported_extensions) 826 .add_filter("All Types", &["*"]) 827 .pick_file() 828 .await; 829 let Some(file) = file else { return }; 830 831 let contents = file.read().await; 832 write_bios_to_idb(key, &contents).await; 833} 834 835async fn upload_save_file(event_loop_proxy: EventLoopProxy<JgenesisUserEvent>) { 836 let file = AsyncFileDialog::new().add_filter("sav", &["sav", "srm"]).pick_file().await; 837 let Some(file) = file else { return }; 838 839 let contents = file.read().await; 840 841 event_loop_proxy 842 .send_event(JgenesisUserEvent::UploadSaveFile { contents }) 843 .expect("Unable to send upload save file event"); 844} 845 846enum OpenEmulatorError { 847 NoSegaCdBios, 848 NoGbaBios, 849 Other(Box<dyn Error>), 850} 851 852impl Display for OpenEmulatorError { 853 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 854 match self { 855 Self::NoSegaCdBios => { 856 write!(f, "No Sega CD BIOS is configured; required for Sega CD emulation") 857 } 858 Self::NoGbaBios => write!(f, "No GBA BIOS is configured; required for GBA emulation"), 859 Self::Other(err) => write!(f, "{err}"), 860 } 861 } 862} 863 864const SCD_BIOS_KEY: &str = "segacd-bios-rom"; 865const GBA_BIOS_KEY: &str = "gba-bios-rom"; 866 867async fn read_bios_from_idb(key: &str) -> Option<Vec<u8>> { 868 try_read_bios_from_idb(key).await.unwrap_or_else(|err| { 869 log::error!( 870 "Error reading BIOS ROM from IndexedDB: {}", 871 err.as_string().unwrap_or_default() 872 ); 873 None 874 }) 875} 876 877async fn try_read_bios_from_idb(key: &str) -> Result<Option<Vec<u8>>, JsValue> { 878 let object = JsFuture::from(js::loadBios(key)).await?; 879 if object.is_null() { 880 return Ok(None); 881 } 882 883 let array = object.dyn_into::<Uint8Array>()?; 884 Ok(Some(array.to_vec())) 885} 886 887async fn write_bios_to_idb(key: &str, bytes: &[u8]) { 888 let array = Uint8Array::new_from_slice(bytes); 889 if let Err(err) = JsFuture::from(js::writeBios(key, array)).await { 890 log::error!("Error saving BIOS ROM to IndexedDB: {}", err.as_string().unwrap_or_default()); 891 } 892} 893 894#[allow(clippy::map_unwrap_or)] 895fn open_emulator( 896 rom: Vec<u8>, 897 bios_rom: Option<Vec<u8>>, 898 rom_file_name: &str, 899 config_ref: &WebConfigRef, 900 save_writer: &mut IndexedDbSaveWriter, 901) -> Result<Emulator, OpenEmulatorError> { 902 let file_ext = Path::new(rom_file_name).extension().map(|ext| ext.to_string_lossy().to_ascii_lowercase()).unwrap_or_else(|| { 903 log::warn!("Unable to determine file extension of uploaded file; defaulting to Genesis emulator"); 904 "md".into() 905 }); 906 907 match file_ext.as_str() { 908 file_ext @ ("sms" | "gg") => { 909 let inputs = config_ref.borrow().inputs.smsgg_inputs(); 910 js::showSmsGgConfig(inputs.0, inputs.1); 911 912 let hardware = match file_ext { 913 "sms" => SmsGgHardware::MasterSystem, 914 "gg" => SmsGgHardware::GameGear, 915 _ => unreachable!("nested match expressions"), 916 }; 917 let emulator = SmsGgEmulator::create( 918 Some(rom), 919 None, 920 hardware, 921 config_ref.borrow().smsgg.to_emulator_config(), 922 save_writer, 923 ); 924 Ok(Emulator::SmsGg(emulator, SmsGgInputs::default())) 925 } 926 file_ext @ ("gen" | "md" | "bin" | "smd" | "chd" | "32x") => { 927 let inputs = config_ref.borrow().inputs.genesis_inputs(); 928 js::showGenesisConfig(inputs.0, inputs.1); 929 930 let genesis_config = config_ref.borrow().to_genesis_config(); 931 932 let emulator = match file_ext { 933 "chd" => { 934 // Sega CD 935 let Some(bios) = bios_rom else { 936 return Err(OpenEmulatorError::NoSegaCdBios); 937 }; 938 939 let mut disc = CdRom::open_chd_in_memory(rom) 940 .map_err(|err| OpenEmulatorError::Other(err.into()))?; 941 942 let hardware = if segacd_core::is_cd_32x_disc(&mut disc) { 943 GenesisHardware::SegaCd32X 944 } else { 945 GenesisHardware::SegaCd 946 }; 947 948 GenesisEmulator::create( 949 hardware, 950 None, 951 Some(bios), 952 Some(disc), 953 genesis_config, 954 save_writer, 955 ) 956 .map_err(|err| OpenEmulatorError::Other(err.into()))? 957 } 958 _ => { 959 // Genesis / 32X 960 let hardware = match file_ext { 961 "32x" => GenesisHardware::Sega32X, 962 _ => GenesisHardware::Standalone, 963 }; 964 GenesisEmulator::create( 965 hardware, 966 Some(rom), 967 None, 968 None, 969 genesis_config, 970 save_writer, 971 ) 972 .map_err(|err| OpenEmulatorError::Other(err.into()))? 973 } 974 }; 975 976 Ok(Emulator::Genesis(emulator, GenesisInputs::default())) 977 } 978 "sfc" | "smc" => { 979 let emulator = SnesEmulator::create( 980 rom, 981 config_ref.borrow().snes.to_emulator_config(), 982 CoprocessorRoms::none(), 983 save_writer, 984 ) 985 .map_err(|err| OpenEmulatorError::Other(err.into()))?; 986 987 let inputs = config_ref.borrow().inputs.snes_inputs(); 988 js::showSnesConfig(inputs.0, inputs.1); 989 990 Ok(Emulator::Snes(emulator, SnesInputs::default())) 991 } 992 "gba" => { 993 let Some(bios) = bios_rom else { 994 return Err(OpenEmulatorError::NoGbaBios); 995 }; 996 997 let emulator = GameBoyAdvanceEmulator::create( 998 rom, 999 bios, 1000 config_ref.borrow().gba.to_emulator_config(), 1001 save_writer, 1002 ) 1003 .map_err(|err| OpenEmulatorError::Other(err.into()))?; 1004 1005 let inputs = config_ref.borrow().inputs.gba_inputs(); 1006 js::showGbaConfig(inputs.0, inputs.1); 1007 1008 Ok(Emulator::Gba(emulator, GbaInputs::default())) 1009 } 1010 _ => { 1011 Err(OpenEmulatorError::Other(format!("Unsupported file extension: {file_ext}").into())) 1012 } 1013 } 1014} 1015 1016#[must_use] 1017#[wasm_bindgen] 1018pub fn build_commit_hash() -> Option<String> { 1019 option_env!("JGENESIS_COMMIT").map(String::from) 1020}