Add pinky-web

This commit is contained in:
Jan Bujak 2018-01-01 18:29:23 +01:00
parent b915572ccc
commit 0bd8e1fabe
19 changed files with 1512 additions and 0 deletions

2
.gitignore vendored
View file

@ -2,6 +2,8 @@ pinky-devui/target
pinky-devui/Cargo.lock
pinky-libretro/target
pinky-libretro/Cargo.lock
pinky-web/target
pinky-web/Cargo.lock
nes-testsuite/target
nes-testsuite/Cargo.lock
mos6502/target

30
pinky-web/Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "pinky-web"
version = "0.1.0"
authors = ["Jan Bujak <j@exia.io>"]
[dependencies]
serde = "1"
serde_derive = "1"
[dependencies.stdweb]
version = "0.3"
[dependencies.nes]
path = "../nes"
[profile.dev]
opt-level = 2
debug = true
rpath = false
lto = false
debug-assertions = true
codegen-units = 4
[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1

47
pinky-web/README.md Normal file
View file

@ -0,0 +1,47 @@
This is the Web frontend for Pinky.
It's mostly meant as a demo for Rust's WebAssembly capabilities
and the [stdweb] project.
[stdweb]: https://github.com/koute/stdweb
[![Become a patron](https://koute.github.io/img/become_a_patron_button.png)](https://www.patreon.com/koute)
### See it in action
* A version [compiled with Rust's native WebAssembly backend] (recommended!).
* A version [compiled to WebAssembly with Emscripten].
* A version [compiled to asm.js with Emscripten].
[compiled with Rust's native WebAssembly backend]: https://koute.github.io/pinky-web
[compiled to WebAssembly with Emscripten]: https://koute.github.io/pinky-web-webasm-emscripten
[compiled to asm.js with Emscripten]: https://koute.github.io/pinky-web-asmjs-emscripten
### Building (using Rust's native WebAssembly backend)
1. Install newest nightly Rust:
$ curl https://sh.rustup.rs -sSf | sh
2. Install WebAssembly target:
$ rustup target add wasm32-unknown-unknown
3. Install [cargo-web]:
$ cargo install -f cargo-web
4. Build it:
$ cargo web start --target-webasm --release
5. Visit `http://localhost:8000` with your browser.
[cargo-web]: https://github.com/koute/cargo-web
### Building for other backends
Replace `--target-webasm` with `--target-webasm-emscripten` or `--target-asmjs-emscripten`
if you want to build it using another backend. You will also have to install the
corresponding targets with `rustup` - `wasm32-unknown-emscripten` and `asmjs-unknown-emscripten`
respectively.

657
pinky-web/src/main.rs Normal file
View file

@ -0,0 +1,657 @@
#![recursion_limit="2048"]
#[macro_use]
extern crate stdweb;
extern crate nes;
#[macro_use]
extern crate serde_derive;
extern crate serde;
use std::cell::RefCell;
use std::rc::Rc;
use std::ops::DerefMut;
use std::cmp::max;
use std::error::Error;
use stdweb::web::{
self,
IEventTarget,
INode,
IElement,
FileReader,
FileReaderResult,
Element,
ArrayBuffer
};
use stdweb::web::event::{
IEvent,
IKeyboardEvent,
ClickEvent,
ChangeEvent,
ProgressLoadEvent,
KeydownEvent,
KeyupEvent,
KeyboardLocation
};
use stdweb::web::html_element::InputElement;
use stdweb::unstable::TryInto;
use stdweb::{Value, UnsafeTypedArray, Once};
macro_rules! enclose {
( [$( $x:ident ),*] $y:expr ) => {
{
$(let $x = $x.clone();)*
$y
}
};
}
fn palette_to_abgr( palette: &nes::Palette ) -> [u32; 64] {
let mut output = [0; 64];
for (index, out) in output.iter_mut().enumerate() {
let (r, g, b) = palette.get_rgb( index as u8 );
*out = ((b as u32) << 16) |
((g as u32) << 8) |
((r as u32) ) |
0xFF000000;
}
output
}
struct PinkyWeb {
state: nes::State,
palette: [u32; 64],
framebuffer: [u32; 256 * 240],
audio_buffer: Vec< f32 >,
audio_chunk_counter: u32,
audio_underrun: Option< usize >,
paused: bool,
busy: bool,
js_ctx: Value
}
impl nes::Context for PinkyWeb {
#[inline]
fn state_mut( &mut self ) -> &mut nes::State {
&mut self.state
}
#[inline]
fn state( &self ) -> &nes::State {
&self.state
}
// Ugh, trying to get gapless low-latency PCM playback
// out of the Web Audio APIs is like driving a rusty
// nail through my hand.
//
// This blog post probably sums it up pretty well:
// http://blog.mecheye.net/2017/09/i-dont-know-who-the-web-audio-api-is-designed-for/
//
// So, this is not perfect, but it's as good as I can make it.
#[inline]
fn on_audio_sample( &mut self, sample: f32 ) {
self.audio_buffer.push( sample );
if self.audio_buffer.len() == 2048 {
self.audio_chunk_counter += 1;
let audio_buffered: f64 = js! {
var h = @{&self.js_ctx};
var samples = @{unsafe { UnsafeTypedArray::new( &self.audio_buffer ) }};
var sample_rate = 44100;
var sample_count = samples.length;
var latency = 0.032;
var audio_buffer;
if( h.empty_audio_buffers.length === 0 ) {
audio_buffer = h.audio.createBuffer( 1, sample_count, sample_rate );
} else {
audio_buffer = h.empty_audio_buffers.pop();
}
audio_buffer.getChannelData( 0 ).set( samples );
var node = h.audio.createBufferSource();
node.connect( h.audio.destination );
node.buffer = audio_buffer;
node.onended = function() {
h.empty_audio_buffers.push( audio_buffer );
};
var buffered = h.play_timestamp - (h.audio.currentTime + latency);
var play_timestamp = Math.max( h.audio.currentTime + latency, h.play_timestamp );
node.start( play_timestamp );
h.play_timestamp = play_timestamp + sample_count / sample_rate;
return buffered;
}.try_into().unwrap();
// Since we're using `request_animation_frame` for synchronization
// we **will** go out of sync with the audio sooner or later,
// which will result in sudden, and very unpleasant, audio pops.
//
// So here we check how much audio exactly we have queued up,
// and if we don't have enough then we'll try to compensate
// by running the emulator for a few more cycles at the end
// of the current frame.
if audio_buffered < 0.000 {
self.audio_underrun = Some( max( self.audio_underrun.unwrap_or( 0 ), 3 ) );
} else if audio_buffered < 0.010 {
self.audio_underrun = Some( max( self.audio_underrun.unwrap_or( 0 ), 2 ) );
} else if audio_buffered < 0.020 {
self.audio_underrun = Some( max( self.audio_underrun.unwrap_or( 0 ), 1 ) );
}
self.audio_buffer.clear();
}
}
}
// This creates a really basic WebGL context for blitting a single texture.
// On some web browsers this is faster than using a 2d canvas.
fn setup_webgl( canvas: &Element ) -> Value {
const FRAGMENT_SHADER: &'static str = r#"
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_sampler;
void main() {
gl_FragColor = vec4( texture2D( u_sampler, vec2( v_texcoord.s, v_texcoord.t ) ).rgb, 1.0 );
}
"#;
const VERTEX_SHADER: &'static str = r#"
attribute vec2 a_position;
attribute vec2 a_texcoord;
uniform mat4 u_matrix;
varying vec2 v_texcoord;
void main() {
gl_Position = u_matrix * vec4( a_position, 0.0, 1.0 );
v_texcoord = a_texcoord;
}
"#;
fn ortho( left: f64, right: f64, bottom: f64, top: f64 ) -> Vec< f64 > {
let mut m = vec![ 1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0 ];
m[ 0 * 4 + 0 ] = 2.0 / (right - left);
m[ 1 * 4 + 1 ] = 2.0 / (top - bottom);
m[ 3 * 4 + 0 ] = (right + left) / (right - left) * -1.0;
m[ 3 * 4 + 1 ] = (top + bottom) / (top - bottom) * -1.0;
return m;
}
js!(
var gl;
var webgl_names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
for( var i = 0; i < webgl_names.length; ++i ) {
var name = webgl_names[ i ];
try {
gl = @{canvas}.getContext( name );
} catch( err ) {}
if( gl ) {
console.log( "WebGL support using context:", name );
break;
}
}
var vertex_shader = gl.createShader( gl.VERTEX_SHADER );
var fragment_shader = gl.createShader( gl.FRAGMENT_SHADER );
gl.shaderSource( vertex_shader, @{VERTEX_SHADER} );
gl.shaderSource( fragment_shader, @{FRAGMENT_SHADER} );
gl.compileShader( vertex_shader );
gl.compileShader( fragment_shader );
if( !gl.getShaderParameter( vertex_shader, gl.COMPILE_STATUS ) ) {
console.error( "WebGL vertex shader compilation failed:", gl.getShaderInfoLog( vertex_shader ) );
return null;
}
if( !gl.getShaderParameter( fragment_shader, gl.COMPILE_STATUS ) ) {
console.error( "WebGL fragment shader compilation failed:", gl.getShaderInfoLog( fragment_shader ) );
return null;
}
var program = gl.createProgram();
gl.attachShader( program, vertex_shader );
gl.attachShader( program, fragment_shader );
gl.linkProgram( program );
if( !gl.getProgramParameter( program, gl.LINK_STATUS ) ) {
console.error( "WebGL program linking failed!" );
return null;
}
gl.useProgram( program );
var vertex_attr = gl.getAttribLocation( program, "a_position" );
var texcoord_attr = gl.getAttribLocation( program, "a_texcoord" );
gl.enableVertexAttribArray( vertex_attr );
gl.enableVertexAttribArray( texcoord_attr );
var sampler_uniform = gl.getUniformLocation( program, "u_sampler" );
gl.uniform1i( sampler_uniform, 0 );
var matrix = @{ortho( 0.0, 256.0, 240.0, 0.0 )};
var matrix_uniform = gl.getUniformLocation( program, "u_matrix" );
gl.uniformMatrix4fv( matrix_uniform, false, matrix );
var texture = gl.createTexture();
gl.bindTexture( gl.TEXTURE_2D, texture );
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 256, 256, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array( 256 * 256 * 4 ) );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST );
var vertex_buffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, vertex_buffer );
var vertices = [
0.0, 0.0,
0.0, 240.0,
256.0, 0.0,
256.0, 240.0
];
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( vertices ), gl.STATIC_DRAW );
gl.vertexAttribPointer( vertex_attr, 2, gl.FLOAT, false, 0, 0 );
var texcoord_buffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, texcoord_buffer );
var texcoords = [
0.0, 0.0,
0.0, 240.0 / 256.0,
1.0, 0.0,
1.0, 240.0 / 256.0
];
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( texcoords ), gl.STATIC_DRAW );
gl.vertexAttribPointer( texcoord_attr, 2, gl.FLOAT, false, 0, 0 );
var index_buffer = gl.createBuffer();
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, index_buffer );
var indices = [
0, 1, 2,
2, 3, 1
];
gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Uint16Array( indices ), gl.STATIC_DRAW );
gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
gl.enable( gl.DEPTH_TEST );
gl.viewport( 0, 0, 256, 240 );
return gl;
)
}
impl PinkyWeb {
fn new( canvas: &Element ) -> Self {
let gl = setup_webgl( &canvas );
let js_ctx = js!(
var h = {};
var canvas = @{canvas};
h.gl = @{gl};
h.audio = new AudioContext();
h.empty_audio_buffers = [];
h.play_timestamp = 0;
if( !h.gl ) {
console.log( "No WebGL; using Canvas API" );
// If the WebGL **is** supported but something else
// went wrong the web browser won't let us create
// a normal canvas context on a WebGL-ified canvas,
// so we recreate a new canvas here to work around that.
var new_canvas = canvas.cloneNode( true );
canvas.parentNode.replaceChild( new_canvas, canvas );
canvas = new_canvas;
h.ctx = canvas.getContext( "2d" );
h.img = h.ctx.createImageData( 256, 240 );
h.buffer = new Uint32Array( h.img.data.buffer );
}
return h;
);
PinkyWeb {
state: nes::State::new(),
palette: palette_to_abgr( &nes::Palette::default() ),
framebuffer: [0; 256 * 240],
audio_buffer: Vec::with_capacity( 44100 ),
audio_chunk_counter: 0,
audio_underrun: None,
paused: true,
busy: false,
js_ctx
}
}
fn pause( &mut self ) {
self.paused = true;
}
fn unpause( &mut self ) {
self.paused = false;
self.busy = false;
}
// This will run the emulator either until we've finished
// a frame, or until we've generated one audio chunk,
// in which case we'll temporairly give back the control
// of the main thread back to the web browser so that
// it can handle other events and process audio.
fn run_a_bit( &mut self ) -> Result< bool, Box< Error > > {
if self.paused {
return Ok( true );
}
let audio_chunk_counter = self.audio_chunk_counter;
loop {
let result = nes::Interface::execute_cycle( self );
match result {
Ok( processed_whole_frame ) => {
if processed_whole_frame {
return Ok( true );
} else if self.audio_chunk_counter != audio_chunk_counter {
return Ok( false );
}
},
Err( error ) => {
js!( console.error( "Execution error:", @{format!( "{}", error )} ); );
self.pause();
return Err( error );
}
}
}
}
fn draw( &mut self ) {
let framebuffer = self.state.framebuffer();
if !self.paused {
for (pixel_in, pixel_out) in framebuffer.iter().zip( self.framebuffer.iter_mut() ) {
*pixel_out = self.palette[ pixel_in.color_in_system_palette_index() as usize ];
}
}
js! {
var h = @{&self.js_ctx};
var framebuffer = @{unsafe { UnsafeTypedArray::new( &self.framebuffer ) }};
if( h.gl ) {
var data = new Uint8Array( framebuffer.buffer, framebuffer.byteOffset, framebuffer.byteLength );
h.gl.texSubImage2D( h.gl.TEXTURE_2D, 0, 0, 0, 256, 240, h.gl.RGBA, h.gl.UNSIGNED_BYTE, data );
h.gl.drawElements( h.gl.TRIANGLES, 6, h.gl.UNSIGNED_SHORT, 0 );
} else {
h.buffer.set( framebuffer );
h.ctx.putImageData( h.img, 0, 0 );
}
}
}
fn on_key( &mut self, key: &str, location: KeyboardLocation, is_pressed: bool ) -> bool {
let button = match (key, location) {
("Enter", _) => nes::Button::Start,
("Shift", KeyboardLocation::Right) => nes::Button::Select,
("ArrowUp", _) => nes::Button::Up,
("ArrowLeft", _) => nes::Button::Left,
("ArrowRight", _) => nes::Button::Right,
("ArrowDown", _) => nes::Button::Down,
// On Edge the arrows have different names
// for some reason.
("Up", _) => nes::Button::Up,
("Left", _) => nes::Button::Left,
("Right", _) => nes::Button::Right,
("Down", _) => nes::Button::Down,
("z", _) => nes::Button::A,
("x", _) => nes::Button::B,
// For those using the Dvorak layout.
(";", _) => nes::Button::A,
("q", _) => nes::Button::B,
// For those using the Dvorak layout **and** Microsoft Edge.
//
// On `keydown` we get ";" as we should, but on `keyup`
// we get "Unidentified". Seriously Microsoft, how buggy can
// your browser be?
("Unidentified", _) if is_pressed == false => nes::Button::A,
_ => return false
};
nes::Interface::set_button_state( self, nes::ControllerPort::First, button, is_pressed );
return true;
}
}
fn emulate_for_a_single_frame( pinky: Rc< RefCell< PinkyWeb > > ) {
pinky.borrow_mut().busy = true;
web::set_timeout( enclose!( [pinky] move || {
let finished_frame = match pinky.borrow_mut().run_a_bit() {
Ok( result ) => result,
Err( error ) => {
handle_error( error );
return;
}
};
if !finished_frame {
web::set_timeout( move || { emulate_for_a_single_frame( pinky ); }, 0 );
} else {
let mut pinky = pinky.borrow_mut();
if let Some( count ) = pinky.audio_underrun.take() {
for _ in 0..count {
if let Err( error ) = pinky.run_a_bit() {
handle_error( error );
return;
}
}
}
pinky.busy = false;
}
}), 0 );
}
fn main_loop( pinky: Rc< RefCell< PinkyWeb > > ) {
// If we're running too slowly there is no point
// in queueing up even more work.
if !pinky.borrow_mut().busy {
emulate_for_a_single_frame( pinky.clone() );
}
pinky.borrow_mut().draw();
web::window().request_animation_frame( move |_| {
main_loop( pinky );
});
}
#[derive(Deserialize)]
struct RomEntry {
name: String,
file: String
}
js_deserializable!( RomEntry );
fn show( id: &str ) {
web::document().get_element_by_id( id ).unwrap().class_list().remove( "hidden" );
}
fn hide( id: &str ) {
web::document().get_element_by_id( id ).unwrap().class_list().add( "hidden" );
}
fn fetch_builtin_rom_list< F: FnOnce( Vec< RomEntry > ) + 'static >( callback: F ) {
let on_rom_list_loaded = Once( move |mut roms: Vec< RomEntry >| {
roms.sort_by( |a, b| a.name.cmp( &b.name ) );
callback( roms );
});
js! {
var req = new XMLHttpRequest();
req.addEventListener( "load" , function() {
var cb = @{on_rom_list_loaded};
cb( JSON.parse( req.responseText ) );
cb.drop();
});
req.open( "GET", "roms/index.json" );
req.send();
}
}
fn support_builtin_roms( roms: Vec< RomEntry >, pinky: Rc< RefCell< PinkyWeb > > ) {
let entries = web::document().get_element_by_id( "rom-list" ).unwrap();
for rom in roms {
let entry = web::document().create_element( "button" );
let name = rom.name;
let file = rom.file;
entry.set_text_content( &name );
entries.append_child( &entry );
entry.add_event_listener( enclose!( [pinky] move |_: ClickEvent| {
hide( "change-rom-menu" );
hide( "side-text" );
show( "loading" );
let builtin_rom_loaded = Once( enclose!( [pinky] move |array_buffer: ArrayBuffer| {
let rom_data: Vec< u8 > = array_buffer.into();
load_rom( &pinky, &rom_data );
}));
js! {
var req = new XMLHttpRequest();
req.addEventListener( "load" , function() {
@{builtin_rom_loaded}( req.response );
});
req.open( "GET", "roms/" + @{&file} );
req.responseType = "arraybuffer";
req.send();
}
}));
}
}
fn support_custom_roms( pinky: Rc< RefCell< PinkyWeb > > ) {
let browse_for_roms_button = web::document().get_element_by_id( "browse-for-roms" ).unwrap();
browse_for_roms_button.add_event_listener( move |event: ChangeEvent| {
let input: InputElement = event.target().unwrap().try_into().unwrap();
let files = input.files().unwrap();
let file = match files.iter().next() {
Some( file ) => file,
None => return
};
hide( "change-rom-menu" );
hide( "side-text" );
show( "loading" );
let reader = FileReader::new();
reader.add_event_listener( enclose!( [pinky, reader] move |_: ProgressLoadEvent| {
let rom_data: Vec< u8 > = match reader.result().unwrap() {
FileReaderResult::ArrayBuffer( buffer ) => buffer,
_ => unreachable!()
}.into();
load_rom( &pinky, &rom_data );
}));
reader.read_as_array_buffer( &file );
});
}
fn support_rom_changing( pinky: Rc< RefCell< PinkyWeb > > ) {
let change_rom_button = web::document().get_element_by_id( "change-rom-button" ).unwrap();
change_rom_button.add_event_listener( enclose!( [pinky] move |_: ClickEvent| {
pinky.borrow_mut().pause();
hide( "viewport" );
hide( "change-rom-button" );
show( "change-rom-menu" );
show( "rom-menu-close" );
}));
let rom_menu_close_button = web::document().get_element_by_id( "rom-menu-close" ).unwrap();
rom_menu_close_button.add_event_listener( move |_: ClickEvent| {
pinky.borrow_mut().unpause();
show( "viewport" );
show( "change-rom-button" );
hide( "change-rom-menu" );
hide( "rom-menu-close" );
});
}
fn support_input( pinky: Rc< RefCell< PinkyWeb > > ) {
web::window().add_event_listener( enclose!( [pinky] move |event: KeydownEvent| {
let handled = pinky.borrow_mut().on_key( &event.key(), event.location(), true );
if handled {
event.prevent_default();
}
}));
web::window().add_event_listener( enclose!( [pinky] move |event: KeyupEvent| {
let handled = pinky.borrow_mut().on_key( &event.key(), event.location(), false );
if handled {
event.prevent_default();
}
}));
}
fn load_rom( pinky: &Rc< RefCell< PinkyWeb > >, rom_data: &[u8] ) {
hide( "loading" );
hide( "error" );
let mut pinky = pinky.borrow_mut();
let pinky = pinky.deref_mut();
if let Err( err ) = nes::Interface::load_rom_from_memory( pinky, rom_data ) {
handle_error( err );
return;
}
pinky.unpause();
show( "viewport" );
show( "change-rom-button" );
}
fn handle_error< E: Into< Box< Error > > >( error: E ) {
let error_message = format!( "{}", error.into() );
web::document().get_element_by_id( "error-description" ).unwrap().set_text_content( &error_message );
hide( "viewport" );
hide( "change-rom-button" );
hide( "rom-menu-close" );
show( "change-rom-menu" );
show( "error" );
}
fn main() {
stdweb::initialize();
let canvas = web::document().get_element_by_id( "viewport" ).unwrap();
let pinky = Rc::new( RefCell::new( PinkyWeb::new( &canvas ) ) );
support_custom_roms( pinky.clone() );
support_rom_changing( pinky.clone() );
fetch_builtin_rom_list( enclose!( [pinky] |roms| {
support_builtin_roms( roms, pinky );
hide( "loading" );
show( "change-rom-menu" );
}));
support_input( pinky.clone() );
web::window().request_animation_frame( move |_| {
main_loop( pinky );
});
stdweb::event_loop();
}

447
pinky-web/static/css/normalize.css vendored Normal file
View file

@ -0,0 +1,447 @@
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
html {
line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}

279
pinky-web/static/index.html Normal file
View file

@ -0,0 +1,279 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1' name='viewport' />
<link rel="stylesheet" type="text/css" href="css/normalize.css" media="screen" />
<style media="screen" type="text/css">
* {
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: black;
color: #999;
padding: 1.5em;
}
#viewport {
text-align: center;
display: box;
margin: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 90%;
}
#menu {
position: absolute;
top: 16px;
right: 16px;
}
#change-rom-menu {
text-align: center;
background-color: #111;
max-width: 25em;
min-width: 20em;
margin: auto;
padding-bottom: 1.5em;
display: flex;
flex-flow:column;
justify-content: flex-start;
max-height: 100%;
}
#change-rom-menu-header {
width: 100%;
font-weight: 400;
font-size: 19pt;
font-style: italic;
color: #bbb;
background-color: black;
margin-top: 0;
padding-top: 0.5em;
padding-bottom: 0.5em;
margin-bottom: 0.75em;
}
#browse-for-roms-button {
margin-bottom: 1.5em;
}
#browse-for-roms-button, #rom-list {
margin-left: 1.5em;
margin-right: 1.5em;
}
#change-rom-menu * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#rom-list {
flex-grow: 0;
flex-basis: 1;
overflow-y: auto;
display: flex;
flex-flow:column;
justify-content: center;
}
#browse-for-roms {
position: fixed;
top: -1000px;
}
button, #browse-for-roms-button {
font-size: 11pt;
color: #bbb;
padding: 8px 16px;
background: #000;
transition: background-color 0.1s ease-in-out;
}
button:hover, #browse-for-roms-button:hover {
background-color: #222;
}
button, #browse-for-roms-button, #rom-list, #change-rom-menu {
border-radius: 4px;
border-bottom: 1px solid #222;
border-top: 1px solid #222;
border-left: 1px solid #101010;
border-right: 1px solid #101010;
box-shadow: 0px 1px 3px #060606;
}
#rom-list button {
border: 0;
border-radius: 0;
box-shadow: none;
}
#rom-menu-close {
float: right;
margin-left: -1.8em;
padding-left: 0.8em;
padding-right: 0.8em;
}
#rom-menu-close:hover {
color: #fff;
transition: color 0.5s ease-in-out;
}
.hidden {
display: none !important;
}
p {
color: #555;
}
h1 {
font-size: 16pt;
font-weight: 400;
color: #999;
}
a, a:visited {
text-decoration: none;
color: #999;
font-weight: 700;
}
.highlight {
color: #bbb;
}
#horizontal-container {
display: flex;
flex-flow: row;
justify-content: flex-start;
width: 100%;
}
#horizontal-container #side-text {
margin-right: 3em;
}
#horizontal-container #change-rom-menu {
flex-basis: 1;
flex-grow: 1;
}
#side-text {
max-width: 30em;
min-width: 20em;
flex-shrink: 1;
}
#loading {
margin: auto;
}
#error-description {
font-style: italic;
margin-left: 1em;
}
</style>
<title>Pinky - an NES emulator</title>
</head>
<body>
<canvas id="viewport" width="256" height="240" class="hidden"></canvas>
<div id="menu">
<button id="change-rom-button" class="hidden">Change ROM</button>
</div>
<div id="horizontal-container">
<div id="side-text">
<h1>Pinky - an NES emulator written in Rust</h1>
<p>
Hi there!
</p>
<p>
This is an NES emulator written in <a href="https://www.rust-lang.org/">Rust</a> completely
from scratch purely based on publicly available documentation.
</p>
<p>
What you see here is its port compiled to <a href="https://en.wikipedia.org/wiki/WebAssembly">WebAssembly</a>
using the magic of <a href="https://github.com/koute/stdweb">the stdweb crate</a> and Rust's native WebAssembly
backend, without the use of Emscripten.
</p>
<!--
<p>
What you see here is its port compiled to <a href="https://en.wikipedia.org/wiki/WebAssembly">WebAssembly</a>
using the magic of <a href="https://github.com/koute/stdweb">the stdweb crate</a> and <a href="http://kripken.github.io/emscripten-site">Emscripten</a>.
</p>
<p>
What you see here is its port compiled to <a href="https://en.wikipedia.org/wiki/Asm.js">asm.js</a>
using the magic of <a href="https://github.com/koute/stdweb">the stdweb crate</a> and <a href="http://kripken.github.io/emscripten-site">Emscripten</a>.
</p>
-->
<p>
You can take a peek at the <a href="https://github.com/koute/pinky/tree/master/pinky-web">code</a> if you're
interested, but first I invite you to take it for a spin and play a few games!
</p>
<p>
You can chose between a few fun freeware NES games, but if you have the ROMs then games like
Super Mario Brothers, Contra, Legend of Zelda, or Final Fantasy will work too!
</p>
<p>
Controls are <span class="highlight">Z</span> and <span class="highlight">X</span>, and <span class="highlight">the arrow keys</span>!
</p>
<!--
<p>
You might also want to check out the version compiled *with* Emscripten <a href="https://koute.github.io/pinky-web-webasm-emscripten">to WebAssembly</a> or <a href="https://koute.github.io/pinky-web-asmjs-emscripten">to asm.js</a>.
</p>
<p>
You might also want to check out the version compiled to WebAssembly *without* Emscripten <a href="https://koute.github.io/pinky-web">here</a>.
</p>
-->
</div>
<div id="loading">Loading...</div>
<div id="unsupported" class="hidden">
<h1>Sorry, your browser is unsupported!</h1>
<p>Maybe try something newer which supports WebAssembly?</p>
</div>
<div id="error" class="hidden">
<h1>Encountered an error!</h1>
<p>The error message is as follows:</p>
<p id="error-description"></p>
<p>Sorry about that! Maybe try another ROM?</p>
</div>
<div id="change-rom-menu" class="hidden">
<div id="change-rom-menu-header"><div id="rom-menu-close" class="hidden">×</div>Select a ROM to load</div>
<label id="browse-for-roms-button">
<input id="browse-for-roms" type="file" required />
<span>Load your own ROM...</span>
</label>
<div style="margin-bottom: 0.5em;">Builtin ROMs</div>
<div id="rom-list"></div>
</div>
</div>
<script src="js/app.js"></script>
<script>
if( typeof Module !== "object" ) { // If not running under Emscripten.
var webassembly_supported = typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
if( !webassembly_supported ) {
document.getElementById( "unsupported" ).className = "";
document.getElementById( "loading" ).className = "hidden";
}
}
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,50 @@
[
{
"name": "Alter Ego",
"file": "alter_ego.nes"
},
{
"name": "Cheril the Goddess",
"file": "cheril_the_goddess.nes"
},
{
"name": "Dushlan",
"file": "dushlan.nes"
},
{
"name": "Lawn Mower",
"file": "lawn_mower.nes"
},
{
"name": "Mad Wizard",
"file": "mad_wizard.nes"
},
{
"name": "Micro Knight 4",
"file": "micro_knight_4_v1_02.nes"
},
{
"name": "Nebs 'n Debs",
"file": "nebs_n_debs_2_15_2017.nes"
},
{
"name": "Owlia",
"file": "owlia.nes"
},
{
"name": "Streemerz",
"file": "streemerz_v02.nes"
},
{
"name": "Super Painter",
"file": "super_painter_1_0.nes"
},
{
"name": "Tiger Jenny",
"file": "tiger_jenny.nes"
},
{
"name": "Yun",
"file": "yun.nes"
}
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.