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:
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