Newer
Older
import {
default as wasm,
Chip8Neo
} from "./chip_ahoyto.js";
const PIXEL_SET_COLOR = 0x50cb93ff;
const PIXEL_UNSET_COLOR = 0x1b1a17ff;
const DISPLAY_WIDTH = 64;
const DISPLAY_HEIGHT = 32;
const BACKGROUNDS = [
"1b1a17",
"023047",
"bc6c25",
"264653",
"283618"
]
const KEYS = {
"1": 0x01,
"2": 0x02,
"3": 0x03,
"4": 0x0c,
"q": 0x04,
"w": 0x05,
"e": 0x06,
"r": 0x0d,
"a": 0x07,
"s": 0x08,
"d": 0x09,
"f": 0x0e,
"z": 0x0a,
"x": 0x00,
"c": 0x0b,
"v": 0x0f
}
const ROM_PATH = "res/roms/pong.ch8";
const ROM_NAME = "pong.ch8";
visualFrequency: VISUAL_HZ,
timerFrequency: TIMER_HZ,
canvas: null,
canvasScaled: null,
canvasCtx: null,
canvasScaledCtx: null,
image: null,
frameStart: new Date().getTime(),
(async () => {
// initializes the WASM module, this is required
// so that the global symbols become available
await wasm();
// initializes the complete set of sub-systems
// and registers the event handlers
init();
register();
// loads the ROM data and converts it into the
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
// updates the ROM information on display
setRom(ROM_NAME, data.length);
// creates the CHIP-8 instance and resets it
state.chip8 = new Chip8Neo();
state.chip8.reset_hard_ws();
state.chip8.load_rom_ws(data);
// runs the sequence as an infinite loop, running
// the associated CPU cycles accordingly
if (state.paused) {
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
continue;
}
let currentTime = new Date().getTime();
// in case the time to draw the next frame has been
// reached the flush of the logic and visuals is done
const ratioLogic = state.logicFrequency / state.visualFrequency;
for (let i = 0; i < ratioLogic; i++) {
state.chip8.clock_ws();
}
const ratioTimer = state.timerFrequency / state.visualFrequency;
for (let i = 0; i < ratioTimer; i++) {
state.chip8.clock_dt_ws();
state.chip8.clock_st_ws();
}
// updates the canvas object with the new
// visual information coming in
updateCanvas(state.chip8.vram_ws());
if (state.frameCount === state.visualFrequency * 3) {
const currentTime = new Date().getTime();
const deltaTime = (currentTime - state.frameStart) / 1000;
const fps = parseInt(Math.round(state.frameCount / deltaTime));
setFps(fps);
state.frameCount = 0;
state.frameStart = new Date().getTime();
}
// increments the number of frames rendered in the current
// section, this value is going to be used to calculate FPS
state.frameCount += 1;
// updates the next update time reference to the, so that it
// can be used to control the game loop
state.nextTickTime = Math.max(
state.nextTickTime + 1000 / state.visualFrequency,
currentTime
);
// 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
setTimeout(resolve, pendingTime);
const register = () => {
registerDrop();
registerKeys();
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];
showToast("This is probably not a CHIP-8 ROM file!", true);
const arrayBuffer = await file.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
state.chip8.reset_hard_ws();
state.chip8.load_rom_ws(data);
showToast(`Loaded ${file.name} ROM successfully!`);
document.addEventListener("dragover", async (event) => {
if (!event.dataTransfer.items || event.dataTransfer.items[0].type) return;
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];
state.chip8.key_press_ws(keyCode);
return;
}
setLogicFrequency(state.logicFrequency + FREQUENCY_DELTA);
setLogicFrequency(state.logicFrequency - FREQUENCY_DELTA);
case "Escape":
const chipCanvas = document.getElementById("chip-canvas");
chipCanvas.classList.remove("fullscreen");
break;
}
});
document.addEventListener("keyup", (event) => {
const keyCode = KEYS[event.key];
state.chip8.key_lift_ws(keyCode);
return;
}
});
const registerButtons = () => {
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");
const buttonBenchmark = document.getElementById("button-benchmark");
buttonBenchmark.addEventListener("click", () => {
buttonBenchmark.classList.add("enabled");
pause();
try {
const initial = Date.now();
const count = 500000000;
state.chip8.clock_ws();
}
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", () => {
const chipCanvas = document.getElementById("chip-canvas");
chipCanvas.classList.add("fullscreen");
});
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];
document.body.style.backgroundColor = `#${background}`;
document.getElementById("footer").style.backgroundColor = `#${background}`;
});
};
const registerToast = () => {
const toast = document.getElementById("toast");
toast.addEventListener("click", (event) => {
toast.classList.remove("visible");
});
};
const showToast = async (message, 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 setRom = (name, size) => {
state.romName = name;
state.romSize = size;
document.getElementById("rom-name").textContent = name;
document.getElementById("rom-size").textContent = String(size);
if (value < 0) showToast("Invalid frequency value!", true);
document.getElementById("logic-frequency").textContent = value;
const setFps = (value) => {
if (value < 0) showToast("Invalid FPS value!", true);
value = Math.max(value, 0);
state.fps = value;
document.getElementById("fps-count").textContent = value;
};
const toggleRunning = () => {
const buttonPause = document.getElementById("button-pause");
if (buttonPause.textContent === "Resume") {
resume();
} else {
pause();
}
};
const pause = () => {
state.paused = true;
const buttonPause = document.getElementById("button-pause");
buttonPause.textContent = "Resume";
}
const resume = () => {
state.paused = false;
const buttonPause = document.getElementById("button-pause");
const initCanvas = () => {
// initializes the off-screen canvas that is going to be
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("chip-canvas");
state.canvasScaledCtx = state.canvasScaled.getContext("2d");
state.canvasScaledCtx.scale(state.canvasScaled.width / state.canvas.width, state.canvasScaled.height / state.canvas.height);
state.canvasScaledCtx.imageSmoothingEnabled = false;
state.image = state.canvasCtx.createImageData(state.canvas.width, state.canvas.height);
state.videoBuff = new DataView(state.image.data.buffer);
const updateCanvas = (pixels) => {
for (let i = 0; i < pixels.length; i++) {
state.videoBuff.setUint32(i * 4, pixels[i] ? PIXEL_SET_COLOR : PIXEL_UNSET_COLOR);
}
state.canvasCtx.putImageData(state.image, 0, 0);
state.canvasScaledCtx.drawImage(state.canvas, 0, 0);