Wasm improvments
Former-commit-id: f51fc18327f6adb0011ff2aff2787d513fb6aa37
This commit is contained in:
parent
0d8a5467e0
commit
90032373a8
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1046,6 +1046,7 @@ dependencies = [
|
|||
"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-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)",
|
||||
"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)",
|
||||
|
@ -1060,7 +1061,6 @@ dependencies = [
|
|||
"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)",
|
||||
"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",
|
||||
]
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ crate-type = ["staticlib", "cdylib"]
|
|||
rustboyadvance-core = {path = "../../rustboyadvance-core/"}
|
||||
jni = { version = "0.16", default-features = false }
|
||||
log = {version = "0.4.8", features = ["release_max_level_info", "max_level_debug"]}
|
||||
ringbuf = "0.2.1"
|
||||
|
||||
[target.'cfg(target_os="android")'.dependencies]
|
||||
android_log = "0.1.3"
|
||||
|
|
|
@ -20,24 +20,9 @@ use android_log;
|
|||
#[cfg(not(target_os = "android"))]
|
||||
use env_logger;
|
||||
|
||||
use ringbuf::{Consumer, Producer, RingBuffer};
|
||||
|
||||
use rustboyadvance_core::prelude::*;
|
||||
use rustboyadvance_core::StereoSample;
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
use rustboyadvance_core::util::audio::AudioRingBuffer;
|
||||
|
||||
struct Hardware {
|
||||
jvm: JavaVM,
|
||||
|
|
|
@ -45,6 +45,7 @@ features = [
|
|||
'WebGlProgram',
|
||||
'WebGlShader',
|
||||
'Window',
|
||||
'AudioContext',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -7,93 +7,120 @@
|
|||
<title>RustBoyAdvance</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #675ea7;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
background-color: #2c2946;
|
||||
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;
|
||||
align-items: 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 {
|
||||
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;
|
||||
position: absolute;
|
||||
}
|
||||
#menu .fileInput {
|
||||
background-color: #675ea7;
|
||||
margin: .4em;
|
||||
#menu ul {
|
||||
list-style: none;
|
||||
}
|
||||
#menu button {
|
||||
background-color: #736ab1;
|
||||
box-shadow: 1px 1px 2px rgba(255, 18, 148, 0.2);
|
||||
font-size: .8em;
|
||||
border-radius: .15em;
|
||||
margin: .4em;
|
||||
#menu li {
|
||||
margin: 0.2em;
|
||||
}
|
||||
#canvas-container {
|
||||
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;
|
||||
#playarea {
|
||||
justify-content: center;
|
||||
background-color: #423c6c;
|
||||
margin: auto;
|
||||
display: flexbox;
|
||||
}
|
||||
#canvas-container.hover {
|
||||
#playarea.hover {
|
||||
border: rgba(9, 250, 0, 0.671);
|
||||
border-style: dashed;
|
||||
border-width: 0.1em;
|
||||
}
|
||||
|
||||
#game-options {
|
||||
display: block;
|
||||
}
|
||||
#game-options > * {
|
||||
width: 100%;
|
||||
margin: 0.2em;
|
||||
}
|
||||
#screen {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
width: 70%;
|
||||
display: block;
|
||||
max-width: 960px;
|
||||
width: 80%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: black;
|
||||
image-rendering: optimizeSpeed;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
|
||||
<div id="menu">
|
||||
<div class="fileInput">
|
||||
<header>
|
||||
<div id="logo">
|
||||
<h1>RustBoyAdvance</h1>
|
||||
</div>
|
||||
</header>
|
||||
<section id="menu">
|
||||
<ul>
|
||||
<li class="fileInput">
|
||||
<label> BIOS </label>
|
||||
<input type="file" id="bios-file-input" />
|
||||
</div>
|
||||
<button id="reloadBios" class="hidden">Reload Bios</button>
|
||||
<div class="fileInput">
|
||||
</li>
|
||||
<li><button id="reloadBios" class="hidden">Reload Bios</button></li>
|
||||
<li class="fileInput">
|
||||
<label> ROM </label>
|
||||
<input type="file" id="rom-file-input" />
|
||||
</div>
|
||||
<button id="startEmulator">Start</button>
|
||||
<button id="fps-test">FPS Test</button>
|
||||
</li>
|
||||
<li><button id="startEmulator">Start</button></li>
|
||||
<li><button id="fps-test">FPS Test</button></li>
|
||||
<li>
|
||||
<label>Skip Bios</label>
|
||||
<input type="checkbox" id="skipBios">
|
||||
</div>
|
||||
<div id="canvas-container">
|
||||
<pre id="fps"></pre>
|
||||
</li>
|
||||
<pre> You can also drag rom files directly into the screen </pre>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="playarea">
|
||||
<pre> Built with WASM</pre>
|
||||
<canvas id="screen" width="240px" , height="160px"></canvas>
|
||||
<img id="logo" width="240px" src="icon.svg"></img>
|
||||
</div>
|
||||
<section id="game-options">
|
||||
<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>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as wasm from "rustboyadvance-wasm";
|
||||
|
||||
var fps_text = document.getElementById('fps');
|
||||
var canvas = document.getElementById("screen");
|
||||
var ctx = canvas.getContext('2d');
|
||||
var intervalId = 0;
|
||||
|
@ -37,22 +38,13 @@ function ensureFilesLoaded() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function startEmulator() {
|
||||
if (!ensureFilesLoaded()) {
|
||||
return;
|
||||
const convertAudioBuffer = buffer => {
|
||||
let length = buffer.length;
|
||||
const floatArray = new Float32Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
floatArray[i] = (buffer[i] - 32767) / 32767;
|
||||
}
|
||||
|
||||
if (intervalId != 0) {
|
||||
console.log("killing emulator");
|
||||
clearInterval(intervalId);
|
||||
intervalId = 0;
|
||||
emulator = null;
|
||||
}
|
||||
|
||||
emulator = new wasm.Emulator(biosData, romData);
|
||||
|
||||
if (shouldSkipBios) {
|
||||
emulator.skip_bios();
|
||||
return floatArray;
|
||||
}
|
||||
|
||||
var fpsCounter = (function() {
|
||||
|
@ -73,11 +65,60 @@ function startEmulator() {
|
|||
}
|
||||
}());
|
||||
|
||||
let fps_text = document.getElementById('fps')
|
||||
intervalId = setInterval(function() {
|
||||
// Create our audio context
|
||||
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);
|
||||
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");
|
||||
|
@ -129,7 +170,7 @@ function loadRom(romFile) {
|
|||
return promise;
|
||||
};
|
||||
|
||||
let dropArea = document.getElementById('canvas-container');
|
||||
let dropArea = document.getElementById('screen');
|
||||
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
dropArea.addEventListener(eventName,
|
||||
|
@ -169,22 +210,26 @@ document.getElementById("startEmulator").addEventListener('click', e => {
|
|||
}
|
||||
}, false);
|
||||
|
||||
['keydown', 'keyup'].forEach(eventName => {
|
||||
window.addEventListener(eventName,
|
||||
e => {
|
||||
// prevent default events
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, false)
|
||||
});
|
||||
document.getElementById("maxFps").addEventListener('change', e => {
|
||||
if (intervalId != 0) {
|
||||
let checked = e.target.checked;
|
||||
clearInterval(intervalId);
|
||||
if (checked) {
|
||||
intervalId = setInterval(emulatorLoop, 0);
|
||||
} else {
|
||||
intervalId = setInterval(emulatorLoop, 16);
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", e => {
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener("keydown", e => {
|
||||
if (null != emulator) {
|
||||
emulator.key_down(e.key)
|
||||
}
|
||||
}, false);
|
||||
|
||||
window.addEventListener("keyup", e => {
|
||||
document.addEventListener("keyup", e => {
|
||||
if (null != emulator) {
|
||||
emulator.key_up(e.key)
|
||||
}
|
||||
|
|
|
@ -3,10 +3,15 @@ use std::rc::Rc;
|
|||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::Clamped;
|
||||
|
||||
use js_sys::Float32Array;
|
||||
|
||||
use web_sys::CanvasRenderingContext2d;
|
||||
use web_sys::AudioContext;
|
||||
|
||||
use rustboyadvance_core::core::keypad as gba_keypad;
|
||||
use rustboyadvance_core::prelude::*;
|
||||
use rustboyadvance_core::util::audio::AudioRingBuffer;
|
||||
|
||||
use bit::BitIndex;
|
||||
|
||||
|
@ -19,6 +24,29 @@ pub struct Emulator {
|
|||
struct Interface {
|
||||
frame: Vec<u8>,
|
||||
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 {
|
||||
|
@ -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 {
|
||||
fn poll(&mut self) -> u16 {
|
||||
|
@ -45,18 +87,16 @@ impl InputInterface for Interface {
|
|||
#[wasm_bindgen]
|
||||
impl Emulator {
|
||||
#[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()
|
||||
.take_buffer(rom.to_vec().into_boxed_slice())
|
||||
.without_backup_to_file()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let interface = Rc::new(RefCell::new(Interface {
|
||||
frame: vec![0; 240 * 160 * 4],
|
||||
keyinput: gba_keypad::KEYINPUT_ALL_RELEASED,
|
||||
}));
|
||||
|
||||
let gba = GameBoyAdvance::new(
|
||||
bios.to_vec().into_boxed_slice(),
|
||||
gamepak,
|
||||
|
@ -65,7 +105,7 @@ impl Emulator {
|
|||
interface.clone(),
|
||||
);
|
||||
|
||||
Emulator { gba, interface }
|
||||
Ok( Emulator { gba, interface } )
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ arrayvec = "0.5.1"
|
|||
rustyline = {version = "6.0.0", optional = true}
|
||||
nom = {version = "5.0.0", optional = true}
|
||||
gdbstub = { version = "0.1.2", optional = true, features = ["std"] }
|
||||
ringbuf = "0.2.1"
|
||||
|
||||
[target.'cfg(target_arch="wasm32")'.dependencies]
|
||||
instant = { version = "0.1.2", features = [ "wasm-bindgen" ] }
|
||||
|
|
|
@ -65,5 +65,5 @@ pub mod prelude {
|
|||
#[cfg(feature = "debugger")]
|
||||
pub use super::debugger::Debugger;
|
||||
pub use super::util::{read_bin_file, write_bin_file};
|
||||
pub use super::{AudioInterface, InputInterface, VideoInterface};
|
||||
pub use super::{AudioInterface, StereoSample, InputInterface, VideoInterface};
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue