lib.rsannotatedlib.rssource1020 lines · 33.9 KB · raw
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}
79
80// 1MB should be big enough for any save file
81const SERIALIZATION_BUFFER_LEN: usize = 1024 * 1024;
82
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}
171
172/// # Panics
173///
174/// This function will panic if it cannot initialize the console logger.
175#[wasm_bindgen(start)]
176pub fn init_logger() {
177    std::panic::set_hook(Box::new(console_error_panic_hook::hook));
178    console_log::init_with_level(log::Level::Info).expect("Unable to initialize logger");
179}
180
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 };
404
405/// # 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}
466
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}