Refactored pixels web frontend to separate sim run

It will now run the sim on a separate loop powered by setTimeout
instead of trying to do it inline with the frame updating
This commit is contained in:
transistor 2022-10-11 14:40:12 -07:00
parent 0b27ac04e7
commit d26e80ffaa
9 changed files with 358 additions and 248 deletions

View File

@ -3,37 +3,16 @@
<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>
<link rel="stylesheet" href="style.css">
<title>Sega Genesis - Moa</title>
<script type="module" src="./interface.js"></script>
</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="button" id="power" value="Power" />
<input type="text" id="speed" value="4.0" />
</div>
@ -42,74 +21,8 @@
<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>
<div id="video-screen">
<canvas id="video" tabindex="0" draw-raw-handle="1" style="width: 640px; height: 448px;" width="640" height="448" />
</div>
</body>
</html>

View File

@ -0,0 +1,69 @@
import * as Emulator 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 = Emulator.smd_to_bin(data);
Emulator.set_rom_data(data);
};
var file_input = document.getElementById("rom-file");
file_input.addEventListener("change", e => {
reader.readAsArrayBuffer(file_input.files[0])
});
function initialize_emulator() {
let host = Emulator.new_host();
let system = Emulator.load_system(host, Emulator.get_load_system_fn());
let last_update = performance.now();
setTimeout(function refreshFrame() {
let current = performance.now();
let diff = Math.min(current - last_update, 100);
let remaining = Math.max((16 * Emulator.get_speed()) - diff, 0);
//console.log(diff, remaining);
last_update = current;
Emulator.run_system_for(system, diff * 1_000_000);
if (Emulator.is_running()) {
setTimeout(refreshFrame, remaining);
}
}, 0);
/*
setTimeout(function refreshFrame() {
let run_time = run_system_for(system, 66_000_000);
setTimeout(refreshFrame, 66 - run_time);
}, 0);
*/
Emulator.host_run_loop(host);
}
document.getElementById("reset").addEventListener("click", () => {
Emulator.request_stop();
//start();
});
document.getElementById("power").addEventListener("click", () => {
if (Emulator.is_running())
Emulator.request_stop();
else
initialize_emulator();
});
document.getElementById("speed").addEventListener("change", (e) => {
Emulator.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 = Emulator.get_frames_since();
}, 1000);
window.addEventListener("load", () => {
Emulator.default();
});

View File

@ -0,0 +1,21 @@
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;
}

View File

