From 75a9c2a004021ddf023a516ad71818e4a1df27cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com> Date: Sun, 13 Nov 2022 22:01:58 +0000 Subject: [PATCH] refactor: separated ts files --- examples/web/gb.ts | 538 +++++++++++++++++++++++++++++++++++++++++ examples/web/index.ts | 541 +----------------------------------------- 2 files changed, 540 insertions(+), 539 deletions(-) create mode 100644 examples/web/gb.ts diff --git a/examples/web/gb.ts b/examples/web/gb.ts new file mode 100644 index 00000000..dd4ac3d0 --- /dev/null +++ b/examples/web/gb.ts @@ -0,0 +1,538 @@ +import { + Emulator, + EmulatorBase, + PixelFormat, + RomInfo +} from "./react/app"; + +import { + Cartridge, + default as _wasm, + GameBoy, + PadKey, + PpuMode +} from "./lib/boytacean.js"; +import info from "./package.json"; + +declare const require: any; + +const LOGIC_HZ = 4194304; +const VISUAL_HZ = 59.7275; +const IDLE_HZ = 10; + +const SAMPLE_RATE = 2; + +const PALETTES = [ + { + name: "basic", + colors: ["ffffff", "c0c0c0", "606060", "000000"] + }, + { + name: "hogwards", + colors: ["b6a571", "8b7e56", "554d35", "201d13"] + }, + { + name: "pacman", + colors: ["ffff00", "ffb897", "3732ff", "000000"] + }, + { + name: "mariobros", + colors: ["f7cec3", "cc9e22", "923404", "000000"] + } +]; + +const KEYS_NAME: Record<string, number> = { + ArrowUp: PadKey.Up, + ArrowDown: PadKey.Down, + ArrowLeft: PadKey.Left, + ArrowRight: PadKey.Right, + Start: PadKey.Start, + Select: PadKey.Select, + A: PadKey.A, + B: PadKey.B +}; + +const ROM_PATH = require("../../res/roms/20y.gb"); + +/** + * Top level class that controls the emulator behaviour + * and "joins" all the elements together to bring input/output + * of the associated machine. + */ +export class GameboyEmulator extends EmulatorBase implements Emulator { + /** + * The Game Boy engine (probably coming from WASM) that + * is going to be used for the emulation. + */ + private gameBoy: GameBoy | null = null; + + /** + * The descriptive name of the engine that is currently + * in use to emulate the system. + */ + private _engine: string | null = null; + + private logicFrequency: number = LOGIC_HZ; + private visualFrequency: number = VISUAL_HZ; + private idleFrequency: number = IDLE_HZ; + + private paused: boolean = false; + private nextTickTime: number = 0; + private fps: number = 0; + private frameStart: number = new Date().getTime(); + private frameCount: number = 0; + private paletteIndex: number = 0; + + private romName: string | null = null; + private romData: Uint8Array | null = null; + private romSize: number = 0; + private cartridge: Cartridge | null = null; + + async main({ romUrl }: { romUrl?: string }) { + // initializes the WASM module, this is required + // so that the global symbols become available + await wasm(); + + // boots the emulator subsystem with the initial + // ROM retrieved from a remote data source + await this.boot({ loadRom: true, romPath: romUrl ?? undefined }); + + // the counter that controls the overflowing cycles + // from tick to tick operation + let pending = 0; + + // runs the sequence as an infinite loop, running + // the associated CPU cycles accordingly + while (true) { + // in case the machine is paused we must delay the execution + // a little bit until the paused state is recovered + if (this.paused) { + await new Promise((resolve) => { + setTimeout(resolve, 1000 / this.idleFrequency); + }); + continue; + } + + // obtains the current time, this value is going + // to be used to compute the need for tick computation + let currentTime = new Date().getTime(); + + try { + pending = this.tick( + currentTime, + pending, + Math.round(this.logicFrequency / this.visualFrequency) + ); + } catch (err) { + // sets the default error message to be displayed + // to the user, this value may be overridden in case + // a better and more explicit message can be determined + let message = String(err); + + // verifies if the current issue is a panic one + // and updates the message value if that's the case + const messageNormalized = (err as Error).message.toLowerCase(); + const isPanic = + messageNormalized.startsWith("unreachable") || + messageNormalized.startsWith("recursive use of an object"); + if (isPanic) { + message = "Unrecoverable error, restarting Game Boy"; + } + + // displays the error information to both the end-user + // and the developer (for diagnostics) + this.trigger("message", { + text: message, + error: true, + timeout: 5000 + }); + console.error(err); + + // pauses the machine, allowing the end-user to act + // on the error in a proper fashion + this.pause(); + + // if we're talking about a panic, proper action must be taken + // which in this case it means restarting both the WASM sub + // system and the machine state (to be able to recover) + // also sets the default color on screen to indicate the issue + if (isPanic) { + await wasm(); + await this.boot({ restore: false }); + + this.trigger("error"); + } + } + + // calculates the amount of time until the next draw operation + // this is the amount of time that is going to be pending + currentTime = new Date().getTime(); + const pendingTime = Math.max(this.nextTickTime - currentTime, 0); + + // waits a little bit for the next frame to be draw, + // this should control the flow of render + await new Promise((resolve) => { + setTimeout(resolve, pendingTime); + }); + } + } + + tick(currentTime: number, pending: number, cycles: number = 70224) { + // in case the time to draw the next frame has not been + // reached the flush of the "tick" logic is skipped + if (currentTime < this.nextTickTime) return pending; + + // calculates the number of ticks that have elapsed since the + // last draw operation, this is critical to be able to properly + // operate the clock of the CPU in frame drop situations + if (this.nextTickTime === 0) this.nextTickTime = currentTime; + let ticks = Math.ceil( + (currentTime - this.nextTickTime) / + ((1 / this.visualFrequency) * 1000) + ); + ticks = Math.max(ticks, 1); + + // initializes the counter of cycles with the pending number + // of cycles coming from the previous tick + let counterCycles = pending; + + let lastFrame = -1; + + while (true) { + // limits the number of cycles to the provided + // cycle value passed as a parameter + if (counterCycles >= cycles) { + break; + } + + // runs the Game Boy clock, this operations should + // include the advance of both the CPU and the PPU + counterCycles += this.gameBoy?.clock() ?? 0; + + // in case the current PPU mode is VBlank and the + // frame is different from the previously rendered + // one then it's time to update the canvas + if ( + this.gameBoy?.ppu_mode() === PpuMode.VBlank && + this.gameBoy?.ppu_frame() !== lastFrame + ) { + lastFrame = this.gameBoy?.ppu_frame(); + + // triggers the frame event indicating that + // a new frame is now available for drawing + this.trigger("frame"); + } + } + + // increments the number of frames rendered in the current + // section, this value is going to be used to calculate FPS + this.frameCount += 1; + + // in case the target number of frames for FPS control + // has been reached calculates the number of FPS and + // flushes the value to the screen + if (this.frameCount >= this.visualFrequency * SAMPLE_RATE) { + const currentTime = new Date().getTime(); + const deltaTime = (currentTime - this.frameStart) / 1000; + const fps = Math.round(this.frameCount / deltaTime); + this.fps = fps; + this.frameCount = 0; + this.frameStart = currentTime; + } + + // updates the next update time reference to the, so that it + // can be used to control the game loop + this.nextTickTime += (1000 / this.visualFrequency) * ticks; + + // calculates the new number of pending (overflow) cycles + // that are going to be added to the next iteration + return counterCycles - cycles; + } + + /** + * Starts the current machine, setting the internal structure in + * a proper state to start drawing and receiving input. + * + * This method can also be used to load a new ROM into the machine. + * + * @param options The options that are going to be used in the + * starting of the machine, includes information on the ROM and + * the emulator engine to use. + */ + async boot({ + engine = "neo", + restore = true, + loadRom = false, + romPath = ROM_PATH, + romName = null, + romData = null + }: { + engine?: string | null; + restore?: boolean; + loadRom?: boolean; + romPath?: string; + romName?: string | null; + romData?: Uint8Array | null; + } = {}) { + // in case a remote ROM loading operation has been + // requested then loads it from the remote origin + if (loadRom) { + ({ name: romName, data: romData } = await this.fetchRom(romPath)); + } else if (romName === null || romData === null) { + [romName, romData] = [this.romName, this.romData]; + } + + // selects the proper engine for execution + // and builds a new instance of it + switch (engine) { + case "neo": + this.gameBoy = new GameBoy(); + break; + default: + if (!this.gameBoy) { + throw new Error("No engine requested"); + } + break; + } + + // runs the initial palette update operation + this.updatePalette(); + + // resets the Game Boy engine to restore it into + // a valid state ready to be used + this.gameBoy.reset(); + this.gameBoy.load_boot_default(); + const cartridge = this.gameBoy.load_rom_ws(romData!); + + // updates the ROM name in case there's extra information + // coming from the cartridge + romName = cartridge.title() ? cartridge.title() : romName; + + // updates the name of the currently selected engine + // to the one that has been provided (logic change) + if (engine) this._engine = engine; + + // updates the complete set of global information that + // is going to be displayed + this.setRom(romName!, romData!, cartridge); + + // in case the restore (state) flag is set + // then resumes the machine execution + if (restore) this.resume(); + + // triggers the booted event indicating that the + // emulator has finished the loading process + this.trigger("booted"); + } + + setRom(name: string, data: Uint8Array, cartridge: Cartridge) { + this.romName = name; + this.romData = data; + this.romSize = data.length; + this.cartridge = cartridge; + } + + get name(): string { + return "Boytacean"; + } + + get device(): string { + return "Game Boy"; + } + + get deviceUrl(): string { + return "https://en.wikipedia.org/wiki/Game_Boy"; + } + + get engines() { + return ["neo"]; + } + + get engine() { + return this._engine; + } + + get version(): string { + return info.version; + } + + get versionUrl(): string { + return "https://gitlab.stage.hive.pt/joamag/boytacean/-/blob/master/CHANGELOG.md"; + } + + get romExts(): string[] { + return ["gb"]; + } + + get pixelFormat(): PixelFormat { + return PixelFormat.RGB; + } + + /** + * Returns the array buffer that contains the complete set of + * pixel data that is going to be drawn. + * + * @returns The current pixel data for the emulator display. + */ + get imageBuffer(): Uint8Array { + return this.gameBoy?.frame_buffer_eager() ?? new Uint8Array(); + } + + get romInfo(): RomInfo { + return { + name: this.romName ?? undefined, + data: this.romData ?? undefined, + size: this.romSize, + extra: { + romType: this.cartridge?.rom_type_s(), + romSize: this.cartridge?.rom_size_s(), + ramSize: this.cartridge?.ram_size_s() + } + }; + } + + get frequency(): number { + return this.logicFrequency; + } + + set frequency(value: number) { + value = Math.max(value, 0); + this.logicFrequency = value; + this.trigger("frequency", value); + } + + get frequencyDelta(): number | null { + return 400000; + } + + get framerate(): number { + return this.fps; + } + + get registers(): Record<string, string | number> { + const registers = this.gameBoy?.registers(); + if (!registers) return {}; + return { + pc: registers.pc, + sp: registers.sp, + a: registers.a, + b: registers.b, + c: registers.c, + d: registers.d, + e: registers.e, + h: registers.h, + l: registers.l, + scy: registers.scy, + scx: registers.scx, + wy: registers.wy, + wx: registers.wx, + ly: registers.ly, + lyc: registers.lyc + }; + } + + getTile(index: number): Uint8Array { + return this.gameBoy?.get_tile_buffer(index) ?? new Uint8Array(); + } + + toggleRunning() { + if (this.paused) { + this.resume(); + } else { + this.pause(); + } + } + + pause() { + this.paused = true; + } + + resume() { + this.paused = false; + this.nextTickTime = new Date().getTime(); + } + + reset() { + this.boot({ engine: null }); + } + + keyPress(key: string) { + const keyCode = KEYS_NAME[key]; + if (keyCode === undefined) return; + this.gameBoy?.key_press(keyCode); + } + + keyLift(key: string) { + const keyCode = KEYS_NAME[key]; + if (keyCode === undefined) return; + this.gameBoy?.key_lift(keyCode); + } + + updatePalette() { + const palette = PALETTES[this.paletteIndex]; + this.gameBoy?.set_palette_colors_ws(palette.colors); + this.paletteIndex += 1; + this.paletteIndex %= PALETTES.length; + } + + benchmark(count = 50000000) { + let cycles = 0; + this.pause(); + try { + const initial = Date.now(); + for (let i = 0; i < count; i++) { + cycles += this.gameBoy?.clock() ?? 0; + } + const delta = (Date.now() - initial) / 1000; + const frequency_mhz = cycles / delta / 1000 / 1000; + return { + delta: delta, + count: count, + cycles: cycles, + frequency_mhz: frequency_mhz + }; + } finally { + this.resume(); + } + } + + private async fetchRom( + romPath: string + ): Promise<{ name: string; data: Uint8Array }> { + // extracts the name of the ROM from the provided + // path by splitting its structure + const romPathS = romPath.split(/\//g); + let romName = romPathS[romPathS.length - 1].split("?")[0]; + const romNameS = romName.split(/\./g); + romName = `${romNameS[0]}.${romNameS[romNameS.length - 1]}`; + + // loads the ROM data and converts it into the + // target byte array buffer (to be used by WASM) + const response = await fetch(romPath); + const blob = await response.blob(); + const arrayBuffer = await blob.arrayBuffer(); + const romData = new Uint8Array(arrayBuffer); + + // returns both the name of the ROM and the data + // contents as a byte array + return { + name: romName, + data: romData + }; + } +} + +declare global { + interface Window { + panic: (message: string) => void; + } +} + +window.panic = (message: string) => { + console.error(message); +}; + +const wasm = async () => { + await _wasm(); + GameBoy.set_panic_hook_ws(); +}; diff --git a/examples/web/index.ts b/examples/web/index.ts index 1e081091..7b114fed 100644 --- a/examples/web/index.ts +++ b/examples/web/index.ts @@ -1,27 +1,5 @@ -import { - Emulator, - EmulatorBase, - PixelFormat, - RomInfo, - startApp -} from "./react/app"; - -import { - Cartridge, - default as _wasm, - GameBoy, - PadKey, - PpuMode -} from "./lib/boytacean.js"; -import info from "./package.json"; - -declare const require: any; - -const LOGIC_HZ = 4194304; -const VISUAL_HZ = 59.7275; -const IDLE_HZ = 10; - -const SAMPLE_RATE = 2; +import { startApp } from "./react/app"; +import { GameboyEmulator } from "./gb"; const BACKGROUNDS = [ "264653", @@ -33,521 +11,6 @@ const BACKGROUNDS = [ "3a5a40" ]; -const PALETTES = [ - { - name: "basic", - colors: ["ffffff", "c0c0c0", "606060", "000000"] - }, - { - name: "hogwards", - colors: ["b6a571", "8b7e56", "554d35", "201d13"] - }, - { - name: "pacman", - colors: ["ffff00", "ffb897", "3732ff", "000000"] - }, - { - name: "mariobros", - colors: ["f7cec3", "cc9e22", "923404", "000000"] - } -]; - -const KEYS_NAME: Record<string, number> = { - ArrowUp: PadKey.Up, - ArrowDown: PadKey.Down, - ArrowLeft: PadKey.Left, - ArrowRight: PadKey.Right, - Start: PadKey.Start, - Select: PadKey.Select, - A: PadKey.A, - B: PadKey.B -}; - -const ROM_PATH = require("../../res/roms/20y.gb"); - -/** - * Top level class that controls the emulator behaviour - * and "joins" all the elements together to bring input/output - * of the associated machine. - */ -class GameboyEmulator extends EmulatorBase implements Emulator { - /** - * The Game Boy engine (probably coming from WASM) that - * is going to be used for the emulation. - */ - private gameBoy: GameBoy | null = null; - - /** - * The descriptive name of the engine that is currently - * in use to emulate the system. - */ - private _engine: string | null = null; - - private logicFrequency: number = LOGIC_HZ; - private visualFrequency: number = VISUAL_HZ; - private idleFrequency: number = IDLE_HZ; - - private paused: boolean = false; - private nextTickTime: number = 0; - private fps: number = 0; - private frameStart: number = new Date().getTime(); - private frameCount: number = 0; - private paletteIndex: number = 0; - - private romName: string | null = null; - private romData: Uint8Array | null = null; - private romSize: number = 0; - private cartridge: Cartridge | null = null; - - async main({ romUrl }: { romUrl?: string }) { - // initializes the WASM module, this is required - // so that the global symbols become available - await wasm(); - - // boots the emulator subsystem with the initial - // ROM retrieved from a remote data source - await this.boot({ loadRom: true, romPath: romUrl ?? undefined }); - - // the counter that controls the overflowing cycles - // from tick to tick operation - let pending = 0; - - // runs the sequence as an infinite loop, running - // the associated CPU cycles accordingly - while (true) { - // in case the machine is paused we must delay the execution - // a little bit until the paused state is recovered - if (this.paused) { - await new Promise((resolve) => { - setTimeout(resolve, 1000 / this.idleFrequency); - }); - continue; - } - - // obtains the current time, this value is going - // to be used to compute the need for tick computation - let currentTime = new Date().getTime(); - - try { - pending = this.tick( - currentTime, - pending, - Math.round(this.logicFrequency / this.visualFrequency) - ); - } catch (err) { - // sets the default error message to be displayed - // to the user, this value may be overridden in case - // a better and more explicit message can be determined - let message = String(err); - - // verifies if the current issue is a panic one - // and updates the message value if that's the case - const messageNormalized = (err as Error).message.toLowerCase(); - const isPanic = - messageNormalized.startsWith("unreachable") || - messageNormalized.startsWith("recursive use of an object"); - if (isPanic) { - message = "Unrecoverable error, restarting Game Boy"; - } - - // displays the error information to both the end-user - // and the developer (for diagnostics) - this.trigger("message", { - text: message, - error: true, - timeout: 5000 - }); - console.error(err); - - // pauses the machine, allowing the end-user to act - // on the error in a proper fashion - this.pause(); - - // if we're talking about a panic, proper action must be taken - // which in this case it means restarting both the WASM sub - // system and the machine state (to be able to recover) - // also sets the default color on screen to indicate the issue - if (isPanic) { - await wasm(); - await this.boot({ restore: false }); - - this.trigger("error"); - } - } - - // calculates the amount of time until the next draw operation - // this is the amount of time that is going to be pending - currentTime = new Date().getTime(); - const pendingTime = Math.max(this.nextTickTime - currentTime, 0); - - // waits a little bit for the next frame to be draw, - // this should control the flow of render - await new Promise((resolve) => { - setTimeout(resolve, pendingTime); - }); - } - } - - tick(currentTime: number, pending: number, cycles: number = 70224) { - // in case the time to draw the next frame has not been - // reached the flush of the "tick" logic is skipped - if (currentTime < this.nextTickTime) return pending; - - // calculates the number of ticks that have elapsed since the - // last draw operation, this is critical to be able to properly - // operate the clock of the CPU in frame drop situations - if (this.nextTickTime === 0) this.nextTickTime = currentTime; - let ticks = Math.ceil( - (currentTime - this.nextTickTime) / - ((1 / this.visualFrequency) * 1000) - ); - ticks = Math.max(ticks, 1); - - // initializes the counter of cycles with the pending number - // of cycles coming from the previous tick - let counterCycles = pending; - - let lastFrame = -1; - - while (true) { - // limits the number of cycles to the provided - // cycle value passed as a parameter - if (counterCycles >= cycles) { - break; - } - - // runs the Game Boy clock, this operations should - // include the advance of both the CPU and the PPU - counterCycles += this.gameBoy?.clock() ?? 0; - - // in case the current PPU mode is VBlank and the - // frame is different from the previously rendered - // one then it's time to update the canvas - if ( - this.gameBoy?.ppu_mode() === PpuMode.VBlank && - this.gameBoy?.ppu_frame() !== lastFrame - ) { - lastFrame = this.gameBoy?.ppu_frame(); - - // triggers the frame event indicating that - // a new frame is now available for drawing - this.trigger("frame"); - } - } - - // increments the number of frames rendered in the current - // section, this value is going to be used to calculate FPS - this.frameCount += 1; - - // in case the target number of frames for FPS control - // has been reached calculates the number of FPS and - // flushes the value to the screen - if (this.frameCount >= this.visualFrequency * SAMPLE_RATE) { - const currentTime = new Date().getTime(); - const deltaTime = (currentTime - this.frameStart) / 1000; - const fps = Math.round(this.frameCount / deltaTime); - this.fps = fps; - this.frameCount = 0; - this.frameStart = currentTime; - } - - // updates the next update time reference to the, so that it - // can be used to control the game loop - this.nextTickTime += (1000 / this.visualFrequency) * ticks; - - // calculates the new number of pending (overflow) cycles - // that are going to be added to the next iteration - return counterCycles - cycles; - } - - /** - * Starts the current machine, setting the internal structure in - * a proper state to start drawing and receiving input. - * - * This method can also be used to load a new ROM into the machine. - * - * @param options The options that are going to be used in the - * starting of the machine, includes information on the ROM and - * the emulator engine to use. - */ - async boot({ - engine = "neo", - restore = true, - loadRom = false, - romPath = ROM_PATH, - romName = null, - romData = null - }: { - engine?: string | null; - restore?: boolean; - loadRom?: boolean; - romPath?: string; - romName?: string | null; - romData?: Uint8Array | null; - } = {}) { - // in case a remote ROM loading operation has been - // requested then loads it from the remote origin - if (loadRom) { - ({ name: romName, data: romData } = await this.fetchRom(romPath)); - } else if (romName === null || romData === null) { - [romName, romData] = [this.romName, this.romData]; - } - - // selects the proper engine for execution - // and builds a new instance of it - switch (engine) { - case "neo": - this.gameBoy = new GameBoy(); - break; - default: - if (!this.gameBoy) { - throw new Error("No engine requested"); - } - break; - } - - // runs the initial palette update operation - this.updatePalette(); - - // resets the Game Boy engine to restore it into - // a valid state ready to be used - this.gameBoy.reset(); - this.gameBoy.load_boot_default(); - const cartridge = this.gameBoy.load_rom_ws(romData!); - - // updates the ROM name in case there's extra information - // coming from the cartridge - romName = cartridge.title() ? cartridge.title() : romName; - - // updates the name of the currently selected engine - // to the one that has been provided (logic change) - if (engine) this._engine = engine; - - // updates the complete set of global information that - // is going to be displayed - this.setRom(romName!, romData!, cartridge); - - // in case the restore (state) flag is set - // then resumes the machine execution - if (restore) this.resume(); - - // triggers the booted event indicating that the - // emulator has finished the loading process - this.trigger("booted"); - } - - setRom(name: string, data: Uint8Array, cartridge: Cartridge) { - this.romName = name; - this.romData = data; - this.romSize = data.length; - this.cartridge = cartridge; - } - - get name(): string { - return "Boytacean"; - } - - get device(): string { - return "Game Boy"; - } - - get deviceUrl(): string { - return "https://en.wikipedia.org/wiki/Game_Boy"; - } - - get engines() { - return ["neo"]; - } - - get engine() { - return this._engine; - } - - get version(): string { - return info.version; - } - - get versionUrl(): string { - return "https://gitlab.stage.hive.pt/joamag/boytacean/-/blob/master/CHANGELOG.md"; - } - - get romExts(): string[] { - return ["gb"]; - } - - get pixelFormat(): PixelFormat { - return PixelFormat.RGB; - } - - /** - * Returns the array buffer that contains the complete set of - * pixel data that is going to be drawn. - * - * @returns The current pixel data for the emulator display. - */ - get imageBuffer(): Uint8Array { - return this.gameBoy?.frame_buffer_eager() ?? new Uint8Array(); - } - - get romInfo(): RomInfo { - return { - name: this.romName ?? undefined, - data: this.romData ?? undefined, - size: this.romSize, - extra: { - romType: this.cartridge?.rom_type_s(), - romSize: this.cartridge?.rom_size_s(), - ramSize: this.cartridge?.ram_size_s() - } - }; - } - - get frequency(): number { - return this.logicFrequency; - } - - set frequency(value: number) { - value = Math.max(value, 0); - this.logicFrequency = value; - this.trigger("frequency", value); - } - - get frequencyDelta(): number | null { - return 400000; - } - - get framerate(): number { - return this.fps; - } - - get registers(): Record<string, string | number> { - const registers = this.gameBoy?.registers(); - if (!registers) return {}; - return { - pc: registers.pc, - sp: registers.sp, - a: registers.a, - b: registers.b, - c: registers.c, - d: registers.d, - e: registers.e, - h: registers.h, - l: registers.l, - scy: registers.scy, - scx: registers.scx, - wy: registers.wy, - wx: registers.wx, - ly: registers.ly, - lyc: registers.lyc - }; - } - - getTile(index: number): Uint8Array { - return this.gameBoy?.get_tile_buffer(index) ?? new Uint8Array(); - } - - toggleRunning() { - if (this.paused) { - this.resume(); - } else { - this.pause(); - } - } - - pause() { - this.paused = true; - } - - resume() { - this.paused = false; - this.nextTickTime = new Date().getTime(); - } - - reset() { - this.boot({ engine: null }); - } - - keyPress(key: string) { - const keyCode = KEYS_NAME[key]; - if (keyCode === undefined) return; - this.gameBoy?.key_press(keyCode); - } - - keyLift(key: string) { - const keyCode = KEYS_NAME[key]; - if (keyCode === undefined) return; - this.gameBoy?.key_lift(keyCode); - } - - updatePalette() { - const palette = PALETTES[this.paletteIndex]; - this.gameBoy?.set_palette_colors_ws(palette.colors); - this.paletteIndex += 1; - this.paletteIndex %= PALETTES.length; - } - - benchmark(count = 50000000) { - let cycles = 0; - this.pause(); - try { - const initial = Date.now(); - for (let i = 0; i < count; i++) { - cycles += this.gameBoy?.clock() ?? 0; - } - const delta = (Date.now() - initial) / 1000; - const frequency_mhz = cycles / delta / 1000 / 1000; - return { - delta: delta, - count: count, - cycles: cycles, - frequency_mhz: frequency_mhz - }; - } finally { - this.resume(); - } - } - - private async fetchRom( - romPath: string - ): Promise<{ name: string; data: Uint8Array }> { - // extracts the name of the ROM from the provided - // path by splitting its structure - const romPathS = romPath.split(/\//g); - let romName = romPathS[romPathS.length - 1].split("?")[0]; - const romNameS = romName.split(/\./g); - romName = `${romNameS[0]}.${romNameS[romNameS.length - 1]}`; - - // loads the ROM data and converts it into the - // target byte array buffer (to be used by WASM) - const response = await fetch(romPath); - const blob = await response.blob(); - const arrayBuffer = await blob.arrayBuffer(); - const romData = new Uint8Array(arrayBuffer); - - // returns both the name of the ROM and the data - // contents as a byte array - return { - name: romName, - data: romData - }; - } -} - -declare global { - interface Window { - panic: (message: string) => void; - } -} - -window.panic = (message: string) => { - console.error(message); -}; - -const wasm = async () => { - await _wasm(); - GameBoy.set_panic_hook_ws(); -}; - (async () => { // parses the current location URL as retrieves // some of the "relevant" GET parameters for logic -- GitLab