1
0
Fork 0
mirror of https://github.com/ethauvin/chip-8.git synced 2025-04-24 08:27:10 -07:00

First commit

This commit is contained in:
Cedric Beust 2020-08-22 11:59:50 -07:00
commit ee139eba9b
55 changed files with 1728 additions and 0 deletions

83
README.md Normal file
View file

@ -0,0 +1,83 @@
# chip8
## A [Chip-8](http://www.cs.columbia.edu/~sedwards/classes/2016/4840-spring/designs/Chip8.pdf) emulator written in Kotlin
<p align="center">
<img src="https://github.com/cbeust/chip8/blob/master/pics/space-invaders-2.gif?raw=true"/>
</p>
# How to run
```
$ ./gradlew run
```
The emulator will load with Space Invaders by default (press 5 to start the game, then 4/5/6 to move around and shoot). Open a new rom by clicking on the "Open rom..." button.
You can pause the emulator at any time (key '`p`'), which will update the disassembly window to show the next instructions about to be executed. You can also adjust the clock speed to make the emulator go slower or faster.
# Architecture
The game creates a [`Computer`](https://github.com/cbeust/chip8/blob/master/src/main/kotlin/com/beust/chip8/Computer.kt) object which is made of a `Display`, `Keyboard`, `FrameBuffer` and `Cpu`.
## Cpu
The CPU reads a new instruction (the next two bytes extracted at the program counter location) at a fixed rate
which defines the clock speed. Two timers are needed: one for the CPU and one for the device timer register,
called `DT`, which needs to tick at 60 Hz according to [the spec](http://www.cs.columbia.edu/~sedwards/classes/2016/4840-spring/designs/Chip8.pdf). Since there is no specific definition for the CPU clock, I used the timing diagram from the document
to set it at around 500Hz:
```kotlin
// CPU clock: around 500 Hz by default
cpuFuture = executor.scheduleAtFixedRate(cpuTick, 0, 1_000_000L / cpuClockHz, TimeUnit.MICROSECONDS)
// Delay Timer: 60 Hz by spec
timerFuture = executor.scheduleAtFixedRate(timerTick, 0, 16L, TimeUnit.MILLISECONDS)
```
<p align="center">
<img width="50%" src="https://github.com/cbeust/chip8/blob/master/pics/tetris-1.png?raw=true"/>
</p>
The next two bytes are then masked and turned into instructions. All the op codes can be found in the `[Ops.kt](https://github.com/cbeust/chip8/blob/master/src/main/kotlin/com/beust/chip8/Ops.kt)` file. Here is an example:
```kotlin
/**
* 7xkk
* Set Vx = Vx + kk
*/
class Add(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.V[x] = unsigned(cpu.V[x] + kk) }
override fun toString() = "ADD V$x, $kk"
}
```
## Display
The `Display` is a simple interface which allows multiple strategies to render the frame buffer:
```kotlin
interface Display {
val pane: Pane
fun draw(frameBuffer: IntArray)
fun clear(frameBuffer: IntArray)
}
```
For example, here is a text based renderer:
<p align="center">
<img width="50%" src="https://github.com/cbeust/chip8/blob/master/pics/space-invaders-text.png?raw=true"/>
</p>
The emulator window will resize gracefully:
<p align="center">
<img width="50%" src="https://github.com/cbeust/chip8/blob/master/pics/space-invaders-small.png?raw=true"/>
</p>
You can also easily alter other aspects of the renderer:
<p align="center">
<img width="50%" src="https://github.com/cbeust/chip8/blob/master/pics/space-invaders-colors.png?raw=true"/>
</p>

89
build.gradle.kts Normal file
View file

@ -0,0 +1,89 @@
@file:Suppress("MayBeConstant")
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
buildscript {
repositories {
jcenter()
mavenCentral()
maven { setUrl("https://plugins.gradle.org/m2") }
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72")
}
}
plugins {
java
id("org.jetbrains.kotlin.jvm") version "1.3.72"
application
id("com.github.johnrengelman.shadow") version "5.2.0"
id("org.openjfx.javafxplugin") version "0.0.8"
}
repositories {
jcenter()
mavenCentral()
maven { setUrl("https://plugins.gradle.org/m2") }
}
object This {
val version = "0.1"
val groupId = "com.beust"
val artifactId = "chip8"
val description = "A CHIP8 emulator"
val url = "https://github.com/cbeust/chip8"
val scm = "github.com/cbeust/chip8.git"
// Should not need to change anything below
val issueManagementUrl = "https://$scm/issues"
val isSnapshot = version.contains("SNAPSHOT")
}
allprojects {
group = This.groupId
version = This.version
}
dependencies {
listOf(kotlin("stdlib"), "com.beust:jcommander:1.72").forEach {
implementation(it)
}
listOf(kotlin("test"), "org.testng:testng:7.0.0", "org.assertj:assertj-core:3.10.0").forEach {
testImplementation(it)
}
}
val test by tasks.getting(Test::class) {
useTestNG()
}
application {
mainClassName = "com.beust.chip8.MainKt"
}
tasks {
named<ShadowJar>("shadowJar") {
archiveBaseName.set(This.artifactId)
mergeServiceFiles()
// excludes = listOf("META-INF/*.DSA", "META-INF/*.RSA", "META-INF/*.SF")
manifest {
attributes(mapOf(
"Implementation-Title" to rootProject.name,
"Implementation-Version" to rootProject.version,
"Implementation-Vendor-Id" to rootProject.group,
// attributes "Build-Time": ZonedDateTime.now(ZoneId.of("UTC"))
// .format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
// "Built-By" to java.net.InetAddress.localHost.hostName,
"Created-By" to "Gradle "+ gradle.gradleVersion,
"Main-Class" to "com.beust.cedlinks.MainKt"))
}
}
}
javafx {
version = "14"
modules = listOf("javafx.controls", "javafx.fxml")
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip

172
gradlew vendored Normal file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

0
pics/breakout-1.png Normal file
View file

BIN
pics/breakout-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
pics/space-invaders-1.mp4 Normal file

Binary file not shown.

BIN
pics/space-invaders-2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

BIN
pics/space-invaders-2.mp4 Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
pics/tetris-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Binary file not shown.

BIN
roms/Chip8 Picture.ch8 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
roms/IBM Logo.ch8 Normal file

Binary file not shown.

BIN
roms/InstructionTest.ch8 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,12 @@
Maze, by David Winter
Drawing a random maze like this one consists in drawing random diagonal
lines. There are two possibilities: right-to-left line, and left-to-right
line. Each line is composed of a 4*4 bitmap. As the lines must form non-
circular angles, the two bitmaps won't be '/' and '\'. The first one
(right line) will be a little bit modified. See at the end of this source.
The maze is composed of 16 lines (as the bitmaps are 4 pixels high), each
line consists of 32 bitmaps.
Bitmaps are drawn in random mode. We choose a random value (0 or 1).
If it is 1, we draw a left line bitmap. If it is 0, we draw a right one.

Binary file not shown.

View file

@ -0,0 +1,12 @@
Maze, by David Winter
Drawing a random maze like this one consists in drawing random diagonal
lines. There are two possibilities: right-to-left line, and left-to-right
line. Each line is composed of a 4*4 bitmap. As the lines must form non-
circular angles, the two bitmaps won't be '/' and '\'. The first one
(right line) will be a little bit modified. See at the end of this source.
The maze is composed of 16 lines (as the bitmaps are 4 pixels high), each
line consists of 32 bitmaps.
Bitmaps are drawn in random mode. We choose a random value (0 or 1).
If it is 1, we draw a left line bitmap. If it is 0, we draw a right one.

Binary file not shown.

View file

@ -0,0 +1,6 @@
This is my particledemo for the Chip-8, SuperChip and MegaChip8.
Works on real hardware as well as emulators
Enjoy!
zeroZshadow

BIN
roms/Pong (alt).ch8 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,49 @@
-----------------------------------------------------------------------------
/////////////////
//////////////////
//// ////
//// ///////////
//// ///////////
//// ////
//// ///////////
//// //////////
www.revival-studios.com
-----------------------------------------------------------------------------
Title : Trip8 / SuperTrip8 demo
Author : Martijn Wenting / Revival Studios
Genre : Demo
System : Chip-8 / SuperChip8
Date : 14/10/2008
Product ID : RS-C8004
-----------------------------------------------------------------------------
All the contents of this package are (c)Copyright 2008 Revival Studios.
The contents of the package may only be spread in its original form, and may not be
published or distributed otherwise without the written permission of the authors.
Description:
------------
The Trip8/SuperTrip8 demo are demo's for the Chip-8 and SuperChip8 systems. The demo consists of an intro, 3D vectorballs, and 4 randomized dot-effects.
Writing a demo for the original Chip-8 interpreter was a real pain, since your framerate basically drops in half for every sprite you need to draw. So even clearing and redrawing a few dots will cause the framerate to drop to near zero :) Originally the demo was going to be bigger and there were much more graphical effects programmed/prototyped, but a lot of these effects turned out to be too much for the original unoptimized Chip-8 interpreters to handle.
Running the Demo:
-----------------
Use the Megachip emulator or any other Chip-8/SuperChip compatible emulator to run the slideshow.
Credits:
--------
Programming/Graphics/Design by: Martijn Wenting
Distribution:
-------------
This package can be freely distributed in its original form.
If you would like to include this slideshow in your rom package, please let me know.
Watch out for more releases soon!
Martijn Wenting / Revival Studios

View file

@ -0,0 +1,11 @@
`
ef
ghabcd`
¢xÐVp
¢~Ðfp
¢„Ðvp
¢ŠÐ†jú`
¢xÐVEaÿEaÐVp
¢~ÐfFbÿFb†$Ðfp
¢„ÐvGcÿGc‡4Ðvp
¢ŠÐ†HdÿHdˆDІ*ÿ 0ÀÿÿÀÀüÀÿðÌÌðÌÃ<ÃÃÃÃ<

View file

@ -0,0 +1,3 @@
This is my first program for the CHIP-8, a simple demo with 4 bouncing sprites.
Enjoy!

BIN
roms/bc_test.ch8 Normal file

Binary file not shown.

BIN
roms/blinky.ch8 Normal file

Binary file not shown.

1
roms/jason.ch8 Normal file
View file

@ -0,0 +1 @@
`<18>ao<61>

View file

@ -0,0 +1,133 @@
package com.beust.chip8
import java.io.File
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import javax.sound.sampled.AudioSystem
import javax.sound.sampled.Clip
/**
* For clients that want to be notified when something happens on the computer.
*/
interface ComputerListener {
fun onKey(key: Int?)
fun onPause() {}
fun onStart() {}
}
class Computer(val display: Display = DisplayGraphics(),
val keyboard: Keyboard = Keyboard(),
val frameBuffer: FrameBuffer = FrameBuffer(),
var cpu: Cpu = Cpu(),
val sound: Boolean = true)
{
var paused = true
var cpuClockHz: Long = 500
set(v) {
println("New clock speed: $v")
field = v
pause()
start()
}
private val executor = Executors.newSingleThreadScheduledExecutor()
private var cpuFuture: ScheduledFuture<*>? = null
private var timerFuture: ScheduledFuture<*>? = null
private var romFile: File? = null
private val soundInputStream by lazy {
AudioSystem.getAudioInputStream(
this::class.java.classLoader.getResource("sound.wav"))
}
private val clip by lazy { AudioSystem.getClip().apply {
open(soundInputStream)
}}
var listener: ComputerListener? = null
private fun unsigned(b: Byte): Int = if (b < 0) b + 0x10 else b.toInt()
fun loadRom(romFile: File, launchTimers: Boolean = true) {
this.romFile = romFile
resetCpu()
if (launchTimers) {
start()
}
}
private fun resetCpu() {
cpu = Cpu()
romFile?.let {
cpu.loadRom(it.readBytes())
}
}
fun stop() {
pause()
clip.stop()
display.clear(frameBuffer.frameBuffer)
resetCpu()
}
fun pause() {
listener?.onPause()
paused = true
cpuFuture?.cancel(true)
timerFuture?.cancel(true)
}
fun start() {
listener?.onStart()
paused = false
launchTimers()
}
private fun nextInstruction(pc: Int = cpu.PC) : Instruction {
fun extract(pc: Int): Pair<Int, Int> {
val b = cpu.memory[pc]
val b0 = unsigned(b.toInt().shr(4).toByte())
val b1 = unsigned(b.toInt().and(0xf).toByte())
return Pair(b0, b1)
}
val (b0, b1) = extract(pc)
val (b2, b3) = extract(pc + 1)
return Instruction(this@Computer, b0, b1, b2, b3)
}
private fun launchTimers() {
val cpuTick = Runnable {
nextInstruction().run()
}
val timerTick = Runnable {
if (cpu.DT > 0) {
cpu.DT--
}
if (cpu.ST > 0) {
if (sound && ! clip.isActive) {
clip.loop(Clip.LOOP_CONTINUOUSLY)
}
cpu.ST--
} else {
clip.stop()
}
}
cpuFuture = executor.scheduleAtFixedRate(cpuTick, 0, 1_000_000L / cpuClockHz, TimeUnit.MICROSECONDS)
timerFuture = executor.scheduleAtFixedRate(timerTick, 0, 16L, TimeUnit.MILLISECONDS)
}
data class AssemblyLine(val counter: Int, val byte0: Byte, val byte1: Byte, val name: String)
fun disassemble(p: Int = cpu.PC): List<AssemblyLine> {
var pc = p
val result = arrayListOf<AssemblyLine>()
repeat(30) {
val inst = nextInstruction(pc)
result.add(AssemblyLine(pc, cpu.memory[pc], cpu.memory[pc + 1], inst.toString()))
pc += 2
}
return result
}
}

View file

@ -0,0 +1,66 @@
package com.beust.chip8
import java.util.*
@Suppress("PropertyName")
class Cpu {
private val PC_START = 0x200
/** Program counter */
var PC: Int = PC_START
/** V registers */
val V: IntArray = IntArray(16)
/** I register */
var I: Int = 0
/** Delay timer */
var DT: Int = 0
/** Sound timer */
var ST: Int = 0
/** Stack pointer */
val SP = Stack<Int>()
val memory = ByteArray(4096)
/** The sprites are in the ROM at address 0x000 - 0x080 */
private val FONT_SPRITES = arrayOf(
0xf0, 0x90, 0x90, 0x90, 0xf0, // 8
0x20, 0x60, 0x20, 0x20, 0x70, // 9
0xf0, 0x10, 0xf0, 0x80, 0xf0, // 0
0xf0, 0x10, 0xf0, 0x10, 0xf0, // 1
0x90, 0x90, 0xf0, 0x10, 0x10, // A
0xf0, 0x80, 0xf0, 0x10, 0xf0, // B
0xf0, 0x80, 0xf0, 0x90, 0xf0, // 2
0xf0, 0x10, 0x20, 0x40, 0x40, // 3
0xf0, 0x90, 0xf0, 0x90, 0xf0, // C
0xf0, 0x90, 0xf0, 0x10, 0xf0, // D
0xf0, 0x90, 0xf0, 0x90, 0x90, // 4
0xe0, 0x90, 0xe0, 0x90, 0xe0, // 5
0xf0, 0x80, 0x80, 0x80, 0xf0, // E
0xe0, 0x90, 0x90, 0x90, 0xe0, // F
0xf0, 0x80, 0xf0, 0x80, 0xf0, // 6
0xf0, 0x80, 0xf0, 0x80, 0x80 // 7
)
init {
// Move the sprites at adress 0x000
FONT_SPRITES.forEachIndexed { index, v ->
memory[index] = v.toByte()
}
}
fun loadRom(romBytes: ByteArray = ByteArray(4096)) {
// Load the rom at 0x200
romBytes.copyInto(memory, 0x200)
}
override fun toString(): String {
val vs = V.filter { it != 0 }.mapIndexed { ind, v -> "v$ind=$v" }
val sp = SP.map { it.h }
return "{Cpu pc=${Integer.toHexString(PC)} i=${I.h} dt=${DT.h} st=${ST.h} sp=${sp} $vs}"
}
}

View file

@ -0,0 +1,119 @@
@file:Suppress("unused")
package com.beust.chip8
import javafx.beans.Observable
import javafx.scene.canvas.Canvas
import javafx.scene.control.TextArea
import javafx.scene.layout.Pane
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
/**
* Interface for rendering on a display.
*/
interface Display {
val pane: Pane
fun draw(frameBuffer: IntArray)
fun clear(frameBuffer: IntArray)
companion object {
const val WIDTH: Int = 64
const val HEIGHT: Int = 32
}
fun index(x: Int, y: Int) = x + WIDTH * y
}
/**
* Display the frame buffer on a TextArea.
*/
class DisplayText: Display, Pane() {
override val pane = this
override fun draw(frameBuffer: IntArray) {
val t = StringBuffer()
repeat(32) { y ->
repeat(64) { x ->
t.append(if (frameBuffer[index(x, y)] == 0) " " else "*")
}
t.append("\n")
}
textArea.text = t.toString()
}
override fun clear(frameBuffer: IntArray) {
textArea.clear()
}
private val textArea = TextArea()
init {
prefWidth = 800.0
prefHeight = 600.0
textArea.prefWidth = prefWidth
textArea.prefHeight = prefHeight
VBox.setVgrow(textArea, Priority.ALWAYS)
children.add(textArea)
}
}
/**
* Display the frame buffer on a canvas.
*/
class DisplayGraphics : Display, Pane() {
private var blockWidth = 12
private var blockHeight = 12
private val SPACE = 0
private val SCREEN = IntArray(Display.WIDTH * Display.HEIGHT)
private val canvas = Canvas((Display.WIDTH + SPACE) * blockWidth.toDouble(),
(Display.HEIGHT + SPACE) * blockHeight.toDouble())
init {
widthProperty().addListener { e: Observable? ->
canvas.width = width
blockWidth = (canvas.width / (Display.WIDTH + SPACE)).toInt()
}
heightProperty().addListener { e: Observable? ->
canvas.height = height
blockHeight = (canvas.height / (Display.HEIGHT + SPACE)).toInt()
}
children.add(canvas)
VBox.setVgrow(this, Priority.ALWAYS)
}
override val pane = this
override fun clear(frameBuffer: IntArray) {
frameBuffer.fill(0)
}
override fun draw(frameBuffer: IntArray) {
frameBuffer.copyInto(SCREEN, 0)
requestLayout()
}
@Override
override fun layoutChildren() {
// Clear the whole canvas
val g = canvas.graphicsContext2D
g.fill = Color.WHITE
g.fillRect(0.0, 0.0, (blockWidth + SPACE) * Display.WIDTH.toDouble(),
(blockHeight + SPACE) * Display.HEIGHT.toDouble())
// Only draw the black blocks
g.fill = Color.BLACK
repeat(Display.WIDTH) { x ->
repeat(Display.HEIGHT) { y ->
if (SCREEN[index(x, y)] == 1) {
val xx = (blockWidth + SPACE) * x.toDouble()
val yy = (blockHeight + SPACE) * y.toDouble()
g.fillRect(xx, yy, blockWidth.toDouble(), blockHeight.toDouble())
}
}
}
}
}

View file

@ -0,0 +1,34 @@
package com.beust.chip8
@Suppress("PrivatePropertyName")
class FrameBuffer {
companion object {
const val WIDTH = 64
const val HEIGHT = 32
}
val frameBuffer = IntArray(WIDTH * HEIGHT)
fun pixel(x: Int, y: Int): Int = frameBuffer[y * WIDTH + x]
fun setPixel(x: Int, y: Int, v: Int) {
frameBuffer[y * WIDTH + x] = v
}
fun clear() {
frameBuffer.fill(0)
}
/**
* Display the frame buffer in the console (debug).
*/
@Suppress("unused")
fun show() {
repeat(HEIGHT) { y ->
repeat(WIDTH) { x ->
val c = if (pixel(x, y) == 0 ) "." else "X"
print(c)
}
println("")
}
}
}

View file

@ -0,0 +1,101 @@
package com.beust.chip8
fun log(s: String) {
// println(s)
}
// Specs: http://www.cs.columbia.edu/~sedwards/classes/2016/4840-spring/designs/Chip8.pdf
// Roms can be found at https://github.com/loktar00/chip8/tree/master/roms
interface KeyListener {
fun onKey(key: Int?)
}
/** Format to hex with leading zeros */
val Int.h get() = String.format("%02X", this)
val Byte.h get() = String.format("%02X", this)
class Nibbles(private val b0: Int, val b1: Int, val b2: Int, val b3: Int) {
fun val2(a: Int, b: Int) = a.shl(4) + b
fun val3(a: Int, b: Int, c: Int) = a.shl(8) + b.shl(4) + c
override fun toString() = (b0.shl(4) + b1).h + " " + (b2.shl(4) + b3).h
}
class Instruction(private val computer: Computer, b0: Int, b1: Int, b2: Int, b3: Int) {
private val op: Op
init {
val n = Nibbles(b0, b1, b2, b3)
val undef = Undef(computer, n)
op = when(b0) {
0 -> {
if (b2 == 0xe && b3 == 0) Cls(computer, n) // 00E0
else if (b2 == 0xe && b3 == 0xe) Ret(computer, n) // 00EE
else Sys(computer, n) // 0nnn
}
1 -> Jmp(computer, n) // 1nnn
2 -> Call(computer, n) // 2nnn
3 -> Se(computer, n) // 3xkk
4 -> Sne(computer, n) // 4xkk
5 -> SeVxVy(computer, n) // 5xy0
6 -> LdV(computer, n) // 6xkk
7 -> Add(computer, n) // 7xkk
8 -> {
when (b3) {
0 -> Ld(computer, n) // 8xy0
1 -> Or(computer, n) // 8xy1
2 -> And(computer, n) // 8xy2
3 -> XorVxVy(computer, n) // 8xy3
4 -> AddVxVy(computer, n) // 8xy4
5 -> SubVxVy(computer, n) // 8xy5
6 -> Shr(computer, n) // 8xy6
7 -> SubnVxVy(computer, n) // 8xy7
0xe -> Shl(computer, n) // 8xyE
else -> { undef }
}
}
9 -> SneVxVy(computer, n) // 9xy0
0xa -> LdI(computer, n) // Annn
0xb -> JumpV0(computer, n) // Bnnn
0xc -> Rnd(computer, n) // Ckk
0xd -> Draw(computer, n) // Dxyn
0xe -> {
if (b2 == 9 && b3 == 0xe) SkipIfPressed(computer, n) // Ex9E
else if (b2 == 0xa && b3 == 0x1) SkipIfNotPressed(computer, n) // ExA1
else { undef }
}
0xf -> {
if (b2 == 0 && b3 == 7) LdVDt(computer, n) // Fx07
else if (b2 == 0 && b3 == 0xa) LdVxK(computer, n) // F0A
else if (b2 == 1 && b3 == 5) LdDt(computer, n) // Fx15
else if (b2 == 1 && b3 == 8) LdSt(computer, n) // Fx18
else if (b2 == 1 && b3 == 0xe) AddI(computer, n) // Fx1E
else if (b2 == 2 && b3 == 9) LdF(computer, n) // Fx29
else if (b2 == 3 && b3 == 3) LdB(computer, n) // Fx33
else if (b2 == 5 && b3 == 5) LdIVx(computer, n) // Fx55
else if (b2 == 6 && b3 == 5) LdVI(computer, n) // Fx65
else undef
}
else -> undef
}
}
override fun toString(): String {
return op.toString()
}
fun run() {
val cpu = computer.cpu
log("${cpu.PC.h}: ${op.nib} - $this")
op.run()
log(" $cpu")
cpu.PC += 2
// cpu.dtCounter++
// if (cpu.dtCounter % 3000 == 0) {
// computer.hardware.show()
// cpu.DT--
// }
}
}

View file

@ -0,0 +1,22 @@
package com.beust.chip8
class Keyboard {
private val keySemaphore = Object()
var key: Int? = null
set(v) {
synchronized(keySemaphore) {
field = v
keySemaphore.notify()
}
}
fun waitForKeyPress(): Int {
return if (key != null) key!!
else {
synchronized(keySemaphore) {
keySemaphore.wait()
key!!
}
}
}
}

View file

@ -0,0 +1,235 @@
package com.beust.chip8
import com.beust.jcommander.JCommander
import com.beust.jcommander.Parameter
import javafx.application.Application
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleStringProperty
import javafx.event.EventHandler
import javafx.fxml.FXMLLoader
import javafx.fxml.Initializable
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.control.ScrollPane
import javafx.scene.control.TextField
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent
import javafx.scene.layout.AnchorPane
import javafx.scene.layout.GridPane
import javafx.scene.layout.VBox
import javafx.stage.FileChooser
import javafx.stage.Stage
import java.io.File
import java.net.URL
import java.nio.file.Paths
import java.util.*
import kotlin.system.exitProcess
class Controller : Initializable {
var currentRomPath: SimpleStringProperty? = null
var computer: Computer? = null
var pauseButton: Button? = null
override fun initialize(location: URL?, resources: ResourceBundle?) {
println("Initializing controller")
}
}
class MyFxApp : Application() {
private val computer = Computer()
private var pauseButton: Button? = null
private var disassembly: GridPane? = null
private val isPaused = SimpleBooleanProperty().apply {
addListener { _, _, newVal -> computer.apply {
// Computer is paused
// Update the pause button label
pauseButton?.text = if (newVal) "Resume" else "Pause"
// Display the disassembly
updateDisassembly(disassembly!!, computer.disassemble())
}
}}
/**
* Display the disassembly.
*/
private fun updateDisassembly(disassembly: GridPane, listing: List<Computer.AssemblyLine>) {
disassembly.children.clear()
listing.forEachIndexed() { row, al ->
var column = 0
Label(al.counter.h).let { label ->
GridPane.setConstraints(label, column++, row)
disassembly.children.add(label)
}
Label(al.byte0.h + " " + al.byte1.h).let { label ->
GridPane.setConstraints(label, column++, row)
disassembly.children.add(label)
}
Label(al.name).let { label ->
GridPane.setConstraints(label, column++, row)
disassembly.children.add(label)
}
}
}
private val currentRomName = SimpleStringProperty()
private val currentRomPath = SimpleStringProperty().apply {
addListener { _, _, newVal -> computer.apply {
// Restart computer
stop()
loadRom(File(newVal))
// Update the rom name
val start = newVal.lastIndexOf(File.separatorChar)
val end = newVal.lastIndexOf(".ch8")
val romName = if (end == -1) newVal.substring(start + 1)
else newVal.substring(start + 1, end)
currentRomName.set(romName)
}}
}
override fun start(primaryStage: Stage) {
val keyListener = object: KeyListener {
override fun onKey(key: Int?) {
computer.keyboard.key = key
}
}
primaryStage.title = "CHIP-8"
val url = this::class.java.classLoader.getResource("main.fxml")
val loader = FXMLLoader(url)
val res = url.openStream()
val root = loader.load<AnchorPane>(res)
pauseButton = root.lookup("#pause") as Button
val scrollPane = root.lookup("#disassemblyScrollPane") as ScrollPane
disassembly = scrollPane.content as GridPane
val controller = loader.getController<Controller>()
controller.let {
it.currentRomPath = currentRomPath
it.computer = computer
it.pauseButton = pauseButton
}
computer.listener = object: ComputerListener {
override fun onKey(key: Int?) {
computer.keyboard.key = key
}
override fun onPause() {
isPaused.set(true)
}
override fun onStart() {
isPaused.set(false)
}
}
val scene = Scene(root)
val bp = root.lookup("#emulator") as VBox
bp.children.add(computer.display.pane)
primaryStage.scene = scene
primaryStage.show()
// Load rom button
(root.lookup("#loadRom") as Button).setOnAction {
computer.pause()
val romFile = FileChooser().run {
initialDirectory = File("roms")
showOpenDialog(Stage())
}
println("Picked file $romFile")
if (romFile != null) {
currentRomPath.set(romFile.absolutePath)
}
}
// Pause button
pauseButton?.setOnAction {
if (computer.paused) {
computer.start()
} else {
computer.pause()
}
}
// CPU clock button
with((root.lookup("#cpuClock") as TextField)) {
fun updateClock() {
computer.cpuClockHz = text.toLong()
}
text = computer.cpuClockHz.toString()
onAction = EventHandler { e ->
updateClock()
}
focusedProperty().addListener { _, _, onFocus ->
if (!onFocus) {
updateClock()
}
}
}
// Current ROM
val currentRomLabel = root.lookup("#currentRom") as Label
currentRomLabel.textProperty().bind(currentRomName)
scene.setOnKeyPressed { event: KeyEvent ->
when(event.code) {
KeyCode.Q -> {
primaryStage.close()
computer.stop()
exitProcess(0)
}
KeyCode.P -> {
computer.pause()
}
else -> {
if (computer.paused) computer.start()
else {
try {
val key = Integer.parseInt(event.code.char, 16)
keyListener.onKey(key)
} catch(ex: NumberFormatException) {
println("Can't parse key " + event.code.char + ", ignoring")
}
}
}
}
}
scene.setOnKeyReleased { event: KeyEvent ->
keyListener.onKey(null)
}
val spaceInvaders = Paths.get("roms", "Space Invaders [David Winter].ch8")
val breakout = Paths.get("roms", "Breakout [Carmelo Cortez, 1979].ch8")
val file3 = Paths.get("roms", "Tetris [Fran Dachille, 1991].ch8")
val file4 = Paths.get("roms", "Clock Program [Bill Fisher, 1981].ch8")
val file5 = Paths.get("roms", "IBM Logo.ch8")
val keypad = Paths.get("roms", "Keypad Test [Hap, 2006].ch8")
currentRomPath.set(spaceInvaders.toFile().absolutePath)
// val rom = file2.toFile() // if (arg.rom != null) File(arg.rom) else file3.toFile()
// chip8.run(rom)
}
}
fun main(args: Array<String>) {
if (args.isNotEmpty()) {
class Arg {
@Parameter(names = arrayOf("--rom"))
var rom: String? = null
@Parameter(names = arrayOf("--start"))
var start: String? = "200"
}
val arg = Arg()
JCommander.newBuilder().addObject(arg).args(args).build()
val computer = Computer(sound = false)
val start = Integer.parseInt(arg.start!!, 16)
val listing = computer.disassemble(start)
println("=== " + arg.rom + " PC=0x" + Integer.toHexString(start))
listing.forEach { println(it) }
} else {
Application.launch(MyFxApp::class.java)
}
}

View file

@ -0,0 +1,411 @@
package com.beust.chip8
import kotlin.random.Random
/**
* An Op is made of two bytes which is sliced into four nibbles. These nibbles are then used
* to determine the op and its data.
*/
sealed class Op(val computer: Computer, val nib: Nibbles) {
protected val nnn by lazy { nib.val3(nib.b1, nib.b2, nib.b3) }
protected val x by lazy { nib.b1 }
protected val y by lazy { nib.b2 }
protected val n by lazy { nib.b3 }
protected val kk by lazy { nib.val2(nib.b2, nib.b3) }
protected val cpu = computer.cpu
open fun run() {
TODO(" NOT IMPLEMENTED $nib")
println("")
}
protected fun unsigned(n: Int): Int = (n + 0x100) and 0xff
}
/**
* 00E0
* Clear screen
*/
class Cls(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
computer.frameBuffer.clear()
computer.display.draw(computer.frameBuffer.frameBuffer)
}
override fun toString() = "CLS"
}
/**
* 00EE
* Return
*/
class Ret(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.PC = cpu.SP.pop() }
override fun toString()= "RET"
}
/**
* 0nnn
* SYS addr (not implemented)
*/
class Sys(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.PC -= 2 } // loop in place if we reach here
override fun toString()= "SYS ${nnn.h}"
}
/**
* 1nnn
* Jump to address
*/
class Jmp(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.PC = nnn - 2 }
override fun toString() = "JMP ${nnn.h}"
}
/**
* 2nnn
* Call, jump to subroutine
*/
class Call(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
cpu.SP.push(cpu.PC)
cpu.PC = nnn - 2
}
override fun toString() = "CALL ${nnn.h}"
}
/**
* 3xkk
* Skip next instruction if Vx = kk
*/
class Se(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { if (cpu.V[x] == kk) cpu.PC += 2 }
override fun toString() = "SE V$x, $kk"
}
/**
* 4xkk
* Skip next instruction if Vx != kk
*/
class Sne(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { if (cpu.V[x] != kk) cpu.PC += 2 }
override fun toString() = "SNE V$x, $kk"
}
/**
* 5xy0
* Skip next instruction if Vx = Vy
*/
class SeVxVy(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { if (cpu.V[x] == cpu.V[y]) cpu.PC += 2 }
override fun toString() = "SE V$x, V$y"
}
/**
* 6xkk
* Set Vx = kk.
*/
class LdV(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { computer.cpu.V[x] = kk }
override fun toString() = "LD V$x, $kk"
}
/**
* 7xkk
* Set Vx = Vx + kk
*/
class Add(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.V[x] = unsigned(cpu.V[x] + kk) }
override fun toString() = "ADD V$x, $kk"
}
/**
* 8xy0
* Set Vx = Vy
*/
class Ld(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.V[x] = cpu.V[y] }
override fun toString() = "LD V$x, V$y"
}
/**
* 8xy1
* Set Vx = Vx OR Vy.
*/
class Or(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.V[x] = cpu.V[x] or cpu.V[y] }
override fun toString() = "OR V$x, V$y"
}
/**
* 8xy2
* Set Vx = Vx AND Vy.
*/
class And(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.V[x] = cpu.V[x] and cpu.V[y] }
override fun toString() = "AND V$x, V$y"
}
/**
* 8xy3
* Set Vx = Vx XOR Vy
*/
class XorVxVy(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.V[x] = cpu.V[x] xor cpu.V[y] }
override fun toString() = "XOR V$x, V$y"
}
/**
* 8xy4
* Set Vx = Vx + Vy, set VF = carry.
*/
class AddVxVy(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
val newValue = cpu.V[x] + cpu.V[y]
cpu.V[0xf] = if (newValue > 0xff) 1 else 0
cpu.V[x] = unsigned(newValue)
}
override fun toString() = "ADD V$x, V$y"
}
/**
* 8xy5
* Set Vx = Vx - Vy, set VF = NOT borrow.
*/
class SubVxVy(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
val newValue = cpu.V[x] - cpu.V[y]
cpu.V[0xf] = if (newValue < 0) 0 else 1
cpu.V[x] = unsigned(newValue)
}
override fun toString() = "SUB V$x, V$y"
}
/**
* 8xy6
* Set Vx = Vx SHR 1. I
*/
class Shr(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
cpu.V[0xf] = cpu.V[x] % 2
cpu.V[x] = cpu.V[x] shr 1
}
override fun toString() = "SHR V$x"
}
/**
* 8xy7
* Set Vx = Vy - Vx, set VF = NOT borrow.
*/
class SubnVxVy(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
val newValue = cpu.V[y] - cpu.V[x]
cpu.V[0xf] = if (newValue < 0) 0 else 1
cpu.V[x] = unsigned(newValue)
}
override fun toString() = "SBUN V$x,V$y"
}
/**
* 8xyE
* Set Vx = Vx SHL 1. I
*/
class Shl(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
cpu.V[0xf] = cpu.V[x] % 2
cpu.V[x] = cpu.V[x] shl 1
}
override fun toString() = "SHL V$x"
}
/**
* 9xy0
* Skip next instruction if Vx != Vy.@*/
class SneVxVy(c: Computer, n: Nibbles): SkipBase(c, n) {
override fun condition(key: Int?, expected: Int) = cpu.V[x] != cpu.V[y]
override fun toString() = "SNE V$x, V$y"
}
/**
* Annn
* Set I = nnn.
*/
class LdI(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.I = nnn }
override fun toString() = "LD I, ${nnn.h}"
}
/**
* Bnnn
* Jump to location nnn + V0. The program counter is set to nnn plus the value of V0.
*/
class JumpV0(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.PC = nnn + cpu.V[0] }
override fun toString() = "JUMP V0"
}
/**
* Cxkk
* Set Vx = random byte AND kk.
*/
class Rnd(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.V[x] = Random.nextInt() and kk }
override fun toString() = "RND V[$x], $kk"
}
/**
* Dxyn
* Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision.
*/
class Draw(c: Computer, nib: Nibbles): Op(c, nib) {
override fun run() {
cpu.V[0xf] = 0
repeat(n) { byte ->
val yy = (cpu.V[y] + byte) % FrameBuffer.HEIGHT
val sprite = cpu.memory[cpu.I + byte].toInt()
repeat(8) { bit ->
val xx = (cpu.V[x] + bit) % FrameBuffer.WIDTH
val color = (sprite shr (7 - bit)) and 1
val oldValue = computer.frameBuffer.pixel(xx, yy)
val newValue = oldValue xor color
cpu.V[0xf] = cpu.V[0xf] or (color and oldValue)
computer.frameBuffer.setPixel(xx, yy, newValue)
}
}
computer.display.draw(computer.frameBuffer.frameBuffer)
}
override fun toString() = "DRAW V$x, V$y, ${n.h}"
}
/**
* Base class for the two ops that deal with key presses: SKP and SKNP.
*/
abstract class SkipBase(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
val key = computer.keyboard.key
val expected = computer.cpu.V[x]
if (condition(key, expected)) {
computer.cpu.PC += 2
}
}
abstract fun condition(key: Int?, expected: Int): Boolean
}
/**
* Ex9E
* Skip if key is pressed
*/
class SkipIfPressed(c: Computer, n: Nibbles): SkipBase(c, n) {
override fun condition(key: Int?, expected: Int) = key != null && key == expected
override fun toString() = "SKP V$x"
}
/**
* ExA1
* Skip if key is not pressed
*/
class SkipIfNotPressed(c: Computer, n: Nibbles): SkipBase(c, n) {
override fun condition(key: Int?, expected: Int) = key == null || key != expected
override fun toString() = "SKNP V$x"
}
/**
* Fx07
* Set Vx = delay timer value.
*/
class LdVDt(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.V[x] = cpu.DT }
override fun toString() = "LD V$x, DT"
}
/**
* Fx0a
* Wait for a key press, store the value of the key in Vx
*/
class LdVxK(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { computer.cpu.V[x] = computer.keyboard.waitForKeyPress() }
override fun toString() = "LD V$x, Keyboard"
}
/**
* Fx15
* Set delay timer = Vx.
*/
class LdDt(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.DT = cpu.V[x] }
override fun toString() = "LD DT, V$x"
}
/**
* Fx18
* Set sound timer = Vx.
*/
class LdSt(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.ST = cpu.V[x] }
override fun toString() = "LD ST, V$x"
}
/**
* Fx1E
* Set I = I + Vx.
*/
class AddI(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.I += cpu.V[x] }
override fun toString() = "ADD I, V$x"
}
/**
* Fx29
* Set I = location of sprite for digit Vx.
*/
class LdF(c: Computer, n: Nibbles): Op(c, n) {
override fun run() { cpu.I = cpu.V[x] * 5 }
override fun toString() = "LD F, V$x"
}
/**
* Fx33
* Store BCD representation of Vx in memory locations I, I+1, and I+2.
*/
class LdB(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
val v = cpu.V[x]
cpu.memory[cpu.I] = (v / 100).toByte()
cpu.memory[cpu.I + 1] = ((v % 100) / 10).toByte()
cpu.memory[cpu.I + 2] = (v % 10).toByte()
}
override fun toString() = "LD B, V$x"
}
/**
* Fx55
* Stores V0 to VX in memory starting at address I. I is then set to I + x + 1.
*/
class LdIVx(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
repeat(x + 1) {
cpu.memory[cpu.I + it] = cpu.V[it].toByte()
}
cpu.I += x + 1
}
override fun toString() = "LD [I], V$x"
}
/**
* Fx65
* Fills V0 to VX with values from memory starting at address I.
*/
class LdVI(c: Computer, n: Nibbles): Op(c, n) {
override fun run() {
repeat(x + 1) {
cpu.V[it] = unsigned(cpu.memory[cpu.I + it].toInt())
}
cpu.I += x + 1
}
override fun toString() = "LD V$x, [I]"
}
class Undef(c: Computer, nib: Nibbles): Op(c, nib) {
override fun toString() = "Undef($n)"
}

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.beust.chip8.Controller" stylesheets="style.css">
<HBox AnchorPane.rightAnchor="5" AnchorPane.leftAnchor="5" AnchorPane.topAnchor="5" AnchorPane.bottomAnchor="5">
<children>
<!-- Left side -->
<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.topAnchor="0.0" AnchorPane.leftAnchor="0">
<AnchorPane id="left">
<VBox AnchorPane.rightAnchor="0.0"
AnchorPane.bottomAnchor="0.0" AnchorPane.topAnchor="0.0" AnchorPane.leftAnchor="0">
<VBox alignment="TOP_CENTER" styleClass="left-side" maxWidth="Infinity">
<children>
<Label text="CHIP-8" alignment="CENTER" />
<Label text="by Cédric Beust" alignment="CENTER"/>
</children>
</VBox>
<padding>
<Insets left="10" top="10" right="10" bottom="10" />
</padding>
<VBox spacing="10" >
<padding>
<Insets top="10" bottom="10"/>
</padding>
<Button text="Open rom..." alignment="CENTER" id="loadRom">
</Button>
<Button text="Pause" id="pause">
</Button>
<HBox maxWidth="Infinity" spacing="10">
<Label text="Clock speed"/>
<TextField id="cpuClock" maxWidth="Infinity" HBox.hgrow="ALWAYS"></TextField>
<Label text="Hz"/>
</HBox>
</VBox>
<ScrollPane id="disassemblyScrollPane" vbarPolicy="AS_NEEDED" hbarPolicy="AS_NEEDED">
<content>
<GridPane id="disassembly" styleClass="disassembly" hgap="2" vgap="2"
prefWidth="250"
VBox.vgrow="ALWAYS" maxWidth="Infinity" maxHeight="Infinity">
<columnConstraints>
<ColumnConstraints percentWidth="20.0"/>
<ColumnConstraints percentWidth="20.0"/>
<ColumnConstraints percentWidth="60.0"/>
</columnConstraints>
</GridPane>
</content>
</ScrollPane>
</VBox>
</AnchorPane>
</VBox>
<!-- Right side -->
<AnchorPane HBox.hgrow="ALWAYS">
<VBox styleClass="right-side"
AnchorPane.bottomAnchor="0" AnchorPane.topAnchor="0"
AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0"
>
<children>
<Label alignment="TOP_CENTER" id="currentRom" text="Current ROM" styleClass="rom-name"
maxWidth="Infinity"/>
<VBox id="emulator" styleClass="emulator"
VBox.vgrow="ALWAYS" maxWidth="Infinity" maxHeight="Infinity" />
</children>
</VBox>
</AnchorPane>
</children>
</HBox>
</AnchorPane>

