diff --git a/README.md b/README.md index ed27bc0eff944d5601db2425fd4b3dbe31597de1..3c2257f43b22138c0e5423fcaaa45fabad53791e 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,20 @@ The work of this emulator was inspired/started by [jc-chip8](https://github.com/ ## Build -### WASM +### 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 -- --features wasm +``` + ## Reason And... yes this is the real inspiration behind the emulator's name: diff --git a/examples/web/chip_ahoyto.d.ts b/examples/web/chip_ahoyto.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..967df317c8fd3731d6413322a1c09cb5f23bac35 --- /dev/null +++ b/examples/web/chip_ahoyto.d.ts @@ -0,0 +1,57 @@ +/* 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; +/** +*/ + clock_ws(): 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_clock_ws: (a: number) => void; + readonly __wbg_chip8classic_free: (a: number) => void; + readonly chip8classic_new: () => number; + readonly __wbindgen_malloc: (a: number) => number; + readonly __wbindgen_exn_store: (a: number) => void; +} + +/** +* 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>; diff --git a/examples/web/chip_ahoyto.js b/examples/web/chip_ahoyto.js new file mode 100644 index 0000000000000000000000000000000000000000..792feab78e86a274080bc876ada195cce5e91bff --- /dev/null +++ b/examples/web/chip_ahoyto.js @@ -0,0 +1,312 @@ + +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 cachegetUint8Memory0 = null; +function getUint8Memory0() { + if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; +} + +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; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} + +function getArrayU8FromWasm0(ptr, len) { + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +} +/** +*/ +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); + } + /** + */ + clock_ws() { + wasm.chip8neo_clock_ws(this.ptr); + } +} + +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; + } + } +} + +async function init(input) { + if (typeof input === 'undefined') { + input = new URL('chip_ahoyto_bg.wasm', import.meta.url); + } + const imports = {}; + imports.wbg = {}; + 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.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + 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_getRandomValues_b14734aa289bc356 = function() { return handleError(function (arg0, arg1) { + getObject(arg0).getRandomValues(getObject(arg1)); + }, arguments) }; + imports.wbg.__wbg_randomFillSync_91e2b39becca6147 = function() { return handleError(function (arg0, arg1, arg2) { + getObject(arg0).randomFillSync(getArrayU8FromWasm0(arg1, arg2)); + }, arguments) }; + imports.wbg.__wbg_newnoargs_e23b458e372830de = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_call_ae78342adc33730a = 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_99737b4dcdf6f0d8 = function() { return handleError(function () { + const ret = self.self; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_window_9b61fbbf3564c4fb = function() { return handleError(function () { + const ret = window.window; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_globalThis_8e275ef40caea3a3 = function() { return handleError(function () { + const ret = globalThis.globalThis; + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_global_5de1e0f82bddcd27 = 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_7af23f65f6c64548 = function(arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); + }; + imports.wbg.__wbg_new_cc9018bd6f283b6f = function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_set_f25e869e4565d2a2 = function(arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); + }; + imports.wbg.__wbg_length_0acb1cf9bbaf8519 = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_newwithlength_8f0657faca9f1422 = function(arg0) { + const ret = new Uint8Array(arg0 >>> 0); + return addHeapObject(ret); + }; + imports.wbg.__wbg_subarray_da527dbd24eafb6b = 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); + }; + + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + + + + const { instance, module } = await load(await input, imports); + + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + + return wasm; +} + +export default init; + diff --git a/examples/web/chip_ahoyto_bg.wasm b/examples/web/chip_ahoyto_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e5178df226f9cd406c380e432a15724c30a377ba Binary files /dev/null and b/examples/web/chip_ahoyto_bg.wasm differ diff --git a/examples/web/chip_ahoyto_bg.wasm.d.ts b/examples/web/chip_ahoyto_bg.wasm.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..2f8c42c5ff436515adc414943c66d06a032d7112 --- /dev/null +++ b/examples/web/chip_ahoyto_bg.wasm.d.ts @@ -0,0 +1,13 @@ +/* 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_clock_ws(a: 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_exn_store(a: number): void; diff --git a/examples/web/index.html b/examples/web/index.html new file mode 100644 index 0000000000000000000000000000000000000000..b50d7ca094ab34a9da9f5c4a71fb09587700089e --- /dev/null +++ b/examples/web/index.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>CHIP-Ahoyto</title> + </head> + <body> + <script type="module" src="./index.js"></script> + </body> +</html> diff --git a/examples/web/index.js b/examples/web/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f35c2539cf474dc994ae36e950f48e326c69d66a --- /dev/null +++ b/examples/web/index.js @@ -0,0 +1,14 @@ +import { default as wasm, Chip8Neo } from "./chip_ahoyto.js"; + +(async () => { + // initializes the WASM module, this is required + // so that the global symbols become available + await wasm(); + + console.info("LOADED"); + + const chip8 = new Chip8Neo(); + chip8.clock_ws(); + + console.info("CLOCK"); +})(); diff --git a/src/chip8_neo.rs b/src/chip8_neo.rs index fcf1a761393603e08f8d37fb2cd85905f03307c6..730a15ae5418c2ca4ddef11e3a09dd10ce59b383 100644 --- a/src/chip8_neo.rs +++ b/src/chip8_neo.rs @@ -75,6 +75,89 @@ impl Chip8 for Chip8Neo { self.reset(); } + fn beep(&self) -> bool { + self.st > 0 + } + + fn pc(&self) -> u16 { + self.pc + } + + fn sp(&self) -> u8 { + self.sp + } + + fn ram(&self) -> Vec<u8> { + self.ram.to_vec() + } + + fn vram(&self) -> Vec<u8> { + self.vram.to_vec() + } + + fn get_state(&self) -> Vec<u8> { + let mut buffer: Vec<u8> = Vec::new(); + buffer.extend(self.ram.iter()); + buffer.extend(self.vram.iter()); + buffer.extend(self.stack.map(|v| v.to_le_bytes()).iter().flatten()); + buffer.extend(self.regs.iter()); + buffer.extend(self.pc.to_le_bytes().iter()); + buffer.extend(self.i.to_le_bytes().iter()); + buffer.extend(self.sp.to_le_bytes().iter()); + buffer.extend(self.dt.to_le_bytes().iter()); + buffer.extend(self.st.to_le_bytes().iter()); + buffer.extend(self.keys.map(|v| v as u8).iter()); + buffer.extend(self.last_key.to_le_bytes().iter()); + buffer + } + + fn set_state(&mut self, state: &[u8]) { + let mut u8_buffer = [0u8; 1]; + let mut u16_buffer = [0u8; 2]; + let mut regs_buffer = [0u8; REGISTERS_SIZE * 2]; + let mut keys_buffer = [0u8; KEYS_SIZE]; + + let mut cursor = Cursor::new(state.to_vec()); + + cursor.read_exact(&mut self.ram).unwrap(); + cursor.read_exact(&mut self.vram).unwrap(); + cursor.read_exact(&mut regs_buffer).unwrap(); + self.stack.clone_from_slice( + regs_buffer + .chunks(2) + .map(|v| { + u16_buffer.clone_from_slice(&v[0..2]); + u16::from_le_bytes(u16_buffer) + }) + .collect::<Vec<u16>>() + .as_slice(), + ); + cursor.read_exact(&mut self.regs).unwrap(); + cursor.read_exact(&mut u16_buffer).unwrap(); + self.pc = u16::from_le_bytes(u16_buffer); + cursor.read_exact(&mut u16_buffer).unwrap(); + self.i = u16::from_le_bytes(u16_buffer); + cursor.read_exact(&mut u8_buffer).unwrap(); + self.sp = u8::from_le_bytes(u8_buffer); + cursor.read_exact(&mut u8_buffer).unwrap(); + self.dt = u8::from_le_bytes(u8_buffer); + cursor.read_exact(&mut u8_buffer).unwrap(); + self.st = u8::from_le_bytes(u8_buffer); + cursor.read_exact(&mut keys_buffer).unwrap(); + self.keys.clone_from_slice( + keys_buffer + .map(|v| if v == 1 { true } else { false }) + .iter() + .as_slice(), + ); + cursor.read_exact(&mut u8_buffer).unwrap(); + self.last_key = u8::from_le_bytes(u8_buffer); + } + + fn load_rom(&mut self, rom: &[u8]) { + self.ram[ROM_START..ROM_START + rom.len()].clone_from_slice(&rom); + } + fn clock(&mut self) { // fetches the current instruction and increments // the PC (program counter) accordingly @@ -224,89 +307,6 @@ impl Chip8 for Chip8Neo { } self.keys[key as usize] = false; } - - fn load_rom(&mut self, rom: &[u8]) { - self.ram[ROM_START..ROM_START + rom.len()].clone_from_slice(&rom); - } - - fn beep(&self) -> bool { - self.st > 0 - } - - fn pc(&self) -> u16 { - self.pc - } - - fn sp(&self) -> u8 { - self.sp - } - - fn ram(&self) -> Vec<u8> { - self.ram.to_vec() - } - - fn vram(&self) -> Vec<u8> { - self.vram.to_vec() - } - - fn get_state(&self) -> Vec<u8> { - let mut buffer: Vec<u8> = Vec::new(); - buffer.extend(self.ram.iter()); - buffer.extend(self.vram.iter()); - buffer.extend(self.stack.map(|v| v.to_le_bytes()).iter().flatten()); - buffer.extend(self.regs.iter()); - buffer.extend(self.pc.to_le_bytes().iter()); - buffer.extend(self.i.to_le_bytes().iter()); - buffer.extend(self.sp.to_le_bytes().iter()); - buffer.extend(self.dt.to_le_bytes().iter()); - buffer.extend(self.st.to_le_bytes().iter()); - buffer.extend(self.keys.map(|v| v as u8).iter()); - buffer.extend(self.last_key.to_le_bytes().iter()); - buffer - } - - fn set_state(&mut self, state: &[u8]) { - let mut u8_buffer = [0u8; 1]; - let mut u16_buffer = [0u8; 2]; - let mut regs_buffer = [0u8; REGISTERS_SIZE * 2]; - let mut keys_buffer = [0u8; KEYS_SIZE]; - - let mut cursor = Cursor::new(state.to_vec()); - - cursor.read_exact(&mut self.ram).unwrap(); - cursor.read_exact(&mut self.vram).unwrap(); - cursor.read_exact(&mut regs_buffer).unwrap(); - self.stack.clone_from_slice( - regs_buffer - .chunks(2) - .map(|v| { - u16_buffer.clone_from_slice(&v[0..2]); - u16::from_le_bytes(u16_buffer) - }) - .collect::<Vec<u16>>() - .as_slice(), - ); - cursor.read_exact(&mut self.regs).unwrap(); - cursor.read_exact(&mut u16_buffer).unwrap(); - self.pc = u16::from_le_bytes(u16_buffer); - cursor.read_exact(&mut u16_buffer).unwrap(); - self.i = u16::from_le_bytes(u16_buffer); - cursor.read_exact(&mut u8_buffer).unwrap(); - self.sp = u8::from_le_bytes(u8_buffer); - cursor.read_exact(&mut u8_buffer).unwrap(); - self.dt = u8::from_le_bytes(u8_buffer); - cursor.read_exact(&mut u8_buffer).unwrap(); - self.st = u8::from_le_bytes(u8_buffer); - cursor.read_exact(&mut keys_buffer).unwrap(); - self.keys.clone_from_slice( - keys_buffer - .map(|v| if v == 1 { true } else { false }) - .iter() - .as_slice(), - ); - cursor.read_exact(&mut u8_buffer).unwrap(); - self.last_key = u8::from_le_bytes(u8_buffer); - } } #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -364,6 +364,25 @@ impl Chip8Neo { } } +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl Chip8Neo { + pub fn load_rom_ws(&mut self, rom: &[u8]) { + self.load_rom(rom) + } + + pub fn reset_ws(&mut self) { + self.reset() + } + + pub fn reset_hard_ws(&mut self) { + self.reset_hard() + } + + pub fn clock_ws(&mut self) { + self.clock() + } +} + impl Default for Chip8Neo { fn default() -> Chip8Neo { Chip8Neo::new()