mirror of
https://github.com/ethauvin/chip-8.git
synced 2025-04-25 00:37:13 -07:00
First commit
This commit is contained in:
commit
ee139eba9b
55 changed files with 1728 additions and 0 deletions
83
README.md
Normal file
83
README.md
Normal 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
89
build.gradle.kts
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
172
gradlew
vendored
Normal 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
0
pics/breakout-1.png
Normal file
BIN
pics/breakout-2.png
Normal 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
BIN
pics/space-invaders-1.mp4
Normal file
Binary file not shown.
BIN
pics/space-invaders-2.gif
Normal file
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
BIN
pics/space-invaders-2.mp4
Normal file
Binary file not shown.
BIN
pics/space-invaders-colors.png
Normal file
BIN
pics/space-invaders-colors.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
pics/space-invaders-small.png
Normal file
BIN
pics/space-invaders-small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
pics/space-invaders-text.png
Normal file
BIN
pics/space-invaders-text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
BIN
pics/tetris-1.png
Normal file
BIN
pics/tetris-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
roms/Breakout [Carmelo Cortez, 1979].ch8
Normal file
BIN
roms/Breakout [Carmelo Cortez, 1979].ch8
Normal file
Binary file not shown.
BIN
roms/Brix [Andreas Gustafsson, 1990].ch8
Normal file
BIN
roms/Brix [Andreas Gustafsson, 1990].ch8
Normal file
Binary file not shown.
BIN
roms/Chip8 Picture.ch8
Normal file
BIN
roms/Chip8 Picture.ch8
Normal file
Binary file not shown.
BIN
roms/Chip8 emulator Logo [Garstyciuks].ch8
Normal file
BIN
roms/Chip8 emulator Logo [Garstyciuks].ch8
Normal file
Binary file not shown.
BIN
roms/Clock Program [Bill Fisher, 1981].ch8
Normal file
BIN
roms/Clock Program [Bill Fisher, 1981].ch8
Normal file
Binary file not shown.
BIN
roms/Delay Timer Test [Matthew Mikolay, 2010].ch8
Normal file
BIN
roms/Delay Timer Test [Matthew Mikolay, 2010].ch8
Normal file
Binary file not shown.
BIN
roms/IBM Logo.ch8
Normal file
BIN
roms/IBM Logo.ch8
Normal file
Binary file not shown.
BIN
roms/InstructionTest.ch8
Normal file
BIN
roms/InstructionTest.ch8
Normal file
Binary file not shown.
BIN
roms/Keypad Test [Hap, 2006].ch8
Normal file
BIN
roms/Keypad Test [Hap, 2006].ch8
Normal file
Binary file not shown.
BIN
roms/Maze (alt) [David Winter, 199x].ch8
Normal file
BIN
roms/Maze (alt) [David Winter, 199x].ch8
Normal file
Binary file not shown.
12
roms/Maze (alt) [David Winter, 199x].txt
Normal file
12
roms/Maze (alt) [David Winter, 199x].txt
Normal 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.
|
BIN
roms/Maze [David Winter, 199x].ch8
Normal file
BIN
roms/Maze [David Winter, 199x].ch8
Normal file
Binary file not shown.
12
roms/Maze [David Winter, 199x].txt
Normal file
12
roms/Maze [David Winter, 199x].txt
Normal 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.
|
BIN
roms/Particle Demo [zeroZshadow, 2008].ch8
Normal file
BIN
roms/Particle Demo [zeroZshadow, 2008].ch8
Normal file
Binary file not shown.
6
roms/Particle Demo [zeroZshadow, 2008].txt
Normal file
6
roms/Particle Demo [zeroZshadow, 2008].txt
Normal 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
BIN
roms/Pong (alt).ch8
Normal file
Binary file not shown.
BIN
roms/Random Number Test [Matthew Mikolay, 2010].ch8
Normal file
BIN
roms/Random Number Test [Matthew Mikolay, 2010].ch8
Normal file
Binary file not shown.
BIN
roms/Sierpinski [Sergey Naydenov, 2010].ch8
Normal file
BIN
roms/Sierpinski [Sergey Naydenov, 2010].ch8
Normal file
Binary file not shown.
BIN
roms/Sirpinski [Sergey Naydenov, 2010].ch8
Normal file
BIN
roms/Sirpinski [Sergey Naydenov, 2010].ch8
Normal file
Binary file not shown.
BIN
roms/Space Invaders [David Winter].ch8
Normal file
BIN
roms/Space Invaders [David Winter].ch8
Normal file
Binary file not shown.
BIN
roms/Stars [Sergey Naydenov, 2010].ch8
Normal file
BIN
roms/Stars [Sergey Naydenov, 2010].ch8
Normal file
Binary file not shown.
BIN
roms/Tetris [Fran Dachille, 1991].ch8
Normal file
BIN
roms/Tetris [Fran Dachille, 1991].ch8
Normal file
Binary file not shown.
BIN
roms/Trip8 Demo (2008) [Revival Studios].ch8
Normal file
BIN
roms/Trip8 Demo (2008) [Revival Studios].ch8
Normal file
Binary file not shown.
49
roms/Trip8 Demo (2008) [Revival Studios].txt
Normal file
49
roms/Trip8 Demo (2008) [Revival Studios].txt
Normal 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
|
||||||
|
|
11
roms/Zero Demo [zeroZshadow, 2007].ch8
Normal file
11
roms/Zero Demo [zeroZshadow, 2007].ch8
Normal 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ÀÿÿÀÀüÀÿðÌÌðÌÃ<ÃÃÃÃ<
|
3
roms/Zero Demo [zeroZshadow, 2007].txt
Normal file
3
roms/Zero Demo [zeroZshadow, 2007].txt
Normal 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
BIN
roms/bc_test.ch8
Normal file
Binary file not shown.
BIN
roms/blinky.ch8
Normal file
BIN
roms/blinky.ch8
Normal file
Binary file not shown.
1
roms/jason.ch8
Normal file
1
roms/jason.ch8
Normal file
|
@ -0,0 +1 @@
|
||||||
|
`<18>ao<61>
|
133
src/main/kotlin/com/beust/chip8/Computer.kt
Normal file
133
src/main/kotlin/com/beust/chip8/Computer.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
66
src/main/kotlin/com/beust/chip8/Cpu.kt
Normal file
66
src/main/kotlin/com/beust/chip8/Cpu.kt
Normal 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}"
|
||||||
|
}
|
||||||
|
}
|
119
src/main/kotlin/com/beust/chip8/Display.kt
Normal file
119
src/main/kotlin/com/beust/chip8/Display.kt
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
34
src/main/kotlin/com/beust/chip8/FrameBuffer.kt
Normal file
34
src/main/kotlin/com/beust/chip8/FrameBuffer.kt
Normal 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("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
src/main/kotlin/com/beust/chip8/Instruction.kt
Normal file
101
src/main/kotlin/com/beust/chip8/Instruction.kt
Normal 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--
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
22
src/main/kotlin/com/beust/chip8/Keyboard.kt
Normal file
22
src/main/kotlin/com/beust/chip8/Keyboard.kt
Normal 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!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
235
src/main/kotlin/com/beust/chip8/Main.kt
Normal file
235
src/main/kotlin/com/beust/chip8/Main.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
411
src/main/kotlin/com/beust/chip8/Ops.kt
Normal file
411
src/main/kotlin/com/beust/chip8/Ops.kt
Normal 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)"
|
||||||
|
}
|
70
src/main/resources/main.fxml
Normal file
70
src/main/resources/main.fxml
Normal 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>
|
BIN
src/main/resources/sound.wav
Normal file
BIN
src/main/resources/sound.wav
Normal file
Binary file not shown.
40
src/main/resources/style.css
Normal file
40
src/main/resources/style.css
Normal 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;
|
||||||
|
}
|
54
src/test/kotlin/com/beust/chip8/OpTest.kt
Normal file
54
src/test/kotlin/com/beust/chip8/OpTest.kt
Normal 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("")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue