Skip to content
Snippets Groups Projects
Commit a006b166 authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

Merge branch 'joamag/wasm' into 'master'

Support for WASM :robot:

See merge request !4
parents 240446b4 dc6bd03d
No related branches found
No related tags found
1 merge request!4Support for WASM 🤖
Pipeline #644 passed
Showing
with 725 additions and 4 deletions
...@@ -10,8 +10,12 @@ edition = "2018" ...@@ -10,8 +10,12 @@ edition = "2018"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
wasm = ["wasm-bindgen"]
[dependencies] [dependencies]
getrandom = { version = "0.2", features = ["js"] } getrandom = { version = "0.2", features = ["js"] }
wasm-bindgen = { version = "0.2", optional = true }
[profile.release] [profile.release]
debug = false debug = false
......
...@@ -23,11 +23,29 @@ The work of this emulator was inspired/started by [jc-chip8](https://github.com/ ...@@ -23,11 +23,29 @@ The work of this emulator was inspired/started by [jc-chip8](https://github.com/
* Full compliant with test CHIP-8 ROMs * Full compliant with test CHIP-8 ROMs
* RAM snapshot saving and loading * RAM snapshot saving and loading
## Build
### WASM for Node.js
```bash
cargo install wasm-pack
wasm-pack build --release --target=nodejs -- --features wasm
```
### WASM for Web
```bash
cargo install wasm-pack
wasm-pack build --release --target=web --out-dir=examples/web -- --features wasm
cd examples/web
python3 -m http.server
```
## Reason ## Reason
And... yes this is the real inspiration behind the emulator's name: And... yes this is the real inspiration behind the emulator's name:
<img src="resources/chips-ahoy.jpeg" alt="Chips Ahoy" width="200" /> <img src="res/chips-ahoy.jpeg" alt="Chips Ahoy" width="200" />
## Inspiration ## Inspiration
......
...@@ -8,7 +8,7 @@ const CYCLE_COUNT: u64 = 5_000_000_000; ...@@ -8,7 +8,7 @@ const CYCLE_COUNT: u64 = 5_000_000_000;
fn main() { fn main() {
let chips: [Box<dyn Chip8>; 2] = [Box::new(Chip8Classic::new()), Box::new(Chip8Neo::new())]; let chips: [Box<dyn Chip8>; 2] = [Box::new(Chip8Classic::new()), Box::new(Chip8Neo::new())];
let rom_path = "./resources/pong.ch8"; let rom_path = "./res/pong.ch8";
let rom = read_file(rom_path); let rom = read_file(rom_path);
for mut chip8 in chips { for mut chip8 in chips {
......
File moved
...@@ -160,7 +160,7 @@ fn main() { ...@@ -160,7 +160,7 @@ fn main() {
// loads the font that is going to be used in the drawing // loads the font that is going to be used in the drawing
// process cycle if necessary // process cycle if necessary
let mut font = ttf_context let mut font = ttf_context
.load_font(format!("./resources/{}", FONT_NAME), FONT_SIZE) .load_font(format!("./res/{}", FONT_NAME), FONT_SIZE)
.unwrap(); .unwrap();
font.set_style(sdl2::ttf::FontStyle::BOLD); font.set_style(sdl2::ttf::FontStyle::BOLD);
font.set_hinting(Hinting::Light); font.set_hinting(Hinting::Light);
...@@ -181,7 +181,7 @@ fn main() { ...@@ -181,7 +181,7 @@ fn main() {
// updates the icon of the window to reflect the image // updates the icon of the window to reflect the image
// and style of the emulator // and style of the emulator
let surface = Surface::from_file("./resources/icon.png").unwrap(); let surface = Surface::from_file("./res/icon.png").unwrap();
window.set_icon(&surface); window.set_icon(&surface);
let mut canvas = window.into_canvas().accelerated().build().unwrap(); let mut canvas = window.into_canvas().accelerated().build().unwrap();
......
/* tslint:disable */
/* eslint-disable */
/**
*/
export class Chip8Classic {
free(): void;
/**
*/
constructor();
}
/**
*/
export class Chip8Neo {
free(): void;
/**
*/
constructor();
/**
* @param {Uint8Array} rom
*/
load_rom_ws(rom: Uint8Array): void;
/**
*/
reset_ws(): void;
/**
*/
reset_hard_ws(): void;
/**
* @returns {Uint8Array}
*/
vram_ws(): Uint8Array;
/**
*/
clock_ws(): void;
/**
*/
clock_dt_ws(): void;
/**
*/
clock_st_ws(): void;
/**
* @param {number} key
*/
key_press_ws(key: number): void;
/**
* @param {number} key
*/
key_lift_ws(key: number): void;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly __wbg_chip8neo_free: (a: number) => void;
readonly chip8neo_new: () => number;
readonly chip8neo_load_rom_ws: (a: number, b: number, c: number) => void;
readonly chip8neo_reset_ws: (a: number) => void;
readonly chip8neo_reset_hard_ws: (a: number) => void;
readonly chip8neo_vram_ws: (a: number, b: number) => void;
readonly chip8neo_clock_ws: (a: number) => void;
readonly chip8neo_clock_dt_ws: (a: number) => void;
readonly chip8neo_clock_st_ws: (a: number) => void;
readonly chip8neo_key_press_ws: (a: number, b: number) => void;
readonly chip8neo_key_lift_ws: (a: number, b: number) => void;
readonly __wbg_chip8classic_free: (a: number) => void;
readonly chip8classic_new: () => number;
readonly __wbindgen_malloc: (a: number) => number;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number) => void;
readonly __wbindgen_exn_store: (a: number) => void;
}
/**
* Synchronously compiles the given `bytes` and instantiates the WebAssembly module.
*
* @param {BufferSource} bytes
*
* @returns {InitOutput}
*/
export function initSync(bytes: BufferSource): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
*
* @returns {Promise<InitOutput>}
*/
export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
let wasm;
const heap = new Array(32).fill(undefined);
heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; }
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 36) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
let cachedUint8Memory0;
function getUint8Memory0() {
if (cachedUint8Memory0.byteLength === 0) {
cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8Memory0;
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
let cachedInt32Memory0;
function getInt32Memory0() {
if (cachedInt32Memory0.byteLength === 0) {
cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachedInt32Memory0;
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_exn_store(addHeapObject(e));
}
}
/**
*/
export class Chip8Classic {
static __wrap(ptr) {
const obj = Object.create(Chip8Classic.prototype);
obj.ptr = ptr;
return obj;
}
__destroy_into_raw() {
const ptr = this.ptr;
this.ptr = 0;
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_chip8classic_free(ptr);
}
/**
*/
constructor() {
const ret = wasm.chip8classic_new();
return Chip8Classic.__wrap(ret);
}
}
/**
*/
export class Chip8Neo {
static __wrap(ptr) {
const obj = Object.create(Chip8Neo.prototype);
obj.ptr = ptr;
return obj;
}
__destroy_into_raw() {
const ptr = this.ptr;
this.ptr = 0;
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_chip8neo_free(ptr);
}
/**
*/
constructor() {
const ret = wasm.chip8neo_new();
return Chip8Neo.__wrap(ret);
}
/**
* @param {Uint8Array} rom
*/
load_rom_ws(rom) {
const ptr0 = passArray8ToWasm0(rom, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
wasm.chip8neo_load_rom_ws(this.ptr, ptr0, len0);
}
/**
*/
reset_ws() {
wasm.chip8neo_reset_ws(this.ptr);
}
/**
*/
reset_hard_ws() {
wasm.chip8neo_reset_hard_ws(this.ptr);
}
/**
* @returns {Uint8Array}
*/
vram_ws() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.chip8neo_vram_ws(retptr, this.ptr);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v0 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
*/
clock_ws() {
wasm.chip8neo_clock_ws(this.ptr);
}
/**
*/
clock_dt_ws() {
wasm.chip8neo_clock_dt_ws(this.ptr);
}
/**
*/
clock_st_ws() {
wasm.chip8neo_clock_st_ws(this.ptr);
}
/**
* @param {number} key
*/
key_press_ws(key) {
wasm.chip8neo_key_press_ws(this.ptr, key);
}
/**
* @param {number} key
*/
key_lift_ws(key) {
wasm.chip8neo_key_lift_ws(this.ptr, key);
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function getImports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_randomFillSync_91e2b39becca6147 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).randomFillSync(getArrayU8FromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbg_getRandomValues_b14734aa289bc356 = function() { return handleError(function (arg0, arg1) {
getObject(arg0).getRandomValues(getObject(arg1));
}, arguments) };
imports.wbg.__wbg_process_e56fd54cf6319b6c = function(arg0) {
const ret = getObject(arg0).process;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_is_object = function(arg0) {
const val = getObject(arg0);
const ret = typeof(val) === 'object' && val !== null;
return ret;
};
imports.wbg.__wbg_versions_77e21455908dad33 = function(arg0) {
const ret = getObject(arg0).versions;
return addHeapObject(ret);
};
imports.wbg.__wbg_node_0dd25d832e4785d5 = function(arg0) {
const ret = getObject(arg0).node;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_is_string = function(arg0) {
const ret = typeof(getObject(arg0)) === 'string';
return ret;
};
imports.wbg.__wbg_static_accessor_NODE_MODULE_26b231378c1be7dd = function() {
const ret = module;
return addHeapObject(ret);
};
imports.wbg.__wbg_require_0db1598d9ccecb30 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).require(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_crypto_b95d7173266618a9 = function(arg0) {
const ret = getObject(arg0).crypto;
return addHeapObject(ret);
};
imports.wbg.__wbg_msCrypto_5a86d77a66230f81 = function(arg0) {
const ret = getObject(arg0).msCrypto;
return addHeapObject(ret);
};
imports.wbg.__wbg_newnoargs_fc5356289219b93b = function(arg0, arg1) {
const ret = new Function(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbg_call_4573f605ca4b5f10 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).call(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbg_self_ba1ddafe9ea7a3a2 = function() { return handleError(function () {
const ret = self.self;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_window_be3cc430364fd32c = function() { return handleError(function () {
const ret = window.window;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_globalThis_56d9c9f814daeeee = function() { return handleError(function () {
const ret = globalThis.globalThis;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_global_8c35aeee4ac77f2b = function() { return handleError(function () {
const ret = global.global;
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbg_buffer_de1150f91b23aa89 = function(arg0) {
const ret = getObject(arg0).buffer;
return addHeapObject(ret);
};
imports.wbg.__wbg_new_97cf52648830a70d = function(arg0) {
const ret = new Uint8Array(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_set_a0172b213e2469e9 = function(arg0, arg1, arg2) {
getObject(arg0).set(getObject(arg1), arg2 >>> 0);
};
imports.wbg.__wbg_length_e09c0b925ab8de5d = function(arg0) {
const ret = getObject(arg0).length;
return ret;
};
imports.wbg.__wbg_newwithlength_e833b89f9db02732 = function(arg0) {
const ret = new Uint8Array(arg0 >>> 0);
return addHeapObject(ret);
};
imports.wbg.__wbg_subarray_9482ae5cd5cd99d3 = function(arg0, arg1, arg2) {
const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbindgen_memory = function() {
const ret = wasm.memory;
return addHeapObject(ret);
};
return imports;
}
function initMemory(imports, maybe_memory) {
}
function finalizeInit(instance, module) {
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
return wasm;
}
function initSync(bytes) {
const imports = getImports();
initMemory(imports);
const module = new WebAssembly.Module(bytes);
const instance = new WebAssembly.Instance(module, imports);
return finalizeInit(instance, module);
}
async function init(input) {
if (typeof input === 'undefined') {
input = new URL('chip_ahoyto_bg.wasm', import.meta.url);
}
const imports = getImports();
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
initMemory(imports);
const { instance, module } = await load(await input, imports);
return finalizeInit(instance, module);
}
export { initSync }
export default init;
File added
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export function __wbg_chip8neo_free(a: number): void;
export function chip8neo_new(): number;
export function chip8neo_load_rom_ws(a: number, b: number, c: number): void;
export function chip8neo_reset_ws(a: number): void;
export function chip8neo_reset_hard_ws(a: number): void;
export function chip8neo_vram_ws(a: number, b: number): void;
export function chip8neo_clock_ws(a: number): void;
export function chip8neo_clock_dt_ws(a: number): void;
export function chip8neo_clock_st_ws(a: number): void;
export function chip8neo_key_press_ws(a: number, b: number): void;
export function chip8neo_key_lift_ws(a: number, b: number): void;
export function __wbg_chip8classic_free(a: number): void;
export function chip8classic_new(): number;
export function __wbindgen_malloc(a: number): number;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_free(a: number, b: number): void;
export function __wbindgen_exn_store(a: number): void;
<!DOCTYPE html>
<html>
<head>
<title>CHIP-Ahoyto</title>
<link rel="icon" href="res/icon.png" />
<link rel="stylesheet" href="index.css" />
</head>
<body>
<canvas id="chip-canvas" width="960" height="480"></canvas>
<div id="overlay" class="overlay">
Drag to load ROM
</div>
<script type="module" src="index.js"></script>
</body>
</html>
import {
default as wasm,
Chip8Neo
} from "./chip_ahoyto.js";
const PIXEL_SET_COLOR = 0x50cb93ff;
const PIXEL_UNSET_COLOR = 0x1b1a17ff;
let LOGIC_HZ = 480;
const TIMER_HZ = 60;
const VISUAL_HZ = 60;
const KEYS = {
"1": 0x01,
"2": 0x02,
"3": 0x03,
"4": 0x0c,
"q": 0x04,
"w": 0x05,
"e": 0x06,
"r": 0x0d,
"a": 0x07,
"s": 0x08,
"d": 0x09,
"f": 0x0e,
"z": 0x0a,
"x": 0x00,
"c": 0x0b,
"v": 0x0f
}
const ROM = "res/roms/pong.ch8";
const state = {
chip8: null,
canvas: null,
canvasScaled: null,
canvasCtx: null,
canvasScaledCtx: null,
image: null,
videoBuff: null
};
(async () => {
// initializes the WASM module, this is required
// so that the global symbols become available
await wasm();
// initializes the complete set of sub-systems
// and registers the event handlers
init();
register();
// loads the ROM data and converts it into the
// target u8 array bufffer
const response = await fetch(ROM);
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
// creates the CHIP-8 instance and resets it
state.chip8 = new Chip8Neo();
state.chip8.reset_hard_ws();
state.chip8.load_rom_ws(data);
// runs the sequence as an infinite loop, running
// the associated CPU cycles accordingly
while (true) {
const ratioLogic = LOGIC_HZ / VISUAL_HZ;
for(let i = 0; i < ratioLogic; i++) {
state.chip8.clock_ws();
}
const ratioTimer = TIMER_HZ / VISUAL_HZ;
for(let i = 0; i < ratioTimer; i++) {
state.chip8.clock_dt_ws();
state.chip8.clock_st_ws();
}
// updates the canvas object with the new
// visual information comming in
updateCanvas(state.chip8.vram_ws());
// waits a little bit for the next frame to be draw
// @todo need to define target time for draw
await new Promise((resolve, reject) => {
setTimeout(resolve, 1000 / VISUAL_HZ);
})
}
})();
const register = () => {
registerDrop();
registerKeys();
}
const registerDrop = () => {
document.addEventListener("drop", async (event) => {
event.preventDefault();
event.stopPropagation();
const overlay = document.getElementById("overlay");
overlay.classList.remove("visible");
if (!event.dataTransfer.files) return;
const file = event.dataTransfer.files[0];
const arrayBuffer = await file.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
state.chip8.reset_hard_ws();
state.chip8.load_rom_ws(data);
});
document.addEventListener("dragover", async (event) => {
event.preventDefault();
const overlay = document.getElementById("overlay");
overlay.classList.add("visible");
});
document.addEventListener("dragenter", async (event) => {
const overlay = document.getElementById("overlay");
overlay.classList.add("visible");
});
document.addEventListener("dragleave", async (event) => {
const overlay = document.getElementById("overlay");
overlay.classList.remove("visible");
});
};
const registerKeys = () => {
document.addEventListener("keydown", (event) => {
const keyCode = KEYS[event.key];
if (keyCode !== undefined) {
state.chip8.key_press_ws(keyCode);
return;
}
switch(event.key) {
case "+":
LOGIC_HZ += 60;
break;
case "-":
LOGIC_HZ += 60;
break;
}
});
document.addEventListener("keyup", (event) => {
const keyCode = KEYS[event.key];
if (keyCode !== undefined) {
state.chip8.key_lift_ws(keyCode);
return;
}
});
}
const init = () => {
initCanvas();
}
const initCanvas = () => {
// initializes the off-screen canvas that is going to be
// used in the drawing proces
state.canvas = document.createElement("canvas");
state.canvas.width = 64;
state.canvas.height = 32;
state.canvasCtx = state.canvas.getContext("2d");
state.canvasScaled = document.getElementById("chip-canvas");
state.canvasScaledCtx = state.canvasScaled.getContext("2d");
state.canvasScaledCtx.scale(state.canvasScaled.width / state.canvas.width, state.canvasScaled.height / state.canvas.height);
state.canvasScaledCtx.imageSmoothingEnabled = false;
state.image = state.canvasCtx.createImageData(state.canvas.width, state.canvas.height);
state.videoBuff = new DataView(state.image.data.buffer);
};
const updateCanvas = (pixels) => {
for (let i = 0; i < pixels.length; i++) {
state.videoBuff.setUint32(i * 4, pixels[i] ? PIXEL_SET_COLOR : PIXEL_UNSET_COLOR);
}
state.canvasCtx.putImageData(state.image, 0, 0);
state.canvasScaledCtx.drawImage(state.canvas, 0, 0);
};
File moved
File moved
File moved
File moved
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment