Skip to content
Snippets Groups Projects
index.ts 40.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { default as wasm, GameBoy, PadKey } from "./lib/boytacean.js";
    
    import info from "./package.json";
    
    const PIXEL_UNSET_COLOR = 0x1b1a17ff;
    
    const LOGIC_HZ = 600;
    const VISUAL_HZ = 60;
    const TIMER_HZ = 60;
    const IDLE_HZ = 10;
    
    const FREQUENCY_DELTA = 60;
    
    
    const DISPLAY_WIDTH = 160;
    const DISPLAY_HEIGHT = 144;
    
    const DISPLAY_RATIO = DISPLAY_WIDTH / DISPLAY_HEIGHT;
    
    const SAMPLE_RATE = 2;
    
    const SOUND_DATA =
        "data:audio/mpeg;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwP+AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwP+AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAA";
    
    const BACKGROUNDS = [
        "264653",
        "1b1a17",
        "023047",
        "bc6c25",
        "283618",
        "2a9d8f",
        "3a5a40"
    ];
    
    const KEYS: Record<string, number> = {
    
        ArrowUp: PadKey.Up,
        ArrowDown: PadKey.Down,
        ArrowLeft: PadKey.Left,
        ArrowRight: PadKey.Right,
        Enter: PadKey.Start,
        " ": PadKey.Select,
        a: PadKey.A,
        s: PadKey.B
    
    };
    
    // @ts-ignore: ts(2580)
    
    João Magalhães's avatar
    João Magalhães committed
    const ROM_PATH = require("../../res/roms/firstwhite.gb");
    
    // Enumeration that describes the multiple pixel
    // formats and the associated byte size.
    enum PixelFormat {
        RGB = 3,
        RGBA = 4
    }
    
    
    type State = {
        gameBoy: GameBoy;
        engine: string;
        logicFrequency: number;
        visualFrequency: number;
        timerFrequency: number;
        idleFrequency: number;
        canvas: HTMLCanvasElement;
        canvasScaled: HTMLCanvasElement;
        canvasCtx: CanvasRenderingContext2D;
        canvasScaledCtx: CanvasRenderingContext2D;
        image: ImageData;
        videoBuff: DataView;
        toastTimeout: number;
        paused: boolean;
        background_index: number;
        nextTickTime: number;
        fps: number;
        frameStart: number;
        frameCount: number;
        romName: string;
        romData: Uint8Array;
        romSize: number;
    };
    
    type Global = {
        modalCallback: Function;
    };
    
    const state: State = {
        gameBoy: null,
        engine: null,
        logicFrequency: LOGIC_HZ,
        visualFrequency: VISUAL_HZ,
        timerFrequency: TIMER_HZ,
        idleFrequency: IDLE_HZ,
        canvas: null,
        canvasScaled: null,
        canvasCtx: null,
        canvasScaledCtx: null,
        image: null,
        videoBuff: null,
        toastTimeout: null,
        paused: false,
        background_index: 0,
        nextTickTime: 0,
        fps: 0,
        frameStart: new Date().getTime(),
        frameCount: 0,
        romName: null,
        romData: null,
        romSize: 0
    };
    
    const global: Global = {
        modalCallback: null
    };
    
    const sound = ((data = SOUND_DATA, volume = 0.2) => {
        const sound = new Audio(data);
        sound.volume = volume;
        sound.muted = true;
        return sound;
    })();
    
    const main = 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
        await init();
        await register();
    
        // start the emulator subsystem with the initial
        // ROM retrieved from a remote data source
        await start({ loadRom: true });
    
        // runs the sequence as an infinite loop, running
        // the associated CPU cycles accordingly
        while (true) {
            // in case the machin is paused we must delay the execution
            // a little bit until the paused state is recovered
            if (state.paused) {
                await new Promise((resolve) => {
                    setTimeout(resolve, 1000 / state.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 {
                tick(currentTime);
            } catch (err) {
                // sets the default error message to be displayed
                // to the user
                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 dianostics)
                showToast(message, true, 5000);
                console.error(err);
    
                // pauses the machine, allowing the end-user to act
                // on the error in a proper fashion
                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 clearCanvas(undefined, {
                        // @ts-ignore: ts(2580)
                        image: require("./res/storm.png"),
    
                        imageScale: 0.2
    
                    });
    
                    await wasm();
                    await start({ restore: false });
                }
            }
    
            // 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(state.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);
            });
        }
    };
    
    const tick = (currentTime: number) => {
        // in case the time to draw the next frame has not been
        // reached the flush of the "tick" logic is skiped
        if (currentTime < state.nextTickTime) return;
    
        // 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 (state.nextTickTime === 0) state.nextTickTime = currentTime;
        let ticks = Math.ceil(
            (currentTime - state.nextTickTime) /
                ((1 / state.visualFrequency) * 1000)
        );
        ticks = Math.max(ticks, 1);
    
    
        let counterTicks = 0;
    
        while (true) {
            // limits the number of ticks to the typical number
            // of ticks required to do a complete PPU draw
            if (counterTicks >= 70224) {
                break;
            }
    
            // runs the Game Boy clock, this operations should
            // include the advance of both the CPU and the PPU
            counterTicks += state.gameBoy.clock();
        }
    
    
        // updates the canvas object with the new
        // visual information coming in
    
        updateCanvas(state.gameBoy.frame_buffer_eager(), PixelFormat.RGB);
    
    
        // increments the number of frames rendered in the current
        // section, this value is going to be used to calculate FPS
        state.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 (state.frameCount === state.visualFrequency * SAMPLE_RATE) {
            const currentTime = new Date().getTime();
            const deltaTime = (currentTime - state.frameStart) / 1000;
            const fps = Math.round(state.frameCount / deltaTime);
            setFps(fps);
            state.frameCount = 0;
            state.frameStart = currentTime;
        }
    
        // updates the next update time reference to the, so that it
        // can be used to control the game loop
        state.nextTickTime += (1000 / state.visualFrequency) * ticks;
    };
    
    
    /**
     * Starts the current machine, setting the internal structure in
     * a proper state to start drwaing and receiving input.
     *
     * @param options The options that are going to be used in the
     * starting of the machine.
     */
    
    const start = async ({
        engine = "neo",
        restore = true,
        loadRom = false,
        romPath = ROM_PATH,
        romName = null as string,
        romData = null as Uint8Array
    } = {}) => {
        // in case a remote ROM loading operation has been
        // requested then loads it from the remote origin
        if (loadRom) {
            [romName, romData] = await fetchRom(romPath);
        } else if (romName === null || romData === null) {
            [romName, romData] = [state.romName, state.romData];
        }
    
        // selects the proper engine for execution
        // and builds a new instance of it
        switch (engine) {
            case "neo":
                state.gameBoy = new GameBoy();
                break;
    
            default:
                if (!state.gameBoy) {
                    throw new Error("No engine requested");
                }
                break;
        }
    
        // resets the Game Boy engine to restore it into
        // a valid state ready to be used
    
        state.gameBoy.reset();
    
        state.gameBoy.load_boot_default();
    
        state.gameBoy.load_rom(romData);
    
        // updates the name of the currently selected engine
        // to the one that has been provided (logic change)
        if (engine) state.engine = engine;
    
        // updates the complete set of global information that
        // is going to be displayed
        setEngine(state.engine);
        setRom(romName, romData);
        setLogicFrequency(state.logicFrequency);
        setFps(state.fps);
    
        // in case the restore (state) flag is set
        // then resumes the machine execution
        if (restore) resume();
    };
    
    const register = async () => {
        await Promise.all([
            registerDrop(),
            registerKeys(),
            registerButtons(),
            registerKeyboard(),
            registerCanvas(),
            registerToast(),
            registerModal()
        ]);
    };
    
    const init = async () => {
        await Promise.all([initBase(), initCanvas()]);
    };
    
    const registerDrop = () => {
        document.addEventListener("drop", async (event) => {
            if (
                !event.dataTransfer.files ||
                event.dataTransfer.files.length === 0
            ) {
                return;
            }
    
            event.preventDefault();
            event.stopPropagation();
    
            const overlay = document.getElementById("overlay");
            overlay.classList.remove("visible");
    
            const file = event.dataTransfer.files[0];
    
            if (!file.name.endsWith(".gb")) {
                showToast("This is probably not a Game Boy ROM file!", true);
                return;
            }
    
            const arrayBuffer = await file.arrayBuffer();
            const romData = new Uint8Array(arrayBuffer);
    
            start({ engine: null, romName: file.name, romData: romData });
    
            showToast(`Loaded ${file.name} ROM successfully!`);
        });
        document.addEventListener("dragover", async (event) => {
            if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
                return;
    
            event.preventDefault();
    
            const overlay = document.getElementById("overlay");
            overlay.classList.add("visible");
        });
        document.addEventListener("dragenter", async (event) => {
            if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
                return;
            const overlay = document.getElementById("overlay");
            overlay.classList.add("visible");
        });
        document.addEventListener("dragleave", async (event) => {
            if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
                return;
            const overlay = document.getElementById("overlay");
            overlay.classList.remove("visible");
        });
    };
    
    const registerKeys = () => {
        document.addEventListener("keydown", (event) => {
            const keyCode = KEYS[event.key];
            if (keyCode !== undefined) {
    
                state.gameBoy.key_press(keyCode);
    
                return;
            }
    
            switch (event.key) {
                case "+":
                    setLogicFrequency(state.logicFrequency + FREQUENCY_DELTA);
                    break;
    
                case "-":
                    setLogicFrequency(state.logicFrequency - FREQUENCY_DELTA);
                    break;
    
                case "Escape":
                    minimize();
                    break;
            }
        });
    
        document.addEventListener("keyup", (event) => {
            const keyCode = KEYS[event.key];
            if (keyCode !== undefined) {
    
                state.gameBoy.key_lift(keyCode);
    
                return;
            }
        });
    };
    
    const registerButtons = () => {
        const engine = document.getElementById("engine");
        engine.addEventListener("click", () => {
            const name = state.engine == "neo" ? "classic" : "neo";
            start({ engine: name });
            showToast(
                `Game Boy running in engine "${name.toUpperCase()}" from now on!`
            );
        });
    
        const logicFrequencyPlus = document.getElementById("logic-frequency-plus");
        logicFrequencyPlus.addEventListener("click", () => {
            setLogicFrequency(state.logicFrequency + FREQUENCY_DELTA);
        });
    
        const logicFrequencyMinus = document.getElementById(
            "logic-frequency-minus"
        );
        logicFrequencyMinus.addEventListener("click", () => {
            setLogicFrequency(state.logicFrequency - FREQUENCY_DELTA);
        });
    
        const buttonPause = document.getElementById("button-pause");
        buttonPause.addEventListener("click", () => {
            toggleRunning();
        });
    
        const buttonReset = document.getElementById("button-reset");
        buttonReset.addEventListener("click", () => {
            reset();
        });
    
        const buttonBenchmark = document.getElementById("button-benchmark");
        buttonBenchmark.addEventListener("click", async () => {
            const result = await showModal(
                "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
                "Confirm"
            );
            if (!result) return;
            buttonBenchmark.classList.add("enabled");
            pause();
            try {
                const initial = Date.now();
                const count = 500000000;
                for (let i = 0; i < count; i++) {
                    state.gameBoy.clock();
                }
                const delta = (Date.now() - initial) / 1000;
                const frequency_mhz = count / delta / 1000 / 1000;
                showToast(
                    `Took ${delta.toFixed(
                        2
                    )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
                        2
                    )} Mhz)!`,
                    undefined,
                    7500
                );
            } finally {
                resume();
                buttonBenchmark.classList.remove("enabled");
            }
        });
    
        const buttonFullscreen = document.getElementById("button-fullscreen");
        buttonFullscreen.addEventListener("click", () => {
            maximize();
        });
    
        const buttonKeyboard = document.getElementById("button-keyboard");
        buttonKeyboard.addEventListener("click", () => {
            const sectionKeyboard = document.getElementById("section-keyboard");
            const separatorKeyboard = document.getElementById("separator-keyboard");
            const sectionNarrative = document.getElementById("section-narrative");
            const separatorNarrative = document.getElementById(
                "separator-narrative"
            );
            if (buttonKeyboard.classList.contains("enabled")) {
                sectionKeyboard.style.display = "none";
                separatorKeyboard.style.display = "none";
                sectionNarrative.style.display = "block";
                separatorNarrative.style.display = "block";
                buttonKeyboard.classList.remove("enabled");
            } else {
                sectionKeyboard.style.display = "block";
                separatorKeyboard.style.display = "block";
                sectionNarrative.style.display = "none";
                separatorNarrative.style.display = "none";
                buttonKeyboard.classList.add("enabled");
            }
        });
    
    
        const buttonDebug = document.getElementById("button-debug");
        buttonDebug.addEventListener("click", () => {
            const sectionDebug = document.getElementById("section-debug");
            const separatorDebug = document.getElementById("separator-debug");
            const sectionNarrative = document.getElementById("section-narrative");
            const separatorNarrative = document.getElementById(
                "separator-narrative"
            );
            if (buttonDebug.classList.contains("enabled")) {
                sectionDebug.style.display = "none";
                separatorDebug.style.display = "none";
                sectionNarrative.style.display = "block";
                separatorNarrative.style.display = "block";
                buttonDebug.classList.remove("enabled");
            } else {
                sectionDebug.style.display = "block";
                separatorDebug.style.display = "block";
                sectionNarrative.style.display = "none";
                separatorNarrative.style.display = "none";
                buttonDebug.classList.add("enabled");
    
                const canvasTiles = document.getElementById(
                    "canvas-tiles"
                ) as HTMLCanvasElement;
    
                const canvasTilesCtx = canvasTiles.getContext("2d");
    
    
                const canvasImage = canvasTilesCtx.createImageData(
                    canvasTiles.width,
                    canvasTiles.height
                );
    
                const videoBuff = new DataView(canvasImage.data.buffer);
    
    
                const drawSprite = (
                    index: number,
                    format: PixelFormat = PixelFormat.RGB
                ) => {
    
                    const pixels = state.gameBoy.get_tile_buffer(index);
                    const line = Math.floor(index / 16);
                    const column = index % 16;
    
                    let offset =
                        (line * canvasTiles.width * 8 + column * 8) *
                        PixelFormat.RGBA;
    
                    let counter = 0;
                    for (let index = 0; index < pixels.length; index += format) {
                        const color =
                            (pixels[index] << 24) |
                            (pixels[index + 1] << 16) |
                            (pixels[index + 2] << 8) |
                            (format == PixelFormat.RGBA ? pixels[index + 3] : 0xff);
                        videoBuff.setUint32(offset, color);
    
                        counter++;
                        if (counter == 8) {
                            counter = 0;
                            offset += (canvasTiles.width - 7) * PixelFormat.RGBA;
                        } else {
                            offset += PixelFormat.RGBA;
                        }
                    }
                    canvasTilesCtx.putImageData(canvasImage, 0, 0);
    
    
                for (let index = 0; index < 256; index++) {
                    drawSprite(index);
                }
    
    
                const vram = state.gameBoy.vram_eager();
                const step = 16;
                for (let index = 0; index < vram.length; index += step) {
                    let line = `${(index + 0x8000).toString(16).padStart(4, "0")}`;
                    for (let j = 0; j < step; j++) {
                        line += ` ${vram[index + j].toString(16).padStart(2, "0")}`;
                    }
                    console.info(line);
                }
    
        const buttonInformation = document.getElementById("button-information");
        buttonInformation.addEventListener("click", () => {
            const sectionDiag = document.getElementById("section-diag");
            const separatorDiag = document.getElementById("separator-diag");
            if (buttonInformation.classList.contains("enabled")) {
                sectionDiag.style.display = "none";
                separatorDiag.style.display = "none";
                buttonInformation.classList.remove("enabled");
            } else {
                sectionDiag.style.display = "block";
                separatorDiag.style.display = "block";
                buttonInformation.classList.add("enabled");
            }
        });
    
        const buttonTheme = document.getElementById("button-theme");
        buttonTheme.addEventListener("click", () => {
            state.background_index =
                (state.background_index + 1) % BACKGROUNDS.length;
            const background = BACKGROUNDS[state.background_index];
            setBackground(background);
        });
    
        const buttonUploadFile = document.getElementById(
            "button-upload-file"
        ) as HTMLInputElement;
        buttonUploadFile.addEventListener("change", async () => {
            if (!buttonUploadFile.files || buttonUploadFile.files.length === 0) {
                return;
            }
    
            const file = buttonUploadFile.files[0];
    
            const arrayBuffer = await file.arrayBuffer();
            const romData = new Uint8Array(arrayBuffer);
    
            buttonUploadFile.value = "";
    
            start({ engine: null, romName: file.name, romData: romData });
    
            showToast(`Loaded ${file.name} ROM successfully!`);
        });
    };
    
    const registerKeyboard = () => {
        const keyboard = document.getElementById("keyboard");
        const keys = keyboard.getElementsByClassName("key");
    
        keyboard.addEventListener("touchstart", function (event) {
            event.preventDefault();
            event.stopPropagation();
        });
    
        keyboard.addEventListener("touchend", function (event) {
            event.preventDefault();
            event.stopPropagation();
        });
    
        Array.prototype.forEach.call(keys, (k: Element) => {
            k.addEventListener("mousedown", function (event) {
                const keyCode = KEYS[this.textContent.toLowerCase()];
                //state.gameBoy.key_press_ws(keyCode); @todo
                event.preventDefault();
                event.stopPropagation();
            });
    
            k.addEventListener("touchstart", function (event) {
                const keyCode = KEYS[this.textContent.toLowerCase()];
                //state.gameBoy.key_press_ws(keyCode); @todo
                event.preventDefault();
                event.stopPropagation();
            });
    
            k.addEventListener("mouseup", function (event) {
                const keyCode = KEYS[this.textContent.toLowerCase()];
                //state.gameBoy.key_lift_ws(keyCode); @todo
                event.preventDefault();
                event.stopPropagation();
            });
    
            k.addEventListener("touchend", function (event) {
                const keyCode = KEYS[this.textContent.toLowerCase()];
                //state.gameBoy.key_lift_ws(keyCode); @todo
                event.preventDefault();
                event.stopPropagation();
            });
        });
    };
    
    const registerCanvas = () => {
        const canvasClose = document.getElementById("canvas-close");
        canvasClose.addEventListener("click", () => {
            minimize();
        });
    };
    
    const registerToast = () => {
        const toast = document.getElementById("toast");
        toast.addEventListener("click", () => {
            toast.classList.remove("visible");
        });
    };
    
    const registerModal = () => {
        const modalClose = document.getElementById("modal-close");
        modalClose.addEventListener("click", () => {
            hideModal(false);
        });
    
        const modalCancel = document.getElementById("modal-cancel");
        modalCancel.addEventListener("click", () => {
            hideModal(false);
        });
    
        const modalConfirm = document.getElementById("modal-confirm");
        modalConfirm.addEventListener("click", () => {
            hideModal(true);
        });
    
        document.addEventListener("keydown", (event) => {
            if (event.key === "Escape") {
                hideModal(false);
            }
        });
    };
    
    const initBase = async () => {
        const background = BACKGROUNDS[state.background_index];
        setBackground(background);
        setVersion(info.version);
    };
    
    const initCanvas = async () => {
        // initializes the off-screen canvas that is going to be
        // used in the drawing process
        state.canvas = document.createElement("canvas");
        state.canvas.width = DISPLAY_WIDTH;
        state.canvas.height = DISPLAY_HEIGHT;
        state.canvasCtx = state.canvas.getContext("2d");
    
        state.canvasScaled = document.getElementById(
    
            "engine-canvas"
    
        ) as HTMLCanvasElement;
        state.canvasScaled.width =
            state.canvasScaled.width * window.devicePixelRatio;
        state.canvasScaled.height =
            state.canvasScaled.height * window.devicePixelRatio;
        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: Uint8Array,
        format: PixelFormat = PixelFormat.RGB
    ) => {
    
        let offset = 0;
    
        for (let index = 0; index < pixels.length; index += format) {
    
            const color =
                (pixels[index] << 24) |
                (pixels[index + 1] << 16) |
                (pixels[index + 2] << 8) |
    
                (format == PixelFormat.RGBA ? pixels[index + 3] : 0xff);
    
            state.videoBuff.setUint32(offset, color);
    
            offset += PixelFormat.RGBA;
    
        }
        state.canvasCtx.putImageData(state.image, 0, 0);
        state.canvasScaledCtx.drawImage(state.canvas, 0, 0);
    };
    
    const clearCanvas = async (
        color = PIXEL_UNSET_COLOR,
        { image = null as string, imageScale = 1 } = {}
    ) => {
        state.canvasScaledCtx.fillStyle = `#${color.toString(16).toUpperCase()}`;
        state.canvasScaledCtx.fillRect(
            0,
            0,
            state.canvasScaled.width,
            state.canvasScaled.height
        );
    
        // in case an image was requested then uses that to load
    
        // an image at the center of the screen properly scaled
    
        if (image) {
            const img = await new Promise<HTMLImageElement>((resolve) => {
                const img = new Image();
                img.onload = () => {
                    resolve(img);
                };
                img.src = image;
            });
            const [imgWidth, imgHeight] = [
    
                img.width * imageScale * window.devicePixelRatio,
                img.height * imageScale * window.devicePixelRatio
    
            ];
            const [x0, y0] = [
                state.canvasScaled.width / 2 - imgWidth / 2,
                state.canvasScaled.height / 2 - imgHeight / 2
            ];
            state.canvasScaledCtx.setTransform(1, 0, 0, 1, 0, 0);
            try {
                state.canvasScaledCtx.drawImage(img, x0, y0, imgWidth, imgHeight);
            } finally {
                state.canvasScaledCtx.scale(
                    state.canvasScaled.width / state.canvas.width,
                    state.canvasScaled.height / state.canvas.height
                );
            }
        }
    };
    
    const showToast = async (message: string, error = false, timeout = 3500) => {
        const toast = document.getElementById("toast");
        toast.classList.remove("error");
        if (error) toast.classList.add("error");
        toast.classList.add("visible");
        toast.textContent = message;
        if (state.toastTimeout) clearTimeout(state.toastTimeout);
        state.toastTimeout = setTimeout(() => {
            toast.classList.remove("visible");
            state.toastTimeout = null;
        }, timeout);
    };
    
    const showModal = async (
        message: string,
        title = "Alert"
    ): Promise<boolean> => {
        const modalContainer = document.getElementById("modal-container");
        const modalTitle = document.getElementById("modal-title");
        const modalText = document.getElementById("modal-text");
        modalContainer.classList.add("visible");
        modalTitle.textContent = title;
        modalText.innerHTML = message.replace(/\n/g, "<br/>");
        const result = (await new Promise((resolve) => {
            global.modalCallback = resolve;
        })) as boolean;
        return result;
    };
    
    const hideModal = async (result = true) => {
        const modalContainer = document.getElementById("modal-container");
        modalContainer.classList.remove("visible");
        if (global.modalCallback) global.modalCallback(result);
        global.modalCallback = null;
    };
    
    const setVersion = (value: string) => {
        document.getElementById("version").textContent = value;
    };
    
    const setEngine = (name: string, upper = true) => {
        name = upper ? name.toUpperCase() : name;
        document.getElementById("engine").textContent = name;
    };
    
    const setRom = (name: string, data: Uint8Array) => {
        state.romName = name;
        state.romData = data;
        state.romSize = data.length;
        document.getElementById("rom-name").textContent = name;
        document.getElementById("rom-size").textContent = String(data.length);
    };
    
    const setLogicFrequency = (value: number) => {
        if (value < 0) showToast("Invalid frequency value!", true);
        value = Math.max(value, 0);
        state.logicFrequency = value;
        document.getElementById("logic-frequency").textContent = String(value);
    };
    
    const setFps = (value: number) => {
        if (value < 0) showToast("Invalid FPS value!", true);
        value = Math.max(value, 0);
        state.fps = value;
        document.getElementById("fps-count").textContent = String(value);
    };
    
    const setBackground = (value: string) => {
        document.body.style.backgroundColor = `#${value}`;
        document.getElementById(
            "footer-background"
        ).style.backgroundColor = `#${value}f2`;
    };
    
    const toggleRunning = () => {
        if (state.paused) {
            resume();
        } else {
            pause();
        }
    };
    
    const pause = () => {
        state.paused = true;
        const buttonPause = document.getElementById("button-pause");
        const img = buttonPause.getElementsByTagName("img")[0];
        const span = buttonPause.getElementsByTagName("span")[0];
        buttonPause.classList.add("enabled");
        // @ts-ignore: ts(2580)
        img.src = require("./res/play.svg");
        span.textContent = "Resume";
    };
    
    const resume = () => {
        state.paused = false;
        state.nextTickTime = new Date().getTime();
        const buttonPause = document.getElementById("button-pause");
        const img = buttonPause.getElementsByTagName("img")[0];
        const span = buttonPause.getElementsByTagName("span")[0];
        buttonPause.classList.remove("enabled");
        // @ts-ignore: ts(2580)
        img.src = require("./res/pause.svg");
        span.textContent = "Pause";
    };
    
    const toggleWindow = () => {
        maximize();
    };
    
    const maximize = () => {
        const canvasContainer = document.getElementById("canvas-container");
        canvasContainer.classList.add("fullscreen");
    
        window.addEventListener("resize", crop);
    
        crop();
    };
    
    const minimize = () => {
        const canvasContainer = document.getElementById("canvas-container");
    
        const engineCanvas = document.getElementById("engine-canvas");
    
        canvasContainer.classList.remove("fullscreen");
    
        engineCanvas.style.width = null;
        engineCanvas.style.height = null;
    
        window.removeEventListener("resize", crop);
    };
    
    const crop = () => {
    
        const engineCanvas = document.getElementById("engine-canvas");
    
    
        // calculates the window ratio as this is fundamental to
        // determine the proper way to crop the fulscreen
        const windowRatio = window.innerWidth / window.innerHeight;
    
        // in case the window is wider (more horizontal than the base ratio)
        // this means that we must crop horizontaly
        if (windowRatio > DISPLAY_RATIO) {
    
            engineCanvas.style.width = `${
    
                window.innerWidth * (DISPLAY_RATIO / windowRatio)
            }px`;
    
            engineCanvas.style.height = `${window.innerHeight}px`;
    
            engineCanvas.style.width = `${window.innerWidth}px`;
            engineCanvas.style.height = `${
    
                window.innerHeight * (windowRatio / DISPLAY_RATIO)
            }px`;
        }
    };
    
    const reset = () => {
        start({ engine: null });
    };
    
    const fetchRom = async (romPath: string): Promise<[string, 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(ROM_PATH);
        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 [romName, romData];
    };
    
    (async () => {
        await main();
    })();