Skip to content

Commit

Permalink
Add config panel (#5)
Browse files Browse the repository at this point in the history
* Enable configurable instructions per frame

* Enable themes

* Enable rom switching
  • Loading branch information
belen-albeza authored Jul 27, 2024
1 parent 9254a7a commit f05bbbf
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 15 deletions.
14 changes: 14 additions & 0 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ <h1>🤖 CHIP-8</h1>
<select id="chip8-rom-selector"></select>
</label>
</p>
<details open>
<summary>Config</summary>
<p>
<label>Instructions per frame
<input type="number" min="1" max="30" value="16" id="chip8-ipf-selector"/>
</label>
<small>(<span id="chip8-config-ips">0</span> instructions/second)</small>
</p>
<p>
<label>Theme
<select id="chip8-config-theme-selector"></select>
</label>
</p>
</details>
</footer>
</main>
<footer>
Expand Down
102 changes: 95 additions & 7 deletions app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,60 @@ import wasmInit, { loadRom, Emu } from "chip8";

const ROMS = [{ name: "poker.ch8", url: "/roms/poker.ch8" }];
const DISPLAY_LEN = 64 * 32;
const THEMES = [
{
name: "Noire Truth",
off: "#1e1c32",
on: "#c6baac",
},
{
name: "1-bit Monitor Glow",
off: "#222323",
on: "#f0f6f0",
},
{
name: "Paperback-2",
off: "#382b26",
on: "#b8c2b9",
},
{
name: "Gato Roboto Goop",
off: "#210009",
on: "#00ffae",
},
{
name: "Y's Postapocalyptic Sunset",
off: "#1d0f44",
on: "#f44e38",
},
];

let animationFrameRequestId: number;

const config = {
cyclesPerFrame: 12,
theme: THEMES[0],
};

main();

async function main() {
const wasm = await wasmInit();

setupRomSelector(ROMS);
startEmulatorWithRom(ROMS[0].url);
}

async function startEmulatorWithRom(romUrl: string) {
if (animationFrameRequestId) {
cancelAnimationFrame(animationFrameRequestId);
}

const emu = await loadRomInEmu(ROMS[0].url);
const wasm = await wasmInit();
const emu = await loadRomInEmu(romUrl);

setupConfigPanel(emu);
emu.setTheme(config.theme.off, config.theme.on);
const sharedBuffer = new Uint8Array(wasm.memory.buffer);

const canvas = document.querySelector<HTMLCanvasElement>("#chip8-canvas");
const ctx = canvas?.getContext("2d");
if (!ctx || !canvas) {
Expand All @@ -21,9 +64,9 @@ async function main() {
const imageData = ctx.createImageData(canvas.width, canvas.height);

const updateCanvas = () => {
let shallHalt = emu.run(16);
let shallHalt = emu.run(config.cyclesPerFrame);

const outputPointer = Emu.display_buffer();
const outputPointer = emu.displayBuffer();
const bufferData = sharedBuffer.slice(
outputPointer,
outputPointer + 4 * DISPLAY_LEN
Expand All @@ -34,15 +77,17 @@ async function main() {
if (shallHalt) {
console.debug("Chip-8 VM halted");
} else {
requestAnimationFrame(updateCanvas);
animationFrameRequestId = requestAnimationFrame(updateCanvas);
}
};

updateCanvas();
}

function setupRomSelector(roms: { name: string; url: string }[]) {
const selectEl = document.querySelector("#chip8-rom-selector");
const selectEl = document.querySelector<HTMLSelectElement>(
"#chip8-rom-selector"
);
for (const { name, url } of roms) {
const option = new Option();
option.value = url;
Expand All @@ -51,6 +96,49 @@ function setupRomSelector(roms: { name: string; url: string }[]) {

selectEl?.appendChild(option);
}

selectEl?.addEventListener("change", () => {
let url = selectEl.value;
startEmulatorWithRom(url);
});
}

function setupConfigPanel(emu: Emu) {
const cyclesInput = document.querySelector(
"#chip8-ipf-selector"
) as HTMLInputElement;

cyclesInput.value = config.cyclesPerFrame.toString();
cyclesInput.addEventListener("change", () => {
const cycles = parseInt(cyclesInput.value);
config.cyclesPerFrame = cycles;
updateCyclesPerSecond();
});

const updateCyclesPerSecond = () => {
const cyclesPerSecond = document.querySelector(
"#chip8-config-ips"
) as HTMLElement;
const count = config.cyclesPerFrame * 60;
cyclesPerSecond.textContent = count.toString();
};

updateCyclesPerSecond();

const themeSelect = document.querySelector<HTMLSelectElement>(
"#chip8-config-theme-selector"
);
for (const [index, { name }] of THEMES.entries()) {
const option = new Option();
option.value = index.toString();
option.innerText = name;
themeSelect?.appendChild(option);
}
themeSelect?.addEventListener("change", () => {
const idx = parseInt(themeSelect.value);
config.theme = THEMES[idx];
emu.setTheme(config.theme.off, config.theme.on);
});
}

async function fetchRom(url: string) {
Expand Down
1 change: 1 addition & 0 deletions chip8/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ wasm-bindgen = "0.2.84"
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
regex = "1.10.5"

[dev-dependencies]
wasm-bindgen-test = "0.3.34"
Expand Down
1 change: 1 addition & 0 deletions chip8/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub use crate::vm::VmError;
pub enum Error {
VmError(VmError),
InvalidRom,
InvalidTheme,
}

impl fmt::Display for Error {
Expand Down
70 changes: 62 additions & 8 deletions chip8/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod error;
mod utils;
mod vm;

use regex::RegexBuilder;
use wasm_bindgen::prelude::*;

use vm::{Vm, DISPLAY_LEN};
Expand All @@ -11,24 +12,45 @@ static mut OUTPUT_BUFFER: [u8; 4 * DISPLAY_LEN] = [0; 4 * DISPLAY_LEN];
pub use error::{Error, VmError};
pub type Result<T> = core::result::Result<T, Error>;

#[derive(Debug, Clone, Copy, PartialEq)]
struct Theme {
off_color: (u8, u8, u8),
on_color: (u8, u8, u8),
}

impl Default for Theme {
fn default() -> Self {
Self {
off_color: (0x00, 0x00, 0x00),
on_color: (0xff, 0xff, 0xff),
}
}
}

#[wasm_bindgen]
#[derive(Debug, PartialEq)]
pub struct Emu {
vm: Vm,
theme: Theme,
}

#[wasm_bindgen]
impl Emu {
#[wasm_bindgen(constructor)]
pub fn new(rom: &[u8]) -> Self {
Self { vm: Vm::new(rom) }
Self {
vm: Vm::new(rom),
theme: Theme::default(),
}
}

#[wasm_bindgen]
pub fn run(&mut self, cycles: usize) -> Result<bool> {
let mut shall_halt = false;

for _ in 0..cycles {
let res = self.vm.tick();
let shall_halt = match res {
shall_halt = match res {
Ok(x) => x,
Err(VmError::InvalidOpcode(_)) => true,
Err(err) => return Err(Error::from(err)),
Expand All @@ -41,11 +63,21 @@ impl Emu {
}
}

Ok(true)
Ok(shall_halt)
}

#[wasm_bindgen]
pub fn display_buffer() -> *const u8 {
#[wasm_bindgen(js_name=setTheme)]
pub fn set_theme(&mut self, off_color: &str, on_color: &str) -> Result<()> {
self.theme = Theme {
off_color: parse_hex_color(off_color)?,
on_color: parse_hex_color(on_color)?,
};

Ok(())
}

#[wasm_bindgen(js_name=displayBuffer)]
pub fn display_buffer(&self) -> *const u8 {
let pointer: *const u8;
unsafe {
pointer = OUTPUT_BUFFER.as_ptr();
Expand All @@ -56,9 +88,11 @@ impl Emu {

fn update_display_buffer(&self) {
for (i, pixel) in self.vm.display.iter().enumerate() {
let r = if *pixel { 0xff } else { 0x00 };
let g = if *pixel { 0xff } else { 0x00 };
let b = if *pixel { 0xff } else { 0x00 };
let (r, g, b) = if *pixel {
self.theme.on_color
} else {
self.theme.off_color
};

unsafe {
OUTPUT_BUFFER[i * 4 + 0] = r;
Expand All @@ -70,6 +104,21 @@ impl Emu {
}
}

fn parse_hex_color(hex: &str) -> Result<(u8, u8, u8)> {
let re = RegexBuilder::new(r"#(?<r>[0-9a-f]{2})(?<g>[0-9a-f]{2})(?<b>[0-9a-f]{2})")
.case_insensitive(true)
.build()
.unwrap();
if let Some(caps) = re.captures(hex) {
let r = u8::from_str_radix(&caps["r"], 16).map_err(|_| Error::InvalidTheme)?;
let g = u8::from_str_radix(&caps["g"], 16).map_err(|_| Error::InvalidTheme)?;
let b = u8::from_str_radix(&caps["b"], 16).map_err(|_| Error::InvalidTheme)?;
Ok((r, g, b))
} else {
Err(Error::InvalidTheme)
}
}

#[wasm_bindgen(js_name=loadRom)]
pub fn load_rom(rom: &[u8]) -> Result<Emu> {
utils::set_panic_hook();
Expand All @@ -91,4 +140,9 @@ mod tests {
let res = load_rom(&rom);
assert_eq!(res, Err(Error::InvalidRom));
}

#[test]
fn parses_color_from_hex() {
assert_eq!(parse_hex_color("#faBAda"), Ok((0xfa, 0xba, 0xda)));
}
}

0 comments on commit f05bbbf

Please sign in to comment.