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()
+}
+