Skip to content

krk/chipsekiz

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

chipsekiz - CHIP-8 emulator

A CHIP-8 interpreter and emulator in java.

chipsekiz logo

Quick Start

This repository contains two different projects, the chipsekiz interpreter and the emulator implemented in Java.

To build them both and run the CHIP-8 emulator:

mvn clean install
java --enable-preview -jar emulator-awt/target/emulator-awt-1.0-SNAPSHOT-jar-with-dependencies.jar

To emulate SuperCHIP-8:

Select SuperCHIP-8 from the VM menu or start the emulator with:

java --enable-preview -jar emulator-awt/target/emulator-awt-1.0-SNAPSHOT-jar-with-dependencies.jar --superchip8

Emulator will launch the chipsekiz demo ROM with the memory debug view, you can load other ROMs from the menu.

Java 14 is required.

Testing

To run all unit tests use:

mvn test

Among other tests, InterpreterTest#testRunRoms executes included ROMs and prints their framebuffer contents to console.

Random selection of test ROM renders from chipsekiz/docs/rendered:

Chip8 emulator Logo Space Invaders Blitz Cave

Sequence Shoot Space Flight Fishie Lunar Lander

Particles Reversi Tron Tapeworm

Tic-Tac-Toe VBrix Zero Pong Breakout Brix hack


Some classical ROMs and some of the newer ROMs from Octo archive @ https://johnearnest.github.io/chip8Archive/ are included in the repository.

chipwar glitchGhost octojam6title RPS

Architecture

The interpreter consists of a loader, a decoder, an executor and a HAL (hardware abstraction layer) working in unison:

architectural data flow

Loader creates a memory image from multiple layout sections which includes the program section and may include a character sprites section. The created memory image is fed into a new VM, inside the interpreter instance. Interpreter interacts with the HAL to get the keyboard status, play a sound and to draw to the screen.

The emulator speed-limits the Interpreter#tick invocations to match the CPU speed expected by the ROMs and provides a custom HAL to the interpreter which display a JFrame.

Implementation

Loader

Loader builds a memory image from zero or more sections and the program (ROM) itself. Sections have a start address and can include data or code, e.g. character sprites are a data section. Overlapping sections are not allowed.

Decode

Decoder reads a 2-byte word and according to a specific ISA, currently only CHIP-8 Wikipedia description, decodes bytes to an Opcode instance or if it is not possible to a DataWord instance.

Executor

Executor sits between the Interpreter, VM and the Decoder. Interpreter would send successfully decoded opcodes to the Executor. When HAL access is required, e.g. to check the pressed key, Executor invokes IHal methods. After executor returns, Interpreter is free to fetch and decode the next instruction.

VM

VM contains the memory, registers, callstack and sound and delay timers. VM does not contain any execution logic, only provides the data model of the virtual machine.

HAL

HAL encapsulates each type of interaction that requires hardware communication, such as drawing to a screen, playing sound and detecting keyboard state. FramebufferHal implementation makes the interpreter testable and is used from InterpreterTest#testRunRoms test, providing an in-memory Hardware Abstraction Layer.

Interpreter

Interpreter orchestrates all of the interaction between the Loader, VM, Decoder, Executor and HAL. First it loads the program and creates the VM with the program and an origin. At every Interpreter#tick, it would run a fetch-decode-execute cycle, decrease the sound and delay timers and update the sound state.

Interpreter has a primitive halt detector. CHIP-8 does not have a halt instruction so when the ROM programmer wants to finish the program and keep what is on the screen, the program would enter an infinite loop.

Entering an infinite loop with a single instruction effectively preserves the screen content and disables the keyboard and sound (after its timer runs out). Halt detector only updates the interpreter status to BLOCKED for awaiting keyboard input or HALTED if an instruction jumps to itself, i.e. simplest infinite loop. An infinite loop with more than one instruction is not detected. Halt detector and interpreter status is used in unit tests when interpreting the ROMs.

To execute an instruction, Interpreter fetches an instruction from the memory pointed by the PC (program counter) register, and PC would be incremented by 2, which is the instruction width for this architecture, same for all opcodes. This 2-byte fetched instruction would then be decoded. If it is not a valid opcode for a given IDecoder, Interpreter would throw a cannot execute data exception, otherwise it would be executed with the help of IExecutor and IHal.

If there is an ITracer instance in the Interpreter, all executed opcodes would be reported to it.

Optional IDebugger instance receives notifications of memory and register changes.

InterpreterTest executes included ROMs via Interpreter until they halt or until they ran for 600 (arbitrary) instruction cycles, whichever is first.

SuperCHIP-8

SuperCHIP-8 components live in the dev.krk.chipsekiz.superchip package. It has a decoder, a hal, an interpreter and a vm, all of which are extended from CHIP-8 components.

Using the library

To create a default instance of the Interpreter:

// 1NNN instruction, used as 0x1200 which jumps to 0x200, passed as origin to the interpreter below, causing an infinite loop.
byte[] program = {0x12, 0x00};
FramebufferHal hal = new FramebufferHal(64, 32);
Interpreter interpreter =
            new Interpreter(loader, decoder, executor, fbhal, Optional.empty(), 0x200, program,
                0x1000, CharacterSprites.DefaultLayout());

for(; /* emulator loop */ ;) {
    // 1. Forward hardware state to Hal, such as the pressed key.
    // 2. Tick the interpreter.
    // 3. Update the screen by rendering (FramebufferHal#renderFramebuffer) or directly from your own IHal implementation.
    // 4. Switch sound on or off by getting FramebufferHal#isSoundActive or in response to IHal#sound.
}

Using the ChipVariationFactory with preset options:

FramebufferHal hal = new FramebufferHal(64, 32);
IChipVariation variation = ChipVariationFactory.createChip8(hal);
IInterpreter interpreter = variation.getInterpreter();

// 1NNN instruction, used as 0x1200 which jumps to 0x200, passed as origin to the interpreter below, causing an infinite loop.
byte[] program = {0x12, 0x00};
interpreter.load(0x200, program)

/* emulator loop */
{
    interpreter.tick();
}

__

Further direction

  • Add VM save and load snapshot feature - save game support for all kinds of programs.
  • Implement other variations of CHIP-8, i.e. CHIP-48, hires, Super CHIP-8 etc.
  • Implement single-step debugger
  • Support loading Octo "cartridges" from animated gif files.

References