Added work-in-progress on pixels frontend

This commit is contained in:
transistor 2022-10-02 21:20:44 -07:00
parent a9b8633531
commit 9ee9d00ca6
10 changed files with 562 additions and 0 deletions

View File

@ -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"

View File

@ -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.

View File

@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style type="text/css">
body {
background-color: #000;
margin: 0;
overflow: hidden;
}
#config {
background-color: #888;
}
#metrics {
float: right;
color: #DDD;
background-color: #000;
}
#metrics input {
width: 2em;
color: #DDD;
background-color: #000;
}
</style>
<title>Hello Pixels + Web</title>
</head>
<body>
<div id="config">
<label>ROM File (only .bin format)</label>
<input type="file" id="rom-file" accept=".bin,.smd,.md" />
<input type="button" id="reset" value="Reset" />
<input type="text" id="speed" value="4.0" />
</div>
<div id="metrics">
<label>FrameRate</label>
<input type="text" id="frame-rate" disabled />
</div>
<script type="module">
import init from "./moa-genesis.js";
import {
set_rom_data,
request_reset,
get_frames_since,
smd_to_bin,
set_speed,
} from "./moa-genesis.js";
var reader = new FileReader();
reader.onloadend = function (e) {
var data = new Uint8Array(reader.result);
// If the SMD file magic number is present, then convert it before loading
if (data[8] == 0xAA && data[9] == 0xBB)
data = smd_to_bin(data);
set_rom_data(data);
};
var file_input = document.getElementById("rom-file");
file_input.addEventListener("change", e => {
reader.readAsArrayBuffer(file_input.files[0])
});
document.getElementById("reset").addEventListener("click", () => {
request_reset();
});
document.getElementById("speed").addEventListener("change", (e) => {
console.log(e.target.value);
set_speed(e.target.value);
});
var file_input = document.getElementById("rom-file");
var frame_rate_el = document.getElementById("frame-rate");
var frame_rate = setInterval(function () {
frame_rate_el.value = get_frames_since();
}, 1000);
/*
var last_update = performance.now();
window.requestIdleCallback(function () {
var current = performance.now();
var diff = current - last_update;
last_update = current;
try {
run_system_for(diff * 1000000);
} catch (e) {
console.error(e);
}
}, { timeout: 100 });
setInterval(function () {
run_system_for(66_000_000);
}, 66);
*/
window.addEventListener("load", () => {
init();
});
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const sample_size = audioContext.sampleRate / 100;
const audioBuffer = audioContext.createBuffer(2, sample_size, audioContext.sampleRate);
</script>
</body>
</html>

View File

@ -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}}/

View File

@ -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<u8>) -> Result<System, Error> {
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<u8>) -> Vec<u8> {
utils::smd_to_bin(input).unwrap()
}
}

View File

@ -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<u8>) -> Result<System, Error>;
pub struct PixelsFrontend {
updater: Option<Box<dyn WindowUpdater>>,
controller: Option<Box<dyn ControllerUpdater>>,
}
impl PixelsFrontend {
pub fn new() -> PixelsFrontend {
PixelsFrontend {
controller: None,
updater: None,
}
}
}
impl Host for PixelsFrontend {
fn add_window(&mut self, updater: Box<dyn WindowUpdater>) -> Result<(), Error> {
self.updater = Some(updater);
Ok(())
}
fn register_controller(&mut self, device: ControllerDevice, input: Box<dyn ControllerUpdater>) -> Result<(), Error> {
if device != ControllerDevice::A {
return Ok(())
}
self.controller = Some(input);
Ok(())
}
fn create_audio_source(&mut self) -> Result<Box<dyn Audio>, 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<dyn FnMut(_)>);
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<dyn FnMut(_)>);
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<ControllerEvent> {
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,
}
}

View File

@ -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};

View File

@ -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));
}

View File

@ -0,0 +1,57 @@
use std::sync::{Mutex, MutexGuard};
static EMULATOR_OPTIONS: Mutex<EmulatorSettings> = Mutex::new(EmulatorSettings::new());
pub struct EmulatorSettings {
pub rom_data: Vec<u8>,
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<u8>) {
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;
}

View File

@ -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<u8>) {
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()
}