├── .gitignore ├── .java-version ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── kotlin ├── Emulator.kt ├── Main.kt ├── apu ├── Apu.kt ├── EnvelopeGenerator.kt ├── FrameCounter.kt ├── LengthCounter.kt ├── NoiseChannel.kt ├── PulseChannel.kt ├── Speaker.kt ├── SweepUnit.kt ├── Timer.kt ├── TriangleChannel.kt └── Utils.kt ├── cartridge ├── Cartridge.kt └── Rom.kt ├── cpu ├── Cpu.kt ├── CpuBus.kt ├── CpuRegister.kt └── Opcode.kt ├── dma └── Dma.kt ├── exception └── UnknownOpcodeException.kt ├── ext ├── BooleanExt.kt ├── ByteArrayExt.kt ├── ByteExt.kt └── FileInputStreamExt.kt ├── interrupts └── Interrupts.kt ├── pad ├── JavaFXKeyEvent.kt ├── Key.kt ├── KeyEvent.kt └── Pad.kt ├── ppu ├── Canvas.kt ├── JavaFXCanvas.kt ├── PaletteRam.kt └── Ppu.kt ├── ram └── Ram.kt └── util └── Util.kt /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | .idea/ 3 | .gradle/ 4 | src/main/resources/ 5 | build 6 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yuiki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kotnes 2 | NES Emulator written in Kotlin. (Under development) 3 | This is written for my study. 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.8.21' 3 | id 'org.openjfx.javafxplugin' version '0.0.13' 4 | } 5 | 6 | javafx { 7 | version = "20" 8 | modules = [ 'javafx.graphics' ] 9 | } 10 | 11 | group 'jp.yuiki' 12 | version '1.0-SNAPSHOT' 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | 18 | dependencies { 19 | } 20 | 21 | compileKotlin { 22 | kotlinOptions.jvmTarget = "17" 23 | } 24 | compileTestKotlin { 25 | kotlinOptions.jvmTarget = "17" 26 | } 27 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuiki/kotnes/96f853fae144e627124df8021be7d4d039492fdf/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Sep 17 22:09:37 JST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kotnes' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/Emulator.kt: -------------------------------------------------------------------------------- 1 | import apu.* 2 | import cartridge.Cartridge 3 | import cartridge.Rom 4 | import cpu.Cpu 5 | import cpu.CpuBus 6 | import dma.Dma 7 | import ext.toUnsignedInt 8 | import interrupts.Interrupts 9 | import pad.KeyEvent 10 | import pad.Pad 11 | import ppu.Canvas 12 | import ppu.Ppu 13 | import ram.Ram 14 | import kotlin.math.round 15 | 16 | class Emulator( 17 | cartridge: Cartridge, 18 | canvas: Canvas, 19 | keyEvent: KeyEvent, 20 | ) { 21 | private val interrupts = Interrupts() 22 | private val chrRam = Ram(0x4000).apply { 23 | cartridge.character.forEachIndexed { idx, data -> 24 | write(idx, data.toUnsignedInt()) 25 | } 26 | } 27 | 28 | private val ppu = Ppu( 29 | chrRam = chrRam, 30 | canvas = canvas, 31 | interrupts = interrupts, 32 | isHorizontalMirror = cartridge.isHorizontalMirror 33 | ) 34 | 35 | private val apu = Apu( 36 | pulse1 = PulseChannel( 37 | envelopeGenerator = EnvelopeGenerator(), 38 | lengthCounter = LengthCounter(), 39 | isChannelOne = true, 40 | ), 41 | pulse2 = PulseChannel( 42 | envelopeGenerator = EnvelopeGenerator(), 43 | lengthCounter = LengthCounter(), 44 | isChannelOne = false, 45 | ), 46 | triangle = TriangleChannel( 47 | lengthCounter = LengthCounter(), 48 | ), 49 | noise = NoiseChannel( 50 | envelopeGenerator = EnvelopeGenerator(), 51 | lengthCounter = LengthCounter(), 52 | ), 53 | speaker = Speaker(), 54 | ) 55 | private val wRam = Ram(0x2048) 56 | private val prgRom = Rom(cartridge.program) 57 | private val dma = Dma(ppu, wRam) 58 | private val pad = Pad(keyEvent) 59 | 60 | private val cpuBus = CpuBus( 61 | ppu = ppu, 62 | apu = apu, 63 | ram = wRam, 64 | rom = prgRom, 65 | dma = dma, 66 | pad = pad 67 | ) 68 | 69 | private val cpu = Cpu( 70 | bus = cpuBus, 71 | interrupts = interrupts 72 | ) 73 | 74 | private var sleepMargin = 0L 75 | 76 | fun start() { 77 | while (true) { 78 | val frameStartNs = System.nanoTime() 79 | stepFrame() 80 | val frameEndNs = System.nanoTime() 81 | 82 | val sleepTimeNs = (FRAME_NS - (frameEndNs - frameStartNs)) + sleepMargin 83 | val sleepTimeMs = sleepTimeNs / 1000_000 84 | val sleepStartNs = System.nanoTime() 85 | if (sleepTimeMs > 0) { 86 | Thread.sleep(sleepTimeMs) 87 | } 88 | val sleepEnd = System.nanoTime() 89 | sleepMargin = sleepTimeNs - (sleepEnd - sleepStartNs) 90 | } 91 | } 92 | 93 | private fun stepFrame() { 94 | while (true) { 95 | var cpuCycle = 0 96 | if (dma.isProcessing) { 97 | dma.run() 98 | cpuCycle = 514 99 | } 100 | cpuCycle += cpu.run() 101 | apu.run(cpuCycle) 102 | if (ppu.run(cpuCycle * 3)) { 103 | break 104 | } 105 | } 106 | apu.flush() 107 | } 108 | 109 | companion object { 110 | private val FRAME_NS = round(1.0 / 60 * 1000_000_000).toInt() // 60fps 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import cartridge.Cartridge 2 | import javafx.application.Application 3 | import javafx.scene.Group 4 | import javafx.scene.Scene 5 | import javafx.scene.canvas.Canvas 6 | import javafx.stage.Stage 7 | import pad.JavaFXKeyEvent 8 | import ppu.JavaFXCanvas 9 | import java.io.File 10 | import kotlin.concurrent.thread 11 | 12 | fun main(args: Array) { 13 | Application.launch(Main::class.java) 14 | } 15 | 16 | class Main : Application() { 17 | override fun start(primaryStage: Stage) { 18 | val root = Group() 19 | val canvas = Canvas(WIDTH, HEIGHT) 20 | root.children += canvas 21 | primaryStage.scene = Scene(root) 22 | primaryStage.title = TITLE 23 | primaryStage.show() 24 | 25 | thread { 26 | val classLoader = Main::class.java.classLoader 27 | val romFile = File(classLoader.getResource(ROM_NAME).file) 28 | val rom = Cartridge(romFile) 29 | val g = canvas.graphicsContext2D 30 | 31 | Emulator( 32 | cartridge = rom, 33 | canvas = JavaFXCanvas(g), 34 | keyEvent = JavaFXKeyEvent(primaryStage.scene) 35 | ).start() 36 | } 37 | } 38 | 39 | companion object { 40 | const val TITLE = "kotnes" 41 | 42 | const val WIDTH = 256.0 * 2 43 | const val HEIGHT = 224.0 * 2 44 | 45 | const val ROM_NAME = "test.nes" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/Apu.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | import cpu.Cpu 4 | import ext.toInt 5 | 6 | class Apu( 7 | private val pulse1: PulseChannel, 8 | private val pulse2: PulseChannel, 9 | private val triangle: TriangleChannel, 10 | private val noise: NoiseChannel, 11 | private val speaker: Speaker, 12 | ) { 13 | 14 | private val frameCounter = FrameCounter( 15 | onHalfFrame = ::onHalfFrame, 16 | onQuarterFrame = ::onQuarterFrame 17 | ) 18 | 19 | private var accumulativeCpuCyclesForMixer = 0 20 | private var accumulativeCpuCyclesForApuCycle = 0 21 | 22 | fun read(addr: Int): Int { 23 | @Suppress("UNREACHABLE_CODE") // false positive 24 | return when (addr) { 25 | 0x15 -> { 26 | val p1 = (pulse1.lengthCounterValue > 0).toInt() 27 | val p2 = (pulse2.lengthCounterValue > 0).toInt() 28 | val triangle = (triangle.lengthCounterValue > 0).toInt() 29 | val noise = (noise.lengthCounterValue > 0).toInt() 30 | // TODO: support DMC and frame interrupt and DMC active 31 | return p1 + (p2 shl 1) + (triangle shl 2) + (noise shl 3) 32 | } 33 | 34 | else -> error("Unsupported read (addr: $addr)") 35 | } 36 | } 37 | 38 | fun write(addr: Int, data: UByte) { 39 | when (addr) { 40 | in 0x0..0x3 -> { 41 | pulse1.write(addr = addr, data = data) 42 | } 43 | 44 | in 0x4..0x07 -> { 45 | pulse2.write(addr = addr - 4, data = data) 46 | } 47 | 48 | in 0x8..0xB -> { 49 | triangle.write(addr = addr, data = data) 50 | } 51 | 52 | in 0xC..0xF -> { 53 | noise.write(addr = addr, data = data) 54 | } 55 | 56 | 0x15 -> { 57 | if (!data.isSetUByte(0u)) { 58 | pulse1.disable() 59 | } 60 | if (!data.isSetUByte(1u)) { 61 | pulse2.disable() 62 | } 63 | if (!data.isSetUByte(2u)) { 64 | triangle.disable() 65 | } 66 | if (!data.isSetUByte(3u)) { 67 | noise.disable() 68 | } 69 | 70 | // TODO: support DMC 71 | } 72 | 73 | 0x17 -> { 74 | frameCounter.updateRegister(data) 75 | } 76 | } 77 | } 78 | 79 | fun run(cycles: Int) { 80 | for (i in 0 until cycles) { 81 | onCpuCycle() 82 | 83 | if (++accumulativeCpuCyclesForApuCycle % 2 == 0) { 84 | onApuCycle() 85 | accumulativeCpuCyclesForApuCycle = 0 86 | } 87 | 88 | if (++accumulativeCpuCyclesForMixer == MIXER_STEP_CYCLES) { 89 | tickMixer() 90 | accumulativeCpuCyclesForMixer = 0 91 | } 92 | } 93 | } 94 | 95 | fun flush() { 96 | speaker.flush() 97 | } 98 | 99 | private fun onCpuCycle() { 100 | triangle.tickTimer() 101 | noise.tickTimer() 102 | 103 | frameCounter.tick() 104 | } 105 | 106 | private fun onApuCycle() { 107 | pulse1.tickTimer() 108 | pulse2.tickTimer() 109 | } 110 | 111 | private fun onQuarterFrame() { 112 | pulse1.tickEnvelope() 113 | pulse2.tickEnvelope() 114 | noise.tickEnvelope() 115 | 116 | triangle.tickLinearCounter() 117 | } 118 | 119 | private fun onHalfFrame() { 120 | pulse1.tickLengthCounter() 121 | pulse2.tickLengthCounter() 122 | triangle.tickLengthCounter() 123 | noise.tickLengthCounter() 124 | 125 | pulse1.tickSweepUnit() 126 | pulse2.tickSweepUnit() 127 | } 128 | 129 | private fun tickMixer() { 130 | // Linear Approximation 131 | // see: https://www.nesdev.org/wiki/APU_Mixer 132 | val mixedPulse = (pulse1.output + pulse2.output).toByte() * 0.00752 133 | val mixed = mixedPulse + 134 | triangle.output.toByte() * 0.00851 + 135 | noise.output.toByte() * 0.00494 136 | // TODO: low & high -pass filter 137 | speaker.write(mixed) 138 | } 139 | 140 | companion object { 141 | const val APU_HZ = 240 142 | 143 | private const val MIXER_STEP_CYCLES = Cpu.CPU_HZ / Speaker.SAMPLE_RATE 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/EnvelopeGenerator.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | // determine volume 4 | class EnvelopeGenerator { 5 | var output: UByte = 0u 6 | private set 7 | 8 | private var register: UByte = 0u 9 | private val volume get() = register.extract(range = 0..3) 10 | private val useConstantVolume get() = register.isSetUByte(4u) // otherwise, use decay 11 | private val isLoop get() = register.isSetUByte(5u) 12 | 13 | private var startFlag = false 14 | private var dividerCounter: UByte = 0u 15 | private var decayLevelCounter: UByte = 0u 16 | 17 | fun tick() { 18 | if (startFlag) { 19 | startFlag = false 20 | resetDecayLevelCounter() 21 | reloadDivider() 22 | } else { 23 | if (dividerCounter > 0u) { 24 | dividerCounter-- 25 | } else { 26 | reloadDivider() 27 | tickDecayLevelCounter() 28 | } 29 | } 30 | 31 | output = if (useConstantVolume) volume else decayLevelCounter 32 | } 33 | 34 | fun setStartFlag() { 35 | startFlag = true 36 | } 37 | 38 | fun updateRegister(register: UByte) { 39 | this.register = register 40 | } 41 | 42 | private fun reloadDivider() { 43 | dividerCounter = volume 44 | } 45 | 46 | private fun tickDecayLevelCounter() { 47 | if (decayLevelCounter > 0u) { 48 | decayLevelCounter-- 49 | } else if (isLoop) { 50 | resetDecayLevelCounter() 51 | } 52 | } 53 | 54 | private fun resetDecayLevelCounter() { 55 | decayLevelCounter = 15u 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/FrameCounter.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | import cpu.Cpu 4 | 5 | class FrameCounter( 6 | private val onQuarterFrame: () -> Any, 7 | private val onHalfFrame: () -> Any, 8 | ) { 9 | 10 | private var value = 0 11 | 12 | private var register: UByte = 0u 13 | 14 | private var accumulativeCycles = 0 15 | 16 | fun tick() { 17 | if (++accumulativeCycles < STEP_CYCLES) return 18 | accumulativeCycles = 0 19 | 20 | val mode = register.isSetUByte(7u) 21 | if (mode) { 22 | runFiveStepSequence() 23 | } else { 24 | runFourStepSequence() 25 | } 26 | } 27 | 28 | fun updateRegister(data: UByte) { 29 | register = data 30 | } 31 | 32 | private fun runFourStepSequence() { 33 | onQuarterFrame() 34 | 35 | when (value) { 36 | 1, 3 -> onHalfFrame() 37 | } 38 | 39 | if (value == 3) { 40 | value = 0 41 | } else { 42 | value++ 43 | } 44 | } 45 | 46 | private fun runFiveStepSequence() { 47 | when (value) { 48 | 0, 1, 2, 4 -> onQuarterFrame() 49 | } 50 | when (value) { 51 | 1, 4 -> onHalfFrame() 52 | } 53 | 54 | if (value == 4) { 55 | value = 0 56 | } else { 57 | value++ 58 | } 59 | } 60 | 61 | companion object { 62 | private const val STEP_CYCLES = Cpu.CPU_HZ / Apu.APU_HZ 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/LengthCounter.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | class LengthCounter { 4 | var value: Int = 1 5 | private set 6 | 7 | var isHalt: Boolean = false 8 | 9 | fun tick() { 10 | if (value == 0 || isHalt) return 11 | value-- 12 | } 13 | 14 | fun reset(value: UInt) { 15 | this.value = requireNotNull(LENGTH_TABLE[value]) 16 | } 17 | 18 | fun disable() { 19 | value = 0 20 | } 21 | 22 | companion object { 23 | // see: https://www.nesdev.org/wiki/APU_Length_Counter 24 | private val LENGTH_TABLE = mapOf( 25 | 0b1_1111u to 30, 26 | 0b1_1101u to 28, 27 | 0b1_1011u to 26, 28 | 0b1_1001u to 24, 29 | 0b1_0111u to 22, 30 | 0b1_0101u to 20, 31 | 0b1_0011u to 18, 32 | 0b1_0001u to 16, 33 | 0b0_1111u to 14, 34 | 0b0_1101u to 12, 35 | 0b0_1011u to 10, 36 | 0b0_1001u to 8, 37 | 0b0_0111u to 6, 38 | 0b0_0101u to 4, 39 | 0b0_0011u to 2, 40 | 0b0_0001u to 254, 41 | 0b1_1110u to 32, 42 | 0b1_1100u to 16, 43 | 0b1_1010u to 72, 44 | 0b1_1000u to 192, 45 | 0b1_0110u to 96, 46 | 0b1_0100u to 48, 47 | 0b1_0010u to 24, 48 | 0b1_0000u to 12, 49 | 0b0_1110u to 26, 50 | 0b0_1100u to 14, 51 | 0b0_1010u to 60, 52 | 0b0_1000u to 28, 53 | 0b0_0110u to 160, 54 | 0b0_0100u to 40, 55 | 0b0_0010u to 20, 56 | 0b0_0000u to 10, 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/NoiseChannel.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | class NoiseChannel( 4 | private val envelopeGenerator: EnvelopeGenerator, 5 | private val lengthCounter: LengthCounter, 6 | ) { 7 | val lengthCounterValue: Int get() = lengthCounter.value 8 | 9 | val output: UByte 10 | get() { 11 | if (lengthCounter.value == 0 || shiftRegister.isSetUInt(0u)) return 0u 12 | return envelopeGenerator.output 13 | } 14 | 15 | private var mode = false 16 | private var periodSetting: UByte = 0u 17 | 18 | private var timer = Timer(onRing = ::onTimerRing) 19 | private var shiftRegister = 1u 20 | 21 | fun write(addr: Int, data: UByte) { 22 | when (addr) { 23 | 0xC -> { 24 | lengthCounter.isHalt = data.isSetUByte(5u) 25 | envelopeGenerator.updateRegister(register = data) 26 | } 27 | 28 | // 0xD is unused 29 | 30 | 0xE -> { 31 | periodSetting = data.extract(0..3) 32 | mode = data.isSetUByte(7u) 33 | } 34 | 35 | 0xF -> { 36 | val value = data.extract(3..7) 37 | lengthCounter.reset(value.toUInt()) 38 | envelopeGenerator.setStartFlag() 39 | } 40 | } 41 | } 42 | 43 | fun disable() { 44 | lengthCounter.disable() 45 | } 46 | 47 | fun tickLengthCounter() { 48 | lengthCounter.tick() 49 | } 50 | 51 | fun tickEnvelope() { 52 | envelopeGenerator.tick() 53 | } 54 | 55 | fun tickTimer() { 56 | timer.tick() 57 | } 58 | 59 | private fun onTimerRing(): UInt { 60 | runShiftRegister() 61 | return PERIOD_TABLE[periodSetting.toInt()].toUInt() 62 | } 63 | 64 | private fun runShiftRegister() { 65 | val feedBackZero = shiftRegister.isSetUInt(0u) 66 | val feedbackOne = (if (mode) shiftRegister.isSetUInt(6u) else shiftRegister.isSetUInt(1u)) 67 | val feedback = feedBackZero xor feedbackOne 68 | 69 | shiftRegister = (shiftRegister shr 1).update(feedback, 14) 70 | } 71 | 72 | companion object { 73 | private val PERIOD_TABLE = 74 | intArrayOf(4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/PulseChannel.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | class PulseChannel( 4 | private val envelopeGenerator: EnvelopeGenerator, 5 | private val lengthCounter: LengthCounter, 6 | isChannelOne: Boolean, // or ch. 2 7 | ) { 8 | val lengthCounterValue: Int get() = lengthCounter.value 9 | 10 | val output: UByte 11 | get() { 12 | val volume = envelopeGenerator.output 13 | if ( 14 | timerPeriod < 8u || 15 | lengthCounter.value == 0 || 16 | sweepUnit.isMute 17 | ) return 0u 18 | 19 | return (sequencerOutput * volume).toUByte() 20 | } 21 | 22 | private var dutyCycle: UByte = 0u 23 | 24 | private val timer = Timer(onRing = ::onTimerRing) 25 | private var timerPeriod = 0u 26 | 27 | private var sequenceCounter = 0 28 | 29 | private var sequencerOutput: UByte = 0u 30 | 31 | private val sweepUnit = SweepUnit( 32 | onTimerPeriodUpdate = { timerPeriod = it }, 33 | useOneComplement = isChannelOne, 34 | ) 35 | 36 | fun write(addr: Int, data: UByte) { 37 | when (addr) { 38 | 0x0 -> { 39 | envelopeGenerator.updateRegister(register = data) 40 | lengthCounter.isHalt = data.isSetUByte(5u) 41 | dutyCycle = data.extract(6..7) 42 | } 43 | 44 | 0x1 -> { 45 | sweepUnit.updateRegister(data) 46 | } 47 | 48 | 0x2 -> { 49 | timerPeriod = timerPeriod.update(data, 0..7) 50 | } 51 | 52 | 0x3 -> { 53 | timerPeriod = timerPeriod.update(data.extract(0..2), 8..10) // TODO: is needed? 54 | 55 | resetSequenceCounter() 56 | envelopeGenerator.setStartFlag() 57 | lengthCounter.reset(data.extract(3..7).toUInt()) 58 | } 59 | } 60 | } 61 | 62 | fun disable() { 63 | lengthCounter.disable() 64 | } 65 | 66 | fun tickTimer() { 67 | timer.tick() 68 | } 69 | 70 | fun tickEnvelope() { 71 | envelopeGenerator.tick() 72 | } 73 | 74 | fun tickLengthCounter() { 75 | lengthCounter.tick() 76 | } 77 | 78 | fun tickSweepUnit() { 79 | sweepUnit.tick(timerPeriod = timerPeriod) 80 | } 81 | 82 | private fun onTimerRing(): UInt { 83 | sequencerOutput = runSequencer(dutyCycle = dutyCycle) 84 | return timerPeriod 85 | } 86 | 87 | // returns 0 or 1 88 | private fun runSequencer(dutyCycle: UByte): UByte { 89 | val waveform = WAVEFORMS[dutyCycle.toInt()] 90 | val output = waveform[sequenceCounter].toUByte() 91 | 92 | if (sequenceCounter < 7) { 93 | sequenceCounter++ 94 | } else { 95 | resetSequenceCounter() 96 | } 97 | return output 98 | } 99 | 100 | private fun resetSequenceCounter() { 101 | sequenceCounter = 0 102 | } 103 | 104 | companion object { 105 | private val WAVEFORMS = arrayOf( 106 | intArrayOf(0, 1, 0, 0, 0, 0, 0, 0), 107 | intArrayOf(0, 1, 1, 0, 0, 0, 0, 0), 108 | intArrayOf(0, 1, 1, 1, 1, 0, 0, 0), 109 | intArrayOf(1, 0, 0, 1, 1, 1, 1, 1), 110 | ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/Speaker.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | import javax.sound.sampled.AudioFormat 4 | import javax.sound.sampled.AudioSystem 5 | import javax.sound.sampled.DataLine 6 | import javax.sound.sampled.SourceDataLine 7 | 8 | class Speaker { 9 | private val format = AudioFormat( 10 | SAMPLE_RATE.toFloat(), 11 | SAMPLE_SIZE_BITS, 12 | CHANNELS, 13 | true, 14 | false, 15 | ) 16 | 17 | private val line = run { 18 | val info = DataLine.Info(SourceDataLine::class.java, format) 19 | AudioSystem.getLine(info) as SourceDataLine 20 | } 21 | 22 | init { 23 | line.open(format, BUFFER_SIZE) 24 | line.start() 25 | } 26 | 27 | private var buffer = ByteArray(BUFFER_SIZE) 28 | private var bufferIndex = 0 29 | 30 | fun write(value: Double) { 31 | if ((line.bufferSize - line.available()) >= DROP_THRESHOLD) return 32 | 33 | // TODO: support DMC 34 | val volume = (value * VOLUME_LEVEL).toInt().toByte() 35 | buffer[bufferIndex++] = 0 36 | buffer[bufferIndex++] = volume 37 | } 38 | 39 | fun flush() { 40 | line.write(buffer, 0, bufferIndex) 41 | bufferIndex = 0 42 | } 43 | 44 | companion object { 45 | const val SAMPLE_RATE = 44100 46 | 47 | private const val SAMPLE_SIZE_BITS = 16 48 | private const val SAMPLE_SIZE_BYTE = SAMPLE_SIZE_BITS / Byte.SIZE_BITS 49 | private const val CHANNELS = 1 50 | private const val BUFFER_SIZE = SAMPLE_RATE * SAMPLE_SIZE_BYTE * CHANNELS / 10 // 100ms 51 | 52 | private const val DROP_THRESHOLD = SAMPLE_RATE * SAMPLE_SIZE_BYTE / 1000 * 32 // 32ms 53 | 54 | private const val VOLUME_LEVEL = 255 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/SweepUnit.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | class SweepUnit( 4 | private val onTimerPeriodUpdate: (period: UInt) -> Unit, 5 | private val useOneComplement: Boolean, // or two's complement 6 | ) { 7 | var isMute: Boolean = false 8 | 9 | private var counter = 0u 10 | private var register: UByte = 0u 11 | private var shouldReload = false 12 | 13 | fun tick(timerPeriod: UInt) { 14 | if (counter == 0u) { 15 | updateTimerPeriod(timerPeriod) 16 | } 17 | 18 | if (counter == 0u || shouldReload) { 19 | counter = register.extract(4..6).toUInt() 20 | shouldReload = false 21 | } else { 22 | counter-- 23 | } 24 | } 25 | 26 | fun updateRegister(data: UByte) { 27 | register = data 28 | 29 | shouldReload = true 30 | } 31 | 32 | private fun updateTimerPeriod(timerPeriod: UInt) { 33 | val shiftCount = register.extract(0..2) 34 | val delta = (timerPeriod shr shiftCount.toInt()).toInt().let { 35 | val isNegate = register.isSetUByte(3u) 36 | if (isNegate) -it.let { c -> if (useOneComplement) c - 1 else c } else it 37 | } 38 | val targetPeriod = (timerPeriod.toInt() + delta).toUInt() 39 | 40 | isMute = timerPeriod < 8u || targetPeriod > 0x7FFu 41 | 42 | val isEnabled = register.isSetUByte(7u) 43 | if (isEnabled && !isMute) { 44 | onTimerPeriodUpdate(targetPeriod) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/Timer.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | class Timer( 4 | private val onRing: () -> UInt, // returns new counter 5 | ) { 6 | private var counter = 0u 7 | 8 | fun tick() { 9 | if (counter > 0u) { 10 | counter-- 11 | return 12 | } 13 | 14 | counter = onRing() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/TriangleChannel.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | class TriangleChannel(private val lengthCounter: LengthCounter) { 4 | val lengthCounterValue: Int get() = lengthCounter.value 5 | 6 | var output: UByte = 0u 7 | private set 8 | 9 | private var counterReloadValue: UByte = 0u 10 | private var controlFlag = false 11 | private var timerPeriod = 0u 12 | 13 | private var timer = Timer(onRing = ::onTimerRing) 14 | private var sequenceIndex: UByte = 0u 15 | 16 | private var reloadLinearCounter = false 17 | private var linearCounter = 0 18 | 19 | fun write(addr: Int, data: UByte) { 20 | when (addr) { 21 | 0x8 -> { 22 | counterReloadValue = data.extract(0..6) 23 | 24 | controlFlag = data.isSetUByte(7u) 25 | lengthCounter.isHalt = controlFlag 26 | } 27 | 28 | // 0x9 is unused 29 | 30 | 0xA -> { 31 | timerPeriod = timerPeriod.update(data, 0..7) 32 | } 33 | 34 | 0xB -> { 35 | timerPeriod = timerPeriod.update(data.extract(0..2), 8..10) 36 | 37 | reloadLinearCounter = true 38 | lengthCounter.reset(value = data.extract(3..7).toUInt()) 39 | } 40 | } 41 | } 42 | 43 | fun disable() { 44 | lengthCounter.disable() 45 | } 46 | 47 | fun tickTimer() { 48 | timer.tick() 49 | } 50 | 51 | fun tickLinearCounter() { 52 | if (reloadLinearCounter) { 53 | linearCounter = counterReloadValue.toInt() 54 | } else if (linearCounter > 0) { 55 | linearCounter-- 56 | } 57 | 58 | if (!controlFlag) { 59 | reloadLinearCounter = false 60 | } 61 | } 62 | 63 | fun tickLengthCounter() { 64 | lengthCounter.tick() 65 | } 66 | 67 | private fun onTimerRing(): UInt { 68 | if (linearCounter > 0 && lengthCounter.value > 0) { 69 | output = runSequencer() 70 | } 71 | return timerPeriod 72 | } 73 | 74 | private fun runSequencer(): UByte { 75 | val index = (sequenceIndex and 0x1Fu).also { sequenceIndex++ } 76 | return if (index < 0x10u) index xor 0xFu else index and 0xFu 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/apu/Utils.kt: -------------------------------------------------------------------------------- 1 | package apu 2 | 3 | import ext.toInt 4 | import kotlin.math.pow 5 | 6 | fun UByte.extract(range: IntRange): UByte { 7 | val offset = Int.SIZE_BITS - 1 - range.last 8 | return ((this.toUInt() shl offset) shr (range.first + offset)).toUByte() 9 | } 10 | 11 | fun UInt.isSetUInt(digit: UByte): Boolean { 12 | val mask = 2.0.pow(digit.toInt()).toUInt() 13 | return this and mask == mask 14 | } 15 | 16 | fun UByte.isSetUByte(digit: UByte): Boolean { 17 | return this.toUInt().isSetUInt(digit) 18 | } 19 | 20 | fun UInt.update(data: Boolean, digit: Int): UInt { 21 | return update(data.toInt(), digit..digit) 22 | } 23 | 24 | fun UInt.update(data: UByte, digits: IntRange): UInt { 25 | return update(data.toInt(), digits) 26 | } 27 | 28 | private fun UInt.update(data: Int, range: IntRange): UInt { 29 | val length = range.last - range.first + 1 30 | val mask = ((2.0.pow(length).toUInt() - 1u) shl range.first).inv() 31 | return (this and mask) or (data.toUInt() shl range.first) 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/cartridge/Cartridge.kt: -------------------------------------------------------------------------------- 1 | package cartridge 2 | 3 | import ext.read 4 | import ext.readAsHex 5 | import ext.readAsInt 6 | import java.io.File 7 | 8 | class Cartridge(rom: File) { 9 | val program: ByteArray 10 | val character: ByteArray 11 | val isHorizontalMirror: Boolean 12 | 13 | init { 14 | val romData = rom.inputStream() 15 | val magicBytes = romData.readAsHex(4) 16 | if (magicBytes != MAGIC_BYTES) { 17 | throw IllegalArgumentException("The file is not iNES file.") 18 | } 19 | val prgPage = romData.readAsInt(1) 20 | val chrPage = romData.readAsInt(1) 21 | val prgSize = prgPage * PRG_PAGE_SIZE 22 | val chrSize = chrPage * CHR_PAGE_SIZE 23 | isHorizontalMirror = romData.readAsInt(1) == 0 24 | val readHeaderBytes = 7 25 | // dump rest header 26 | romData.read(HEADER_SIZE - readHeaderBytes) 27 | program = romData.read(prgSize) 28 | character = romData.read(chrSize) 29 | } 30 | 31 | companion object { 32 | const val MAGIC_BYTES = "4E45531A" 33 | 34 | const val HEADER_SIZE = 0x10 35 | const val PRG_PAGE_SIZE = 0x4000 36 | const val CHR_PAGE_SIZE = 0x2000 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/cartridge/Rom.kt: -------------------------------------------------------------------------------- 1 | package cartridge 2 | 3 | import ext.toUnsignedInt 4 | 5 | class Rom( 6 | private val data: ByteArray, 7 | ) { 8 | val size get() = data.size 9 | 10 | fun read(addr: Int) = data[addr].toUnsignedInt() 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/cpu/Cpu.kt: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import exception.UnknownOpcodeException 4 | import ext.toInt 5 | import interrupts.Interrupts 6 | 7 | class Cpu( 8 | private val bus: CpuBus, 9 | private val interrupts: Interrupts, 10 | ) { 11 | private var registers = CpuRegister() 12 | private var hasBranched = false 13 | 14 | init { 15 | reset() 16 | } 17 | 18 | private fun reset() { 19 | registers = CpuRegister().apply { 20 | pc = readWord(0xFFFC) 21 | } 22 | } 23 | 24 | data class OperandData(val operand: Int, val additionalCycle: Int = 0) 25 | 26 | private fun getOperand(mode: AddressingMode) = 27 | when (mode) { 28 | AddressingMode.ACCUMULATOR -> OperandData(0x00) 29 | AddressingMode.IMMEDIATE -> OperandData(fetch(registers.pc)) 30 | AddressingMode.ABSOLUTE -> OperandData(fetchWord(registers.pc)) 31 | AddressingMode.ZERO_PAGE -> OperandData(fetch(registers.pc)) 32 | AddressingMode.ZERO_PAGE_X -> OperandData(fetch(registers.pc) + registers.x and 0xFF) 33 | AddressingMode.ZERO_PAGE_Y -> OperandData(fetch(registers.pc) + registers.y and 0xFF) 34 | AddressingMode.ABSOLUTE_X -> { 35 | val addr = fetchWord(registers.pc) + registers.x and 0xFFFF 36 | val additionalCycle = (addr and 0xFF00 != (addr + registers.x) and 0xFF00).toInt() 37 | OperandData(addr, additionalCycle) 38 | } 39 | 40 | AddressingMode.ABSOLUTE_Y -> { 41 | val addr = fetchWord(registers.pc) + registers.y and 0xFFFF 42 | val additionalCycle = (addr and 0xFF00 != (addr + registers.y) and 0xFF00).toInt() 43 | OperandData(addr, additionalCycle) 44 | } 45 | 46 | AddressingMode.IMPLIED -> OperandData(0x00) 47 | AddressingMode.RELATIVE -> { 48 | val baseAddr = fetch(registers.pc) 49 | val addr = baseAddr + if (baseAddr < 0x80) registers.pc else registers.pc - 0x100 50 | val additionalCycle = (addr and 0xFF00 != registers.pc and 0xFF00).toInt() 51 | OperandData(addr, additionalCycle) 52 | } 53 | 54 | AddressingMode.INDIRECT_X -> { 55 | val baseAddr = (fetch(registers.pc) + registers.x) and 0xFF 56 | val addr = (read(baseAddr) + (read((baseAddr + 1) and 0xFF) shl 8)) and 0xFFFF 57 | OperandData(addr) 58 | } 59 | 60 | AddressingMode.INDIRECT_Y -> { 61 | val fetchedAddr = fetch(registers.pc) 62 | val baseAddr = read(fetchedAddr) + (read((fetchedAddr + 1) and 0xFF) shl 8) 63 | val addr = baseAddr + registers.y 64 | val additionalCycle = (addr and 0xFF00 != baseAddr and 0xFF00).toInt() 65 | OperandData(addr and 0xFFFF, additionalCycle) 66 | } 67 | 68 | AddressingMode.INDIRECT -> { 69 | val baseAddr = fetchWord(registers.pc) 70 | val addr = 71 | (read(baseAddr) + (read((baseAddr and 0xFF00) or (((baseAddr and 0xFF) + 1) and 0xFF)) shl 8)) and 0xFFFF 72 | OperandData(addr) 73 | } 74 | } 75 | 76 | private fun fetch(addr: Int): Int { 77 | registers.pc += 1 78 | return read(addr) 79 | } 80 | 81 | private fun fetchWord(addr: Int): Int { 82 | registers.pc += 2 83 | return readWord(addr) 84 | } 85 | 86 | private fun read(addr: Int) = bus.read(addr) 87 | 88 | private fun readWord(addr: Int) = read(addr) or (read(addr + 1) shl 8) 89 | 90 | private fun write(addr: Int, data: Int) { 91 | bus.write(addr, data) 92 | } 93 | 94 | private fun push(data: Int) { 95 | write(0x100 + (registers.sp and 0xFF), data) 96 | registers.sp-- 97 | } 98 | 99 | private fun pop(): Int { 100 | registers.sp++ 101 | return read(0x100 + (registers.sp and 0xFF)) 102 | } 103 | 104 | private fun branch(addr: Int) { 105 | hasBranched = true 106 | registers.pc = addr 107 | } 108 | 109 | private fun pushStatus() { 110 | push(registers.status) 111 | } 112 | 113 | private fun popStatus() { 114 | val status = pop() 115 | registers.n = status and 0x80 != 0 116 | registers.v = status and 0x40 != 0 117 | registers.r = status and 0x20 != 0 118 | registers.b = status and 0x10 != 0 119 | registers.d = status and 0x08 != 0 120 | registers.i = status and 0x04 != 0 121 | registers.z = status and 0x02 != 0 122 | registers.c = status and 0x01 != 0 123 | } 124 | 125 | private fun popPc() { 126 | registers.pc = pop() 127 | registers.pc += pop() shl 8 128 | } 129 | 130 | private fun exec(instruction: Instruction, mode: AddressingMode, operand: Int) { 131 | hasBranched = false 132 | 133 | when (instruction) { 134 | Instruction.ADC -> { 135 | val data = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 136 | val result: Int = data + registers.a + registers.c.toInt() 137 | registers.n = result and 0x80 != 0 138 | registers.v = registers.a xor data and 0x80 == 0 && registers.a xor result and 0x80 != 0 139 | registers.z = result and 0xFF == 0 140 | registers.c = result > 0xFF 141 | registers.a = result and 0xFF 142 | } 143 | 144 | Instruction.SBC -> { 145 | val data = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 146 | val result: Int = registers.a - data - (!registers.c).toInt() 147 | registers.n = result and 0x80 != 0 148 | registers.v = registers.a xor data and 0x80 != 0 && registers.a xor result and 0x80 != 0 149 | registers.z = result and 0xFF == 0 150 | registers.c = result >= 0 151 | registers.a = result and 0xFF 152 | } 153 | 154 | Instruction.AND -> { 155 | val data = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 156 | val result = data and registers.a 157 | registers.n = result and 0x80 != 0 158 | registers.z = result == 0 159 | registers.a = result and 0xFF 160 | } 161 | 162 | Instruction.ORA -> { 163 | val data = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 164 | val result = data or registers.a 165 | registers.n = result and 0x80 != 0 166 | registers.z = result == 0 167 | registers.a = result and 0xFF 168 | } 169 | 170 | Instruction.EOR -> { 171 | val data = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 172 | val result = data xor registers.a 173 | registers.n = result and 0x80 != 0 174 | registers.z = result == 0 175 | registers.a = result and 0xFF 176 | } 177 | 178 | Instruction.ASL -> { 179 | if (mode == AddressingMode.ACCUMULATOR) { 180 | val result = (registers.a shl 1) and 0xFF 181 | registers.n = result != 0 182 | registers.z = result == 0 183 | registers.c = registers.a and 0x80 != 0 184 | registers.a = result 185 | } else { 186 | val data = read(operand) 187 | val result = (data shl 1) and 0xFF 188 | registers.n = result != 0 189 | registers.z = result == 0 190 | registers.c = data and 0x80 != 0 191 | write(operand, result) 192 | } 193 | } 194 | 195 | Instruction.LSR -> { 196 | if (mode == AddressingMode.ACCUMULATOR) { 197 | val result = (registers.a shr 1) and 0xFF 198 | registers.z = result == 0 199 | registers.c = registers.a and 0x01 != 0 200 | registers.a = result 201 | } else { 202 | val data = read(operand) 203 | val result = data shr 1 204 | registers.z = result == 0 205 | registers.c = data and 0x01 != 0 206 | write(operand, result) 207 | } 208 | registers.n = false 209 | } 210 | 211 | Instruction.ROL -> { 212 | if (mode == AddressingMode.ACCUMULATOR) { 213 | val result = (registers.a shl 1) and 0xFF or registers.c.toInt() 214 | registers.n = result and 0x80 != 0 215 | registers.z = result == 0 216 | registers.c = registers.a and 0x80 != 0 217 | registers.a = result 218 | } else { 219 | val data = read(operand) 220 | val result = (data shl 1) and 0xFF or registers.c.toInt() 221 | registers.n = result and 0x80 != 0 222 | registers.z = result == 0 223 | registers.c = data and 0x80 != 0 224 | write(operand, result) 225 | } 226 | } 227 | 228 | Instruction.ROR -> { 229 | if (mode == AddressingMode.ACCUMULATOR) { 230 | val result = (registers.a shr 1) or if (registers.c) 0x80 else 0x00 231 | registers.n = result and 0x80 != 0 232 | registers.z = result == 0 233 | registers.c = registers.a and 0x01 != 0 234 | registers.a = result 235 | } else { 236 | val data = read(operand) 237 | val result = (data shr 1) and 0xFF or if (registers.c) 0x80 else 0x00 238 | registers.n = result and 0x80 != 0 239 | registers.z = result == 0 240 | registers.c = data and 0x01 != 0 241 | write(operand, result) 242 | } 243 | } 244 | 245 | Instruction.BCC -> if (!registers.c) branch(operand) 246 | Instruction.BCS -> if (registers.c) branch(operand) 247 | Instruction.BEQ -> if (registers.z) branch(operand) 248 | Instruction.BNE -> if (!registers.z) branch(operand) 249 | Instruction.BVC -> if (!registers.v) branch(operand) 250 | Instruction.BVS -> if (registers.v) branch(operand) 251 | Instruction.BPL -> if (!registers.n) branch(operand) 252 | Instruction.BMI -> if (registers.n) branch(operand) 253 | Instruction.BIT -> { 254 | val data = read(operand) 255 | registers.n = data and 0x80 != 0 256 | registers.v = data and 0x40 != 0 257 | registers.z = registers.a and data == 0 258 | } 259 | 260 | Instruction.JMP -> branch(operand) 261 | Instruction.JSR -> { 262 | val pc = registers.pc - 1 263 | push(pc shr 8 and 0xFF) 264 | push(pc and 0xFF) 265 | branch(operand) 266 | } 267 | 268 | Instruction.RTS -> { 269 | popPc() 270 | registers.pc++ 271 | } 272 | 273 | Instruction.BRK -> { 274 | if (registers.i) { 275 | return 276 | } 277 | push((registers.pc shr 8) and 0xFF) 278 | push(registers.pc and 0xFF) 279 | pushStatus() 280 | registers.i = true 281 | registers.b = true 282 | registers.pc = readWord(0xFFFE) 283 | } 284 | 285 | Instruction.RTI -> { 286 | popStatus() 287 | popPc() 288 | this.registers.r = true 289 | } 290 | 291 | Instruction.CMP -> { 292 | val data = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 293 | val result = registers.a - data 294 | registers.n = result and 0x80 != 0 295 | registers.z = result and 0xFF == 0 296 | registers.c = result >= 0 297 | } 298 | 299 | Instruction.CPX -> { 300 | val data = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 301 | val result = registers.x - data 302 | registers.n = result and 0x80 != 0 303 | registers.z = result and 0xFF == 0 304 | registers.c = result >= 0 305 | } 306 | 307 | Instruction.CPY -> { 308 | val data = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 309 | val result = registers.y - data 310 | registers.n = result and 0x80 != 0 311 | registers.z = result and 0xFF == 0 312 | registers.c = result >= 0 313 | } 314 | 315 | Instruction.INC -> { 316 | val data = read(operand) + 1 and 0xFF 317 | registers.n = data and 0x80 != 0 318 | registers.z = data == 0 319 | write(operand, data) 320 | } 321 | 322 | Instruction.DEC -> { 323 | val data = read(operand) - 1 and 0xFF 324 | registers.n = data and 0x80 != 0 325 | registers.z = data == 0 326 | write(operand, data) 327 | } 328 | 329 | Instruction.INX -> { 330 | registers.x = registers.x + 1 and 0xFF 331 | registers.n = registers.x and 0x80 != 0 332 | registers.z = registers.x == 0 333 | } 334 | 335 | Instruction.DEX -> { 336 | registers.x = registers.x - 1 and 0xFF 337 | registers.n = registers.x and 0x80 != 0 338 | registers.z = registers.x == 0 339 | } 340 | 341 | Instruction.INY -> { 342 | registers.y = registers.y + 1 and 0xFF 343 | registers.n = registers.y and 0x80 != 0 344 | registers.z = registers.y == 0 345 | } 346 | 347 | Instruction.DEY -> { 348 | registers.y = registers.y - 1 and 0xFF 349 | registers.n = registers.y and 0x80 != 0 350 | registers.z = registers.y == 0 351 | } 352 | 353 | Instruction.CLC -> registers.c = false 354 | Instruction.SEC -> registers.c = true 355 | Instruction.CLI -> registers.i = false 356 | Instruction.SEI -> registers.i = true 357 | Instruction.CLD -> registers.d = false 358 | Instruction.SED -> registers.d = true 359 | Instruction.CLV -> registers.v = false 360 | Instruction.LDA -> { 361 | registers.a = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 362 | registers.n = registers.a and 0x80 != 0 363 | registers.z = registers.a == 0 364 | } 365 | 366 | Instruction.LDX -> { 367 | registers.x = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 368 | registers.n = registers.x and 0x80 != 0 369 | registers.z = registers.x == 0 370 | } 371 | 372 | Instruction.LDY -> { 373 | registers.y = if (mode == AddressingMode.IMMEDIATE) operand else read(operand) 374 | registers.n = registers.y and 0x80 != 0 375 | registers.z = registers.y == 0 376 | } 377 | 378 | Instruction.STA -> write(operand, registers.a) 379 | Instruction.STX -> write(operand, registers.x) 380 | Instruction.STY -> write(operand, registers.y) 381 | Instruction.TAX -> { 382 | registers.x = registers.a 383 | registers.n = registers.x and 0x80 != 0 384 | registers.z = registers.x == 0 385 | } 386 | 387 | Instruction.TXA -> { 388 | registers.a = registers.x 389 | registers.n = registers.a and 0x80 != 0 390 | registers.z = registers.a == 0 391 | } 392 | 393 | Instruction.TAY -> { 394 | registers.y = registers.a 395 | registers.n = registers.y and 0x80 != 0 396 | registers.z = registers.y == 0 397 | } 398 | 399 | Instruction.TYA -> { 400 | registers.a = registers.y 401 | registers.n = registers.a and 0x80 != 0 402 | registers.z = registers.a == 0 403 | } 404 | 405 | Instruction.TSX -> { 406 | registers.x = registers.sp 407 | registers.n = registers.x and 0x80 != 0 408 | registers.z = registers.x == 0 409 | } 410 | 411 | Instruction.TXS -> registers.sp = registers.x 412 | Instruction.PHA -> push(registers.a) 413 | Instruction.PLA -> { 414 | registers.a = pop() 415 | registers.n = registers.a and 0x80 != 0 416 | registers.z = registers.a == 0 417 | } 418 | 419 | Instruction.PHP -> { 420 | registers.b = true 421 | pushStatus() 422 | registers.b = false 423 | } 424 | 425 | Instruction.PLP -> { 426 | popStatus() 427 | registers.r = true 428 | registers.b = false 429 | } 430 | 431 | Instruction.NOP -> { 432 | } 433 | 434 | Instruction.NOPD -> { 435 | registers.pc++ 436 | } 437 | 438 | Instruction.NOPI -> { 439 | registers.pc += 2 440 | } 441 | 442 | Instruction.LAX -> { 443 | registers.a = read(operand) 444 | registers.x = registers.a 445 | registers.n = registers.a and 0x80 != 0 446 | registers.z = registers.a == 0 447 | } 448 | 449 | Instruction.SAX -> { 450 | val result = registers.a and registers.x 451 | write(operand, result) 452 | } 453 | 454 | Instruction.DCP -> { 455 | val data = (read(operand) - 1) and 0xFF 456 | registers.n = ((registers.a - data) and 0x1FF) and 0x80 != 0 457 | registers.z = (registers.a - data) and 0x1FF == 0 458 | write(operand, data) 459 | } 460 | 461 | Instruction.ISB -> { 462 | val data = (read(operand) + 1) and 0xFF 463 | val result = (data.inv() and 0xFF) + registers.a + registers.c.toInt() 464 | val overflow = ((registers.a xor data) and 0x80 == 0) && ((registers.a xor result) and 0x80) != 0 465 | registers.v = overflow 466 | registers.c = result > 0xFF 467 | registers.n = result and 0x80 != 0 468 | registers.z = result == 0 469 | registers.a = result and 0xFF 470 | write(operand, data) 471 | } 472 | 473 | Instruction.SLO -> { 474 | var data = read(operand) 475 | registers.c = data and 0x80 != 0 476 | data = (data shl 1) and 0xFF 477 | registers.a = registers.a or data 478 | registers.n = registers.a and 0x80 != 0 479 | registers.z = registers.a and 0xFF == 0 480 | write(operand, data) 481 | } 482 | 483 | Instruction.RLA -> { 484 | val data = ((read(operand) shl 1) and 0xFF) + registers.c.toInt() 485 | registers.c = read(operand) and 0x80 != 0 486 | registers.a = (data and registers.a) and 0xFF 487 | registers.n = registers.a and 0x80 != 0 488 | registers.z = registers.a and 0xFF == 0 489 | write(operand, data) 490 | } 491 | 492 | Instruction.SRE -> { 493 | var data = read(operand) 494 | registers.c = data and 0x01 != 0 495 | data = data shr 1 496 | registers.a = registers.a xor data 497 | registers.n = registers.a and 0x80 != 0 498 | registers.z = registers.a and 0xFF == 0 499 | write(operand, data) 500 | } 501 | 502 | Instruction.RRA -> { 503 | var data = read(operand) 504 | val carry = data and 0x01 != 0 505 | data = (data shr 1) or if (registers.c) 0x80 else 0x00 506 | val result = data + registers.a + carry.toInt() 507 | val overflow = ((registers.a xor data) and 0x80) == 0 && ((registers.a xor result) and 0x80) != 0 508 | registers.v = overflow 509 | registers.n = result and 0x80 != 0 510 | registers.z = result and 0xFF == 0 511 | registers.a = result and 0xFF 512 | registers.c = result > 0xFF 513 | write(operand, data) 514 | } 515 | } 516 | } 517 | 518 | private fun processNmi() { 519 | interrupts.isNmiAsserted = false 520 | registers.b = false 521 | push((registers.pc shr 8) and 0xFF) 522 | push(registers.pc and 0xFF) 523 | pushStatus() 524 | registers.i = true 525 | registers.pc = readWord(0xFFFA) 526 | } 527 | 528 | fun run(): Int { 529 | if (interrupts.isNmiAsserted) { 530 | processNmi() 531 | } 532 | val pc = registers.pc 533 | val opcode = opcodes[fetch(pc)] ?: throw UnknownOpcodeException() 534 | val (instruction, mode, cycle) = opcode 535 | val (operand, additionalCycle) = getOperand(mode) 536 | exec(instruction, mode, operand) 537 | return cycle + additionalCycle + hasBranched.toInt() 538 | } 539 | 540 | companion object { 541 | const val CPU_HZ = 1789773 542 | } 543 | } 544 | -------------------------------------------------------------------------------- /src/main/kotlin/cpu/CpuBus.kt: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import apu.Apu 4 | import cartridge.Rom 5 | import dma.Dma 6 | import pad.Pad 7 | import ppu.Ppu 8 | import ram.Ram 9 | 10 | class CpuBus( 11 | private val ppu: Ppu, 12 | private val apu: Apu, 13 | private val ram: Ram, 14 | private val rom: Rom, 15 | private val dma: Dma, 16 | private val pad: Pad, 17 | ) { 18 | fun read(addr: Int) = 19 | when { 20 | addr < 0x2000 -> ram.read(addr % 0x800) 21 | addr < 0x4000 -> ppu.read((addr - 0x2000) % 8) 22 | addr == 0x4015 -> apu.read(0x15) 23 | addr == 0x4016 -> pad.read() 24 | addr >= 0xC000 -> { 25 | val offset = -if (rom.size <= 0x4000) 0xC000 else 0x8000 26 | rom.read(addr + offset) 27 | } 28 | 29 | addr >= 0x8000 -> rom.read(addr - 0x8000) 30 | else -> 0 31 | } 32 | 33 | fun write(addr: Int, data: Int) { 34 | when { 35 | addr < 0x2000 -> ram.write(addr % 0x800, data) 36 | addr < 0x4000 -> ppu.write((addr - 0x2000) % 8, data) 37 | addr == 0x4014 -> dma.write(data) 38 | addr == 0x4016 -> pad.write(data) 39 | addr < 0x4020 -> apu.write(addr - 0x4000, data.toUByte()) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/cpu/CpuRegister.kt: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import ext.toInt 4 | 5 | class CpuRegister { 6 | var a = 0 // accumulator 7 | var x = 0 // index register 8 | var y = 0 // index register 9 | var sp = 0xFD // stack pointer 10 | var n = false // status negative 11 | var v = false // status overflow 12 | var r = true // status reserved 13 | var b = false // status break 14 | var d = false // status decimal 15 | var i = true // status interrupt 16 | var z = false // status zero 17 | var c = false // status carry 18 | val status 19 | get() = 20 | (n.toInt() shl 7) or 21 | (v.toInt() shl 6) or 22 | (r.toInt() shl 5) or 23 | (b.toInt() shl 4) or 24 | (d.toInt() shl 3) or 25 | (i.toInt() shl 2) or 26 | (z.toInt() shl 1) or 27 | c.toInt() 28 | var pc = 0 // program counter 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/cpu/Opcode.kt: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | data class Opcode( 4 | val instruction: Instruction, 5 | val mode: AddressingMode, 6 | val cycle: Int, 7 | ) 8 | 9 | enum class Instruction { 10 | ADC, SBC, AND, ORA, EOR, ASL, LSR, ROL, ROR, BCC, 11 | BCS, BEQ, BNE, BVC, BVS, BPL, BMI, BIT, JMP, JSR, 12 | RTS, BRK, RTI, CMP, CPX, CPY, INC, DEC, INX, DEX, 13 | INY, DEY, CLC, SEC, CLI, SEI, CLD, SED, CLV, LDA, 14 | LDX, LDY, STA, STX, STY, TAX, TXA, TAY, TYA, TSX, 15 | TXS, PHA, PLA, PHP, PLP, NOP, NOPD, NOPI, LAX, 16 | SAX, DCP, ISB, SLO, RLA, SRE, RRA 17 | } 18 | 19 | enum class AddressingMode { 20 | ACCUMULATOR, 21 | IMMEDIATE, 22 | ABSOLUTE, 23 | ZERO_PAGE, 24 | ZERO_PAGE_X, 25 | ZERO_PAGE_Y, 26 | ABSOLUTE_X, 27 | ABSOLUTE_Y, 28 | IMPLIED, 29 | RELATIVE, 30 | INDIRECT_X, 31 | INDIRECT_Y, 32 | INDIRECT 33 | } 34 | 35 | val opcodes = mapOf( 36 | 0x69 to Opcode(Instruction.ADC, AddressingMode.IMMEDIATE, 2), 37 | 0x65 to Opcode(Instruction.ADC, AddressingMode.ZERO_PAGE, 3), 38 | 0x75 to Opcode(Instruction.ADC, AddressingMode.ZERO_PAGE_X, 4), 39 | 0x6D to Opcode(Instruction.ADC, AddressingMode.ABSOLUTE, 4), 40 | 0x7D to Opcode(Instruction.ADC, AddressingMode.ABSOLUTE_X, 4), 41 | 0x79 to Opcode(Instruction.ADC, AddressingMode.ABSOLUTE_Y, 4), 42 | 0x61 to Opcode(Instruction.ADC, AddressingMode.INDIRECT_X, 6), 43 | 0x71 to Opcode(Instruction.ADC, AddressingMode.INDIRECT_Y, 5), 44 | 0xE9 to Opcode(Instruction.SBC, AddressingMode.IMMEDIATE, 2), 45 | 0xE5 to Opcode(Instruction.SBC, AddressingMode.ZERO_PAGE, 3), 46 | 0xF5 to Opcode(Instruction.SBC, AddressingMode.ZERO_PAGE_X, 4), 47 | 0xED to Opcode(Instruction.SBC, AddressingMode.ABSOLUTE, 4), 48 | 0xFD to Opcode(Instruction.SBC, AddressingMode.ABSOLUTE_X, 4), 49 | 0xF9 to Opcode(Instruction.SBC, AddressingMode.ABSOLUTE_Y, 4), 50 | 0xE1 to Opcode(Instruction.SBC, AddressingMode.INDIRECT_X, 6), 51 | 0xF1 to Opcode(Instruction.SBC, AddressingMode.INDIRECT_Y, 5), 52 | 0x29 to Opcode(Instruction.AND, AddressingMode.IMMEDIATE, 2), 53 | 0x25 to Opcode(Instruction.AND, AddressingMode.ZERO_PAGE, 3), 54 | 0x35 to Opcode(Instruction.AND, AddressingMode.ZERO_PAGE_X, 4), 55 | 0x2D to Opcode(Instruction.AND, AddressingMode.ABSOLUTE, 4), 56 | 0x3D to Opcode(Instruction.AND, AddressingMode.ABSOLUTE_X, 4), 57 | 0x39 to Opcode(Instruction.AND, AddressingMode.ABSOLUTE_Y, 4), 58 | 0x21 to Opcode(Instruction.AND, AddressingMode.INDIRECT_X, 6), 59 | 0x31 to Opcode(Instruction.AND, AddressingMode.INDIRECT_Y, 5), 60 | 0x09 to Opcode(Instruction.ORA, AddressingMode.IMMEDIATE, 2), 61 | 0x05 to Opcode(Instruction.ORA, AddressingMode.ZERO_PAGE, 3), 62 | 0x15 to Opcode(Instruction.ORA, AddressingMode.ZERO_PAGE_X, 4), 63 | 0x0D to Opcode(Instruction.ORA, AddressingMode.ABSOLUTE, 4), 64 | 0x1D to Opcode(Instruction.ORA, AddressingMode.ABSOLUTE_X, 4), 65 | 0x19 to Opcode(Instruction.ORA, AddressingMode.ABSOLUTE_Y, 4), 66 | 0x01 to Opcode(Instruction.ORA, AddressingMode.INDIRECT_X, 6), 67 | 0x11 to Opcode(Instruction.ORA, AddressingMode.INDIRECT_Y, 5), 68 | 0x49 to Opcode(Instruction.EOR, AddressingMode.IMMEDIATE, 2), 69 | 0x45 to Opcode(Instruction.EOR, AddressingMode.ZERO_PAGE, 3), 70 | 0x55 to Opcode(Instruction.EOR, AddressingMode.ZERO_PAGE_X, 4), 71 | 0x4D to Opcode(Instruction.EOR, AddressingMode.ABSOLUTE, 4), 72 | 0x5D to Opcode(Instruction.EOR, AddressingMode.ABSOLUTE_X, 4), 73 | 0x59 to Opcode(Instruction.EOR, AddressingMode.ABSOLUTE_Y, 4), 74 | 0x41 to Opcode(Instruction.EOR, AddressingMode.INDIRECT_X, 6), 75 | 0x51 to Opcode(Instruction.EOR, AddressingMode.INDIRECT_Y, 5), 76 | 0x0A to Opcode(Instruction.ASL, AddressingMode.ACCUMULATOR, 2), 77 | 0x06 to Opcode(Instruction.ASL, AddressingMode.ZERO_PAGE, 5), 78 | 0x16 to Opcode(Instruction.ASL, AddressingMode.ZERO_PAGE_X, 6), 79 | 0x0E to Opcode(Instruction.ASL, AddressingMode.ABSOLUTE, 6), 80 | 0x1E to Opcode(Instruction.ASL, AddressingMode.ABSOLUTE_X, 6), 81 | 0x4A to Opcode(Instruction.LSR, AddressingMode.ACCUMULATOR, 2), 82 | 0x46 to Opcode(Instruction.LSR, AddressingMode.ZERO_PAGE, 5), 83 | 0x56 to Opcode(Instruction.LSR, AddressingMode.ZERO_PAGE_X, 6), 84 | 0x4E to Opcode(Instruction.LSR, AddressingMode.ABSOLUTE, 6), 85 | 0x5E to Opcode(Instruction.LSR, AddressingMode.ABSOLUTE_X, 6), 86 | 0x2A to Opcode(Instruction.ROL, AddressingMode.ACCUMULATOR, 2), 87 | 0x26 to Opcode(Instruction.ROL, AddressingMode.ZERO_PAGE, 5), 88 | 0x36 to Opcode(Instruction.ROL, AddressingMode.ZERO_PAGE_X, 6), 89 | 0x2E to Opcode(Instruction.ROL, AddressingMode.ABSOLUTE, 6), 90 | 0x3E to Opcode(Instruction.ROL, AddressingMode.ABSOLUTE_X, 6), 91 | 0x6A to Opcode(Instruction.ROR, AddressingMode.ACCUMULATOR, 2), 92 | 0x66 to Opcode(Instruction.ROR, AddressingMode.ZERO_PAGE, 5), 93 | 0x76 to Opcode(Instruction.ROR, AddressingMode.ZERO_PAGE_X, 6), 94 | 0x6E to Opcode(Instruction.ROR, AddressingMode.ABSOLUTE, 6), 95 | 0x7E to Opcode(Instruction.ROR, AddressingMode.ABSOLUTE_X, 6), 96 | 0x90 to Opcode(Instruction.BCC, AddressingMode.RELATIVE, 2), 97 | 0xB0 to Opcode(Instruction.BCS, AddressingMode.RELATIVE, 2), 98 | 0xF0 to Opcode(Instruction.BEQ, AddressingMode.RELATIVE, 2), 99 | 0xD0 to Opcode(Instruction.BNE, AddressingMode.RELATIVE, 2), 100 | 0x50 to Opcode(Instruction.BVC, AddressingMode.RELATIVE, 2), 101 | 0x70 to Opcode(Instruction.BVS, AddressingMode.RELATIVE, 2), 102 | 0x10 to Opcode(Instruction.BPL, AddressingMode.RELATIVE, 2), 103 | 0x30 to Opcode(Instruction.BMI, AddressingMode.RELATIVE, 2), 104 | 0x24 to Opcode(Instruction.BIT, AddressingMode.ZERO_PAGE, 3), 105 | 0x2C to Opcode(Instruction.BIT, AddressingMode.ABSOLUTE, 4), 106 | 0x4C to Opcode(Instruction.JMP, AddressingMode.ABSOLUTE, 3), 107 | 0x6C to Opcode(Instruction.JMP, AddressingMode.INDIRECT, 4), 108 | 0x20 to Opcode(Instruction.JSR, AddressingMode.ABSOLUTE, 6), 109 | 0x60 to Opcode(Instruction.RTS, AddressingMode.IMPLIED, 6), 110 | 0x00 to Opcode(Instruction.BRK, AddressingMode.IMPLIED, 7), 111 | 0x40 to Opcode(Instruction.RTI, AddressingMode.IMPLIED, 6), 112 | 0xC9 to Opcode(Instruction.CMP, AddressingMode.IMMEDIATE, 2), 113 | 0xC5 to Opcode(Instruction.CMP, AddressingMode.ZERO_PAGE, 3), 114 | 0xD5 to Opcode(Instruction.CMP, AddressingMode.ZERO_PAGE_X, 4), 115 | 0xCD to Opcode(Instruction.CMP, AddressingMode.ABSOLUTE, 4), 116 | 0xDD to Opcode(Instruction.CMP, AddressingMode.ABSOLUTE_X, 4), 117 | 0xD9 to Opcode(Instruction.CMP, AddressingMode.ABSOLUTE_Y, 4), 118 | 0xC1 to Opcode(Instruction.CMP, AddressingMode.INDIRECT_X, 6), 119 | 0xD1 to Opcode(Instruction.CMP, AddressingMode.INDIRECT_Y, 5), 120 | 0xE0 to Opcode(Instruction.CPX, AddressingMode.IMMEDIATE, 2), 121 | 0xE4 to Opcode(Instruction.CPX, AddressingMode.ZERO_PAGE, 3), 122 | 0xEC to Opcode(Instruction.CPX, AddressingMode.ABSOLUTE, 4), 123 | 0xC0 to Opcode(Instruction.CPY, AddressingMode.IMMEDIATE, 2), 124 | 0xC4 to Opcode(Instruction.CPY, AddressingMode.ZERO_PAGE, 3), 125 | 0xCC to Opcode(Instruction.CPY, AddressingMode.ABSOLUTE, 4), 126 | 0xE6 to Opcode(Instruction.INC, AddressingMode.ZERO_PAGE, 5), 127 | 0xF6 to Opcode(Instruction.INC, AddressingMode.ZERO_PAGE_X, 6), 128 | 0xEE to Opcode(Instruction.INC, AddressingMode.ABSOLUTE, 6), 129 | 0xFE to Opcode(Instruction.INC, AddressingMode.ABSOLUTE_X, 6), 130 | 0xC6 to Opcode(Instruction.DEC, AddressingMode.ZERO_PAGE, 5), 131 | 0xD6 to Opcode(Instruction.DEC, AddressingMode.ZERO_PAGE_X, 6), 132 | 0xCE to Opcode(Instruction.DEC, AddressingMode.ABSOLUTE, 6), 133 | 0xDE to Opcode(Instruction.DEC, AddressingMode.ABSOLUTE_X, 6), 134 | 0xE8 to Opcode(Instruction.INX, AddressingMode.IMPLIED, 2), 135 | 0xCA to Opcode(Instruction.DEX, AddressingMode.IMPLIED, 2), 136 | 0xC8 to Opcode(Instruction.INY, AddressingMode.IMPLIED, 2), 137 | 0x88 to Opcode(Instruction.DEY, AddressingMode.IMPLIED, 2), 138 | 0x18 to Opcode(Instruction.CLC, AddressingMode.IMPLIED, 2), 139 | 0x38 to Opcode(Instruction.SEC, AddressingMode.IMPLIED, 2), 140 | 0x58 to Opcode(Instruction.CLI, AddressingMode.IMPLIED, 2), 141 | 0x78 to Opcode(Instruction.SEI, AddressingMode.IMPLIED, 2), 142 | 0xD8 to Opcode(Instruction.CLD, AddressingMode.IMPLIED, 2), 143 | 0xF8 to Opcode(Instruction.SED, AddressingMode.IMPLIED, 2), 144 | 0xB8 to Opcode(Instruction.CLV, AddressingMode.IMPLIED, 2), 145 | 0xA9 to Opcode(Instruction.LDA, AddressingMode.IMMEDIATE, 2), 146 | 0xA5 to Opcode(Instruction.LDA, AddressingMode.ZERO_PAGE, 3), 147 | 0xB5 to Opcode(Instruction.LDA, AddressingMode.ZERO_PAGE_X, 4), 148 | 0xAD to Opcode(Instruction.LDA, AddressingMode.ABSOLUTE, 4), 149 | 0xBD to Opcode(Instruction.LDA, AddressingMode.ABSOLUTE_X, 4), 150 | 0xB9 to Opcode(Instruction.LDA, AddressingMode.ABSOLUTE_Y, 4), 151 | 0xA1 to Opcode(Instruction.LDA, AddressingMode.INDIRECT_X, 6), 152 | 0xB1 to Opcode(Instruction.LDA, AddressingMode.INDIRECT_Y, 5), 153 | 0xA2 to Opcode(Instruction.LDX, AddressingMode.IMMEDIATE, 2), 154 | 0xA6 to Opcode(Instruction.LDX, AddressingMode.ZERO_PAGE, 3), 155 | 0xB6 to Opcode(Instruction.LDX, AddressingMode.ZERO_PAGE_Y, 4), 156 | 0xAE to Opcode(Instruction.LDX, AddressingMode.ABSOLUTE, 4), 157 | 0xBE to Opcode(Instruction.LDX, AddressingMode.ABSOLUTE_Y, 4), 158 | 0xA0 to Opcode(Instruction.LDY, AddressingMode.IMMEDIATE, 2), 159 | 0xA4 to Opcode(Instruction.LDY, AddressingMode.ZERO_PAGE, 3), 160 | 0xB4 to Opcode(Instruction.LDY, AddressingMode.ZERO_PAGE_X, 4), 161 | 0xAC to Opcode(Instruction.LDY, AddressingMode.ABSOLUTE, 4), 162 | 0xBC to Opcode(Instruction.LDY, AddressingMode.ABSOLUTE_X, 4), 163 | 0x85 to Opcode(Instruction.STA, AddressingMode.ZERO_PAGE, 3), 164 | 0x95 to Opcode(Instruction.STA, AddressingMode.ZERO_PAGE_X, 4), 165 | 0x8D to Opcode(Instruction.STA, AddressingMode.ABSOLUTE, 4), 166 | 0x9D to Opcode(Instruction.STA, AddressingMode.ABSOLUTE_X, 4), 167 | 0x99 to Opcode(Instruction.STA, AddressingMode.ABSOLUTE_Y, 4), 168 | 0x81 to Opcode(Instruction.STA, AddressingMode.INDIRECT_X, 6), 169 | 0x91 to Opcode(Instruction.STA, AddressingMode.INDIRECT_Y, 5), 170 | 0x86 to Opcode(Instruction.STX, AddressingMode.ZERO_PAGE, 3), 171 | 0x96 to Opcode(Instruction.STX, AddressingMode.ZERO_PAGE_Y, 4), 172 | 0x8E to Opcode(Instruction.STX, AddressingMode.ABSOLUTE, 4), 173 | 0x84 to Opcode(Instruction.STY, AddressingMode.ZERO_PAGE, 3), 174 | 0x94 to Opcode(Instruction.STY, AddressingMode.ZERO_PAGE_X, 4), 175 | 0x8C to Opcode(Instruction.STY, AddressingMode.ABSOLUTE, 4), 176 | 0xAA to Opcode(Instruction.TAX, AddressingMode.IMPLIED, 2), 177 | 0x8A to Opcode(Instruction.TXA, AddressingMode.IMPLIED, 2), 178 | 0xA8 to Opcode(Instruction.TAY, AddressingMode.IMPLIED, 2), 179 | 0x98 to Opcode(Instruction.TYA, AddressingMode.IMPLIED, 2), 180 | 0x9A to Opcode(Instruction.TXS, AddressingMode.IMPLIED, 2), 181 | 0xBA to Opcode(Instruction.TSX, AddressingMode.IMPLIED, 2), 182 | 0x48 to Opcode(Instruction.PHA, AddressingMode.IMPLIED, 3), 183 | 0x68 to Opcode(Instruction.PLA, AddressingMode.IMPLIED, 4), 184 | 0x08 to Opcode(Instruction.PHP, AddressingMode.IMPLIED, 3), 185 | 0x28 to Opcode(Instruction.PLP, AddressingMode.IMPLIED, 4), 186 | 0x1A to Opcode(Instruction.NOP, AddressingMode.IMPLIED, 2), 187 | 0x3A to Opcode(Instruction.NOP, AddressingMode.IMPLIED, 2), 188 | 0x5A to Opcode(Instruction.NOP, AddressingMode.IMPLIED, 2), 189 | 0x7A to Opcode(Instruction.NOP, AddressingMode.IMPLIED, 2), 190 | 0xDA to Opcode(Instruction.NOP, AddressingMode.IMPLIED, 2), 191 | 0xEA to Opcode(Instruction.NOP, AddressingMode.IMPLIED, 2), 192 | 0xFA to Opcode(Instruction.NOP, AddressingMode.IMPLIED, 2), 193 | 0x80 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 2), 194 | 0x82 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 2), 195 | 0x89 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 2), 196 | 0xC2 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 2), 197 | 0xE2 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 2), 198 | 0x04 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 3), 199 | 0x44 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 3), 200 | 0x64 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 3), 201 | 0x14 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 4), 202 | 0x34 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 4), 203 | 0x54 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 4), 204 | 0x74 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 4), 205 | 0xD4 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 4), 206 | 0xF4 to Opcode(Instruction.NOPD, AddressingMode.IMPLIED, 4), 207 | 0x0C to Opcode(Instruction.NOPI, AddressingMode.IMPLIED, 4), 208 | 0x1C to Opcode(Instruction.NOPI, AddressingMode.IMPLIED, 4), 209 | 0x3C to Opcode(Instruction.NOPI, AddressingMode.IMPLIED, 4), 210 | 0x5C to Opcode(Instruction.NOPI, AddressingMode.IMPLIED, 4), 211 | 0x7C to Opcode(Instruction.NOPI, AddressingMode.IMPLIED, 4), 212 | 0xDC to Opcode(Instruction.NOPI, AddressingMode.IMPLIED, 4), 213 | 0xFC to Opcode(Instruction.NOPI, AddressingMode.IMPLIED, 4), 214 | 0xA3 to Opcode(Instruction.LAX, AddressingMode.INDIRECT_X, 6), 215 | 0xA7 to Opcode(Instruction.LAX, AddressingMode.ZERO_PAGE, 3), 216 | 0xAF to Opcode(Instruction.LAX, AddressingMode.ABSOLUTE, 4), 217 | 0xB3 to Opcode(Instruction.LAX, AddressingMode.INDIRECT_Y, 5), 218 | 0xB7 to Opcode(Instruction.LAX, AddressingMode.ZERO_PAGE_Y, 4), 219 | 0xBF to Opcode(Instruction.LAX, AddressingMode.ABSOLUTE_Y, 4), 220 | 0x83 to Opcode(Instruction.SAX, AddressingMode.INDIRECT_X, 6), 221 | 0x87 to Opcode(Instruction.SAX, AddressingMode.ZERO_PAGE, 3), 222 | 0x8F to Opcode(Instruction.SAX, AddressingMode.ABSOLUTE, 4), 223 | 0x97 to Opcode(Instruction.SAX, AddressingMode.ZERO_PAGE_Y, 4), 224 | 0xEB to Opcode(Instruction.SBC, AddressingMode.IMMEDIATE, 2), 225 | 0xC3 to Opcode(Instruction.DCP, AddressingMode.INDIRECT_X, 8), 226 | 0xC7 to Opcode(Instruction.DCP, AddressingMode.ZERO_PAGE, 5), 227 | 0xCF to Opcode(Instruction.DCP, AddressingMode.ABSOLUTE, 6), 228 | 0xD3 to Opcode(Instruction.DCP, AddressingMode.INDIRECT_Y, 8), 229 | 0xD7 to Opcode(Instruction.DCP, AddressingMode.ZERO_PAGE_X, 6), 230 | 0xDB to Opcode(Instruction.DCP, AddressingMode.ABSOLUTE_Y, 7), 231 | 0xDF to Opcode(Instruction.DCP, AddressingMode.ABSOLUTE_X, 7), 232 | 0xE3 to Opcode(Instruction.ISB, AddressingMode.INDIRECT_X, 8), 233 | 0xE7 to Opcode(Instruction.ISB, AddressingMode.ZERO_PAGE, 5), 234 | 0xEF to Opcode(Instruction.ISB, AddressingMode.ABSOLUTE, 6), 235 | 0xF3 to Opcode(Instruction.ISB, AddressingMode.INDIRECT_Y, 8), 236 | 0xF7 to Opcode(Instruction.ISB, AddressingMode.ZERO_PAGE_X, 6), 237 | 0xFB to Opcode(Instruction.ISB, AddressingMode.ABSOLUTE_Y, 7), 238 | 0xFF to Opcode(Instruction.ISB, AddressingMode.ABSOLUTE_X, 7), 239 | 0x03 to Opcode(Instruction.SLO, AddressingMode.INDIRECT_X, 8), 240 | 0x07 to Opcode(Instruction.SLO, AddressingMode.ZERO_PAGE, 5), 241 | 0x0F to Opcode(Instruction.SLO, AddressingMode.ABSOLUTE, 6), 242 | 0x13 to Opcode(Instruction.SLO, AddressingMode.INDIRECT_Y, 8), 243 | 0x17 to Opcode(Instruction.SLO, AddressingMode.ZERO_PAGE_X, 6), 244 | 0x1B to Opcode(Instruction.SLO, AddressingMode.ABSOLUTE_Y, 7), 245 | 0x1F to Opcode(Instruction.SLO, AddressingMode.ABSOLUTE_X, 7), 246 | 0x23 to Opcode(Instruction.RLA, AddressingMode.INDIRECT_X, 8), 247 | 0x27 to Opcode(Instruction.RLA, AddressingMode.ZERO_PAGE, 5), 248 | 0x2F to Opcode(Instruction.RLA, AddressingMode.ABSOLUTE, 6), 249 | 0x33 to Opcode(Instruction.RLA, AddressingMode.INDIRECT_Y, 7), 250 | 0x37 to Opcode(Instruction.RLA, AddressingMode.ZERO_PAGE_X, 6), 251 | 0x3B to Opcode(Instruction.RLA, AddressingMode.ABSOLUTE_Y, 7), 252 | 0x3F to Opcode(Instruction.RLA, AddressingMode.ABSOLUTE_X, 7), 253 | 0x43 to Opcode(Instruction.SRE, AddressingMode.INDIRECT_X, 8), 254 | 0x47 to Opcode(Instruction.SRE, AddressingMode.ZERO_PAGE, 5), 255 | 0x4F to Opcode(Instruction.SRE, AddressingMode.ABSOLUTE, 6), 256 | 0x53 to Opcode(Instruction.SRE, AddressingMode.INDIRECT_Y, 8), 257 | 0x57 to Opcode(Instruction.SRE, AddressingMode.ZERO_PAGE_X, 6), 258 | 0x5B to Opcode(Instruction.SRE, AddressingMode.ABSOLUTE_Y, 7), 259 | 0x5F to Opcode(Instruction.SRE, AddressingMode.ABSOLUTE_X, 7), 260 | 0x63 to Opcode(Instruction.RRA, AddressingMode.INDIRECT_X, 8), 261 | 0x67 to Opcode(Instruction.RRA, AddressingMode.ZERO_PAGE, 5), 262 | 0x6F to Opcode(Instruction.RRA, AddressingMode.ABSOLUTE, 6), 263 | 0x73 to Opcode(Instruction.RRA, AddressingMode.INDIRECT_Y, 8), 264 | 0x77 to Opcode(Instruction.RRA, AddressingMode.ZERO_PAGE_X, 6), 265 | 0x7B to Opcode(Instruction.RRA, AddressingMode.ABSOLUTE_Y, 7), 266 | 0x7F to Opcode(Instruction.RRA, AddressingMode.ABSOLUTE_X, 7) 267 | ) 268 | -------------------------------------------------------------------------------- /src/main/kotlin/dma/Dma.kt: -------------------------------------------------------------------------------- 1 | package dma 2 | 3 | import ppu.Ppu 4 | import ram.Ram 5 | 6 | class Dma( 7 | private val ppu: Ppu, 8 | private val ram: Ram, 9 | ) { 10 | var isProcessing = false 11 | var ramAddr = 0 12 | 13 | fun run() { 14 | if (!isProcessing) return 15 | for (i in 0 until 0x100) { 16 | ppu.transferSprite(i, ram.read(ramAddr + i)) 17 | } 18 | isProcessing = false 19 | } 20 | 21 | fun write(data: Int) { 22 | ramAddr = data shl 8 23 | isProcessing = true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/exception/UnknownOpcodeException.kt: -------------------------------------------------------------------------------- 1 | package exception 2 | 3 | class UnknownOpcodeException : RuntimeException() 4 | -------------------------------------------------------------------------------- /src/main/kotlin/ext/BooleanExt.kt: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | fun Boolean.toInt() = if (this) 1 else 0 4 | -------------------------------------------------------------------------------- /src/main/kotlin/ext/ByteArrayExt.kt: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | fun ByteArray.toHex() = 4 | this.map { String.format("%02X", it) }.reduce { acc, s -> acc + s } 5 | -------------------------------------------------------------------------------- /src/main/kotlin/ext/ByteExt.kt: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | fun Byte.toUnsignedInt() = this.toInt() and 0xFF 4 | -------------------------------------------------------------------------------- /src/main/kotlin/ext/FileInputStreamExt.kt: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | import java.io.FileInputStream 4 | import java.math.BigInteger 5 | 6 | fun FileInputStream.read(len: Int) = 7 | ByteArray(len).apply { read(this) } 8 | 9 | fun FileInputStream.readAsHex(len: Int) = 10 | read(len).toHex() 11 | 12 | fun FileInputStream.readAsInt(len: Int) = 13 | BigInteger(read(len).toHex(), 16).toInt() 14 | -------------------------------------------------------------------------------- /src/main/kotlin/interrupts/Interrupts.kt: -------------------------------------------------------------------------------- 1 | package interrupts 2 | 3 | class Interrupts { 4 | var isNmiAsserted = false 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/pad/JavaFXKeyEvent.kt: -------------------------------------------------------------------------------- 1 | package pad 2 | 3 | import javafx.scene.Scene 4 | import javafx.scene.input.KeyCode 5 | 6 | class JavaFXKeyEvent( 7 | private val scene: Scene, 8 | ) : KeyEvent { 9 | override fun listen(listener: KeyEventListener) { 10 | scene.setOnKeyPressed { 11 | when (it.code) { 12 | KeyCode.K -> listener.onKeyDown(Key.A) 13 | KeyCode.J -> listener.onKeyDown(Key.B) 14 | KeyCode.SHIFT -> listener.onKeyDown(Key.SELECT) 15 | KeyCode.ENTER -> listener.onKeyDown(Key.START) 16 | KeyCode.W -> listener.onKeyDown(Key.UP) 17 | KeyCode.S -> listener.onKeyDown(Key.DOWN) 18 | KeyCode.A -> listener.onKeyDown(Key.LEFT) 19 | KeyCode.D -> listener.onKeyDown(Key.RIGHT) 20 | else -> { 21 | } 22 | } 23 | } 24 | scene.setOnKeyReleased { 25 | when (it.code) { 26 | KeyCode.K -> listener.onKeyUp(Key.A) 27 | KeyCode.J -> listener.onKeyUp(Key.B) 28 | KeyCode.SHIFT -> listener.onKeyUp(Key.SELECT) 29 | KeyCode.ENTER -> listener.onKeyUp(Key.START) 30 | KeyCode.W -> listener.onKeyUp(Key.UP) 31 | KeyCode.S -> listener.onKeyUp(Key.DOWN) 32 | KeyCode.A -> listener.onKeyUp(Key.LEFT) 33 | KeyCode.D -> listener.onKeyUp(Key.RIGHT) 34 | else -> { 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/pad/Key.kt: -------------------------------------------------------------------------------- 1 | package pad 2 | 3 | enum class Key(val keyCode: Int) { 4 | A(0), 5 | B(1), 6 | SELECT(2), 7 | START(3), 8 | UP(4), 9 | DOWN(5), 10 | LEFT(6), 11 | RIGHT(7) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/pad/KeyEvent.kt: -------------------------------------------------------------------------------- 1 | package pad 2 | 3 | interface KeyEvent { 4 | fun listen(listener: KeyEventListener) 5 | } 6 | 7 | interface KeyEventListener { 8 | fun onKeyDown(key: Key) 9 | fun onKeyUp(key: Key) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/pad/Pad.kt: -------------------------------------------------------------------------------- 1 | package pad 2 | 3 | import ext.toInt 4 | 5 | class Pad( 6 | keyEvent: KeyEvent, 7 | ) { 8 | private var isSet = false 9 | private var index = 0 10 | private val registers = Array(8, init = { false }) 11 | private val buffer = Array(8, init = { false }) 12 | 13 | init { 14 | keyEvent.listen(object : KeyEventListener { 15 | override fun onKeyDown(key: Key) { 16 | buffer[key.keyCode] = true 17 | } 18 | 19 | override fun onKeyUp(key: Key) { 20 | buffer[key.keyCode] = false 21 | } 22 | }) 23 | } 24 | 25 | // TODO: fix a bug (use nestest ROM) 26 | fun read() = registers[index++].toInt() 27 | 28 | fun write(data: Int) { 29 | if (data != 0) { 30 | isSet = true 31 | } else if (isSet && data == 0) { 32 | isSet = false 33 | index = 0 34 | buffer.forEachIndexed { idx, b -> registers[idx] = b } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/ppu/Canvas.kt: -------------------------------------------------------------------------------- 1 | package ppu 2 | 3 | interface Canvas { 4 | fun bulkDrawDots(data: List) { 5 | data.forEach { drawDot(it.x, it.y, it.r, it.g, it.b) } 6 | } 7 | 8 | fun drawDot(x: Int, y: Int, r: Int, g: Int, b: Int) 9 | fun rendered() 10 | 11 | class RenderingData( 12 | val x: Int, 13 | val y: Int, 14 | val r: Int, 15 | val g: Int, 16 | val b: Int, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/ppu/JavaFXCanvas.kt: -------------------------------------------------------------------------------- 1 | package ppu 2 | 3 | import javafx.application.Platform 4 | import javafx.scene.canvas.GraphicsContext 5 | 6 | class JavaFXCanvas( 7 | graphics: GraphicsContext, 8 | ) : Canvas { 9 | private val pw = graphics.pixelWriter 10 | 11 | override fun bulkDrawDots(data: List) { 12 | Platform.runLater { 13 | super.bulkDrawDots(data) 14 | } 15 | } 16 | 17 | override fun rendered() {} 18 | 19 | override fun drawDot(x: Int, y: Int, r: Int, g: Int, b: Int) { 20 | val c = (0xFF shl 24) or (r shl 16) or (g shl 8) or b 21 | val actualX = x * 2 22 | val actualY = y * 2 23 | pw.setArgb(actualX, actualY, c) 24 | pw.setArgb(actualX + 1, actualY, c) 25 | pw.setArgb(actualX, actualY + 1, c) 26 | pw.setArgb(actualX + 1, actualY + 1, c) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/ppu/PaletteRam.kt: -------------------------------------------------------------------------------- 1 | package ppu 2 | 3 | import ram.Ram 4 | 5 | class PaletteRam { 6 | private val ram = Ram(0x20) 7 | 8 | val data 9 | get() = 10 | (0 until ram.size).map { 11 | ram.read(it) 12 | }.mapIndexed { idx, data -> 13 | return@mapIndexed when { 14 | isSpriteMirror(idx) -> ram.read(idx - 0x10) 15 | isBackgroundMirror(idx) -> ram.read(0x00) 16 | else -> data 17 | } 18 | }.toIntArray() 19 | 20 | fun write(addr: Int, data: Int) { 21 | ram.write(calcPaletteAddr(addr), data) 22 | } 23 | 24 | private fun calcPaletteAddr(addr: Int): Int { 25 | val paletteAddr = (addr and 0xFF) % 0x20 26 | return paletteAddr - if (isSpriteMirror(paletteAddr)) 0x10 else 0 27 | } 28 | 29 | private fun isSpriteMirror(addr: Int) = 30 | addr == 0x10 || addr == 0x14 || addr == 0x18 || addr == 0x1C 31 | 32 | private fun isBackgroundMirror(addr: Int) = 33 | addr == 0x04 || addr == 0x08 || addr == 0x0c 34 | } 35 | -------------------------------------------------------------------------------- /src/main/kotlin/ppu/Ppu.kt: -------------------------------------------------------------------------------- 1 | package ppu 2 | 3 | import interrupts.Interrupts 4 | import ram.Ram 5 | import util.pairs 6 | import util.twoDim 7 | 8 | class Ppu( 9 | private val chrRam: Ram, 10 | private val canvas: Canvas, 11 | private val interrupts: Interrupts, 12 | private val isHorizontalMirror: Boolean, 13 | ) { 14 | private val vRam = Ram(0x2000) 15 | private val palette = PaletteRam() 16 | 17 | private var cycle = 0 18 | private var line = 0 19 | private val background = mutableListOf() 20 | 21 | private var ppuAddr = 0 22 | private var isLowerPpuAddr = false 23 | private val isVRamAddr get() = ppuAddr >= 0x2000 24 | private var vRamReadBuf = 0 25 | 26 | private val spriteRam = Ram(0x100) 27 | private var spriteRamAddr = 0 28 | 29 | private val registers = IntArray(8) 30 | private val sprites = arrayOfNulls(0x1000) 31 | 32 | private val isIrqEnabled get() = registers[0] and 0x80 != 0 33 | 34 | private var scrollX = 0 35 | private var scrollY = 0 36 | private var nameTableId = 0 37 | private val scrollTileX get() = (scrollX + (nameTableId % 2) * 256) / 8 38 | private val scrollTileY get() = (scrollY + (nameTableId / 2) * 240) / 8 39 | private val tileY get() = line / 8 + scrollTileY 40 | 41 | private val vRamOffset get() = if (registers[0x00] and 0x04 != 0) 32 else 1 42 | 43 | private val isBackgroundEnabled get() = registers[0x01] and 0x08 != 0 44 | private val isSpriteEnabled get() = registers[0x01] and 0x10 != 0 45 | 46 | private var isHorizontalScroll = true 47 | 48 | private val isSprite8x8 get() = registers[0x00] and 0x20 == 0 49 | 50 | class Tile( 51 | val sprite: List, 52 | val paletteId: Int, 53 | val scrollX: Int, 54 | val scrollY: Int, 55 | ) 56 | 57 | class SpriteWithAttributes( 58 | val sprites: List, 59 | val x: Int, 60 | val y: Int, 61 | val attrs: Int, 62 | ) 63 | 64 | private fun hasSpriteHit(): Boolean { 65 | val y = spriteRam.read(0) 66 | return y == line && isBackgroundEnabled && isSpriteEnabled 67 | } 68 | 69 | private fun setSpriteHit() { 70 | registers[0x02] = registers[0x02] or 0x40 71 | } 72 | 73 | private fun clearSpriteHit() { 74 | registers[0x02] = registers[0x02] and 0xBF 75 | } 76 | 77 | private fun clearVBlank() { 78 | registers[0x02] = registers[0x02] and 0x7F 79 | } 80 | 81 | fun run(cycle: Int): Boolean { 82 | this.cycle += cycle 83 | 84 | if (line == 0) { 85 | buildSprites() 86 | } 87 | 88 | if (this.cycle >= 341) { 89 | this.cycle -= 341 90 | line++ 91 | 92 | if (hasSpriteHit()) { 93 | setSpriteHit() 94 | } 95 | 96 | if (line <= 240 && line % 8 == 0 && scrollY <= 240) { 97 | buildBackground() 98 | } 99 | 100 | if (line == 241) { 101 | registers[2] = registers[2] or 0x80 102 | if (isIrqEnabled) { 103 | interrupts.isNmiAsserted = true 104 | } 105 | } 106 | 107 | if (line == 262) { 108 | clearVBlank() 109 | clearSpriteHit() 110 | line = 0 111 | render() 112 | background.clear() 113 | interrupts.isNmiAsserted = false 114 | return true 115 | } 116 | } 117 | 118 | return false 119 | } 120 | 121 | private fun render() { 122 | val renderingData = mutableListOf() 123 | val colorData = palette.data 124 | if (isBackgroundEnabled) { 125 | background.forEachIndexed { idx, tile -> 126 | val offsetX = tile.scrollX % 8 127 | val offsetY = tile.scrollY % 8 128 | val tileX = (idx % 33) * 8 129 | val tileY = (idx / 33) * 8 130 | 131 | pairs((0 until 8), (0 until 8)).forEach { 132 | val (i, j) = it 133 | val paletteIdx = tile.paletteId * 4 + tile.sprite[i][j] 134 | val colorId = colorData[paletteIdx] 135 | val color = COLORS[colorId] 136 | val x = tileX + j - offsetX 137 | val y = tileY + i - offsetY 138 | renderingData += Canvas.RenderingData(x, y, color[0], color[1], color[2]) 139 | } 140 | } 141 | } 142 | 143 | if (isSpriteEnabled) { 144 | sprites.forEach { sprite -> 145 | if (sprite == null) return@forEach 146 | val isVerticalReverse = sprite.attrs and 0x80 != 0 147 | val isHorizontalReverse = sprite.attrs and 0x40 != 0 148 | val isLowPriority = sprite.attrs and 0x20 != 0 149 | val paletteId = sprite.attrs and 0x03 150 | pairs((0 until sprite.sprites.size), (0 until 8)).forEach { 151 | val (i, j) = it 152 | val x = sprite.x + if (isHorizontalReverse) 7 - j else j 153 | val y = sprite.y + if (isVerticalReverse) sprite.sprites.size - 1 - i else i 154 | if (!(isLowPriority && shouldPixelHide(x, y)) && sprite.sprites[i][j] != 0) { 155 | val colorId = colorData[paletteId * 4 + sprite.sprites[i][j] + 0x10] 156 | val color = COLORS[colorId] 157 | renderingData += Canvas.RenderingData(x, y, color[0], color[1], color[2]) 158 | } 159 | } 160 | } 161 | } 162 | 163 | canvas.bulkDrawDots(renderingData) 164 | } 165 | 166 | private fun buildBackground() { 167 | val clampedTileY = tileY % 30 168 | val tableIdOffset = if ((tileY / 30) % 2 != 0) 2 else 0 169 | for (x in 0..32) { 170 | val tileX = x + scrollTileX 171 | val clampedTileX = tileX % 32 172 | val nameTableId = (tileX / 32) % 2 + tableIdOffset 173 | val offset = nameTableId * 0x400 174 | background += buildTile(clampedTileX, clampedTileY, offset) 175 | } 176 | } 177 | 178 | private fun buildTile(tileX: Int, tileY: Int, offset: Int): Tile { 179 | val tileNumber = tileY * 32 + tileX 180 | val spriteAddr = calcSpriteAddr(tileNumber + offset) 181 | val spriteId = vRam.read(spriteAddr) 182 | val bgTableOffset = if (registers[0] and 0x10 != 0) 0x1000 else 0x0000 183 | val sprite = buildSprite(spriteId, bgTableOffset) 184 | val attr = getAttribute(tileX, tileY, offset) 185 | val blockId = getBlockId(tileX, tileY) 186 | val paletteId = attr shr (blockId * 2) and 0x03 187 | return Tile(sprite, paletteId, scrollX, scrollY) 188 | } 189 | 190 | private fun calcSpriteAddr(addr: Int): Int { 191 | if (!isHorizontalMirror) return addr 192 | if (addr in 0x400 until 0x800 || addr >= 0x0C00) { 193 | return addr - 0x400 194 | } 195 | return addr 196 | } 197 | 198 | private fun buildSprites() { 199 | for (i in 0 until 0x100 step 4) { 200 | val y = spriteRam.read(i) - 8 201 | if (y < 0) return 202 | var spriteId = spriteRam.read(i + 1) 203 | val attr = spriteRam.read(i + 2) 204 | val x = spriteRam.read(i + 3) 205 | var offset: Int 206 | if (isSprite8x8) { 207 | offset = if (registers[0] and 0x08 != 0) 0x1000 else 0x0000 208 | } else { 209 | offset = 0x1000 * (spriteId and 0x01) 210 | spriteId = spriteId and 0xFE 211 | } 212 | val sprite = buildSprite(spriteId, offset) 213 | sprites[i / 4] = SpriteWithAttributes(sprite, x, y, attr) 214 | } 215 | } 216 | 217 | private fun buildSprite(id: Int, offset: Int = 0): List { 218 | val height = if (isSprite8x8) 1 else 2 219 | return twoDim(8, 8 * height).apply { 220 | (0 until height).forEach { idx -> 221 | pairs((0..15), (0..7)).forEach { 222 | val (i, j) = it 223 | val ram = chrRam.read((id + idx) * 16 + i + offset) 224 | if ((ram and (0x80 shr j)) != 0) { 225 | this[idx * 8 + i % 8][j] += 0x01 shl (i / 8) 226 | } 227 | } 228 | } 229 | } 230 | } 231 | 232 | private fun shouldPixelHide(x: Int, y: Int): Boolean { 233 | val tileX = x / 8 234 | val tileY = y / 8 235 | val bgIdx = tileY * 33 + tileX 236 | if (bgIdx >= background.size) return true 237 | val sprite = background[bgIdx].sprite 238 | return sprite[y % 8][x % 8] % 4 != 0 239 | } 240 | 241 | private fun getAttribute(tileX: Int, tileY: Int, offset: Int): Int { 242 | val addr = tileX / 4 + (tileY / 4) * 8 + 0x03C0 + offset 243 | val spriteAddr = calcSpriteAddr(addr) 244 | return vRam.read(spriteAddr) 245 | } 246 | 247 | private fun getBlockId(tileX: Int, tileY: Int) = 248 | (tileX % 4) / 2 + ((tileY % 4) / 2) * 2 249 | 250 | fun read(addr: Int) = 251 | when (addr) { 252 | PPUSTATUS -> { 253 | val data = registers[0x02] 254 | isHorizontalScroll = true 255 | clearVBlank() 256 | data 257 | } 258 | 259 | PPUDATA -> readVRam() 260 | else -> 0 261 | } 262 | 263 | private fun readVRam(): Int { 264 | val buf = vRamReadBuf 265 | if (ppuAddr >= 0x2000) { 266 | val addr = calcVRamAddr() 267 | ppuAddr += vRamOffset 268 | if (addr >= 0x3F00) return vRam.read(addr) 269 | vRamReadBuf = vRam.read(addr) 270 | } else { 271 | vRamReadBuf = chrRam.read(ppuAddr) 272 | ppuAddr += vRamOffset 273 | } 274 | return buf 275 | } 276 | 277 | private fun calcVRamAddr() = ppuAddr - if (ppuAddr in 0x3000 until 0x3f00) 0x3000 else 0x2000 278 | 279 | fun write(addr: Int, data: Int) { 280 | when (addr) { 281 | OAMADDR -> spriteRamAddr = data 282 | OAMDATA -> { 283 | spriteRam.write(spriteRamAddr, data) 284 | spriteRamAddr++ 285 | } 286 | 287 | PPUSCROLL -> writeScrollData(data) 288 | PPUADDR -> writePpuAddr(data) 289 | PPUDATA -> writePpuData(data) 290 | else -> { 291 | if (addr == 0) { 292 | nameTableId = data and 0x03 293 | } 294 | this.registers[addr] = data 295 | } 296 | } 297 | } 298 | 299 | private fun writePpuAddr(data: Int) { 300 | if (isLowerPpuAddr) { 301 | ppuAddr += data 302 | isLowerPpuAddr = false 303 | nameTableId = (ppuAddr and 0b110000000000) shr 10 304 | } else { 305 | ppuAddr = data shl 8 306 | isLowerPpuAddr = true 307 | } 308 | } 309 | 310 | private fun writePpuData(data: Int) { 311 | if (isVRamAddr) { 312 | if (ppuAddr in 0x3F00 until 0x4000) { 313 | palette.write(ppuAddr, data) 314 | } else { 315 | vRam.write(calcVRamAddr(), data) 316 | } 317 | } else { 318 | chrRam.write(ppuAddr, data) 319 | } 320 | ppuAddr += vRamOffset 321 | } 322 | 323 | private fun writeScrollData(data: Int) { 324 | if (isHorizontalScroll) { 325 | scrollX = data and 0xFF 326 | } else { 327 | scrollY = data and 0xFF 328 | } 329 | isHorizontalScroll = !isHorizontalScroll 330 | } 331 | 332 | fun transferSprite(idx: Int, data: Int) { 333 | val addr = idx + spriteRamAddr 334 | spriteRam.write(addr % 0x100, data) 335 | } 336 | 337 | companion object { 338 | const val PPUSTATUS = 0x02 339 | const val OAMADDR = 0x03 340 | const val OAMDATA = 0x04 341 | const val PPUSCROLL = 0x05 342 | const val PPUADDR = 0x06 343 | const val PPUDATA = 0x07 344 | 345 | private val COLORS = arrayOf( 346 | arrayOf(0x80, 0x80, 0x80), arrayOf(0x00, 0x3D, 0xA6), arrayOf(0x00, 0x12, 0xB0), arrayOf(0x44, 0x00, 0x96), 347 | arrayOf(0xA1, 0x00, 0x5E), arrayOf(0xC7, 0x00, 0x28), arrayOf(0xBA, 0x06, 0x00), arrayOf(0x8C, 0x17, 0x00), 348 | arrayOf(0x5C, 0x2F, 0x00), arrayOf(0x10, 0x45, 0x00), arrayOf(0x05, 0x4A, 0x00), arrayOf(0x00, 0x47, 0x2E), 349 | arrayOf(0x00, 0x41, 0x66), arrayOf(0x00, 0x00, 0x00), arrayOf(0x05, 0x05, 0x05), arrayOf(0x05, 0x05, 0x05), 350 | arrayOf(0xC7, 0xC7, 0xC7), arrayOf(0x00, 0x77, 0xFF), arrayOf(0x21, 0x55, 0xFF), arrayOf(0x82, 0x37, 0xFA), 351 | arrayOf(0xEB, 0x2F, 0xB5), arrayOf(0xFF, 0x29, 0x50), arrayOf(0xFF, 0x22, 0x00), arrayOf(0xD6, 0x32, 0x00), 352 | arrayOf(0xC4, 0x62, 0x00), arrayOf(0x35, 0x80, 0x00), arrayOf(0x05, 0x8F, 0x00), arrayOf(0x00, 0x8A, 0x55), 353 | arrayOf(0x00, 0x99, 0xCC), arrayOf(0x21, 0x21, 0x21), arrayOf(0x09, 0x09, 0x09), arrayOf(0x09, 0x09, 0x09), 354 | arrayOf(0xFF, 0xFF, 0xFF), arrayOf(0x0F, 0xD7, 0xFF), arrayOf(0x69, 0xA2, 0xFF), arrayOf(0xD4, 0x80, 0xFF), 355 | arrayOf(0xFF, 0x45, 0xF3), arrayOf(0xFF, 0x61, 0x8B), arrayOf(0xFF, 0x88, 0x33), arrayOf(0xFF, 0x9C, 0x12), 356 | arrayOf(0xFA, 0xBC, 0x20), arrayOf(0x9F, 0xE3, 0x0E), arrayOf(0x2B, 0xF0, 0x35), arrayOf(0x0C, 0xF0, 0xA4), 357 | arrayOf(0x05, 0xFB, 0xFF), arrayOf(0x5E, 0x5E, 0x5E), arrayOf(0x0D, 0x0D, 0x0D), arrayOf(0x0D, 0x0D, 0x0D), 358 | arrayOf(0xFF, 0xFF, 0xFF), arrayOf(0xA6, 0xFC, 0xFF), arrayOf(0xB3, 0xEC, 0xFF), arrayOf(0xDA, 0xAB, 0xEB), 359 | arrayOf(0xFF, 0xA8, 0xF9), arrayOf(0xFF, 0xAB, 0xB3), arrayOf(0xFF, 0xD2, 0xB0), arrayOf(0xFF, 0xEF, 0xA6), 360 | arrayOf(0xFF, 0xF7, 0x9C), arrayOf(0xD7, 0xE8, 0x95), arrayOf(0xA6, 0xED, 0xAF), arrayOf(0xA2, 0xF2, 0xDA), 361 | arrayOf(0x99, 0xFF, 0xFC), arrayOf(0xDD, 0xDD, 0xDD), arrayOf(0x11, 0x11, 0x11), arrayOf(0x11, 0x11, 0x11) 362 | ) 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/main/kotlin/ram/Ram.kt: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | class Ram( 4 | size: Int, 5 | ) { 6 | private val data = IntArray(size) 7 | 8 | val size get() = data.size 9 | 10 | fun read(addr: Int) = data[addr] 11 | 12 | fun write(addr: Int, data: Int) { 13 | this.data[addr] = data 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/util/Util.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | fun pairs(x: IntRange, y: IntRange) = x.flatMap { i -> y.map { j -> Pair(i, j) } } 4 | 5 | fun twoDim(x: Int, y: Int) = (0 until y).map { IntArray(x) } 6 | --------------------------------------------------------------------------------