diff --git a/Cargo.toml b/Cargo.toml index 444a27d7bd0e9ee54b62e8a112bb55f5e84701d7..46eec6a6e6cc72110995b8aa5ea0fc78dbe96d9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,12 @@ edition = "2018" [lib] crate-type = ["cdylib", "rlib"] +[features] +wasm = ["wasm-bindgen"] + [dependencies] getrandom = { version = "0.2", features = ["js"] } +wasm-bindgen = { version = "0.2", optional = true } [profile.release] debug = false diff --git a/README.md b/README.md index f9e947d70ac1ce7e84db101c36e6ca6b8740d95f..fb0e7965bcb562226953fd79b9123c9d676445a0 100644 --- a/README.md +++ b/README.md @@ -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 * 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 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 diff --git a/examples/benchmark/resources/pong.ch8 b/examples/benchmark/res/pong.ch8 similarity index 100% rename from examples/benchmark/resources/pong.ch8 rename to examples/benchmark/res/pong.ch8 diff --git a/examples/benchmark/src/main.rs b/examples/benchmark/src/main.rs index 5dfefe1a37c319b7fbdfe27610f630b3d0ab5283..cb5c7d1274d6cb45b505f807905d72981e697853 100644 --- a/examples/benchmark/src/main.rs +++ b/examples/benchmark/src/main.rs @@ -8,7 +8,7 @@ const CYCLE_COUNT: u64 = 5_000_000_000; fn main() { 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); for mut chip8 in chips { diff --git a/examples/sdl/resources/OpenSans-Bold.ttf b/examples/sdl/res/OpenSans-Bold.ttf similarity index 100% rename from examples/sdl/resources/OpenSans-Bold.ttf rename to examples/sdl/res/OpenSans-Bold.ttf diff --git a/examples/sdl/resources/Roboto-Bold.ttf b/examples/sdl/res/Roboto-Bold.ttf similarity index 100% rename from examples/sdl/resources/Roboto-Bold.ttf rename to examples/sdl/res/Roboto-Bold.ttf diff --git a/examples/sdl/resources/RobotoMono-Bold.ttf b/examples/sdl/res/RobotoMono-Bold.ttf similarity index 100% rename from examples/sdl/resources/RobotoMono-Bold.ttf rename to examples/sdl/res/RobotoMono-Bold.ttf diff --git a/examples/sdl/resources/VT323-Regular.ttf b/examples/sdl/res/VT323-Regular.ttf similarity index 100% rename from examples/sdl/resources/VT323-Regular.ttf rename to examples/sdl/res/VT323-Regular.ttf diff --git a/examples/sdl/resources/icon.png b/examples/sdl/res/icon.png similarity index 100% rename from examples/sdl/resources/icon.png rename to examples/sdl/res/icon.png diff --git a/examples/sdl/src/main.rs b/examples/sdl/src/main.rs index fee2d737401c60acce335b379f0c85347576e504..88257adf63a6be9cdd869b49bbfc6df3a1e2820f 100644 --- a/examples/sdl/src/main.rs +++ b/examples/sdl/src/main.rs @@ -160,7 +160,7 @@ fn main() { // loads the font that is going to be used in the drawing // process cycle if necessary let mut font = ttf_context - .load_font(format!("./resources/{}", FONT_NAME), FONT_SIZE) + .load_font(format!("./res/{}", FONT_NAME), FONT_SIZE) .unwrap(); font.set_style(sdl2::ttf::FontStyle::BOLD); font.set_hinting(Hinting::Light); @@ -181,7 +181,7 @@ fn main() { // updates the icon of the window to reflect the image // 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); let mut canvas = window.into_canvas().accelerated().build().unwrap(); diff --git a/examples/web/chip_ahoyto.d.ts b/examples/web/chip_ahoyto.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..77a84e7b09140f1e0bc08057123af9e331d6602c --- /dev/null +++ b/examples/web/chip_ahoyto.d.ts @@ -0,0 +1,91 @@ +/* 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>; diff --git a/examples/web/chip_ahoyto.js b/examples/web/chip_ahoyto.js new file mode 100644 index 0000000000000000000000000000000000000000..c83bdce964192eb55dedb6c28076f9889d7a43fa --- /dev/null +++ b/examples/web/chip_ahoyto.js @@ -0,0 +1,386 @@ + +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; diff --git a/examples/web/chip_ahoyto_bg.wasm b/examples/web/chip_ahoyto_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..8ebcfa0bd0912d5f934cf9ea18f88daeb2dd8275 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..d5813e8d652675613b57214d6de048b5aa3d2b01 --- /dev/null +++ b/examples/web/chip_ahoyto_bg.wasm.d.ts @@ -0,0 +1,20 @@ +/* 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; diff --git a/examples/web/index.html b/examples/web/index.html new file mode 100644 index 0000000000000000000000000000000000000000..6def33c85844e6a68f7dbfbebf19e9de1da75642 --- /dev/null +++ b/examples/web/index.html @@ -0,0 +1,15 @@ +<!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> diff --git a/examples/web/index.js b/examples/web/index.js new file mode 100644 index 0000000000000000000000000000000000000000..26b77b7efb66220d42c5a245560052c88e4c2a4a --- /dev/null +++ b/examples/web/index.js @@ -0,0 +1,187 @@ +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); +}; diff --git a/resources/chips-ahoy.jpeg b/res/chips-ahoy.jpeg similarity index 100% rename from resources/chips-ahoy.jpeg rename to res/chips-ahoy.jpeg diff --git a/resources/roms/bc_test.ch8 b/res/roms/bc_test.ch8 similarity index 100% rename from resources/roms/bc_test.ch8 rename to res/roms/bc_test.ch8 diff --git a/resources/roms/bmp.ch8 b/res/roms/bmp.ch8 similarity index 100% rename from resources/roms/bmp.ch8 rename to res/roms/bmp.ch8 diff --git a/resources/roms/breakout.ch8 b/res/roms/breakout.ch8 similarity index 100% rename from resources/roms/breakout.ch8 rename to res/roms/breakout.ch8 diff --git a/resources/roms/chip8_picture.ch8 b/res/roms/chip8_picture.ch8 similarity index 100% rename from resources/roms/chip8_picture.ch8 rename to res/roms/chip8_picture.ch8 diff --git a/resources/roms/chipwar.ch8 b/res/roms/chipwar.ch8 similarity index 100% rename from resources/roms/chipwar.ch8 rename to res/roms/chipwar.ch8 diff --git a/resources/roms/ibm_logo.ch8 b/res/roms/ibm_logo.ch8 similarity index 100% rename from resources/roms/ibm_logo.ch8 rename to res/roms/ibm_logo.ch8 diff --git a/resources/roms/keypad.ch8 b/res/roms/keypad.ch8 similarity index 100% rename from resources/roms/keypad.ch8 rename to res/roms/keypad.ch8 diff --git a/resources/roms/maze.ch8 b/res/roms/maze.ch8 similarity index 100% rename from resources/roms/maze.ch8 rename to res/roms/maze.ch8 diff --git a/resources/roms/octojam_6_title.ch8 b/res/roms/octojam_6_title.ch8 similarity index 100% rename from resources/roms/octojam_6_title.ch8 rename to res/roms/octojam_6_title.ch8 diff --git a/resources/roms/pong.ch8 b/res/roms/pong.ch8 similarity index 100% rename from resources/roms/pong.ch8 rename to res/roms/pong.ch8 diff --git a/resources/roms/pong_paul.ch8 b/res/roms/pong_paul.ch8 similarity index 100% rename from resources/roms/pong_paul.ch8 rename to res/roms/pong_paul.ch8 diff --git a/resources/roms/sirpinski.ch8 b/res/roms/sirpinski.ch8 similarity index 100% rename from resources/roms/sirpinski.ch8 rename to res/roms/sirpinski.ch8 diff --git a/resources/roms/snake.ch8 b/res/roms/snake.ch8 similarity index 100% rename from resources/roms/snake.ch8 rename to res/roms/snake.ch8 diff --git a/resources/roms/soccer.ch8 b/res/roms/soccer.ch8 similarity index 100% rename from resources/roms/soccer.ch8 rename to res/roms/soccer.ch8 diff --git a/resources/roms/space_invaders.ch8 b/res/roms/space_invaders.ch8 similarity index 100% rename from resources/roms/space_invaders.ch8 rename to res/roms/space_invaders.ch8 diff --git a/resources/roms/spacejam.ch8 b/res/roms/spacejam.ch8 similarity index 100% rename from resources/roms/spacejam.ch8 rename to res/roms/spacejam.ch8 diff --git a/resources/roms/test_opcode.ch8 b/res/roms/test_opcode.ch8 similarity index 100% rename from resources/roms/test_opcode.ch8 rename to res/roms/test_opcode.ch8 diff --git a/resources/roms/tetris.ch8 b/res/roms/tetris.ch8 similarity index 100% rename from resources/roms/tetris.ch8 rename to res/roms/tetris.ch8 diff --git a/resources/roms/tic_tac_toe.ch8 b/res/roms/tic_tac_toe.ch8 similarity index 100% rename from resources/roms/tic_tac_toe.ch8 rename to res/roms/tic_tac_toe.ch8 diff --git a/src/chip8_classic.rs b/src/chip8_classic.rs index 5b6917e902e3bdb77026f38117a06afef77f7b77..a89ddcc5fe296d13f90bff20daf1fca91379bae1 100644 --- a/src/chip8_classic.rs +++ b/src/chip8_classic.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use crate::{chip8::Chip8, util::random}; -#[cfg(feature = "web")] +#[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; /// The width of the screen in pixels. @@ -50,7 +50,7 @@ static FONT_SET: [u8; 80] = [ 0xf0, 0x80, 0xf0, 0x80, 0x80, // F ]; -#[cfg_attr(feature = "web", wasm_bindgen)] +#[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct Chip8Classic { vram: [u8; SCREEN_PIXEL_WIDTH * SCREEN_PIXEL_HEIGHT], ram: [u8; RAM_SIZE], @@ -66,7 +66,6 @@ pub struct Chip8Classic { keys: [bool; NUM_KEYS], } -#[cfg_attr(feature = "web", wasm_bindgen)] impl Chip8 for Chip8Classic { fn name(&self) -> &str { "classic" @@ -152,8 +151,9 @@ impl Chip8 for Chip8Classic { } } +#[cfg_attr(feature = "wasm", wasm_bindgen)] impl Chip8Classic { - #[cfg_attr(feature = "web", wasm_bindgen(constructor))] + #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] pub fn new() -> Chip8Classic { let mut chip8 = Chip8Classic { vram: [0u8; SCREEN_PIXEL_WIDTH * SCREEN_PIXEL_HEIGHT], diff --git a/src/chip8_neo.rs b/src/chip8_neo.rs index 0e8aecac4717915ed7b13ae17adb8a876a9d01c4..fa81ef540b7b1412a4d6e964e06172e64ca5e932 100644 --- a/src/chip8_neo.rs +++ b/src/chip8_neo.rs @@ -2,6 +2,9 @@ use std::io::{Cursor, Read}; use crate::{chip8::Chip8, util::random}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + pub const DISPLAY_WIDTH: usize = 64; pub const DISPLAY_HEIGHT: usize = 32; @@ -33,6 +36,7 @@ static FONT_SET: [u8; 80] = [ 0xf0, 0x80, 0xf0, 0x80, 0x80, // F ]; +#[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct Chip8Neo { ram: [u8; RAM_SIZE], vram: [u8; DISPLAY_WIDTH * DISPLAY_HEIGHT], @@ -47,7 +51,6 @@ pub struct Chip8Neo { last_key: u8, } -#[cfg_attr(feature = "web", wasm_bindgen)] impl Chip8 for Chip8Neo { fn name(&self) -> &str { "neo" @@ -72,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 @@ -221,93 +307,11 @@ 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)] impl Chip8Neo { - #[cfg_attr(feature = "web", wasm_bindgen(constructor))] + #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] pub fn new() -> Chip8Neo { let mut chip8 = Chip8Neo { ram: [0u8; RAM_SIZE], @@ -359,3 +363,48 @@ 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 vram_ws(&self) -> Vec<u8> { + self.vram() + } + + pub fn clock_ws(&mut self) { + self.clock() + } + + pub fn clock_dt_ws(&mut self) { + self.clock_dt() + } + + pub fn clock_st_ws(&mut self) { + self.clock_st() + } + + pub fn key_press_ws(&mut self, key: u8) { + self.key_press(key) + } + + pub fn key_lift_ws(&mut self, key: u8) { + self.key_lift(key) + } +} + +impl Default for Chip8Neo { + fn default() -> Chip8Neo { + Chip8Neo::new() + } +}