Wasm improvments

Former-commit-id: f51fc18327f6adb0011ff2aff2787d513fb6aa37
This commit is contained in:
Michel Heily 2020-04-15 15:32:48 +03:00
parent 0d8a5467e0
commit 90032373a8
10 changed files with 252 additions and 123 deletions

2
Cargo.lock generated
View file

@ -1046,6 +1046,7 @@ dependencies = [
"nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "num 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"ringbuf 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rustyline 6.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustyline 6.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1060,7 +1061,6 @@ dependencies = [
"env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"jni 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "jni 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ringbuf 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rustboyadvance-core 0.1.0", "rustboyadvance-core 0.1.0",
] ]

View file

@ -13,7 +13,6 @@ crate-type = ["staticlib", "cdylib"]
rustboyadvance-core = {path = "../../rustboyadvance-core/"} rustboyadvance-core = {path = "../../rustboyadvance-core/"}
jni = { version = "0.16", default-features = false } jni = { version = "0.16", default-features = false }
log = {version = "0.4.8", features = ["release_max_level_info", "max_level_debug"]} log = {version = "0.4.8", features = ["release_max_level_info", "max_level_debug"]}
ringbuf = "0.2.1"
[target.'cfg(target_os="android")'.dependencies] [target.'cfg(target_os="android")'.dependencies]
android_log = "0.1.3" android_log = "0.1.3"

View file

@ -20,24 +20,9 @@ use android_log;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
use env_logger; use env_logger;
use ringbuf::{Consumer, Producer, RingBuffer};
use rustboyadvance_core::prelude::*; use rustboyadvance_core::prelude::*;
use rustboyadvance_core::StereoSample; use rustboyadvance_core::StereoSample;
use rustboyadvance_core::util::audio::AudioRingBuffer;
struct AudioRingBuffer {
pub prod: Producer<i16>,
pub cons: Consumer<i16>,
}
impl AudioRingBuffer {
fn new() -> AudioRingBuffer {
let rb = RingBuffer::new(4096 * 2);
let (prod, cons) = rb.split();
AudioRingBuffer { prod, cons }
}
}
struct Hardware { struct Hardware {
jvm: JavaVM, jvm: JavaVM,

View file

@ -45,6 +45,7 @@ features = [
'WebGlProgram', 'WebGlProgram',
'WebGlShader', 'WebGlShader',
'Window', 'Window',
'AudioContext',
] ]

View file

@ -7,93 +7,120 @@
<title>RustBoyAdvance</title> <title>RustBoyAdvance</title>
<style> <style>
body { body {
background-color: #675ea7; background-color: #2c2946;
font-family: "Courier New", Courier, monospace; font-family: Source Code, Roboto, Monospace;
margin: 0;
color: azure;
} }
#logo { header {
padding: 1em;
padding-top: 6em;
background-color: #13121d;
background-image: url("icon.svg");
background-repeat: no-repeat;
background-position: center;
background-size: 400px;
display: flex;
text-align: center; text-align: center;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0.1px 2px 4px rgba(0, 0, 0, 0.7);
}
header h1 {
color: azure;
padding: .1em;
font-size: medium;
} }
#menu { #menu {
background-color: #423c6c;
display: flexbox;
box-shadow: 0.1px 2px 4px rgba(0, 0, 0, 0.7);
top: 0.2em;
left: 0.2em;
padding: .1em; padding: .1em;
position: absolute;
} }
#menu .fileInput { #menu ul {
background-color: #675ea7; list-style: none;
margin: .4em;
} }
#menu button { #menu li {
background-color: #736ab1; margin: 0.2em;
box-shadow: 1px 1px 2px rgba(255, 18, 148, 0.2);
font-size: .8em;
border-radius: .15em;
margin: .4em;
} }
#canvas-container { #playarea {
box-shadow: 0.1px 2px 4px rgba(0, 0, 0, 0.7);
display: inline-block;
text-align: center;
position:fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 70%;
height: 70%;
align-items: center;
justify-content: center; justify-content: center;
background-color: #423c6c; margin: auto;
display: flexbox;
} }
#canvas-container.hover { #playarea.hover {
border: rgba(9, 250, 0, 0.671); border: rgba(9, 250, 0, 0.671);
border-style: dashed; border-style: dashed;
border-width: 0.1em; border-width: 0.1em;
} }
#game-options {
display: block;
}
#game-options > * {
width: 100%;
margin: 0.2em;
}
#screen { #screen {
text-align: center; display: block;
margin: auto; max-width: 960px;
width: 70%; width: 80%;
margin-left: auto;
margin-right: auto;
background-color: black; background-color: black;
image-rendering: optimizeSpeed; image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges; image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges; image-rendering: -webkit-crisp-edges;
image-rendering: pixelated; image-rendering: pixelated;
image-rendering: crisp-edges; image-rendering: crisp-edges;
box-shadow: 0.1px 2px 4px rgba(0, 0, 0, 0.7);
padding: 0.2em;
} }
.hidden {
display: none, #screen.hover {
border: rgba(9, 250, 0, 0.671);
border-style: dashed;
border-width: 0.1em;
} }
</style> </style>
</head> </head>
<body> <body>
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript> <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<div id="menu"> <header>
<div class="fileInput"> <div id="logo">
<h1>RustBoyAdvance</h1>
</div>
</header>
<section id="menu">
<ul>
<li class="fileInput">
<label> BIOS </label> <label> BIOS </label>
<input type="file" id="bios-file-input" /> <input type="file" id="bios-file-input" />
</div> </li>
<button id="reloadBios" class="hidden">Reload Bios</button> <li><button id="reloadBios" class="hidden">Reload Bios</button></li>
<div class="fileInput"> <li class="fileInput">
<label> ROM </label> <label> ROM </label>
<input type="file" id="rom-file-input" /> <input type="file" id="rom-file-input" />
</div> </li>
<button id="startEmulator">Start</button> <li><button id="startEmulator">Start</button></li>
<button id="fps-test">FPS Test</button> <li><button id="fps-test">FPS Test</button></li>
<li>
<label>Skip Bios</label> <label>Skip Bios</label>
<input type="checkbox" id="skipBios"> <input type="checkbox" id="skipBios">
</div> </li>
<div id="canvas-container"> <pre> You can also drag rom files directly into the screen </pre>
<pre id="fps"></pre> </ul>
</section>
<section id="playarea">
<pre> Built with WASM</pre>
<canvas id="screen" width="240px" , height="160px"></canvas> <canvas id="screen" width="240px" , height="160px"></canvas>
<img id="logo" width="240px" src="icon.svg"></img> <section id="game-options">
</div> <span id="fps">FPS</span>
<label>
<label>Max FPS</label>
<input type="checkbox" id="maxFps">
</label>
</section>
</section>
<footer></footer>
<script src="./bootstrap.js"></script> <script src="./bootstrap.js"></script>
</body> </body>

View file

@ -1,5 +1,6 @@
import * as wasm from "rustboyadvance-wasm"; import * as wasm from "rustboyadvance-wasm";
var fps_text = document.getElementById('fps');
var canvas = document.getElementById("screen"); var canvas = document.getElementById("screen");
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
var intervalId = 0; var intervalId = 0;
@ -37,22 +38,13 @@ function ensureFilesLoaded() {
return true; return true;
} }
function startEmulator() { const convertAudioBuffer = buffer => {
if (!ensureFilesLoaded()) { let length = buffer.length;
return; const floatArray = new Float32Array(length);
for (let i = 0; i < length; i++) {
floatArray[i] = (buffer[i] - 32767) / 32767;
} }
return floatArray;
if (intervalId != 0) {
console.log("killing emulator");
clearInterval(intervalId);
intervalId = 0;
emulator = null;
}
emulator = new wasm.Emulator(biosData, romData);
if (shouldSkipBios) {
emulator.skip_bios();
} }
var fpsCounter = (function() { var fpsCounter = (function() {
@ -73,11 +65,60 @@ function startEmulator() {
} }
}()); }());
let fps_text = document.getElementById('fps') // Create our audio context
intervalId = setInterval(function() { const audioContext = new (window.AudioContext || window.webkitAudioContext)();
console.log("audio context " + audioContext);
const playAudio = emulator => {
let audioData = emulator.collect_audio_samples();
let frameCount = audioData.length / 2;
const audioBuffer = audioContext.createBuffer(
2,
frameCount,
audioContext.sampleRate
);
for (let channel = 0; channel < 2; channel++) {
let nowBuffering = audioBuffer.getChannelData(channel);
for (let i = 0; i < frameCount; i++) {
// audio data frames are interleaved
nowBuffering[i] = audioData[i*2 + channel];
}
}
const audioSource = audioContext.createBufferSource();
audioSource.buffer = audioBuffer;
audioSource.connect(audioContext.destination);
audioSource.start();
}
const emulatorLoop = function() {
emulator.run_frame(ctx); emulator.run_frame(ctx);
fps_text.innerHTML = fpsCounter(); fps_text.innerHTML = fpsCounter();
}, 16); playAudio(emulator);
}
function startEmulator() {
if (!ensureFilesLoaded()) {
return;
}
if (intervalId != 0) {
console.log("killing emulator");
clearInterval(intervalId);
intervalId = 0;
emulator = null;
}
emulator = new wasm.Emulator(biosData, romData);
if (shouldSkipBios) {
emulator.skip_bios();
}
intervalId = setInterval(emulatorLoop, 16);
} }
const biosCached = localStorage.getItem("biosCached"); const biosCached = localStorage.getItem("biosCached");
@ -129,7 +170,7 @@ function loadRom(romFile) {
return promise; return promise;
}; };
let dropArea = document.getElementById('canvas-container'); let dropArea = document.getElementById('screen');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, dropArea.addEventListener(eventName,
@ -169,22 +210,26 @@ document.getElementById("startEmulator").addEventListener('click', e => {
} }
}, false); }, false);
['keydown', 'keyup'].forEach(eventName => { document.getElementById("maxFps").addEventListener('change', e => {
window.addEventListener(eventName, if (intervalId != 0) {
e => { let checked = e.target.checked;
// prevent default events clearInterval(intervalId);
e.preventDefault(); if (checked) {
e.stopPropagation(); intervalId = setInterval(emulatorLoop, 0);
}, false) } else {
}); intervalId = setInterval(emulatorLoop, 16);
}
window.addEventListener("keydown", e => { }
})
document.addEventListener("keydown", e => {
if (null != emulator) { if (null != emulator) {
emulator.key_down(e.key) emulator.key_down(e.key)
} }
}, false); }, false);
window.addEventListener("keyup", e => { document.addEventListener("keyup", e => {
if (null != emulator) { if (null != emulator) {
emulator.key_up(e.key) emulator.key_up(e.key)
} }

View file

@ -3,10 +3,15 @@ use std::rc::Rc;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped; use wasm_bindgen::Clamped;
use js_sys::Float32Array;
use web_sys::CanvasRenderingContext2d; use web_sys::CanvasRenderingContext2d;
use web_sys::AudioContext;
use rustboyadvance_core::core::keypad as gba_keypad; use rustboyadvance_core::core::keypad as gba_keypad;
use rustboyadvance_core::prelude::*; use rustboyadvance_core::prelude::*;
use rustboyadvance_core::util::audio::AudioRingBuffer;
use bit::BitIndex; use bit::BitIndex;
@ -19,6 +24,29 @@ pub struct Emulator {
struct Interface { struct Interface {
frame: Vec<u8>, frame: Vec<u8>,
keyinput: u16, keyinput: u16,
sample_rate: i32,
audio_ctx: AudioContext,
audio_ring_buffer: AudioRingBuffer,
}
impl Drop for Interface {
fn drop(&mut self) {
let _ = self.audio_ctx.clone();
}
}
impl Interface {
fn new(audio_ctx: AudioContext) -> Result<Interface, JsValue> {
Ok(
Interface {
frame: vec![0; 240 * 160 * 4],
keyinput: gba_keypad::KEYINPUT_ALL_RELEASED,
sample_rate: audio_ctx.sample_rate() as i32,
audio_ctx: audio_ctx,
audio_ring_buffer: AudioRingBuffer::new(),
}
)
}
} }
impl VideoInterface for Interface { impl VideoInterface for Interface {
@ -34,7 +62,21 @@ impl VideoInterface for Interface {
} }
} }
impl AudioInterface for Interface {} fn convert_sample(s: i16) -> f32 {
((s as f32) / 32767_f32)
}
impl AudioInterface for Interface {
fn get_sample_rate(&self) -> i32 {
self.sample_rate
}
fn push_sample(&mut self, samples: StereoSample<i16>) {
self.audio_ring_buffer.prod.push(samples.0).unwrap();
self.audio_ring_buffer.prod.push(samples.1).unwrap();
}
}
impl InputInterface for Interface { impl InputInterface for Interface {
fn poll(&mut self) -> u16 { fn poll(&mut self) -> u16 {
@ -45,18 +87,16 @@ impl InputInterface for Interface {
#[wasm_bindgen] #[wasm_bindgen]
impl Emulator { impl Emulator {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(bios: &[u8], rom: &[u8]) -> Emulator { pub fn new(bios: &[u8], rom: &[u8]) -> Result<Emulator, JsValue> {
let audio_ctx = web_sys::AudioContext::new()?;
let interface = Rc::new(RefCell::new(Interface::new(audio_ctx)?));
let gamepak = GamepakBuilder::new() let gamepak = GamepakBuilder::new()
.take_buffer(rom.to_vec().into_boxed_slice()) .take_buffer(rom.to_vec().into_boxed_slice())
.without_backup_to_file() .without_backup_to_file()
.build() .build()
.unwrap(); .unwrap();
let interface = Rc::new(RefCell::new(Interface {
frame: vec![0; 240 * 160 * 4],
keyinput: gba_keypad::KEYINPUT_ALL_RELEASED,
}));
let gba = GameBoyAdvance::new( let gba = GameBoyAdvance::new(
bios.to_vec().into_boxed_slice(), bios.to_vec().into_boxed_slice(),
gamepak, gamepak,
@ -65,7 +105,7 @@ impl Emulator {
interface.clone(), interface.clone(),
); );
Emulator { gba, interface } Ok( Emulator { gba, interface } )
} }
pub fn skip_bios(&mut self) { pub fn skip_bios(&mut self) {
@ -129,4 +169,16 @@ impl Emulator {
} }
} }
} }
pub fn collect_audio_samples(&self) -> Result<Float32Array, JsValue> {
let mut interface = self.interface.borrow_mut();
let consumer = &mut interface.audio_ring_buffer.cons;
let mut samples = Vec::with_capacity(consumer.len());
while let Some(sample) = consumer.pop() {
samples.push(convert_sample(sample));
}
Ok(Float32Array::from(samples.as_slice()))
}
} }

View file

@ -29,6 +29,7 @@ arrayvec = "0.5.1"
rustyline = {version = "6.0.0", optional = true} rustyline = {version = "6.0.0", optional = true}
nom = {version = "5.0.0", optional = true} nom = {version = "5.0.0", optional = true}
gdbstub = { version = "0.1.2", optional = true, features = ["std"] } gdbstub = { version = "0.1.2", optional = true, features = ["std"] }
ringbuf = "0.2.1"
[target.'cfg(target_arch="wasm32")'.dependencies] [target.'cfg(target_arch="wasm32")'.dependencies]
instant = { version = "0.1.2", features = [ "wasm-bindgen" ] } instant = { version = "0.1.2", features = [ "wasm-bindgen" ] }

View file

@ -65,5 +65,5 @@ pub mod prelude {
#[cfg(feature = "debugger")] #[cfg(feature = "debugger")]
pub use super::debugger::Debugger; pub use super::debugger::Debugger;
pub use super::util::{read_bin_file, write_bin_file}; pub use super::util::{read_bin_file, write_bin_file};
pub use super::{AudioInterface, InputInterface, VideoInterface}; pub use super::{AudioInterface, StereoSample, InputInterface, VideoInterface};
} }

View file

@ -127,3 +127,22 @@ macro_rules! host_breakpoint {
} }
}; };
} }
pub mod audio {
use ringbuf::{Consumer, Producer, RingBuffer};
pub struct AudioRingBuffer {
pub prod: Producer<i16>,
pub cons: Consumer<i16>,
}
impl AudioRingBuffer {
pub fn new() -> AudioRingBuffer {
let rb = RingBuffer::new(4096 * 2);
let (prod, cons) = rb.split();
AudioRingBuffer { prod, cons }
}
}
}