Binary file not shown.

View file

@ -0,0 +1,40 @@
AnchorPane {
-fx-background-color: #43e6ef;
}
.left-side {
-fx-font-size: 20px;
-fx-font-weight: bold;
-fx-text-fill: #333333;
}
.rom-name {
-fx-font-size: 20px;
-fx-font-weight: bold;
}
.right-side {
}
.emulator {
-fx-padding: 20px;
-fx-font-family: Consolas, "Courier New";
}
.buttons {
-fx-fill-width: true;
}
ScrollPane, .disassembly {
-fx-background-color: #000000;
}
Button, GridPane, Label {
-fx-max-width: 10000;
-fx-max-height: 10000;
}
.disassembly > Label {
-fx-text-fill: green;
-fx-font-weight: bold;
}

View file

@ -0,0 +1,54 @@
package com.beust.chip8
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test
@Test
class OpTest {
private fun readPixels(c: Computer, w: Int, h: Int): List<Int> {
val result = arrayListOf<Int>()
repeat(w) { y ->
repeat(h) { x ->
result.add(c.frameBuffer.pixel(x, y))
}
}
return result
}
fun draw() {
val cpu = Cpu().apply {
I = 0x200
}
val c = Computer(cpu = cpu, sound = false)
val bytes = listOf(0xf0, 0x90, 0xf0, 0x90, 0xf0)
val memory = c.cpu.memory
bytes.forEachIndexed {
index, b -> memory[index] = b.toByte()
}
val op = Draw(c, Nibbles(0xd, 0, 0, 5))
op.run()
val p = c.frameBuffer.pixel(3, 1)
val expected = listOf(
1, 1, 1, 1, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 0,
1, 1, 1, 1, 0, 0, 0, 0,
1, 0, 0, 1, 0, 0, 0, 0,
1, 1, 1, 1, 0, 0, 0, 0)
val result = readPixels(c, 5, 8)
assertThat(result).isEqualTo(expected)
assertThat(cpu.V[0xf]).isEqualTo(0)
op.run()
val expected2 = listOf(
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0)
assertThat(readPixels(c, 5, 8)).isEqualTo(expected2)
assertThat(cpu.V[0xf]).isEqualTo(1)
println("")
}
}