Added work-in-progress on pixels frontend
This commit is contained in:
parent
a9b8633531
commit
9ee9d00ca6
|
@ -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"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
|
@ -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}}/
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue