Skip to content
Snippets Groups Projects
app.tsx 30.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • João Magalhães's avatar
    João Magalhães committed
    import React, { FC, useEffect, useRef, useState } from "react";
    
    import ReactDOM from "react-dom/client";
    
    
    const FREQUENCY_DELTA = 100000;
    
    
    declare const require: any;
    
    
        ButtonIncrement,
        ButtonSwitch,
    
        ClearHandler,
    
        Display,
    
        Footer,
    
        KeyboardChip8,
    
        PanelSplit,
    
        Paragraph,
    
        Section,
    
        Title,
        Toast
    
    } from "./components";
    
    
    import "./app.css";
    
    
    export type Callback<T> = (owner: T, params?: any) => void;
    
    
    /**
     * Abstract class that implements the basic functionality
    
     * part of the definition of the Observer pattern.
    
     *
     * @see {@link https://en.wikipedia.org/wiki/Observer_pattern}
     */
    export class Observable {
        private events: Record<string, [Callback<this>]> = {};
    
        bind(event: string, callback: Callback<this>) {
            const callbacks = this.events[event] ?? [];
            if (callbacks.includes(callback)) return;
            callbacks.push(callback);
            this.events[event] = callbacks;
        }
    
    
        unbind(event: string, callback: Callback<this>) {
            const callbacks = this.events[event] ?? [];
            if (!callbacks.includes(callback)) return;
            const index = callbacks.indexOf(callback);
            callbacks.splice(index, 1);
            this.events[event] = callbacks;
        }
    
    
        trigger(event: string, params?: any) {
    
            const callbacks = this.events[event] ?? [];
    
            callbacks.forEach((c) => c(this, params));
    
    export type RomInfo = {
        name?: string;
        data?: Uint8Array;
        size?: number;
        extra?: Record<string, string | undefined>;
    };
    
    
    export type BenchmarkResult = {
        delta: number;
        count: number;
    
        cycles: number;
    
        frequency_mhz: number;
    };
    
    
    export enum Feature {
        Debug = 1,
        Palettes,
    
        Benchmark,
        Keyboard,
        KeyboardChip8,
        KeyboardGB
    
    export interface ObservableI {
        bind(event: string, callback: Callback<this>): void;
    
        unbind(event: string, callback: Callback<this>): void;
    
        trigger(event: string): void;
    }
    
    
    /**
     * Top level interface that declares the main abstract
     * interface of an emulator structured entity.
     * Should allow typical hardware operations to be performed.
     */
    
    export interface Emulator extends ObservableI {
    
    João Magalhães's avatar
    João Magalhães committed
        /**
    
         * The descriptive name of the emulator.
    
    João Magalhães's avatar
    João Magalhães committed
         */
    
        get name(): string;
    
         * The name of the the hardware that is being emulated
         * by the emulator (eg: Super Nintendo).
    
        get device(): string;
    
        /**
         * A URL to a website that describes the device that is
         * being emulated by the emulator (eg: Wikipedia link).
         */
    
        get deviceUrl(): string | undefined;
    
    João Magalhães's avatar
    João Magalhães committed
        /**
    
         * A semantic version string for the current version
         * of the emulator.
    
    João Magalhães's avatar
    João Magalhães committed
         *
         * @see {@link https://semver.org}
         */
    
        get version(): string;
    
         * The URL to the page describing the current version
    
    João Magalhães's avatar
    João Magalhães committed
         * of the emulator.
         */
    
        get versionUrl(): string;
    
    
        /**
         * The features available and compatible with the emulator,
         * these values will influence the associated GUIs.
         */
        get features(): Feature[];
    
    
         * The complete set of engine names that can be used
    
         * in the re-boot operation.
         */
    
        get engines(): string[];
    
        /**
         * The name of the current execution engine being used
         * by the emulator.
         */
    
        get engine(): string | null;
    
        /**
         * The complete set of file extensions that this emulator
         * supports.
         */
        get romExts(): string[];
    
    
         * The pixel format of the emulator's display
    
         * image buffer (eg: RGB).
         */
    
        get pixelFormat(): PixelFormat;
    
         * Gets the complete image buffer as a sequence of
    
         * bytes that respects the current pixel format from
         * `getPixelFormat()`. This method returns an in memory
         * pointer to the heap and not a copy.
         */
    
        get imageBuffer(): Uint8Array;
    
         * Gets information about the ROM that is currently
         * loaded in the emulator, using a structure containing
         * the information about the ROM that is currently
    
         * loaded in the emulator.
         */
    
        get romInfo(): RomInfo;
    
        /**
         * The current CPU frequency (logic) of the emulator,
         * should impact other elements of the emulator.
         */
        get frequency(): number;
    
        set frequency(value: number);
    
        /**
         * The recommended frequency delta in hertz for scale up
         * and scale down operations in the CPU frequency.
         */
        get frequencyDelta(): number | null;
    
    
         * The current logic framerate of the running emulator.
    
        get framerate(): number;
    
    João Magalhães's avatar
    João Magalhães committed
        /**
         * A dictionary that contains the register names associated
         * with their value either as strings or numbers.
         */
    
        get registers(): Record<string, string | number>;
    
    
    João Magalhães's avatar
    João Magalhães committed
        /**
         * Obtains the pixel buffer for the VRAM tile at the given
         * index.
         *
         * @param index The index of the tile to obtain pixel buffer.
         * @returns The pixel buffer of the tile at the given index.
         */
    
        getTile(index: number): Uint8Array;
    
    
        /**
         * Boot (or reboots) the emulator according to the provided
         * set of options.
         *
         * @param options The options that are going to be used for
         * the booting operation of the emulator.
         */
        boot(options: any): void;
    
    
        /**
         * Toggle the running state of the emulator between paused
         * and running, prevents consumers from the need to access
         * the current running state of the emulator to implement
         * a logic toggle.
         */
    
        toggleRunning(): void;
        pause(): void;
        resume(): void;
    
    João Magalhães's avatar
    João Magalhães committed
    
        /**
         * Resets the emulator machine to the start state and
         * re-loads the ROM that is currently set in the emulator.
         */
    
        keyPress(key: string): void;
    
        keyLift(key: string): void;
    
    
        /**
         * Changes the palette of the emulator to the "next" one.
         */
        changePalette?: { (): void };
    
         * Runs a benchmark operation in the emulator, effectively
         * measuring the performance of it.
    
         * @param count The number of benchmark iterations to be
         * run, increasing this value will make the benchmark take
         * more time to be executed.
    
         * @returns The result metrics from the benchmark run.
    
        benchmark?: { (count?: number): BenchmarkResult };
    
    export class EmulatorBase extends Observable {
        get deviceUrl(): string | undefined {
            return undefined;
        }
    
        get versionUrl(): string | undefined {
            return undefined;
        }
    
        get features(): Feature[] {
            return [];
        }
    
    
        get frequencyDelta(): number | null {
            return FREQUENCY_DELTA;
        }
    
    /**
     * Enumeration that describes the multiple pixel
     * formats and the associated size in bytes.
     */
    export enum PixelFormat {
        RGB = 3,
        RGBA = 4
    
    type AppProps = {
    
        emulator: Emulator;
    
        fullscreen?: boolean;
        debug?: boolean;
        keyboard?: boolean;
    
        backgrounds?: string[];
    };
    
    
    const isTouchDevice = () => {
        return "ontouchstart" in window || navigator.maxTouchPoints > 0;
    };
    
    
    export const App: FC<AppProps> = ({
        emulator,
        fullscreen = false,
        debug = false,
        keyboard = false,
        backgrounds = ["264653"]
    }) => {
    
        const [paused, setPaused] = useState(false);
    
        const [fullscreenState, setFullscreenState] = useState(fullscreen);
    
        const [backgroundIndex, setBackgroundIndex] = useState(0);
    
        const [romInfo, setRomInfo] = useState<RomInfo>({});
        const [framerate, setFramerate] = useState(0);
    
        const [keyaction, setKeyaction] = useState<string>();
        const [modalTitle, setModalTitle] = useState<string>();
        const [modalText, setModalText] = useState<string>();
    
        const [modalVisible, setModalVisible] = useState(false);
        const [toastText, setToastText] = useState<string>();
        const [toastError, setToastError] = useState(false);
        const [toastVisible, setToastVisible] = useState(false);
    
        const [keyboardVisible, setKeyboardVisible] = useState(
            isTouchDevice() || keyboard
        );
    
        const [infoVisible, setInfoVisible] = useState(true);
    
        const [debugVisible, setDebugVisible] = useState(debug);
    
        const toastCounterRef = useRef(0);
    
        const frameRef = useRef<boolean>(false);
    
        const errorRef = useRef<boolean>(false);
    
        const modalCallbackRef =
            useRef<(value: boolean | PromiseLike<boolean>) => void>();
    
        useEffect(() => {
            document.body.style.backgroundColor = `#${getBackground()}`;
        }, [backgroundIndex]);
        useEffect(() => {
            switch (keyaction) {
    
                    emulator.frequency +=
                        emulator.frequencyDelta ?? FREQUENCY_DELTA;
    
                    setKeyaction(undefined);
                    break;
                case "Minus":
    
                    emulator.frequency -=
                        emulator.frequencyDelta ?? FREQUENCY_DELTA;
    
                    setKeyaction(undefined);
                    break;
    
                case "Escape":
    
                    setFullscreenState(false);
    
                    setKeyaction(undefined);
                    break;
                case "Fullscreen":
    
                    setFullscreenState(!fullscreenState);
    
                    setKeyaction(undefined);
                    break;
            }
        }, [keyaction]);
        useEffect(() => {
    
            const onFullChange = (event: Event) => {
    
                if (
                    !document.fullscreenElement &&
                    !(document as any).webkitFullscreenElement
                ) {
    
                    setFullscreenState(false);
                }
            };
    
            const onKeyDown = (event: KeyboardEvent) => {
    
                switch (event.key) {
                    case "+":
                        setKeyaction("Plus");
                        event.stopPropagation();
                        event.preventDefault();
                        break;
                    case "-":
                        setKeyaction("Minus");
                        event.stopPropagation();
                        event.preventDefault();
                        break;
                    case "Escape":
                        setKeyaction("Escape");
                        event.stopPropagation();
                        event.preventDefault();
                        break;
    
                }
                if (event.key === "f" && event.ctrlKey === true) {
                    setKeyaction("Fullscreen");
                    event.stopPropagation();
                    event.preventDefault();
                }
    
            };
            const onBooted = () => {
    
                setRomInfo(emulator.romInfo);
    
                setPaused(false);
    
            };
            const onMessage = (
                emulator: Emulator,
                params: Record<string, any> = {}
            ) => {
    
                showToast(params.text, params.error, params.timeout);
    
            document.addEventListener("fullscreenchange", onFullChange);
    
            document.addEventListener("webkitfullscreenchange", onFullChange);
    
            document.addEventListener("keydown", onKeyDown);
            emulator.bind("booted", onBooted);
            emulator.bind("message", onMessage);
            return () => {
    
                document.removeEventListener("fullscreenchange", onFullChange);
    
                document.removeEventListener(
                    "webkitfullscreenchange",
                    onFullChange
                );
    
                document.removeEventListener("keydown", onKeyDown);
                emulator.unbind("booted", onBooted);
                emulator.unbind("message", onMessage);
            };
    
        const getPauseText = () => (paused ? "Resume" : "Pause");
        const getPauseIcon = () =>
            paused ? require("../res/play.svg") : require("../res/pause.svg");
    
        const getBackground = () => backgrounds[backgroundIndex];
    
    
        const showModal = async (
            text: string,
            title = "Alert"
        ): Promise<boolean> => {
            setModalText(text);
            setModalTitle(title);
            setModalVisible(true);
            const result = (await new Promise((resolve) => {
                modalCallbackRef.current = resolve;
            })) as boolean;
            return result;
        };
    
        const showToast = async (text: string, error = false, timeout = 3500) => {
            setToastText(text);
            setToastError(error);
            setToastVisible(true);
            toastCounterRef.current++;
            const counter = toastCounterRef.current;
            await new Promise((resolve) => {
                setTimeout(() => {
                    if (counter !== toastCounterRef.current) return;
                    setToastVisible(false);
                    resolve(true);
                }, timeout);
            });
        };
    
        const hasFeature = (feature: Feature) => {
            return emulator.features.includes(feature);
        };
    
        const onFile = async (file: File) => {
    
            const fileExtension = file.name.split(".").pop() ?? "";
            if (!emulator.romExts.includes(fileExtension)) {
    
                    `This is probably not a ${emulator.device} ROM file!`,
    
                return;
            }
    
            const arrayBuffer = await file.arrayBuffer();
            const romData = new Uint8Array(arrayBuffer);
    
            emulator.boot({ engine: null, romName: file.name, romData: romData });
    
            showToast(`Loaded ${file.name} ROM successfully!`);
        };
    
        const onModalConfirm = () => {
            if (modalCallbackRef.current) {
                modalCallbackRef.current(true);
                modalCallbackRef.current = undefined;
            }
            setModalVisible(false);
        };
        const onModalCancel = () => {
            if (modalCallbackRef.current) {
                modalCallbackRef.current(false);
                modalCallbackRef.current = undefined;
            }
            setModalVisible(false);
        };
    
        const onToastCancel = () => {
            setToastVisible(false);
        };
    
        const onPauseClick = () => {
            emulator.toggleRunning();
            setPaused(!paused);
    
        const onResetClick = () => {
            emulator.reset();
        };
    
        const onBenchmarkClick = async () => {
    
            if (!emulator.benchmark) return;
    
            const result = await showModal(
                "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
                "Confirm"
            );
    
            if (!result) return;
            const { delta, count, frequency_mhz } = emulator.benchmark();
    
            await showToast(
    
                `Took ${delta.toFixed(
                    2
                )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
                    2
                )} Mhz)!`,
                undefined,
                7500
    
    João Magalhães's avatar
    João Magalhães committed
        const onFullscreenClick = () => {
    
            setFullscreenState(!fullscreenState);
    
        const onKeyboardClick = () => {
    
            setKeyboardVisible(!keyboardVisible);
    
    João Magalhães's avatar
    João Magalhães committed
        const onInformationClick = () => {
    
            setInfoVisible(!infoVisible);
        };
        const onDebugClick = () => {
            setDebugVisible(!debugVisible);
    
    João Magalhães's avatar
    João Magalhães committed
        };
    
        const onThemeClick = () => {
            setBackgroundIndex((backgroundIndex + 1) % backgrounds.length);
        };
    
        const onPaletteClick = () => {
    
            if (!emulator.changePalette) return;
            emulator.changePalette();
    
        const onUploadFile = async (file: File) => {
            const arrayBuffer = await file.arrayBuffer();
            const romData = new Uint8Array(arrayBuffer);
            emulator.boot({ engine: null, romName: file.name, romData: romData });
            showToast(`Loaded ${file.name} ROM successfully!`);
        };
    
        const onEngineChange = (engine: string) => {
            emulator.boot({ engine: engine.toLowerCase() });
    
                `${emulator.device} running in engine "${engine}" from now on!`
    
        const onFrequencyChange = (value: number) => {
            emulator.frequency = value * 1000 * 1000;
        };
        const onFrequencyReady = (handler: (value: number) => void) => {
            emulator.bind("frequency", (emulator: Emulator, frequency: number) => {
                handler(frequency / 1000000);
            });
        };
    
        const onMinimize = () => {
    
            setFullscreenState(!fullscreenState);
    
        const onKeyDown = (key: string) => {
            emulator.keyPress(key);
        };
        const onKeyUp = (key: string) => {
            emulator.keyLift(key);
        };
    
    João Magalhães's avatar
    João Magalhães committed
        const onDrawHandler = (handler: DrawHandler) => {
    
            if (frameRef.current) return;
            frameRef.current = true;
    
            emulator.bind("frame", () => {
    
                handler(emulator.imageBuffer, PixelFormat.RGB);
                setFramerate(emulator.framerate);
    
        const onClearHandler = (handler: ClearHandler) => {
            if (errorRef.current) return;
            errorRef.current = true;
            emulator.bind("error", async () => {
                await handler(undefined, require("../res/storm.png"), 0.2);
            });
        };
    
            <div className="app">
    
                <Overlay text={"Drag to load ROM"} onFile={onFile} />
    
                <Modal
                    title={modalTitle}
                    text={modalText}
    
                    visible={modalVisible}
    
                    onConfirm={onModalConfirm}
                    onCancel={onModalCancel}
                />
    
                <Toast
                    text={toastText}
                    error={toastError}
                    visible={toastVisible}
                    onCancel={onToastCancel}
                />
    
                <Footer color={getBackground()}>
                    Built with ❤️ by{" "}
    
    João Magalhães's avatar
    João Magalhães committed
                    <Link href="https://joao.me" target="_blank">
    
    João Magalhães's avatar
    João Magalhães committed
                    </Link>
    
                </Footer>
    
                <PanelSplit
                    left={
    
                        <div className="display-container">
    
                                fullscreen={fullscreenState}
    
                                onDrawHandler={onDrawHandler}
    
                                onClearHandler={onClearHandler}
    
                                onMinimize={onMinimize}
                            />
    
                    <Section visible={keyboardVisible} separatorBottom={true}>
    
                        {hasFeature(Feature.KeyboardChip8) && (
                            <KeyboardChip8
                                onKeyDown={onKeyDown}
                                onKeyUp={onKeyUp}
                            />
                        )}
                        {hasFeature(Feature.KeyboardGB) && (
                            <KeyboardGB
                                fullscreen={fullscreenState}
                                onKeyDown={onKeyDown}
                                onKeyUp={onKeyUp}
                            />
                        )}
    
                    </Section>
    
                        text={emulator.name}
                        version={emulator.version}
    
                        versionUrl={
    
                            emulator.versionUrl ? emulator.versionUrl : undefined
    
                        iconSrc={require("../res/thunder.png")}
                    ></Title>
    
                    <Section>
                        <Paragraph>
                            This is a{" "}
    
                            {emulator.deviceUrl ? (
                                <Link href={emulator.deviceUrl} target="_blank">
                                    {emulator.device}
    
                                </Link>
                            ) : (
    
                            emulator built using the{" "}
                            <Link href="https://www.rust-lang.org" target="_blank">
                                Rust Programming Language
                            </Link>{" "}
                            and is running inside this browser with the help of{" "}
                            <Link href="https://webassembly.org/" target="_blank">
                                WebAssembly
                            </Link>
                            .
                        </Paragraph>
                        <Paragraph>
                            You can check the source code of it at{" "}
                            <Link
                                href="https://gitlab.stage.hive.pt/joamag/boytacean"
                                target="_blank"
                            >
                                GitLab
                            </Link>
                            .
                        </Paragraph>
                        <Paragraph>
                            TIP: Drag and Drop ROM files to the Browser to load the
                            ROM.
                        </Paragraph>
                    </Section>
    
                    {debugVisible && (
                        <Section>
    
                            <div
                                style={{
                                    display: "inline-block",
                                    verticalAlign: "top",
                                    marginRight: 32,
                                    width: 256
                                }}
                            >
                                <h3>VRAM Tiles</h3>
                                <Tiles
                                    getTile={(index) => emulator.getTile(index)}
                                    tileCount={384}
                                    width={"100%"}
                                    contentBox={false}
                                />
                            </div>
                            <div
                                style={{
                                    display: "inline-block",
                                    verticalAlign: "top"
                                }}
                            >
                                <h3>Registers</h3>
    
                                <RegistersGB
                                    getRegisters={() => emulator.registers}
                                />
    
                        </Section>
                    )}
                    {infoVisible && (
                        <Section>
                            <Info>
                                <Pair
                                    key="button-engine"
                                    name={"Engine"}
                                    valueNode={
                                        <ButtonSwitch
    
                                            options={emulator.engines.map((e) =>
                                                e.toUpperCase()
                                            )}
    
                                            size={"large"}
                                            style={["simple"]}
                                            onChange={onEngineChange}
                                        />
                                    }
                                />
                                <Pair
                                    key="rom"
                                    name={"ROM"}
                                    value={romInfo.name ?? "-"}
                                />
                                <Pair
                                    key="rom-size"
                                    name={"ROM Size"}
                                    value={
    
                                        romInfo.size
                                            ? `${new Intl.NumberFormat().format(
                                                  romInfo.size
                                              )} bytes`
                                            : "-"
    
                                    }
                                />
                                <Pair
                                    key="button-frequency"
                                    name={"CPU Frequency"}
                                    valueNode={
                                        <ButtonIncrement
    
                                            value={emulator.frequency / 1000 / 1000}
    
                                            delta={
                                                (emulator.frequencyDelta ??
                                                    FREQUENCY_DELTA) /
                                                1000 /
                                                1000
                                            }
    
                                            min={0}
                                            suffix={"MHz"}
    
                                            decimalPlaces={2}
    
                                            onChange={onFrequencyChange}
                                            onReady={onFrequencyReady}
    
                                        />
                                    }
                                />
                                <Pair
                                    key="rom-type"
                                    name={"ROM Type"}
                                    value={
                                        romInfo.extra?.romType
                                            ? `${romInfo.extra?.romType}`
                                            : "-"
                                    }
                                />
                                <Pair
                                    key="framerate"
                                    name={"Framerate"}
                                    value={`${framerate} fps`}
                                />
                            </Info>
                        </Section>
                    )}
    
                    <Section>
    
                        <ButtonContainer>
                            <Button
    
                                text={getPauseText()}
                                image={getPauseIcon()}
                                imageAlt="pause"
    
                                enabled={paused}
    
                                style={["simple", "border", "padded"]}
    
                                onClick={onPauseClick}
    
                            />
                            <Button
                                text={"Reset"}
                                image={require("../res/reset.svg")}
                                imageAlt="reset"
    
                                style={["simple", "border", "padded"]}
    
                                onClick={onResetClick}
                            />
    
                            {hasFeature(Feature.Benchmark) && (
                                <Button
                                    text={"Benchmark"}
                                    image={require("../res/bolt.svg")}
                                    imageAlt="benchmark"
                                    style={["simple", "border", "padded"]}
                                    onClick={onBenchmarkClick}
                                />
                            )}
    
    João Magalhães's avatar
    João Magalhães committed
                            <Button
                                text={"Fullscreen"}
                                image={require("../res/maximise.svg")}
                                imageAlt="maximise"
    
                                style={["simple", "border", "padded"]}
    
    João Magalhães's avatar
    João Magalhães committed
                                onClick={onFullscreenClick}
                            />
    
                            {hasFeature(Feature.Keyboard) && (
                                <Button
                                    text={"Keyboard"}
                                    image={require("../res/dialpad.svg")}
                                    imageAlt="keyboard"
                                    enabled={keyboardVisible}
                                    style={["simple", "border", "padded"]}
                                    onClick={onKeyboardClick}
                                />
                            )}
    
    João Magalhães's avatar
    João Magalhães committed
                            <Button
                                text={"Information"}
                                image={require("../res/info.svg")}
    
                                imageAlt="information"
    
                                enabled={infoVisible}
                                style={["simple", "border", "padded"]}
    
    João Magalhães's avatar
    João Magalhães committed
                                onClick={onInformationClick}
                            />
    
                            {hasFeature(Feature.Debug) && (
                                <Button
                                    text={"Debug"}
                                    image={require("../res/bug.svg")}
                                    imageAlt="debug"
                                    enabled={debugVisible}
                                    style={["simple", "border", "padded"]}
                                    onClick={onDebugClick}
                                />
                            )}
    
                            <Button
                                text={"Theme"}
                                image={require("../res/marker.svg")}
    
                                imageAlt="theme"
    
                                style={["simple", "border", "padded"]}
    
                                onClick={onThemeClick}
                            />
    
                            {hasFeature(Feature.Palettes) && (
                                <Button
                                    text={"Palette"}
                                    image={require("../res/brightness.svg")}
                                    imageAlt="palette"
                                    style={["simple", "border", "padded"]}
                                    onClick={onPaletteClick}
                                />
                            )}
    
    João Magalhães's avatar
    João Magalhães committed
                                text={"Load ROM"}
    
                                image={require("../res/upload.svg")}
                                imageAlt="upload"
                                file={true}
                                accept={".gb"}
    
                                style={["simple", "border", "padded"]}
    
                                onFile={onUploadFile}
                            />
    
                    </Section>
                </PanelSplit>
    
    export const startApp = (
        element: string,
    
        {
            emulator,
            fullscreen = false,
            debug = false,
            keyboard = false,
            backgrounds = []
        }: {
            emulator: Emulator;
            fullscreen?: boolean;
            debug?: boolean;
            keyboard?: boolean;
            backgrounds: string[];
        }
    
        const elementRef = document.getElementById(element);
    
        if (!elementRef) return;
    
    
        const root = ReactDOM.createRoot(elementRef);
    
        root.render(
            <App
                emulator={emulator}
                fullscreen={fullscreen}
                debug={debug}
                keyboard={keyboard}
                backgrounds={backgrounds}
            />
        );
    
    
    export default App;