Skip to content
Snippets Groups Projects
gb.ts 31.4 KiB
Newer Older
    BenchmarkResult,
    Compilation,
    Compiler,
    Emulator,
    EmulatorBase,
    Frequency,
    FrequencySpecs,
    HelpPanel,
    RomInfo,
    Size
import { PALETTES, PALETTES_MAP } from "./palettes";
import { base64ToBuffer, bufferToBase64 } from "./util";
import {
    DebugAudio,
    DebugSettings,
    HelpFaqs,
    HelpKeyboard,
    SerialSection,
    TestSection
} from "../react";

import {
    Cartridge,
    default as _wasm,
    GameBoy,
    PadKey,
    GameBoyMode,
    GameBoySpeed
} from "../lib/boytacean";
import info from "../package.json";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const require: any;

/**
 * The frequency at which the Game Boy emulator should
 * run "normally".
 */
const LOGIC_HZ = 4194304;
/**
 * The frequency at witch the the visual loop is going to
 * run, increasing this value will have a consequence in
 * the visual frames per second (FPS) of emulation.
 */
const VISUAL_HZ = 59.7275;

/**
 * The frequency of the pause polling update operation,
 * increasing this value will make resume from emulation
 * paused state fasted.
 */
const IDLE_HZ = 10;

const DISPLAY_WIDTH = 160;
const DISPLAY_HEIGHT = 144;
const DISPLAY_SCALE = 2;

/**
 * The rate at which the local storage RAM state flush
 * operation is going to be performed, this value is the
 * number of seconds in between flush operations (eg: 5 seconds).
 */
const STORE_RATE = 5;

/**
 * The sample rate that is going to be used for FPS calculus,
 * meaning that every N seconds we will calculate the number
 * of frames rendered divided by the N seconds.
 */
const FPS_SAMPLE_RATE = 3;

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/demo/pocket.gb");
/**
 * Enumeration with the values for the complete set of available
 * serial devices that can be used in the emulator.
 */
export enum SerialDevice {
    Null = "null",
    Logger = "logger",
    Printer = "printer"
}

/**
 * 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;

    /**
     * If the GB running mode should be automatically inferred
     * from the GBC flag in the cartridge. Meaning that if the
     * cartridge is a GBC compatible or GBC only the GBC emulation
     * mode is going to be used, otherwise the DMG mode is used
     * instead. This should provide an optimal usage experience.
     */
    private autoMode = false;

    private logicFrequency = LOGIC_HZ;
    private visualFrequency = VISUAL_HZ;
    private idleFrequency = IDLE_HZ;
    private paused = false;
    private nextTickTime = 0;
    private fps = 0;
    private frameStart: number = EmulatorBase.now();
    private frameCount = 0;
    private paletteIndex = 0;
    private storeCycles: number = LOGIC_HZ * STORE_RATE;

    private romName: string | null = null;
    private romData: Uint8Array | null = null;
    private romSize = 0;
    private cartridge: Cartridge | null = null;

    private _serialDevice: SerialDevice = SerialDevice.Null;

    /**
     * Associative map for extra settings to be used in
     * opaque local storage operations, associated setting
     * name with its value as a string.
     */
    private extraSettings: Record<string, string> = {};

    constructor(extraSettings = {}) {
        super();
        this.extraSettings = extraSettings;
    }

    /**
     * Runs the initialization and main loop execution for
     * the Game Boy emulator.
     * The main execution of this function should be an
     * infinite loop running machine `tick` operations.
     *
     * @param options The set of options that are going to be
     * used in he Game Boy emulator initialization.
     */
    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 = EmulatorBase.now();
Loading
Loading full blame...