diff --git a/emulator/frontends/pixels/Cargo.toml b/emulator/frontends/pixels/Cargo.toml new file mode 100644 index 0000000..309c7e2 --- /dev/null +++ b/emulator/frontends/pixels/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "moa_pixels" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = "0.4" +pixels = "0.9" +winit = "0.26" + +moa_core = { path = "../../core" } +moa_genesis = { path = "../../systems/genesis" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1" +console_log = "0.2" +wasm-bindgen = "0.2.78" +wasm-bindgen-futures = "0.4" +web-sys = "0.3" +wgpu = { version = "0.12", features = ["webgl"] } +instant = { version = "0.1", features = [ "stdweb" ] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +env_logger = "0.9" +pollster = "0.2" +instant = "0.1" + diff --git a/emulator/frontends/pixels/README.md b/emulator/frontends/pixels/README.md new file mode 100644 index 0000000..bf0a043 --- /dev/null +++ b/emulator/frontends/pixels/README.md @@ -0,0 +1,7 @@ + +Moa Frontend using Pixels +========================= + +This is a frontend for the moa emulator that uses the [pixels]() crate as the rendering library, +and which can be compiled to wasm and run in a web browser. + diff --git a/emulator/frontends/pixels/assets/moa-genesis/index.html b/emulator/frontends/pixels/assets/moa-genesis/index.html new file mode 100644 index 0000000..2e72431 --- /dev/null +++ b/emulator/frontends/pixels/assets/moa-genesis/index.html @@ -0,0 +1,115 @@ + + + + + + + Hello Pixels + Web + + +
+ + + + +
+ +
+ + +
+ + + + diff --git a/emulator/frontends/pixels/justfile b/emulator/frontends/pixels/justfile new file mode 100644 index 0000000..d5607e0 --- /dev/null +++ b/emulator/frontends/pixels/justfile @@ -0,0 +1,11 @@ +serve package: (build package) + miniserve --index index.html ./dist/{{package}}/ + +build package: + mkdir -p ./dist/{{package}}/ + cp ./assets/{{package}}/* ./dist/{{package}}/ + cargo build --release --target wasm32-unknown-unknown + wasm-bindgen --target web --no-typescript --out-dir ./dist/{{package}}/ ./target/wasm32-unknown-unknown/release/{{package}}.wasm + +clean package: + rm -rf ./dist/{{package}}/ diff --git a/emulator/frontends/pixels/src/bin/moa-genesis.rs b/emulator/frontends/pixels/src/bin/moa-genesis.rs new file mode 100644 index 0000000..cb46a89 --- /dev/null +++ b/emulator/frontends/pixels/src/bin/moa-genesis.rs @@ -0,0 +1,27 @@ + +use moa_pixels::{PixelsFrontend, start}; + +use moa_core::{System, Error}; +use moa_genesis::{SegaGenesisOptions, build_genesis}; + +fn load_system(host: &mut PixelsFrontend, rom_data: Vec) -> Result { + let mut options = SegaGenesisOptions::new(); + options.rom_data = Some(rom_data); + build_genesis(host, options) +} + +fn main() { + start(load_system); +} + +#[cfg(target_arch = "wasm32")] +mod web { + use wasm_bindgen::prelude::*; + use moa_genesis::utils; + + #[wasm_bindgen] + pub fn smd_to_bin(input: Vec) -> Vec { + utils::smd_to_bin(input).unwrap() + } +} + diff --git a/emulator/frontends/pixels/src/frontend.rs b/emulator/frontends/pixels/src/frontend.rs new file mode 100644 index 0000000..52c9f51 --- /dev/null +++ b/emulator/frontends/pixels/src/frontend.rs @@ -0,0 +1,252 @@ + +use std::rc::Rc; + +use pixels::{Pixels, SurfaceTexture}; +use winit::dpi::LogicalSize; +use winit::event::{Event, VirtualKeyCode, WindowEvent, ElementState}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; +use instant::Instant; + +use moa_core::{System, Error, Clock}; +use moa_core::host::{Host, WindowUpdater, ControllerDevice, ControllerEvent, ControllerUpdater, Audio, DummyAudio}; +use moa_core::host::gfx::Frame; + +use crate::settings; + +const WIDTH: u32 = 320; +const HEIGHT: u32 = 224; + +pub type LoadSystemFn = fn (&mut PixelsFrontend, Vec) -> Result; + +pub struct PixelsFrontend { + updater: Option>, + controller: Option>, +} + +impl PixelsFrontend { + pub fn new() -> PixelsFrontend { + PixelsFrontend { + controller: None, + updater: None, + } + } +} + +impl Host for PixelsFrontend { + fn add_window(&mut self, updater: Box) -> Result<(), Error> { + self.updater = Some(updater); + Ok(()) + } + + fn register_controller(&mut self, device: ControllerDevice, input: Box) -> Result<(), Error> { + if device != ControllerDevice::A { + return Ok(()) + } + + self.controller = Some(input); + Ok(()) + } + + fn create_audio_source(&mut self) -> Result, Error> { + Ok(Box::new(DummyAudio())) + } +} + +pub async fn run(load: LoadSystemFn) { + loop { + let host = PixelsFrontend::new(); + run_loop(host, load).await + } +} + +pub async fn run_loop(mut host: PixelsFrontend, load: LoadSystemFn) { + let event_loop = EventLoop::new(); + let window = { + let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64); + WindowBuilder::new() + .with_title("Hello Pixels + Web") + .with_inner_size(size) + .with_min_inner_size(size) + .build(&event_loop) + .expect("WindowBuilder error") + }; + + let window = Rc::new(window); + + #[cfg(target_arch = "wasm32")] + { + use wasm_bindgen::JsCast; + use winit::platform::web::WindowExtWebSys; + + // Retrieve current width and height dimensions of browser client window + let get_window_size = || { + let client_window = web_sys::window().unwrap(); + LogicalSize::new( + client_window.inner_width().unwrap().as_f64().unwrap(), + client_window.inner_height().unwrap().as_f64().unwrap(), + ) + }; + + let window = Rc::clone(&window); + + // Initialize winit window with current dimensions of browser client + window.set_inner_size(get_window_size()); + + let client_window = web_sys::window().unwrap(); + + // Attach winit canvas to body element + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| { + body.append_child(&web_sys::Element::from(window.canvas())) + .ok() + }) + .expect("couldn't append canvas to document body"); + + // Listen for resize event on browser client. Adjust winit window dimensions + // on event trigger + let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_e: web_sys::Event| { + let size = get_window_size(); + window.set_inner_size(size) + }) as Box); + client_window + .add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); + + /* + let host = host.clone(); + let mut system = load(&mut host.lock().unwrap(), settings::get().rom_data.clone()).unwrap(); + let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_e: web_sys::Event| { + let run_timer = Instant::now(); + let nanoseconds_per_frame = (16_600_000 as f32 * settings::get().speed) as Clock; + if let Err(err) = system.run_for(nanoseconds_per_frame) { + log::error!("{:?}", err); + } + log::info!("ran simulation for {:?}ms in {:?}ms", nanoseconds_per_frame / 1_000_000, run_timer.elapsed().as_millis()); + + let mut settings = settings::get(); + if settings.reset { + settings.reset = false; + + match load(&mut host.lock().unwrap(), settings.rom_data.clone()) { + Ok(s) => { system = s; }, + Err(err) => log::error!("{:?}", err), + } + } + }) as Box); + client_window + .set_interval_with_callback_and_timeout_and_arguments_0(closure.as_ref().unchecked_ref(), 17) + .unwrap(); + closure.forget(); + */ + } + + //let mut input = WinitInputHelper::new(); + let mut pixels = { + let window_size = window.inner_size(); + let surface_texture = + SurfaceTexture::new(window_size.width, window_size.height, window.as_ref()); + Pixels::new_async(WIDTH, HEIGHT, surface_texture) + .await + .expect("Pixels error") + }; + + let mut last_frame = Frame::new(WIDTH, HEIGHT); + let mut update_timer = Instant::now(); + let mut system = load(&mut host, settings::get().rom_data.clone()).unwrap(); + event_loop.run(move |event, _, control_flow| { + + // Draw the current frame + if let Event::RedrawRequested(_) = event { + settings::increment_frames(); + log::info!("updated after {:4}ms", update_timer.elapsed().as_millis()); + update_timer = Instant::now(); + + let run_timer = Instant::now(); + let nanoseconds_per_frame = (16_600_000 as f32 * settings::get().speed) as Clock; + if let Err(err) = system.run_for(nanoseconds_per_frame) { + log::error!("{:?}", err); + } + log::info!("ran simulation for {:?}ms in {:?}ms", nanoseconds_per_frame / 1_000_000, run_timer.elapsed().as_millis()); + + if let Some(updater) = host.updater.as_mut() { + let buffer = pixels.get_frame(); + if let Ok(frame) = updater.take_frame() { + last_frame = frame; + } + + for y in 0..last_frame.height { + for x in 0..last_frame.width { + let pixel = last_frame.bitmap[((y * last_frame.width) + x) as usize]; + + let i = ((y * WIDTH) + x) as usize; + buffer[i * 4] = (pixel >> 16) as u8; + buffer[i * 4 + 1] = (pixel >> 8) as u8; + buffer[i * 4 + 2] = pixel as u8; + buffer[i * 4 + 3] = 255; + } + } + } + + if pixels + .render() + .map_err(|e| println!("pixels.render() failed: {}", e)) + .is_err() + { + *control_flow = ControlFlow::Exit; + return; + } + + window.request_redraw(); + } + + let mut key = None; + if let Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, .. } = event { + if let Some(keycode) = input.virtual_keycode { + match input.state { + ElementState::Pressed => { + key = map_controller_a(keycode, true); + } + ElementState::Released => { + key = map_controller_a(keycode, false); + } + } + } + } + + if let Some(updater) = host.controller.as_mut() { + if let Some(key) = key { + updater.update_controller(key); + } + } + + let mut settings = settings::get(); + if settings.reset { + settings.reset = false; + + match load(&mut host, settings.rom_data.clone()) { + Ok(s) => { system = s; }, + Err(err) => log::error!("{:?}", err), + } + } + }); +} + +pub fn map_controller_a(key: VirtualKeyCode, state: bool) -> Option { + match key { + VirtualKeyCode::A => { Some(ControllerEvent::ButtonA(state)) }, + VirtualKeyCode::O => { Some(ControllerEvent::ButtonB(state)) }, + VirtualKeyCode::E => { Some(ControllerEvent::ButtonC(state)) }, + VirtualKeyCode::Up => { Some(ControllerEvent::DpadUp(state)) }, + VirtualKeyCode::Down => { Some(ControllerEvent::DpadDown(state)) }, + VirtualKeyCode::Left => { Some(ControllerEvent::DpadLeft(state)) }, + VirtualKeyCode::Right => { Some(ControllerEvent::DpadRight(state)) }, + VirtualKeyCode::Return => { Some(ControllerEvent::Start(state)) }, + VirtualKeyCode::M => { Some(ControllerEvent::Mode(state)) }, + _ => None, + } +} + diff --git a/emulator/frontends/pixels/src/lib.rs b/emulator/frontends/pixels/src/lib.rs new file mode 100644 index 0000000..95fc48c --- /dev/null +++ b/emulator/frontends/pixels/src/lib.rs @@ -0,0 +1,15 @@ + +mod settings; +mod frontend; +pub use crate::frontend::{PixelsFrontend, LoadSystemFn}; + +#[cfg(target_arch = "wasm32")] +pub mod web; +#[cfg(target_arch = "wasm32")] +pub use crate::web::{start}; + +#[cfg(not(target_arch = "wasm32"))] +pub mod native; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::native::{start}; + diff --git a/emulator/frontends/pixels/src/native.rs b/emulator/frontends/pixels/src/native.rs new file mode 100644 index 0000000..3d8c0f0 --- /dev/null +++ b/emulator/frontends/pixels/src/native.rs @@ -0,0 +1,10 @@ +#![cfg(not(target_arch = "wasm32"))] + +use crate::frontend::{self, LoadSystemFn}; + +pub fn start(load: LoadSystemFn) { + env_logger::init(); + + pollster::block_on(frontend::run(load)); +} + diff --git a/emulator/frontends/pixels/src/settings.rs b/emulator/frontends/pixels/src/settings.rs new file mode 100644 index 0000000..c61485e --- /dev/null +++ b/emulator/frontends/pixels/src/settings.rs @@ -0,0 +1,57 @@ + +use std::sync::{Mutex, MutexGuard}; + +static EMULATOR_OPTIONS: Mutex = Mutex::new(EmulatorSettings::new()); + +pub struct EmulatorSettings { + pub rom_data: Vec, + pub run: bool, + pub reset: bool, + pub speed: f32, + pub frames_since: usize, +} + +impl EmulatorSettings { + const fn new() -> Self { + Self { + rom_data: vec![], + run: true, + reset: false, + speed: 4.0, + frames_since: 0, + } + } +} + +pub fn get<'a>() -> MutexGuard<'a, EmulatorSettings> { + EMULATOR_OPTIONS.lock().unwrap() +} + +pub fn set_rom_data(rom_data: Vec) { + get().rom_data = rom_data; +} + +pub fn get_frames_since() -> usize { + let mut options = get(); + let frames_since = options.frames_since; + options.frames_since = 0; + frames_since +} + +pub fn increment_frames() { + get().frames_since += 1; +} + +pub fn request_reset() { + get().reset = true; +} + +pub fn toggle_run() { + let mut options = get(); + options.run = !options.run; +} + +pub fn set_speed(speed: f32) { + get().speed = speed; +} + diff --git a/emulator/frontends/pixels/src/web.rs b/emulator/frontends/pixels/src/web.rs new file mode 100644 index 0000000..f5f6c52 --- /dev/null +++ b/emulator/frontends/pixels/src/web.rs @@ -0,0 +1,41 @@ +#![cfg(target_arch = "wasm32")] + +use wasm_bindgen::prelude::*; + +use crate::settings; +use crate::frontend::{self, LoadSystemFn}; + +pub fn start(load: LoadSystemFn) { + settings::set_rom_data(include_bytes!("../sonic.bin").to_vec()); + + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init_with_level(log::Level::Info).expect("error initializing logger"); + + wasm_bindgen_futures::spawn_local(frontend::run(load)); +} + +#[wasm_bindgen] +pub fn set_rom_data(rom_data: Vec) { + settings::set_rom_data(rom_data); +} + +#[wasm_bindgen] +pub fn request_reset() { + settings::request_reset(); +} + +#[wasm_bindgen] +pub fn toggle_run() { + settings::toggle_run(); +} + +#[wasm_bindgen] +pub fn set_speed(speed: f32) { + settings::set_speed(speed); +} + +#[wasm_bindgen] +pub fn get_frames_since() -> usize { + settings::get_frames_since() +} +