@ -18,10 +18,18 @@ fn main() {
mod web {
use wasm_bindgen::prelude::*;
use moa_genesis::utils;
use moa_pixels::LoadSystemFnHandle;
use super::load_system;
#[wasm_bindgen]
pub fn smd_to_bin(input: Vec<u8>) -> Vec<u8> {
utils::smd_to_bin(input).unwrap()
}
#[wasm_bindgen]
pub fn get_load_system_fn() -> LoadSystemFnHandle {
LoadSystemFnHandle::new(load_system)
}
}

View File

@ -1,34 +1,41 @@
use std::rc::Rc;
use instant::Instant;
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::{System, Error};
use moa_core::host::{Host, WindowUpdater, ControllerDevice, ControllerEvent, ControllerUpdater, Audio, DummyAudio};
use moa_core::host::gfx::Frame;
use moa_common::audio::{AudioMixer, AudioSource, CpalAudioOutput};
use crate::settings;
use crate::create_window;
const WIDTH: u32 = 320;
const HEIGHT: u32 = 224;
pub const WIDTH: u32 = 320;
pub 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>>,
//mixer: Arc<Mutex<AudioMixer>>,
//audio_output: CpalAudioOutput,
}
impl PixelsFrontend {
pub fn new() -> PixelsFrontend {
settings::get().run = true;
//let mixer = AudioMixer::with_default_rate();
//let audio_output = CpalAudioOutput::create_audio_output(mixer.lock().unwrap().get_sink());
PixelsFrontend {
controller: None,
updater: None,
//mixer,
//audio_output,
}
}
}
@ -49,102 +56,17 @@ impl Host for PixelsFrontend {
}
fn create_audio_source(&mut self) -> Result<Box<dyn Audio>, Error> {
//let source = AudioSource::new(self.mixer.clone());
//Ok(Box::new(source))
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) {
pub async fn run_loop(mut host: PixelsFrontend) {
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);
let window = create_window(&event_loop);
#[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 =
@ -154,46 +76,43 @@ pub async fn run_loop(mut host: PixelsFrontend, load: LoadSystemFn) {
.expect("Pixels error")
};
let mut last_size = (WIDTH, HEIGHT);
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();
//let mut update_timer = Instant::now();
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());
//log::warn!("updated after {:4}ms", update_timer.elapsed().as_millis());
//update_timer = Instant::now();
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 (last_frame.width, last_frame.height) != last_size {
last_size = (last_frame.width, last_frame.height);
pixels.resize_buffer(last_frame.width, last_frame.height);
}
let buffer = pixels.get_frame();
buffer
.chunks_mut(4)
.zip(last_frame.bitmap.iter())
.for_each(|(dest, pixel)| {
dest[0] = (pixel >> 16) as u8;
dest[1] = (pixel >> 8) as u8;
dest[2] = *pixel as u8;
dest[3] = 255;
});
}
if pixels
.render()
.map_err(|e| println!("pixels.render() failed: {}", e))
.map_err(|e| log::error!("pixels.render() failed: {}", e))
.is_err()
{
*control_flow = ControlFlow::Exit;
@ -203,34 +122,44 @@ pub async fn run_loop(mut host: PixelsFrontend, load: LoadSystemFn) {
window.request_redraw();
}
let mut key = None;
// Process key inputs and pass them to the emulator's controller device
if let Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, .. } = event {
if let Some(keycode) = input.virtual_keycode {
match input.state {
let key = match input.state {
ElementState::Pressed => {
key = map_controller_a(keycode, true);
map_controller_a(keycode, true)
}
ElementState::Released => {
key = map_controller_a(keycode, false);
map_controller_a(keycode, false)
}
};
if let Some(updater) = host.controller.as_mut() {
if let Some(key) = key {
updater.update_controller(key);
}
}
}
}
if let Some(updater) = host.controller.as_mut() {
if let Some(key) = key {
updater.update_controller(key);
}
}
// Check if the run flag is no longer true, and exit the loop
if !settings::get().run {
// Clear the screen
let buffer = pixels.get_frame();
buffer.iter_mut().for_each(|byte| *byte = 0);
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),
if pixels
.render()
.map_err(|e| log::error!("pixels.render() failed: {}", e))
.is_err()
{
*control_flow = ControlFlow::Exit;
return;
}
window.request_redraw();
*control_flow = ControlFlow::Exit;
}
});
}
@ -238,8 +167,8 @@ pub async fn run_loop(mut host: PixelsFrontend, load: LoadSystemFn) {
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::S => { Some(ControllerEvent::ButtonB(state)) },
VirtualKeyCode::D => { Some(ControllerEvent::ButtonC(state)) },
VirtualKeyCode::Up => { Some(ControllerEvent::DpadUp(state)) },
VirtualKeyCode::Down => { Some(ControllerEvent::DpadDown(state)) },
VirtualKeyCode::Left => { Some(ControllerEvent::DpadLeft(state)) },

View File

@ -6,10 +6,10 @@ pub use crate::frontend::{PixelsFrontend, LoadSystemFn};
#[cfg(target_arch = "wasm32")]
pub mod web;
#[cfg(target_arch = "wasm32")]
pub use crate::web::{start};
pub use crate::web::{start, create_window, LoadSystemFnHandle};
#[cfg(not(target_arch = "wasm32"))]
pub mod native;
#[cfg(not(target_arch = "wasm32"))]
pub use crate::native::{start};
pub use crate::native::{start, create_window};

View File

@ -1,5 +1,10 @@
#![cfg(not(target_arch = "wasm32"))]
use std::rc::Rc;
use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::window::{Window, WindowBuilder};
use crate::frontend::{self, LoadSystemFn};
pub fn start(load: LoadSystemFn) {
@ -8,3 +13,16 @@ pub fn start(load: LoadSystemFn) {
pollster::block_on(frontend::run(load));
}
pub fn create_window<T>(event_loop: &EventLoop<T>) -> Rc<Window> {
let size = LogicalSize::new(frontend::WIDTH as f64, frontend::HEIGHT as f64);
let window = 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);
window
}

View File

@ -6,7 +6,6 @@ static EMULATOR_OPTIONS: Mutex<EmulatorSettings> = Mutex::new(EmulatorSettings::
pub struct EmulatorSettings {
pub rom_data: Vec<u8>,
pub run: bool,
pub reset: bool,
pub speed: f32,
pub frames_since: usize,
}
@ -15,8 +14,7 @@ impl EmulatorSettings {
const fn new() -> Self {
Self {
rom_data: vec![],
run: true,
reset: false,
run: false,
speed: 4.0,
frames_since: 0,
}
@ -42,8 +40,8 @@ pub fn increment_frames() {
get().frames_since += 1;
}
pub fn request_reset() {
get().reset = true;
pub fn request_stop() {
get().run = false;
}
pub fn toggle_run() {
@ -51,7 +49,3 @@ pub fn toggle_run() {
options.run = !options.run;
}
pub fn set_speed(speed: f32) {
get().speed = speed;
}

View File

@ -1,17 +1,24 @@
#![cfg(target_arch = "wasm32")]
use std::rc::Rc;
use instant::Instant;
use wasm_bindgen::prelude::*;
use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::window::{Window, WindowBuilder};
use moa_core::{Clock, System};
use crate::settings;
use crate::frontend::{self, LoadSystemFn};
use crate::frontend::{self, PixelsFrontend, 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");
console_log::init_with_level(log::Level::Warn).expect("error initializing logger");
wasm_bindgen_futures::spawn_local(frontend::run(load));
//wasm_bindgen_futures::spawn_local(frontend::run(load));
}
#[wasm_bindgen]
@ -20,8 +27,8 @@ pub fn set_rom_data(rom_data: Vec<u8>) {
}
#[wasm_bindgen]
pub fn request_reset() {
settings::request_reset();
pub fn request_stop() {
settings::request_stop();
}
#[wasm_bindgen]
@ -29,9 +36,19 @@ pub fn toggle_run() {
settings::toggle_run();
}
#[wasm_bindgen]
pub fn is_running() -> bool {
settings::get().run
}
#[wasm_bindgen]
pub fn set_speed(speed: f32) {
settings::set_speed(speed);
settings::get().speed = speed;
}
#[wasm_bindgen]
pub fn get_speed() -> f32 {
settings::get().speed
}
#[wasm_bindgen]
@ -39,3 +56,144 @@ pub fn get_frames_since() -> usize {
settings::get_frames_since()
}
#[wasm_bindgen]
pub struct HostHandle(PixelsFrontend);
#[wasm_bindgen]
pub fn new_host() -> HostHandle {
HostHandle(PixelsFrontend::new())
}
#[wasm_bindgen]
pub fn host_run_loop(handle: HostHandle) {
wasm_bindgen_futures::spawn_local(frontend::run_loop(handle.0));
}
#[wasm_bindgen]
pub struct SystemHandle(System);
#[wasm_bindgen]
pub struct LoadSystemFnHandle(LoadSystemFn);
impl LoadSystemFnHandle {
pub fn new(load: LoadSystemFn) -> Self {
Self(load)
}
}
#[wasm_bindgen]
pub fn load_system(handle: &mut HostHandle, load: LoadSystemFnHandle) -> SystemHandle {
let system = load.0(&mut handle.0, settings::get().rom_data.clone()).unwrap();
SystemHandle(system)
}
#[wasm_bindgen]
pub fn run_system_for(handle: &mut SystemHandle, nanos: u32) -> usize {
let run_timer = Instant::now();
let nanoseconds_per_frame = nanos as Clock;
//let nanoseconds_per_frame = (16_600_000 as f32 * settings::get().speed) as Clock;
if let Err(err) = handle.0.run_for(nanoseconds_per_frame) {
log::error!("{:?}", err);
}
let run_time = run_timer.elapsed().as_millis();
log::warn!("ran simulation for {:?}ms in {:?}ms", nanoseconds_per_frame / 1_000_000, run_time);
run_time as usize
}
pub fn create_window<T>(event_loop: &EventLoop<T>) -> Rc<Window> {
use web_sys::HtmlCanvasElement;
use wasm_bindgen::JsCast;
use winit::platform::web::{WindowExtWebSys, WindowBuilderExtWebSys};
let canvas = web_sys::window()
.and_then(|win| win.document())
.and_then(|doc| doc.get_element_by_id("video"))
.and_then(|el| el.dyn_into::<web_sys::HtmlCanvasElement>().ok())
.expect("document to have canvas");
let window = {
let size = LogicalSize::new(frontend::WIDTH as f64, frontend::HEIGHT as f64);
WindowBuilder::new()
.with_canvas(Some(canvas))
.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);
// 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.get_element_by_id("video-screen"))
.and_then(|el| {
while let Some(child) = el.first_child() {
el.remove_child(&child);
}
el.append_child(&web_sys::Element::from(window.canvas()))
.ok()
})
.expect("couldn't append canvas to document body");
*/
{
let window = window.clone();
// 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 mut update_timer = Instant::now();
let mut system = load(&mut host, 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 settings = settings::get();
if settings.run {
//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();
*/
window
}