├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── gradle.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── ca │ │ └── craigthomas │ │ └── chip8java │ │ └── emulator │ │ ├── common │ │ └── IO.java │ │ ├── components │ │ ├── CentralProcessingUnit.java │ │ ├── Emulator.java │ │ ├── EmulatorState.java │ │ ├── Keyboard.java │ │ ├── Memory.java │ │ └── Screen.java │ │ ├── listeners │ │ ├── OpenROMFileActionListener.java │ │ ├── QuitActionListener.java │ │ └── ResetMenuItemActionListener.java │ │ └── runner │ │ ├── Arguments.java │ │ └── Runner.java └── resources │ └── FONTS.chip8 └── test ├── java └── ca │ └── craigthomas │ └── chip8java │ └── emulator │ ├── common │ └── IOTest.java │ ├── components │ ├── CentralProcessingUnitTest.java │ ├── KeyboardTest.java │ ├── MemoryTest.java │ └── ScreenTest.java │ └── listeners │ ├── OpenROMFileActionListenerTest.java │ ├── QuitActionListenerTest.java │ └── ResetMenuItemActionListenerTest.java └── resources ├── test.chip8 └── test_stream_file.bin /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Build Test Coverage 2 | on: [push, pull_request] 3 | jobs: 4 | run: 5 | runs-on: ubuntu-20.04 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v3 9 | - name: Setup JDK 11 10 | uses: actions/setup-java@v3 11 | with: 12 | java-version: '11' 13 | distribution: 'adopt' 14 | - name: Install xvfb for headless testing 15 | run: sudo apt-get install xvfb 16 | - name: Grant execute permission for gradlew 17 | run: chmod +x gradlew 18 | - name: Build with Gradle 19 | run: xvfb-run --auto-servernum ./gradlew build 20 | - name: Codecov 21 | uses: codecov/codecov-action@v4.2.0 22 | env: 23 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at craig.thomas@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thank you for considering making a contribution to the project! Below are some guidelines for contributing 4 | to the project. Please feel free to propose changes to the guidelines in a pull request if you feel something is missing 5 | or needs more clarity. 6 | 7 | # Table of Contents 8 | 9 | 1. [How can I contribute?](#how-can-i-contribute) 10 | 1. [Reporting bugs](#reporting-bugs) 11 | 2. [Suggesting features](#suggesting-features) 12 | 3. [Contributing code](#contributing-code) 13 | 4. [Pull requests](#pull-requests) 14 | 2. [Style guidelines](#style-guidelines) 15 | 1. [Git commit messages](#git-commit-messages) 16 | 2. [Code style](#code-style) 17 | 18 | # How can I contribute? 19 | 20 | There are several different way that you can contribute to the development of the project, any of which are most 21 | welcome. 22 | 23 | ## Reporting bugs 24 | 25 | Bugs are reported through the [GitHub Issue](https://github.com/craigthomas/Chip8Java/issues) interface. Please check 26 | first to see if the bug you are reporting has already been reported. When reporting a bug, please take the time to 27 | describe the following details: 28 | 29 | 1. **Descriptive Title** - please ensure that the title to your issue is clear and descriptive of the problem. 30 | 2. **Steps for Reproduction** - outline all the steps exactly that are required to reproduce the bug. For example, 31 | start by explaining how you started the program, which ROM you were running, and any other command line switches 32 | that were set, as well as what environment you are running in (e.g. Windows, Linux, MacOS, etc). 33 | 3. **Describe the Bug Behaviour** - describe what happens with the emulator, and why you think that this behaviour 34 | represents a bug. 35 | 4. **Describe Expected Behaviour** - describe what behaviour you believe should occur as a result of the steps 36 | that you took up to the point where the bug occurred. 37 | 38 | Please feel free to provide additional context to help a developer track down the bug: 39 | 40 | * **Can you reproduce the bug reliably or is it intermittent?** If it is intermittent, please try to explain what 41 | would normally provoke the bug, or under what conditions you saw it occur last time. 42 | * **Does it happen for any other ROMs?** It is helpful if you can try and pinpoint the buggy behaviour to a certain 43 | ROM or handful of ROMs. 44 | 45 | ## Suggesting features 46 | 47 | When suggesting features or enhancements for the emulator, it is best to check out the open issues first to see whether 48 | or not such a feature is already under development. It is also worthwhile checking any open branches to see if the 49 | feature you are requesting is already in development. Finally, before submitting your suggestion, please ensure that 50 | the feature does not already exist by updating your local copy with the latest version from our `master` branch. 51 | 52 | To submit a feature request, please open a new [GitHub Issue](https://github.com/craigthomas/Chip8Java/issues) and 53 | provide the following details: 54 | 55 | 1. **Descriptive Title** - please ensure that the title to your issue is clear and descriptive of the enhancement or 56 | functionality you wish to see. 57 | 2. **Step by Step Description** - describe how the functionality of the system should occur with a step-by-step breakdown 58 | of how you expect the emulator to run. For example, if you wish to have a new debugging key added, describe how the 59 | emulator execution flow will change when the key is pressed. 60 | 3. **Use Animated GIFs** - if you are feeling ambitious, or if you feel words do not adequately describe the proposed 61 | functionality, please submit an animated GIF, or a drawing of the new proposed functionality. 62 | 4. **Explain Usefulness** - please take a brief moment to describe why you feel the new functionality would be useful. 63 | 64 | ## Contributing code 65 | 66 | Code contributions should be made using the following process: 67 | 68 | 1. **Fork the Repository** - create a fork of the respository using the Fork button in GitHub. 69 | 2. **Make Code Changes** - using your forked repository, make changes to the code as you see fit. We recommend creating a 70 | branch for your code changes so that you can easily update your own local master without creating too many merge 71 | conflicts. 72 | 3. **Submit Pull Request** - when you are ready, submit a pull request back to the `master` branch of this repository. 73 | The pull request will be reviewed, and you may be asked questions about the changes you are proposing. In some cases, 74 | we may ask you to make adjustments to the code to fit in with the overall style and behavioiur of the rest of the 75 | project. 76 | 77 | There are also some additional guidelines you should follow when coding up enhancements or bugfixes: 78 | 79 | 1. Please reference any open issues that the Pull Request will close by writing `Closes #` with the issue number (e.g. `Closes #12`). 80 | 2. New functionality should have unit and/or integration tests that exercise at least 50% of the code that was added. 81 | 3. For bug fixes, please ensure that you have a test that covers buggy input conditions so that we reduce the likelihood of 82 | a regression in the future. 83 | 4. Please ensure all functions have appropriately descriptive docstrings, as well as descriptions for inputs and outputs. 84 | 85 | If you don't know where to start, then take a look for issues marked `beginner` or `help-wanted`. Any issues with the `beginner` tag 86 | will generally only require one or two lines of code to fix. Issues marked `help-wanted` may be more complex than beginner issues, 87 | but should be scoped in such a way to ease you in to the codebase. 88 | 89 | ## Pull requests 90 | 91 | Please follow all the instructions as mentioned in the Pull Request template. When you submit your pull request, please ensure that 92 | all of the required [status checks](https://help.github.com/articles/about-status-checks/) have succeeded. If the status checks 93 | are failing, and you believe that the failures are not related to your change, please leave a description within the pull request why 94 | you believe the failures are not related to your code changes. A maintainer will re-run the checks manually, and investigate further. 95 | 96 | A maintainer will review your pull request, and may ask you to perform some additional design work, tests, or other changes prior 97 | to approving and merging your code. 98 | 99 | # Style guidelines 100 | 101 | In general, there are two sets of style guidelines that we ask contributors to follow. 102 | 103 | ## Git commit messages 104 | 105 | * For large changesets, provide detailed descriptions in your commit logs regarding what was changed. The git commit message should 106 | look like this: 107 | 108 | ``` 109 | $ git commit -m "A brief title / description of the commit 110 | > 111 | > A more descriptive set of paragraphs about the changeset." 112 | ``` 113 | 114 | * Limit your first line to 70 characters. 115 | * If you are just changing documentation, please include `[ci skip]` in the commit title. 116 | * Please reference issue numbers and pull requests in the commit description where applicable using `#` and the issue number (e.g. 117 | `#24`). 118 | * Squash commits are welcome. 119 | 120 | ## Code style 121 | 122 | * Please examine the source code to become aware of general style and layout. 123 | * Please ensure any docstrings describe the functionality of the functions, including what the input and output parameters 124 | do. 125 | * We use camel caps for all function names and variables. 126 | * Constants declared as `final static` should be in all caps with snake case (e.g. `GLOBAL_VARIABLE_1`). 127 | * Please do not use docstrings on unit test functions. Instead, use descriptive names for the functions (e.g. 128 | `testCpuRaisesNMIWhenOverflowOccurs`). 129 | * We prefer descriptive variable names for variables over short ones (e.g. `counter` is better than `x`). 130 | * Top level class declarations should have an open brace on a new line: 131 | ``` 132 | public class foo 133 | { 134 | ... 135 | } 136 | ``` 137 | * Other code blcoks should have an open brace on the current line: 138 | ``` 139 | public void someFunction() { 140 | ... 141 | } 142 | ``` 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013-2015 Craig Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for your contribution! Before submitting this PR, please make sure: 2 | 3 | - [ ] The unit test suite runs without any errors or warnings 4 | - If the unit test suite fails, and you believe the failure is due to the test suite, please let us know in your PR description 5 | - We recommend using `gradlew` to run the unit test suite with the command: 6 | ``` 7 | gradlew clean test 8 | ``` 9 | - [ ] You have added unit tests to cover the functionality you have added 10 | - We ask that at least 50% of the changeset you are submitting has been covered by tests 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yet Another (Super) Chip 8 Emulator 2 | 3 | [](https://github.com/craigthomas/Chip8Java/actions) 4 | [](https://codecov.io/gh/craigthomas/Chip8Java) 5 | [](https://libraries.io/github/craigthomas/Chip8Java) 6 | [](https://github.com/craigthomas/Chip8Java/releases) 7 | [](https://github.com/craigthomas/Chip8Java/releases) 8 | [](https://opensource.org/licenses/MIT) 9 | 10 | An Octo compatible XO Chip, Super Chip, and Chip 8 emulator. 11 | 12 | ## Table of Contents 13 | 14 | 1. [What is it?](#what-is-it) 15 | 2. [License](#license) 16 | 3. [Compiling](#compiling) 17 | 4. [Running](#running) 18 | 1. [Requirements](#requirements) 19 | 2. [Starting the Emulator](#starting-the-emulator) 20 | 3. [Running a ROM](#running-a-rom) 21 | 4. [Screen Scale](#screen-scale) 22 | 5. [Instructions Per Second](#instructions-per-second) 23 | 6. [Quirks Modes](#quirks-modes) 24 | 1. [Shift Quirks](#shift-quirks) 25 | 2. [Index Quirks](#index-quirks) 26 | 3. [Jump Quirks](#jump-quirks) 27 | 4. [Clip Quirks](#clip-quirks) 28 | 5. [Logic Quirks](#logic-quirks) 29 | 7. [Memory Size](#memory-size) 30 | 8. [Colors](#colors) 31 | 5. [Customization](#customization) 32 | 1. [Keys](#keys) 33 | 2. [Debug Keys](#debug-keys) 34 | 6. [ROM Compatibility](#rom-compatibility) 35 | 7. [Third Party Licenses and Attributions](#third-party-licenses-and-attributions) 36 | 1. [JCommander](#jcommander) 37 | 2. [Apache Commons IO](#apache-commons-io) 38 | 39 | ## What is it? 40 | 41 | This project is a Chip 8 emulator written in Java. There are two other versions 42 | of the emulator written in different languages: 43 | 44 | * [Chip8Python](https://github.com/craigthomas/Chip8Python) 45 | * [Chip8C](https://github.com/craigthomas/Chip8C) 46 | 47 | The original goal of these projects was to learn how to code a simple emulator. 48 | 49 | In addition to supporting Chip 8 ROMs, the emulator also supports the 50 | [XO Chip](https://johnearnest.github.io/Octo/docs/XO-ChipSpecification.html) 51 | and [Super Chip](https://github.com/JohnEarnest/Octo/blob/gh-pages/docs/SuperChip.md) specifications. 52 | Note that while there are no special flags that are needed to run an XO Chip, 53 | Super Chip, or normal Chip 8 ROM, there are other compatibility flags that 54 | may need to be set for the ROM to run properly. See the [Quirks Modes](#quirks-modes) 55 | documentation below for more information. 56 | 57 | 58 | ## License 59 | 60 | This project makes use of an MIT license. Please see the file called 61 | LICENSE for more information. Note that this project may make use of other 62 | software that has separate license terms. See the section called `Third 63 | Party Licenses and Attributions` below for more information on those 64 | software components. 65 | 66 | 67 | ## Compiling 68 | 69 | To compile the project, you will need a Java Development Kit (JDK) version 8 or greater installed. 70 | Recently, Oracle has changed their license agreement to make personal and developmental use of their 71 | JDK free. However, some other use cases may require a paid subscription. Oracle's version of the 72 | JDK can be downloaded [here](https://www.oracle.com/technetwork/java/javase/downloads/index.html). 73 | Alternatively, if you prefer to use a JRE with an open-source license (GPL v2 with Classpath 74 | Exception), you may visit [https://adoptopenjdk.net](https://adoptopenjdk.net) and install the 75 | latest Java Development Kit (JDK) for your system. Again, JDK version 8 or better will work correctly. 76 | 77 | To build the project, switch to the root of the source directory, and 78 | type: 79 | 80 | ./gradlew build 81 | 82 | On Windows, switch to the root of the source directory, and type: 83 | 84 | gradlew.bat build 85 | 86 | The compiled JAR file will be placed in the `build/libs` directory, as a file called 87 | `emulator-2.0.0-all.jar`. 88 | 89 | 90 | ## Running 91 | 92 | ### Requirements 93 | 94 | You will need a copy of the Java Runtime Environment (JRE) version 8 or greater installed 95 | in order to run the compiled JAR file. For most systems, you can install Java 8 JRE by visiting 96 | [http://java.com](http://java.com) and installing the Oracle Java Runtime Environment for your 97 | platform. This version of the JRE is free for personal use but contains a custom binary license 98 | from Oracle. Alternatively, if you prefer to use a JRE with an open-source license (GPL 99 | v2 with Classpath Exception), you may visit [https://adoptopenjdk.net](https://adoptopenjdk.net) 100 | and install the latest Java Development Kit (JDK) for your system, which will include an appropriate JRE. 101 | 102 | ### Starting the Emulator 103 | 104 | By default, the emulator can start up without a ROM loaded. Simply double-click 105 | the JAR file, or run it with the following command line: 106 | 107 | java -jar emulator-2.0.0-all.jar 108 | 109 | ### Running a ROM 110 | 111 | The command-line interface currently requires a single argument, which 112 | is the full path to a Chip 8 ROM: 113 | 114 | java -jar emulator-2.0.0-all.jar /path/to/rom/filename 115 | 116 | This will start the emulator with the specified ROM. 117 | 118 | ### Screen Scale 119 | 120 | The `--scale` switch will scale the size of the window (the original size 121 | at 1x scale is 64 x 32): 122 | 123 | java -jar emulator-2.0.0-all.jar /path/to/rom/filename --scale 10 124 | 125 | The command above will scale the window so that it is 10 times the normal 126 | size. 127 | 128 | ### Instructions Per Second 129 | 130 | The `--ticks` switch will limit the number of instructions per second that the 131 | emulator is allowed to run. By default, the value is set to 1,000. Minimum values 132 | are 200. Use this switch to adjust the running time of ROMs that execute too quickly. 133 | For simplicity, each instruction is assumed to take the same amount of time. 134 | 135 | ### Quirks Modes 136 | 137 | Over time, various extensions to the Chip8 mnemonics were developed, which 138 | resulted in an interesting fragmentation of the Chip8 language specification. 139 | As discussed in Octo's [Mastering SuperChip](https://github.com/JohnEarnest/Octo/blob/gh-pages/docs/SuperChip.md) 140 | documentation, one version of the SuperChip instruction set subtly changed 141 | the meaning of a few instructions from their original Chip8 definitions. 142 | This change went mostly unnoticed for many implementations of the Chip8 143 | language. Problems arose when people started writing programs using the 144 | updated language model - programs written for "pure" Chip8 ceased to 145 | function correctly on emulators making use of the altered specification. 146 | 147 | To address this issue, [Octo](https://github.com/JohnEarnest/Octo) implements 148 | a number of _quirks_ modes so that all Chip8 software can run correctly, 149 | regardless of which specification was used when developing the Chip8 program. 150 | This same approach is used here, such that there are several `quirks` flags 151 | that can be passed to the emulator at startup to force it to run with 152 | adjustments to the language specification. 153 | 154 | Additional quirks and their impacts on the running Chip8 interpreter are 155 | examined in great depth at Chromatophore's [HP48-Superchip](https://github.com/Chromatophore/HP48-Superchip) 156 | repository. Many thanks for this detailed explanation of various quirks 157 | found in the wild! 158 | 159 | #### Shift Quirks 160 | 161 | The `--shift_quirks` flag will change the way that register shift operations work. 162 | In the original language specification two registers were required: the 163 | destination register `x`, and the source register `y`. The source register `y` 164 | value was shifted one bit left or right, and stored in `x`. For example, 165 | shift left was defined as: 166 | 167 | Vx = Vy << 1 168 | 169 | However, with the updated language specification, the source and destination 170 | register are assumed to always be the same, thus the `y` register is ignored and 171 | instead the value is sourced from `x` as such: 172 | 173 | Vx = Vx << 1 174 | 175 | #### Index Quirks 176 | 177 | The `--index_quirks` flag controls whether post-increments are made to the index register 178 | following various register based operations. For load (`Fn65`) and store (`Fn55`) register 179 | operations, the original specification for the Chip8 language results in the index 180 | register being post-incremented by the number of registers stored. With the Super 181 | Chip8 specification, this behavior is not always adhered to. Setting `--index_quirks` 182 | will prevent the post-increment of the index register from occurring after either of these 183 | instructions. 184 | 185 | 186 | #### Jump Quirks 187 | 188 | The `--jump_quirks` controls how jumps to various addresses are made with the jump (`Bnnn`) 189 | instruction. In the original Chip8 language specification, the jump is made by taking the 190 | contents of register 0, and adding it to the encoded numeric value, such as: 191 | 192 | PC = V0 + nnn 193 | 194 | With the Super Chip8 specification, the highest 4 bits of the instruction encode the 195 | register to use (`Bxnn`) such. The behavior of `--jump_quirks` becomes: 196 | 197 | PC = Vx + nn 198 | 199 | 200 | #### Clip Quirks 201 | 202 | The `--clip_quirks` controls whether sprites are allowed to wrap around the display. 203 | By default, sprits will wrap around the borders of the screen. If turned on, then 204 | sprites will not be allowed to wrap. 205 | 206 | 207 | #### Logic Quirks 208 | 209 | The `--logic_quirks` controls whether the F register is cleared after logic operations 210 | such as AND, OR, and XOR. By default, F is left undefined following these operations. 211 | With the flag turned on, F will always be cleared. 212 | 213 | ### Memory Size 214 | 215 | The original specification of the Chip8 language defined a 4K memory size 216 | for the interpreter. The addition of the XO Chip extensions require a 64K 217 | memory size for the interpreter. By default, the interpreter will start w 218 | ith a 64K memory size, but this behavior can be controlled with the 219 | `--mem_size_4k` flag, which will start the emulator with 4K. 220 | 221 | ### Colors 222 | 223 | The original Chip8 language specification called for pixels to be turned 224 | on or off. It did not specify what color the pixel states had to be. The 225 | emulator lets the user specify what colors they want to use when the emulator 226 | is running. Color values are specified by using HTML hex values such as 227 | `AABBCC` without the leading `#`. There are currently 4 color values that can 228 | be set: 229 | 230 | * `--color_0` specifies the background color. This defaults to `000000`. 231 | * `--color_1` specifies bitplane 1 color. This defaults to `FF33CC`. 232 | * `--color_2` specifies bitplane 2 color. This defaults to `33CCFF`. 233 | * `--color_3` specifies bitplane 1 and 2 overlap color. This defaults to `FFFFFF`. 234 | 235 | For Chip8 and SuperChip 8 programs, only the background `color color_0` 236 | (for pixels turned off) and the bitplane 1 `color color_1` (for pixels turned 237 | on) are used. Only XO Chip programs will use `color_2` and `color_3` when 238 | the additional bitplanes are potentially used. 239 | 240 | ## Customization 241 | 242 | The file `components/Keyboard.java` contains several variables that can be 243 | changed to customize the operation of the emulator. The Chip 8 has 16 keys: 244 | 245 | ### Keys 246 | 247 | The original Chip 8 had a keypad with the numbered keys 0 - 9 and A - F (16 248 | keys in total). The original key configuration was as follows: 249 | 250 | 251 | | `1` | `2` | `3` | `C` | 252 | |-----|-----|-----|-----| 253 | | `4` | `5` | `6` | `D` | 254 | | `7` | `8` | `9` | `E` | 255 | | `A` | `0` | `B` | `F` | 256 | 257 | The Chip8Java emulator maps them to the following keyboard keys by default: 258 | 259 | | `1` | `2` | `3` | `4` | 260 | |-----|-----|-----|-----| 261 | | `Q` | `W` | `E` | `R` | 262 | | `A` | `S` | `D` | `F` | 263 | | `Z` | `X` | `C` | `V` | 264 | 265 | 266 | ### Debug Keys 267 | 268 | Pressing a debug key at any time will cause the emulator to enter into a 269 | different mode of operation. The debug keys are: 270 | 271 | | Keyboard Key | Effect | 272 | |:------------:|--------------------------------| 273 | | `ESC` | Quits the emulator | 274 | 275 | ## ROM Compatibility 276 | 277 | Here are the list of public domain ROMs and their current status with the emulator, along 278 | with links to public domain repositories where applicable. 279 | 280 | ### Chip 8 ROMs 281 | 282 | | ROM Name | Working | Flags | 283 | |:--------------------------------------------------------------------------------------------------|:------------------:|:-------------:| 284 | | [1D Cellular Automata](https://johnearnest.github.io/chip8Archive/play.html?p=1dcell) | :heavy_check_mark: | | 285 | | [8CE Attourny - Disc 1](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d1) | :heavy_check_mark: | | 286 | | [8CE Attourny - Disc 2](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d2) | :heavy_check_mark: | | 287 | | [8CE Attourny - Disc 3](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d3) | :heavy_check_mark: | | 288 | | [Bad Kaiju Ju](https://johnearnest.github.io/chip8Archive/play.html?p=BadKaiJuJu) | :heavy_check_mark: | | 289 | | [Br8kout](https://johnearnest.github.io/chip8Archive/play.html?p=br8kout) | :heavy_check_mark: | | 290 | | [Carbon8](https://johnearnest.github.io/chip8Archive/play.html?p=carbon8) | :heavy_check_mark: | | 291 | | [Cave Explorer](https://johnearnest.github.io/chip8Archive/play.html?p=caveexplorer) | :heavy_check_mark: | | 292 | | [Chipquarium](https://johnearnest.github.io/chip8Archive/play.html?p=chipquarium) | :heavy_check_mark: | | 293 | | [Danm8ku](https://johnearnest.github.io/chip8Archive/play.html?p=danm8ku) | :heavy_check_mark: | | 294 | | [down8](https://johnearnest.github.io/chip8Archive/play.html?p=down8) | :heavy_check_mark: | | 295 | | [Falling Ghosts](https://veganjay.itch.io/falling-ghosts) | :heavy_check_mark: | | 296 | | [Flight Runner](https://johnearnest.github.io/chip8Archive/play.html?p=flightrunner) | :heavy_check_mark: | | 297 | | [Fuse](https://johnearnest.github.io/chip8Archive/play.html?p=fuse) | :heavy_check_mark: | | 298 | | [Ghost Escape](https://johnearnest.github.io/chip8Archive/play.html?p=ghostEscape) | :heavy_check_mark: | | 299 | | [Glitch Ghost](https://johnearnest.github.io/chip8Archive/play.html?p=glitchGhost) | :heavy_check_mark: | | 300 | | [Horse World Online](https://johnearnest.github.io/chip8Archive/play.html?p=horseWorldOnline) | :heavy_check_mark: | | 301 | | [Invisible Man](https://mremerson.itch.io/invisible-man) | :heavy_check_mark: | `clip_quirks` | 302 | | [Knumber Knower](https://internet-janitor.itch.io/knumber-knower) | :heavy_check_mark: | | 303 | | [Masquer8](https://johnearnest.github.io/chip8Archive/play.html?p=masquer8) | :heavy_check_mark: | | 304 | | [Mastermind](https://johnearnest.github.io/chip8Archive/play.html?p=mastermind) | :x: | | 305 | | [Mini Lights Out](https://johnearnest.github.io/chip8Archive/play.html?p=mini-lights-out) | :heavy_check_mark: | | 306 | | [Octo: a Chip 8 Story](https://johnearnest.github.io/chip8Archive/play.html?p=octoachip8story) | :x: | | 307 | | [Octogon Trail](https://tarsi.itch.io/octogon-trail) | :question: | | 308 | | [Octojam 1 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam1title) | :heavy_check_mark: | | 309 | | [Octojam 2 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam2title) | :heavy_check_mark: | | 310 | | [Octojam 3 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam3title) | :heavy_check_mark: | | 311 | | [Octojam 4 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam4title) | :heavy_check_mark: | | 312 | | [Octojam 5 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam5title) | :heavy_check_mark: | | 313 | | [Octojam 6 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam6title) | :heavy_check_mark: | | 314 | | [Octojam 7 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam7title) | :heavy_check_mark: | | 315 | | [Octojam 8 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam8title) | :heavy_check_mark: | | 316 | | [Octojam 9 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam9title) | :heavy_check_mark: | | 317 | | [Octojam 10 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam10title) | :heavy_check_mark: | | 318 | | [Octo Rancher](https://johnearnest.github.io/chip8Archive/play.html?p=octorancher) | :question: | | 319 | | [Outlaw](https://johnearnest.github.io/chip8Archive/play.html?p=outlaw) | :heavy_check_mark: | | 320 | | [Pet Dog](https://johnearnest.github.io/chip8Archive/play.html?p=petdog) | :question: | | 321 | | [Piper](https://johnearnest.github.io/chip8Archive/play.html?p=piper) | :heavy_check_mark: | | 322 | | [Pumpkin "Dress" Up](https://johnearnest.github.io/chip8Archive/play.html?p=pumpkindressup) | :heavy_check_mark: | | 323 | | [RPS](https://johnearnest.github.io/chip8Archive/play.html?p=RPS) | :question: | | 324 | | [Slippery Slope](https://johnearnest.github.io/chip8Archive/play.html?p=slipperyslope) | :heavy_check_mark: | | 325 | | [Snek](https://johnearnest.github.io/chip8Archive/play.html?p=snek) | :heavy_check_mark: | | 326 | | [Space Jam](https://johnearnest.github.io/chip8Archive/play.html?p=spacejam) | :heavy_check_mark: | | 327 | | [Spock Paper Scissors](https://johnearnest.github.io/chip8Archive/play.html?p=spockpaperscissors) | :heavy_check_mark: | | 328 | | [Super Pong](https://johnearnest.github.io/chip8Archive/play.html?p=superpong) | :heavy_check_mark: | | 329 | | [Tank!](https://johnearnest.github.io/chip8Archive/play.html?p=tank) | :heavy_check_mark: | | 330 | | [TOMB STON TIPP](https://johnearnest.github.io/chip8Archive/play.html?p=tombstontipp) | :heavy_check_mark: | | 331 | | [WDL](https://johnearnest.github.io/chip8Archive/play.html?p=wdl) | :x: | | 332 | 333 | ### Super Chip ROMs 334 | 335 | | ROM Name | Working | Flags | 336 | |:---------------------------------------------------------------------------------------------|:------------------:|:-----:| 337 | | [Applejak](https://johnearnest.github.io/chip8Archive/play.html?p=applejak) | :x: | | 338 | | [Bulb](https://johnearnest.github.io/chip8Archive/play.html?p=bulb) | :x: | | 339 | | [Black Rainbow](https://johnearnest.github.io/chip8Archive/play.html?p=blackrainbow) | :heavy_check_mark: | | 340 | | [Chipcross](https://tobiasvl.itch.io/chipcross) | :heavy_check_mark: | | 341 | | [Chipolarium](https://tobiasvl.itch.io/chipolarium) | :heavy_check_mark: | | 342 | | [Collision Course](https://ninjaweedle.itch.io/collision-course) | :heavy_check_mark: | | 343 | | [Dodge](https://johnearnest.github.io/chip8Archive/play.html?p=dodge) | :x: | | 344 | | [DVN8](https://johnearnest.github.io/chip8Archive/play.html?p=DVN8) | :heavy_check_mark: | | 345 | | [Eaty the Alien](https://johnearnest.github.io/chip8Archive/play.html?p=eaty) | :heavy_check_mark: | | 346 | | [Grad School Simulator 2014](https://johnearnest.github.io/chip8Archive/play.html?p=gradsim) | :heavy_check_mark: | | 347 | | [Horsey Jump](https://johnearnest.github.io/chip8Archive/play.html?p=horseyJump) | :question: | | 348 | | [Knight](https://johnearnest.github.io/chip8Archive/play.html?p=knight) | :x: | | 349 | | [Mondri8](https://johnearnest.github.io/chip8Archive/play.html?p=mondrian) | :heavy_check_mark: | | 350 | | [Octopeg](https://johnearnest.github.io/chip8Archive/play.html?p=octopeg) | :heavy_check_mark: | | 351 | | [Octovore](https://johnearnest.github.io/chip8Archive/play.html?p=octovore) | :heavy_check_mark: | | 352 | | [Rocto](https://johnearnest.github.io/chip8Archive/play.html?p=rockto) | :heavy_check_mark: | | 353 | | [Sens8tion](https://johnearnest.github.io/chip8Archive/play.html?p=sens8tion) | :heavy_check_mark: | | 354 | | [Snake](https://johnearnest.github.io/chip8Archive/play.html?p=snake) | :heavy_check_mark: | | 355 | | [Squad](https://johnearnest.github.io/chip8Archive/play.html?p=squad) | :x: | | 356 | | [Sub-Terr8nia](https://johnearnest.github.io/chip8Archive/play.html?p=sub8) | :heavy_check_mark: | | 357 | | [Super Octogon](https://johnearnest.github.io/chip8Archive/play.html?p=octogon) | :heavy_check_mark: | | 358 | | [Super Square](https://johnearnest.github.io/chip8Archive/play.html?p=supersquare) | :heavy_check_mark: | | 359 | | [The Binding of COSMAC](https://johnearnest.github.io/chip8Archive/play.html?p=binding) | :heavy_check_mark: | | 360 | | [Turnover '77](https://johnearnest.github.io/chip8Archive/play.html?p=turnover77) | :heavy_check_mark: | | 361 | 362 | ### XO Chip ROMs 363 | 364 | | ROM Name | Working | Flags | 365 | |:------------------------------------------------------------------------------------------------------|:------------------:|:-----:| 366 | | [An Evening to Die For](https://johnearnest.github.io/chip8Archive/play.html?p=anEveningToDieFor) | :heavy_check_mark: | | 367 | | [Business Is Contagious](https://johnearnest.github.io/chip8Archive/play.html?p=businessiscontagious) | :heavy_check_mark: | | 368 | | [Chicken Scratch](https://johnearnest.github.io/chip8Archive/play.html?p=chickenScratch) | :heavy_check_mark: | | 369 | | [Civiliz8n](https://johnearnest.github.io/chip8Archive/play.html?p=civiliz8n) | :heavy_check_mark: | | 370 | | [Flutter By](https://johnearnest.github.io/chip8Archive/play.html?p=flutterby) | :heavy_check_mark: | | 371 | | [Into The Garlicscape](https://johnearnest.github.io/chip8Archive/play.html?p=garlicscape) | :heavy_check_mark: | | 372 | | [jub8 Song 1](https://johnearnest.github.io/chip8Archive/play.html?p=jub8-1) | :heavy_check_mark: | | 373 | | [jub8 Song 2](https://johnearnest.github.io/chip8Archive/play.html?p=jub8-2) | :heavy_check_mark: | | 374 | | [Kesha Was Biird](https://johnearnest.github.io/chip8Archive/play.html?p=keshaWasBiird) | :heavy_check_mark: | | 375 | | [Kesha Was Niinja](https://johnearnest.github.io/chip8Archive/play.html?p=keshaWasNiinja) | :heavy_check_mark: | | 376 | | [Octo paint](https://johnearnest.github.io/chip8Archive/play.html?p=octopaint) | :heavy_check_mark: | | 377 | | [Octo Party Mix!](https://johnearnest.github.io/chip8Archive/play.html?p=OctoPartyMix) | :heavy_check_mark: | | 378 | | [Octoma](https://johnearnest.github.io/chip8Archive/play.html?p=octoma) | :heavy_check_mark: | | 379 | | [Red October V](https://johnearnest.github.io/chip8Archive/play.html?p=redOctober) | :heavy_check_mark: | | 380 | | [Skyward](https://johnearnest.github.io/chip8Archive/play.html?p=skyward) | :heavy_check_mark: | | 381 | | [Spock Paper Scissors](https://johnearnest.github.io/chip8Archive/play.html?p=spockpaperscissors) | :heavy_check_mark: | | 382 | | [T8NKS](https://johnearnest.github.io/chip8Archive/play.html?p=t8nks) | :question: | | 383 | | [Tapeworm](https://tarsi.itch.io/tapeworm) | :heavy_check_mark: | | 384 | | [Truck Simul8or](https://johnearnest.github.io/chip8Archive/play.html?p=trucksimul8or) | :heavy_check_mark: | | 385 | | [SK8 H8 1988](https://johnearnest.github.io/chip8Archive/play.html?p=sk8) | :heavy_check_mark: | | 386 | | [Super NeatBoy](https://johnearnest.github.io/chip8Archive/play.html?p=superneatboy) | :heavy_check_mark: | | 387 | | [Wonky Pong](https://johnearnest.github.io/chip8Archive/play.html?p=wonkypong) | :heavy_check_mark: | | 388 | 389 | ## Third Party Licenses and Attributions 390 | 391 | ### JCommander 392 | 393 | This links to the JCommander library, which is licensed under the 394 | Apache License, Version 2.0. The license can be downloaded from 395 | http://www.apache.org/licenses/LICENSE-2.0.html. The source code for this 396 | software is available from [https://github.com/cbeust/jcommander](https://github.com/cbeust/jcommander) 397 | 398 | ### Apache Commons IO 399 | 400 | This links to the Apache Commons IO, which is licensed under the 401 | Apache License, Version 2.0. The license can be downloaded from 402 | http://www.apache.org/licenses/LICENSE-2.0.html. The source code for this 403 | software is available from http://commons.apache.org/io -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | plugins { 8 | id 'java' 9 | id 'jacoco' 10 | id 'application' 11 | id 'com.github.johnrengelman.shadow' version '8.1.1' 12 | } 13 | 14 | group = 'ca.craigthomas' 15 | version = '2.0.1' 16 | 17 | mainClassName = 'ca.craigthomas.chip8java.emulator.runner.Runner' 18 | 19 | sourceCompatibility = 1.8 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | implementation 'com.beust:jcommander:1.82' 27 | implementation 'commons-io:commons-io:2.18.0' 28 | testImplementation 'org.mockito:mockito-core:5.+' 29 | testImplementation 'junit:junit:4.13.2' 30 | } 31 | 32 | jacocoTestReport { 33 | reports { 34 | xml.required = true 35 | html.required = true 36 | } 37 | } 38 | 39 | task stage { 40 | dependsOn shadowJar 41 | } 42 | 43 | test { 44 | testLogging { 45 | outputs.upToDateWhen {false} 46 | showStandardStreams = true 47 | } 48 | } 49 | 50 | check.dependsOn jacocoTestReport -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigthomas/Chip8Java/b4f1debcd6f45a3ba5fae54bbe99c428a3c688bb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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='"-Xmx64m"' 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="-Xmx64m" 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 = 'chip8java' 2 | -------------------------------------------------------------------------------- /src/main/java/ca/craigthomas/chip8java/emulator/common/IO.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013-2018 Craig Thomas 3 | * This project uses an MIT style license - see LICENSE for details. 4 | */ 5 | package ca.craigthomas.chip8java.emulator.common; 6 | 7 | import org.apache.commons.io.IOUtils; 8 | 9 | import java.io.*; 10 | import java.util.logging.Logger; 11 | 12 | public class IO 13 | { 14 | // The logger for the class 15 | private final static Logger LOGGER = Logger.getLogger(IO.class.getName()); 16 | 17 | /** 18 | * Attempts to open the specified filename as an InputStream. Will return null if there is 19 | * an error. 20 | * 21 | * @param filename The String containing the full path to the filename to open 22 | * @return An opened InputStream, or null if there is an error 23 | */ 24 | public static InputStream openInputStream(String filename) { 25 | try { 26 | return new FileInputStream(new File(filename)); 27 | } catch (Exception e) { 28 | LOGGER.severe("Error opening file: " + e.getMessage()); 29 | return null; 30 | } 31 | } 32 | 33 | /** 34 | * Attempts to open the specified resource as an InputStream. Will return null if there is 35 | * an error. 36 | * 37 | * @param filename The String containing the full path to the filename to open 38 | * @return An opened InputStream, or null if there is an error 39 | */ 40 | public static InputStream openInputStreamFromResource(String filename) { 41 | try { 42 | return IO.class.getClassLoader().getResourceAsStream(filename); 43 | } catch (Exception e) { 44 | LOGGER.severe("Error opening resource file: " + e.getMessage()); 45 | return null; 46 | } 47 | } 48 | 49 | /** 50 | * Closes an open input or output stream. 51 | * 52 | * @param stream the stream to close 53 | */ 54 | public static boolean closeStream(Closeable stream) { 55 | try { 56 | stream.close(); 57 | return true; 58 | } catch (Exception e) { 59 | LOGGER.severe("Error closing stream: " + e.getMessage()); 60 | return false; 61 | } 62 | } 63 | 64 | /** 65 | * Copies an array of bytes to an array of shorts, starting at the offset 66 | * in the target memory array. 67 | * 68 | * @param stream the stream with the bytes to copy 69 | * @param target the target short array 70 | * @param offset where in the target array to copy bytes to 71 | * @return true if the source was not null, false otherwise 72 | */ 73 | public static boolean copyStreamToShortArray(InputStream stream, short[] target, int offset) { 74 | int byteCounter = offset; 75 | byte[] source; 76 | 77 | if (target == null) { 78 | return false; 79 | } 80 | 81 | try { 82 | source = IOUtils.toByteArray(stream); 83 | } catch (Exception e) { 84 | LOGGER.severe("Error copying stream: " + e.getMessage()); 85 | return false; 86 | } 87 | 88 | if (source.length > (target.length - offset)) { 89 | return false; 90 | } 91 | 92 | for (byte data : source) { 93 | target[byteCounter] = data; 94 | byteCounter++; 95 | } 96 | 97 | return true; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/ca/craigthomas/chip8java/emulator/components/CentralProcessingUnit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013-2025 Craig Thomas 3 | * This project uses an MIT style license - see LICENSE for details. 4 | */ 5 | package ca.craigthomas.chip8java.emulator.components; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.util.Random; 9 | import java.util.Timer; 10 | import java.util.TimerTask; 11 | import java.util.logging.Logger; 12 | import javax.sound.sampled.*; 13 | 14 | /** 15 | * A class to emulate a Super Chip 8 CPU. There are several good resources out on the 16 | * web that describe the internals of the Chip 8 CPU. For example: 17 | *
18 | * http://devernay.free.fr/hacks/chip8/C8TECH10.HTM 19 | * http://michael.toren.net/mirrors/chip8/chip8def.htm 20 | *
21 | * As usual, a simple Google search will find you other excellent examples. 22 | * 23 | * @author Craig Thomas 24 | */ 25 | public class CentralProcessingUnit extends Thread 26 | { 27 | // The normal mode for the CPU 28 | protected final static int MODE_NORMAL = 1; 29 | 30 | // The extended mode for the CPU 31 | protected final static int MODE_EXTENDED = 2; 32 | 33 | // The logger for the class 34 | private final static Logger LOGGER = Logger.getLogger(Emulator.class.getName()); 35 | 36 | // The total number of registers in the Chip 8 CPU 37 | private static final int NUM_REGISTERS = 16; 38 | 39 | // The start location of the program counter 40 | public static final int PROGRAM_COUNTER_START = 0x200; 41 | 42 | // The start location of the stack pointer 43 | private static final int STACK_POINTER_START = 0x52; 44 | 45 | // The audio playback rate 46 | private static final int AUDIO_PLAYBACK_RATE = 48000; 47 | 48 | /** 49 | * The minimum number of audio samples we want to generate. The minimum amount 50 | * of time an audio clip can be played is 1/60th of a second (the frequency 51 | * that the sound timer is decremented). Since we initialize the 52 | * audio mixer to require 48000 samples per second, this means each 1/60th 53 | * of a second requires 800 samples. The audio pattern buffer is only 54 | * 128 bits long, so we will need to repeat it to fill at least 1/60th of a 55 | * second with audio (resampled at the correct frequency). To be safe, 56 | * we'll construct a buffer of at least 4/60ths of a second of 57 | * audio. We can be bigger than the minimum number of samples below, but 58 | * we don't want less than that. 59 | */ 60 | private static final int MIN_AUDIO_SAMPLES = 3200; 61 | 62 | // The maximum number of cycles per second allowed 63 | public static final int DEFAULT_MAX_TICKS = 1000; 64 | 65 | // The internal 8-bit registers 66 | protected short[] v; 67 | 68 | // The RPL register storage 69 | protected short[] rpl; 70 | 71 | // The index register 72 | protected int index; 73 | 74 | // The stack pointer register 75 | protected int stack; 76 | 77 | // The program counter 78 | protected int pc; 79 | 80 | // The delay register 81 | protected short delay; 82 | 83 | // The sound register 84 | protected short sound; 85 | 86 | // The current operand 87 | protected int operand; 88 | 89 | // The current pitch 90 | protected int pitch; 91 | 92 | // The current sound playback rate 93 | protected double playbackRate; 94 | 95 | // The currently selected bitplane 96 | protected int bitplane; 97 | 98 | // The internal memory for the Chip 8 99 | private final Memory memory; 100 | 101 | // The screen object for the Chip 8 102 | private final Screen screen; 103 | 104 | // The keyboard object for the Chip 8 105 | private final Keyboard keyboard; 106 | 107 | // A Random number generator used for the class 108 | private final Random random; 109 | 110 | // A description of the last operation 111 | protected String lastOpDesc; 112 | 113 | // The current operating mode for the CPU 114 | protected int mode; 115 | 116 | // Whether the CPU is waiting for a keypress 117 | private boolean awaitingKeypress = false; 118 | 119 | // Whether shift quirks are enabled 120 | private boolean shiftQuirks = false; 121 | 122 | // Whether logic quirks are enabled 123 | private boolean logicQuirks = false; 124 | 125 | // Whether jump quirks are enabled 126 | private boolean jumpQuirks = false; 127 | 128 | // Whether index quirks are enabled 129 | private boolean indexQuirks = false; 130 | 131 | // Whether clip quirks are enabled 132 | private boolean clipQuirks = false; 133 | 134 | // The 16-byte audio pattern buffer 135 | protected int [] audioPatternBuffer; 136 | 137 | // Whether an audio pattern is being played 138 | private boolean soundPlaying = false; 139 | 140 | // Stores the generated sound clip 141 | Clip generatedClip = null; 142 | 143 | // How many ticks have passed 144 | private int tickCounter = 0; 145 | 146 | // The maximum number of ticks allowed per cycle 147 | private int maxTicks = 1000; 148 | 149 | CentralProcessingUnit(Memory memory, Keyboard keyboard, Screen screen) { 150 | this.random = new Random(); 151 | this.memory = memory; 152 | this.screen = screen; 153 | this.keyboard = keyboard; 154 | Timer timer = new Timer("Delay Timer"); 155 | timer.schedule(new TimerTask() { 156 | @Override 157 | public void run() { 158 | decrementTimers(); 159 | tickCounter = 0; 160 | } 161 | }, 0, 17L); 162 | mode = MODE_NORMAL; 163 | reset(); 164 | } 165 | 166 | /** 167 | * Sets the maximum allowed number of operations allowed per second 168 | */ 169 | public void setMaxTicks(int maxTicksAllowed) { 170 | if (maxTicksAllowed < 200) { 171 | maxTicksAllowed = 200; 172 | } 173 | 174 | maxTicks = maxTicksAllowed / 60; 175 | } 176 | 177 | /** 178 | * Sets the shiftQuirks to true or false. 179 | * 180 | * @param enableQuirk a boolean enabling shift quirks or disabling shift quirks 181 | */ 182 | public void setShiftQuirks(boolean enableQuirk) { 183 | shiftQuirks = enableQuirk; 184 | } 185 | 186 | /** 187 | * Sets the logicQuirks to true or false. 188 | * 189 | * @param enableQuirk a boolean enabling logic quirks or disabling logic quirks 190 | */ 191 | public void setLogicQuirks(boolean enableQuirk) { 192 | logicQuirks = enableQuirk; 193 | } 194 | 195 | /** 196 | * Sets the jumpQuirks to true or false. 197 | * 198 | * @param enableQuirk a boolean enabling jump quirks or disabling jump quirks 199 | */ 200 | public void setJumpQuirks(boolean enableQuirk) { 201 | jumpQuirks = enableQuirk; 202 | } 203 | 204 | /** 205 | * Sets the indexQuirks to true or false. 206 | * 207 | * @param enableQuirk a boolean enabling index quirks or disabling index quirks 208 | */ 209 | public void setIndexQuirks(boolean enableQuirk) { 210 | indexQuirks = enableQuirk; 211 | } 212 | 213 | /** 214 | * Sets the clipQuirks to true or false. 215 | * 216 | * @param enableQuirk a boolean enabling clip quirks or disabling clip quirks 217 | */ 218 | public void setClipQuirks(boolean enableQuirk) { 219 | clipQuirks = enableQuirk; 220 | } 221 | 222 | /** 223 | * Fetch the next instruction from memory, increment the program counter 224 | * to the next instruction, and execute the instruction. 225 | */ 226 | public void fetchIncrementExecute() { 227 | if (tickCounter < maxTicks) { 228 | operand = memory.read(pc); 229 | operand = operand << 8; 230 | operand += memory.read(pc + 1); 231 | operand = operand & 0x0FFFF; 232 | pc += 2; 233 | int opcode = (operand & 0x0F000) >> 12; 234 | executeInstruction(opcode); 235 | tickCounter++; 236 | } 237 | } 238 | 239 | /** 240 | * Given an opcode, execute the correct function. 241 | * 242 | * @param opcode The operation to execute 243 | */ 244 | protected void executeInstruction(int opcode) { 245 | switch (opcode) { 246 | case 0x0: 247 | switch (operand & 0x00FF) { 248 | case 0xE0: 249 | screen.clearScreen(bitplane); 250 | lastOpDesc = "CLS"; 251 | break; 252 | 253 | case 0xEE: 254 | returnFromSubroutine(); 255 | break; 256 | 257 | case 0xFB: 258 | scrollRight(); 259 | break; 260 | 261 | case 0xFC: 262 | scrollLeft(); 263 | break; 264 | 265 | case 0xFD: 266 | kill(); 267 | break; 268 | 269 | case 0xFE: 270 | disableExtendedMode(); 271 | break; 272 | 273 | case 0xFF: 274 | enableExtendedMode(); 275 | break; 276 | 277 | default: 278 | switch (operand & 0xF0) { 279 | case 0xC0: 280 | scrollDown(operand); 281 | break; 282 | 283 | case 0xD0: 284 | scrollUp(operand); 285 | break; 286 | 287 | default: 288 | lastOpDesc = "Operation " + toHex(operand, 4) + " not supported"; 289 | break; 290 | } 291 | break; 292 | } 293 | break; 294 | 295 | case 0x1: 296 | jumpToAddress(); 297 | break; 298 | 299 | case 0x2: 300 | jumpToSubroutine(); 301 | break; 302 | 303 | case 0x3: 304 | skipIfRegisterEqualValue(); 305 | break; 306 | 307 | case 0x4: 308 | skipIfRegisterNotEqualValue(); 309 | break; 310 | 311 | case 0x5: 312 | int op = operand & 0x000F; 313 | if (op == 0) { 314 | skipIfRegisterEqualRegister(); 315 | return; 316 | } 317 | 318 | if (op == 2) { 319 | storeSubsetOfRegistersInMemory(); 320 | return; 321 | } 322 | 323 | if (op == 3) { 324 | loadSubsetOfRegistersFromMemory(); 325 | return; 326 | } 327 | 328 | lastOpDesc = "Operation " + toHex(operand, 4) + " not supported"; 329 | break; 330 | 331 | case 0x6: 332 | moveValueToRegister(); 333 | break; 334 | 335 | case 0x7: 336 | addValueToRegister(); 337 | break; 338 | 339 | case 0x8: 340 | switch (operand & 0x000F) { 341 | case 0x0: 342 | moveRegisterIntoRegister(); 343 | break; 344 | 345 | case 0x1: 346 | logicalOr(); 347 | break; 348 | 349 | case 0x2: 350 | logicalAnd(); 351 | break; 352 | 353 | case 0x3: 354 | exclusiveOr(); 355 | break; 356 | 357 | case 0x4: 358 | addRegisterToRegister(); 359 | break; 360 | 361 | case 0x5: 362 | subtractRegisterFromRegister(); 363 | break; 364 | 365 | case 0x6: 366 | rightShift(); 367 | break; 368 | 369 | case 0x7: 370 | subtractRegisterFromRegister1(); 371 | break; 372 | 373 | case 0xE: 374 | leftShift(); 375 | break; 376 | 377 | default: 378 | lastOpDesc = "Operation " + toHex(operand, 4) + " not supported"; 379 | break; 380 | } 381 | break; 382 | 383 | case 0x9: 384 | skipIfRegisterNotEqualRegister(); 385 | break; 386 | 387 | case 0xA: 388 | loadIndexWithValue(); 389 | break; 390 | 391 | case 0xB: 392 | jumpToRegisterPlusValue(); 393 | break; 394 | 395 | case 0xC: 396 | generateRandomNumber(); 397 | break; 398 | 399 | case 0xD: 400 | drawSprite(); 401 | break; 402 | 403 | case 0xE: 404 | switch (operand & 0x00FF) { 405 | case 0x9E: 406 | skipIfKeyPressed(); 407 | break; 408 | 409 | case 0xA1: 410 | skipIfKeyNotPressed(); 411 | break; 412 | 413 | default: 414 | lastOpDesc = "Operation " + toHex(operand, 4) + " not supported"; 415 | break; 416 | } 417 | break; 418 | 419 | case 0xF: 420 | switch (operand & 0x00FF) { 421 | case 0x00: 422 | indexLoadLong(); 423 | break; 424 | 425 | case 0x01: 426 | setBitplane(); 427 | break; 428 | 429 | case 0x02: 430 | loadAudioPatternBuffer(); 431 | break; 432 | 433 | case 0x07: 434 | moveDelayTimerIntoRegister(); 435 | break; 436 | 437 | case 0x0A: 438 | waitForKeypress(); 439 | break; 440 | 441 | case 0x15: 442 | moveRegisterIntoDelayRegister(); 443 | break; 444 | 445 | case 0x18: 446 | moveRegisterIntoSoundRegister(); 447 | break; 448 | 449 | case 0x1E: 450 | addRegisterIntoIndex(); 451 | break; 452 | 453 | case 0x29: 454 | loadIndexWithSprite(); 455 | break; 456 | 457 | case 0x30: 458 | loadIndexWithExtendedSprite(); 459 | break; 460 | 461 | case 0x33: 462 | storeBCDInMemory(); 463 | break; 464 | 465 | case 0x3A: 466 | loadPitch(); 467 | break; 468 | 469 | case 0x55: 470 | storeRegistersInMemory(); 471 | break; 472 | 473 | case 0x65: 474 | readRegistersFromMemory(); 475 | break; 476 | 477 | case 0x75: 478 | storeRegistersInRPL(); 479 | break; 480 | 481 | case 0x85: 482 | readRegistersFromRPL(); 483 | break; 484 | 485 | default: 486 | lastOpDesc = "Operation " + toHex(operand, 4) + " not supported"; 487 | break; 488 | } 489 | break; 490 | 491 | default: 492 | lastOpDesc = "Operation " + toHex(operand, 4) + " not supported"; 493 | break; 494 | } 495 | } 496 | 497 | /** 498 | * 00FB - SCRR 499 | * Scrolls the screen right by 4 pixels. 500 | */ 501 | private void scrollRight() { 502 | screen.scrollRight(bitplane); 503 | lastOpDesc = "Scroll Right"; 504 | } 505 | 506 | /** 507 | * 00FC - SCRL 508 | * Scrolls the screen left by 4 pixels. 509 | */ 510 | private void scrollLeft() { 511 | screen.scrollLeft(bitplane); 512 | lastOpDesc = "Scroll Left"; 513 | } 514 | 515 | /** 516 | * 00EE - RTS 517 | * Return from subroutine. Pop the current value in the stack pointer off of 518 | * the stack, and set the program counter to the value popped. 519 | */ 520 | protected void returnFromSubroutine() { 521 | stack -= 1; 522 | pc = memory.read(stack) << 8; 523 | stack -= 1; 524 | pc += memory.read(stack); 525 | lastOpDesc = "RTS"; 526 | } 527 | 528 | /** 529 | * 1nnn - JUMP nnn 530 | * Jump to address. 531 | */ 532 | protected void jumpToAddress() { 533 | pc = operand & 0x0FFF; 534 | lastOpDesc = "JUMP " + toHex(operand & 0x0FFF, 3); 535 | } 536 | 537 | /** 538 | * 2nnn - CALL nnn 539 | * Jump to subroutine. Save the current program counter on the stack. 540 | */ 541 | protected void jumpToSubroutine() { 542 | memory.write(pc & 0x00FF, stack); 543 | stack += 1; 544 | memory.write((pc & 0xFF00) >> 8, stack); 545 | stack += 1; 546 | pc = operand & 0x0FFF; 547 | lastOpDesc = "CALL " + toHex(operand & 0x0FFF, 3); 548 | } 549 | 550 | /** 551 | * 3xnn - SKE Vx, nn 552 | * Skip if register contents equal to constant value. The program counter is 553 | * updated to skip the next instruction by advancing it by 2 bytes. 554 | */ 555 | protected void skipIfRegisterEqualValue() { 556 | int x = (operand & 0x0F00) >> 8; 557 | if (v[x] == (operand & 0x00FF)) { 558 | pc += 2; 559 | if (memory.read(pc - 2) == 0xF0 && memory.read(pc - 1) == 0x00) { 560 | pc += 2; 561 | } 562 | } 563 | lastOpDesc = "SKE V" + toHex(x, 1) + ", " + toHex(operand & 0x00FF, 2); 564 | } 565 | 566 | /** 567 | * 4xnn - SKNE Vx, nn 568 | * Skip if register contents not equal to constant value. The program 569 | * counter is updated to skip the next instruction by advancing it by 2 570 | * bytes. 571 | */ 572 | protected void skipIfRegisterNotEqualValue() { 573 | int x = (operand & 0x0F00) >> 8; 574 | if (v[x] != (operand & 0x00FF)) { 575 | pc += 2; 576 | if (memory.read(pc - 2) == 0xF0 && memory.read(pc - 1) == 0x00) { 577 | pc += 2; 578 | } 579 | } 580 | lastOpDesc = "SKNE V" + toHex(x, 1) + ", " + toHex(operand & 0x00FF, 2); 581 | } 582 | 583 | /** 584 | * 5xy0 - SKE Vx, Vy 585 | * Skip if source register is equal to target register. The program counter 586 | * is updated to skip the next instruction by advancing it by 2 bytes. 587 | */ 588 | protected void skipIfRegisterEqualRegister() { 589 | int x = (operand & 0x0F00) >> 8; 590 | int y = (operand & 0x00F0) >> 4; 591 | if (v[x] == v[y]) { 592 | pc += 2; 593 | if (memory.read(pc - 2) == 0xF0 && memory.read(pc - 1) == 0x00) { 594 | pc += 2; 595 | } 596 | } 597 | lastOpDesc = "SKE V" + toHex(x, 1) + ", V" + toHex(y, 1); 598 | } 599 | 600 | /** 601 | * 5xy2 - STORSUB [I], Vx, Vy 602 | * Store a subset of registers from x to y in memory starting at index. 603 | */ 604 | protected void storeSubsetOfRegistersInMemory() { 605 | int x = (operand & 0x0F00) >> 8; 606 | int y = (operand & 0x00F0) >> 4; 607 | int pointer = 0; 608 | 609 | if (y >= x) { 610 | for (int z = x; z < y + 1; z++) { 611 | memory.write(v[z], index + pointer); 612 | pointer++; 613 | } 614 | } else { 615 | for (int z = x; z > (y - 1); z--) { 616 | memory.write(v[z], index + pointer); 617 | pointer++; 618 | } 619 | } 620 | lastOpDesc = "STORSUB [I], V" + toHex(x, 1) + ", V" + toHex(y, 1); 621 | } 622 | 623 | /** 624 | * 5xy3 - LOADSUB [I], Vx, Vy 625 | * Load a subset of registers from x to y in memory starting at index. 626 | */ 627 | protected void loadSubsetOfRegistersFromMemory() { 628 | int x = (operand & 0x0F00) >> 8; 629 | int y = (operand & 0x00F0) >> 4; 630 | int pointer = 0; 631 | 632 | if (y >= x) { 633 | for (int z = x; z < y + 1; z++) { 634 | v[z] = memory.read(index + pointer); 635 | pointer++; 636 | } 637 | } else { 638 | for (int z = x; z > (y - 1); z--) { 639 | v[z] = memory.read(index + pointer); 640 | pointer++; 641 | } 642 | } 643 | lastOpDesc = "LOADSUB [I], V" + toHex(x, 1) + ", V" + toHex(y, 1); 644 | } 645 | 646 | /** 647 | * 6xnn - LOAD Vx, nn 648 | * Move the constant value into the specified register. 649 | */ 650 | protected void moveValueToRegister() { 651 | int x = (operand & 0x0F00) >> 8; 652 | v[x] = (short) (operand & 0x00FF); 653 | lastOpDesc = "LOAD V" + toHex(x, 1) + ", " + toHex(operand & 0x00FF, 2); 654 | } 655 | 656 | /** 657 | * 7xnn - ADD Vx, nn 658 | * Add the constant value to the specified register. 659 | */ 660 | protected void addValueToRegister() { 661 | int x = (operand & 0x0F00) >> 8; 662 | v[x] = (short) ((v[x] + (operand & 0x00FF)) % 256); 663 | lastOpDesc = "ADD V" + toHex(x, 1) + ", " + toHex(operand & 0x00FF, 2); 664 | } 665 | 666 | /** 667 | * 8xy0 - LOAD Vx, Vy 668 | * Move the value of the source register into the value of the target 669 | * register. 670 | */ 671 | protected void moveRegisterIntoRegister() { 672 | int x = (operand & 0x0F00) >> 8; 673 | int y = (operand & 0x00F0) >> 4; 674 | v[x] = v[y]; 675 | lastOpDesc = "LOAD V" + toHex(x, 1) + ", V" + toHex(y, 1); 676 | } 677 | 678 | /** 679 | * 8xy1 - OR Vx, Vy 680 | * Perform a logical OR operation between the source and the target 681 | * register, and store the result in the target register. 682 | */ 683 | protected void logicalOr() { 684 | int x = (operand & 0x0F00) >> 8; 685 | int y = (operand & 0x00F0) >> 4; 686 | v[x] |= v[y]; 687 | if (logicQuirks) { 688 | v[0xF] = 0; 689 | } 690 | lastOpDesc = "OR V" + toHex(x, 1) + ", V" + toHex(y, 1); 691 | } 692 | 693 | /** 694 | * 8xy2 - AND Vx, Vy 695 | * Perform a logical AND operation between the source and the target 696 | * register, and store the result in the target register. 697 | */ 698 | protected void logicalAnd() { 699 | int x = (operand & 0x0F00) >> 8; 700 | int y = (operand & 0x00F0) >> 4; 701 | v[x] &= v[y]; 702 | if (logicQuirks) { 703 | v[0xF] = 0; 704 | } 705 | lastOpDesc = "AND V" + toHex(x, 1) + ", V" + toHex(y, 1); 706 | } 707 | 708 | /** 709 | * 8xy3 - XOR Vx, Vy 710 | * Perform a logical XOR operation between the source and the target 711 | * register, and store the result in the target register. 712 | */ 713 | protected void exclusiveOr() { 714 | int x = (operand & 0x0F00) >> 8; 715 | int y = (operand & 0x00F0) >> 4; 716 | v[x] ^= v[y]; 717 | if (logicQuirks) { 718 | v[0xF] = 0; 719 | } 720 | lastOpDesc = "XOR V" + toHex(x, 1) + ", V" + toHex(y, 1); 721 | } 722 | 723 | /** 724 | * 8xy4 - ADD Vx, Vy 725 | * Add the value in the source register to the value in the target register, 726 | * and store the result in the target register. If a carry is generated, set 727 | * a carry flag in register VF. 728 | */ 729 | protected void addRegisterToRegister() { 730 | int x = (operand & 0x0F00) >> 8; 731 | int y = (operand & 0x00F0) >> 4; 732 | short carry = (v[x] + v[y]) > 255 ? (short) 1 : (short) 0; 733 | v[x] = (short) ((v[x] + v[y]) % 256); 734 | v[0xF] = carry; 735 | lastOpDesc = "ADD V" + toHex(x, 1) + ", V" + toHex(y, 1); 736 | } 737 | 738 | /** 739 | * 8xy5 - SUB Vx, Vy 740 | * Subtract the value in the target register from the value in the source 741 | * register, and store the result in the target register. If a borrow is NOT 742 | * generated, set a carry flag in register VF. 743 | */ 744 | protected void subtractRegisterFromRegister() { 745 | int x = (operand & 0x0F00) >> 8; 746 | int y = (operand & 0x00F0) >> 4; 747 | short borrow = (v[x] >= v[y]) ? (short) 1 : (short) 0; 748 | v[x] = (v[x] >= v[y]) ? (short) (v[x] - v[y]) : (short) (256 + v[x] - v[y]); 749 | v[0xF] = borrow; 750 | lastOpDesc = "SUBN V" + toHex(x, 1) + ", V" + toHex(y, 1); 751 | } 752 | 753 | /** 754 | * 8xy6 - SHR Vx, Vy 755 | * Shift the bits in the specified register 1 bit to the right. Bit 0 will 756 | * be shifted into register VF. 757 | */ 758 | protected void rightShift() { 759 | int x = (operand & 0x0F00) >> 8; 760 | int y = (operand & 0x00F0) >> 4; 761 | short bit_one; 762 | if (shiftQuirks) { 763 | bit_one = (short) (v[x] & 0x1); 764 | v[x] = (short) (v[x] >> 1); 765 | } else { 766 | bit_one = (short) (v[y] & 0x1); 767 | v[x] = (short) (v[y] >> 1); 768 | } 769 | v[0xF] = bit_one; 770 | lastOpDesc = "SHR V" + toHex(x, 1) + ", V" + toHex(y, 1); 771 | } 772 | 773 | /** 774 | * 8xy7 - SUBN Vx, Vy 775 | * Subtract the value in the target register from the value in the source 776 | * register, and store the result in the target register. If a borrow is NOT 777 | * generated, set a carry flag in register VF. 778 | */ 779 | protected void subtractRegisterFromRegister1() { 780 | int x = (operand & 0x0F00) >> 8; 781 | int y = (operand & 0x00F0) >> 4; 782 | short not_borrow = (v[y] >= v[x]) ? (short) 1 : (short) 0; 783 | v[x] = (v[y] >= v[x]) ? (short) (v[y] - v[x]) : (short) (256 + v[y] - v[x]); 784 | v[0xF] = not_borrow; 785 | lastOpDesc = "SUBN V" + toHex(x, 1) + ", V" + toHex(y, 1); 786 | } 787 | 788 | /** 789 | * 8xyE - SHL Vx, Vy 790 | * Shift the bits in the specified register 1 bit to the left. Bit 7 will be 791 | * shifted into register VF. 792 | */ 793 | protected void leftShift() { 794 | int x = (operand & 0x0F00) >> 8; 795 | int y = (operand & 0x00F0) >> 4; 796 | short bit_seven; 797 | if (shiftQuirks) { 798 | bit_seven = (short) ((v[x] & 0x80) >> 7); 799 | v[x] = (short) ((v[x] << 1) & 0xFF); 800 | } else { 801 | bit_seven = (short) ((v[y] & 0x80) >> 7); 802 | v[x] = (short) ((v[y] << 1) & 0xFF); 803 | } 804 | v[0xF] = bit_seven; 805 | lastOpDesc = "SHL V" + toHex(x, 1) + ", V" + toHex(y, 1); 806 | } 807 | 808 | /** 809 | * 9xy0 - SKNE Vx, Vy 810 | * Skip if source register is equal to target register. The program counter 811 | * is updated to skip the next instruction by advancing it by 2 bytes. 812 | */ 813 | protected void skipIfRegisterNotEqualRegister() { 814 | int x = (operand & 0x0F00) >> 8; 815 | int y = (operand & 0x00F0) >> 4; 816 | if (v[x] != v[y]) { 817 | pc += 2; 818 | if (memory.read(pc - 2) == 0xF0 && memory.read(pc - 1) == 0x00) { 819 | pc += 2; 820 | } 821 | } 822 | lastOpDesc = "SKNE V" + toHex(x, 1) + ", V" + toHex(y, 1); 823 | } 824 | 825 | /** 826 | * Annn - LOAD I, nnn 827 | * Load index register with constant value. 828 | */ 829 | protected void loadIndexWithValue() { 830 | index = (short) (operand & 0x0FFF); 831 | lastOpDesc = "LOAD I, " + toHex(index, 3); 832 | } 833 | 834 | /** 835 | * Bnnn - JUMP V0 + nnn 836 | * Load the program counter with the memory value located at the specified 837 | * operand plus the value of the index register. 838 | */ 839 | protected void jumpToRegisterPlusValue() { 840 | if (jumpQuirks) { 841 | int x = (operand & 0xF00) >> 8; 842 | pc = v[x] + (operand & 0x00FF); 843 | lastOpDesc = "JUMP V" + toHex(x, 1) + " + " + toHex(operand & 0x00FF, 4); 844 | } else { 845 | pc = v[0] + (operand & 0x0FFF); 846 | lastOpDesc = "JUMP V0 + " + toHex(operand & 0x0FFF, 3); 847 | } 848 | } 849 | 850 | /** 851 | * Cxnn - RAND Vx, nn 852 | * A random number between 0 and 255 is generated. The contents of it are 853 | * then ANDed with the constant value passed in the operand. The result is 854 | * stored in the target register. 855 | */ 856 | protected void generateRandomNumber() { 857 | int value = operand & 0x00FF; 858 | int x = (operand & 0x0F00) >> 8; 859 | v[x] = (short) (value & random.nextInt(256)); 860 | lastOpDesc = "RAND V" + toHex(x, 1) + ", " + toHex(value, 2); 861 | } 862 | 863 | /** 864 | * Dxyn - DRAW x, y, num_bytes 865 | * Draws the sprite pointed to in the index register at the specified x and 866 | * y coordinates. Drawing is done via an XOR routine, meaning that if the 867 | * target pixel is already turned on, and a pixel is set to be turned on at 868 | * that same location via the draw, then the pixel is turned off. The 869 | * routine will wrap the pixels if they are drawn off the edge of the 870 | * screen. Each sprite is 8 bits (1 byte) wide. The num_bytes parameter sets 871 | * how tall the sprite is. Consecutive bytes in the memory pointed to by the 872 | * index register make up the bytes of the sprite. Each bit in the sprite 873 | * byte determines whether a pixel is turned on (1) or turned off (0). If 874 | * writing a pixel to a location causes that pixel to be turned off, then VF 875 | * will be set to 1. 876 | */ 877 | protected void drawSprite() { 878 | int x = (operand & 0x0F00) >> 8; 879 | int y = (operand & 0x00F0) >> 4; 880 | int numBytes = (operand & 0xF); 881 | v[0xF] = 0; 882 | 883 | String drawOperation = "DRAW"; 884 | if ((numBytes == 0)) { 885 | if (bitplane == 3) { 886 | drawExtendedSprite(v[x], v[y], 1, index); 887 | drawExtendedSprite(v[x], v[y], 2, index + 32); 888 | } else { 889 | drawExtendedSprite(v[x], v[y], bitplane, index); 890 | } 891 | drawOperation = "DRAWEX"; 892 | } else { 893 | if (bitplane == 3) { 894 | drawNormalSprite(v[x], v[y], numBytes, 1, index); 895 | drawNormalSprite(v[x], v[y], numBytes, 2, index + numBytes); 896 | } else { 897 | drawNormalSprite(v[x], v[y], numBytes, bitplane, index); 898 | } 899 | } 900 | lastOpDesc = drawOperation + " V" + toHex(x, 1) + ", V" + toHex(y, 1); 901 | } 902 | 903 | /** 904 | * Draws the sprite on the screen based on the Super Chip 8 extensions. 905 | * Sprites are considered to be 16 bytes high. 906 | * 907 | * @param xPos the x position to draw the sprite at 908 | * @param yPos the y position to draw the sprite at 909 | * @param bitplane the bitplane to draw to 910 | * @param activeIndex the effective index to use when loading sprite data 911 | */ 912 | private void drawExtendedSprite(int xPos, int yPos, int bitplane, int activeIndex) { 913 | for (int yIndex = 0; yIndex < 16; yIndex++) { 914 | for (int xByte = 0; xByte < 2; xByte++) { 915 | short colorByte = memory.read(activeIndex + (yIndex * 2) + xByte); 916 | int yCoord = yPos + yIndex; 917 | if (yCoord < screen.getHeight()) { 918 | yCoord = yCoord % screen.getHeight(); 919 | short mask = 0x80; 920 | 921 | for (int xIndex = 0; xIndex < 8; xIndex++) { 922 | int xCoord = xPos + xIndex + (xByte * 8); 923 | if ((!clipQuirks) || (xCoord < screen.getWidth())) { 924 | xCoord = xCoord % screen.getWidth(); 925 | 926 | boolean turnedOn = (colorByte & mask) > 0; 927 | boolean currentOn = screen.getPixel(xCoord, yCoord, bitplane); 928 | 929 | v[0xF] += (turnedOn && currentOn) ? (short) 1 : (short) 0; 930 | screen.drawPixel(xCoord, yCoord, turnedOn ^ currentOn, bitplane); 931 | mask = (short) (mask >> 1); 932 | } 933 | } 934 | } else { 935 | v[0xF] += 1; 936 | } 937 | } 938 | } 939 | } 940 | 941 | /** 942 | * Draws a sprite on the screen while in NORMAL mode. 943 | * 944 | * @param xPos the X position of the sprite 945 | * @param yPos the Y position of the sprite 946 | * @param numBytes the number of bytes to draw 947 | * @param bitplane the bitplane to draw to 948 | * @param activeIndex the effective index to use when loading sprite data 949 | */ 950 | private void drawNormalSprite(int xPos, int yPos, int numBytes, int bitplane, int activeIndex) { 951 | for (int yIndex = 0; yIndex < numBytes; yIndex++) { 952 | short colorByte = memory.read(activeIndex + yIndex); 953 | int yCoord = yPos + yIndex; 954 | if ((!clipQuirks) || (yCoord < screen.getHeight())) { 955 | yCoord = yCoord % screen.getHeight(); 956 | short mask = 0x80; 957 | for (int xIndex = 0; xIndex < 8; xIndex++) { 958 | int xCoord = xPos + xIndex; 959 | if ((!clipQuirks) || (xCoord < screen.getWidth())) { 960 | xCoord = xCoord % screen.getWidth(); 961 | 962 | boolean turnedOn = (colorByte & mask) > 0; 963 | boolean currentOn = screen.getPixel(xCoord, yCoord, bitplane); 964 | 965 | v[0xF] |= (turnedOn && currentOn) ? (short) 1 : (short) 0; 966 | screen.drawPixel(xCoord, yCoord, turnedOn ^ currentOn, bitplane); 967 | mask = (short) (mask >> 1); 968 | } 969 | } 970 | } 971 | } 972 | } 973 | 974 | /** 975 | * Ex9E - SKPR Vx 976 | * Check to see if the key specified in the source register is pressed, and 977 | * if it is, skips the next instruction. 978 | */ 979 | protected void skipIfKeyPressed() { 980 | int x = (operand & 0x0F00) >> 8; 981 | int keyToCheck = v[x]; 982 | if (keyboard.isKeyPressed(keyToCheck)) { 983 | pc += 2; 984 | if (memory.read(pc - 2) == 0xF0 && memory.read(pc - 1) == 0x00) { 985 | pc += 2; 986 | } 987 | } 988 | lastOpDesc = "SKPR V" + toHex(x, 1); 989 | } 990 | 991 | /** 992 | * ExA1 - SKUP Vx 993 | * Check for the specified keypress in the source register and if it is NOT 994 | * pressed, will skip the next instruction. 995 | */ 996 | protected void skipIfKeyNotPressed() { 997 | int x = (operand & 0x0F00) >> 8; 998 | int keyToCheck = v[x]; 999 | if (!keyboard.isKeyPressed(keyToCheck)) { 1000 | pc += 2; 1001 | if (memory.read(pc - 2) == 0xF0 && memory.read(pc - 1) == 0x00) { 1002 | pc += 2; 1003 | } 1004 | } 1005 | lastOpDesc = "SKUP V" + toHex(x, 1); 1006 | } 1007 | 1008 | /** 1009 | * F000 - LOADLONG 1010 | * Loads the index register with a 16-bit long value. Consumes the next two 1011 | * bytes from memory and increments the PC by two bytes. 1012 | */ 1013 | protected void indexLoadLong() { 1014 | index = (memory.read(pc) << 8) + memory.read(pc + 1); 1015 | pc += 2; 1016 | lastOpDesc = "LOADLONG " + toHex(index, 4); 1017 | } 1018 | 1019 | /** 1020 | * Fn01 - BITPLANE n 1021 | * Selects the active bitplane for screen drawing operations. Bitplane 1022 | * selection is as follows: 1023 | * 0 - no bitplane selected 1024 | * 1 - first bitplane selected 1025 | * 2 - second bitplane selected 1026 | * 3 - first and second bitplane selected 1027 | */ 1028 | protected void setBitplane() { 1029 | int bitplane = (operand & 0x0F00) >> 8; 1030 | this.bitplane = bitplane; 1031 | lastOpDesc = "BITPLANE " + toHex(bitplane, 1); 1032 | } 1033 | 1034 | /** 1035 | * F002 - AUDIO 1036 | * Loads he 16-byte audio pattern buffer with 16 bytes from memory 1037 | * pointed to by the index register. 1038 | */ 1039 | protected void loadAudioPatternBuffer() { 1040 | for (int x = 0; x < 16; x++) { 1041 | audioPatternBuffer[x] = memory.read(index + x); 1042 | } 1043 | try { 1044 | calculateAudioWaveform(); 1045 | } catch (Exception e) { 1046 | throw new RuntimeException(e); 1047 | } 1048 | lastOpDesc = "AUDIO " + toHex(index, 4); 1049 | } 1050 | 1051 | /** 1052 | * Fx07 - LOAD Vx, DELAY 1053 | * Move the value of the delay timer into the target register. 1054 | */ 1055 | protected void moveDelayTimerIntoRegister() { 1056 | int x = (operand & 0x0F00) >> 8; 1057 | v[x] = delay; 1058 | lastOpDesc = "LOAD V" + toHex(x, 1) + ", DELAY"; 1059 | } 1060 | 1061 | /** 1062 | * Fx0A - KEYD Vx 1063 | * Stop execution until a key is pressed. Move the value of the key pressed 1064 | * into the specified register. 1065 | */ 1066 | protected void waitForKeypress() { 1067 | awaitingKeypress = true; 1068 | } 1069 | 1070 | /** 1071 | * Returns whether the CPU is waiting for a keypress before continuing. 1072 | * 1073 | * @return false if the CPU is waiting for a keypress, true otherwise 1074 | */ 1075 | protected boolean isAwaitingKeypress() { 1076 | return awaitingKeypress; 1077 | } 1078 | 1079 | /** 1080 | * Reads a keypress from keyboard, decodes it, and places the value in the 1081 | * specified register. If no key is waiting, returns without doing anything. 1082 | */ 1083 | protected void decodeKeypressAndContinue() { 1084 | int currentKey = keyboard.getCurrentKey(); 1085 | if (currentKey == -1) { 1086 | return; 1087 | } 1088 | 1089 | int x = (operand & 0x0F00) >> 8; 1090 | v[x] = (short) currentKey; 1091 | lastOpDesc = "KEYD V" + toHex(x, 1); 1092 | awaitingKeypress = false; 1093 | } 1094 | 1095 | /** 1096 | * Fx15 - LOAD DELAY, Vx 1097 | * Move the value stored in the specified source register into the delay 1098 | * timer. 1099 | */ 1100 | protected void moveRegisterIntoDelayRegister() { 1101 | int x = (operand & 0x0F00) >> 8; 1102 | delay = v[x]; 1103 | lastOpDesc = "LOAD DELAY, V" + toHex(x, 1); 1104 | } 1105 | 1106 | /** 1107 | * Fx18 - LOAD SOUND, Vx 1108 | * Move the value stored in the specified source register into the sound 1109 | * timer. 1110 | */ 1111 | protected void moveRegisterIntoSoundRegister() { 1112 | int x = (operand & 0x0F00) >> 8; 1113 | sound = v[x]; 1114 | lastOpDesc = "LOAD SOUND, V" + toHex(x, 1); 1115 | } 1116 | 1117 | /** 1118 | * Fx1E - ADD I, Vx 1119 | * Add the value of the register into the index register value. 1120 | */ 1121 | protected void addRegisterIntoIndex() { 1122 | int x = (operand & 0x0F00) >> 8; 1123 | index += v[x]; 1124 | lastOpDesc = "ADD I, V" + toHex(x, 1); 1125 | } 1126 | 1127 | /** 1128 | * Fx29 - LOAD I, Vx 1129 | * Load the index with the sprite indicated in the source register. All 1130 | * sprites are 5 bytes long, so the location of the specified sprite is its 1131 | * index multiplied by 5. 1132 | */ 1133 | protected void loadIndexWithSprite() { 1134 | int x = (operand & 0x0F00) >> 8; 1135 | index = v[x] * 5; 1136 | lastOpDesc = "LOAD I, V" + toHex(x, 1); 1137 | } 1138 | 1139 | /** 1140 | * Fx30 - LOAD I, Vx 1141 | * Load the index with the sprite indicated in the source register. All 1142 | * sprites are 10 bytes long, so the location of the specified sprite is its 1143 | * index multiplied by 10. 1144 | */ 1145 | protected void loadIndexWithExtendedSprite() { 1146 | int x = (operand & 0x0F00) >> 8; 1147 | index = v[x] * 10; 1148 | lastOpDesc = "LOADEXT I, V" + toHex(x, 1); 1149 | } 1150 | 1151 | /** 1152 | * Fx33 - BCD Vx 1153 | * Take the value stored in source and place the digits in the following 1154 | * locations: 1155 | *
1156 | * hundreds -> self.memory[index] tens -> self.memory[index + 1] ones -> 1157 | * self.memory[index + 2] 1158 | *
1159 | * For example, if the value is 123, then the following values will be 1160 | * placed at the specified locations: 1161 | *
1162 | * 1 -> self.memory[index] 2 -> self.memory[index + 1] 3 ->
1163 | * self.memory[index + 2]
1164 | */
1165 | protected void storeBCDInMemory() {
1166 | int x = (operand & 0x0F00) >> 8;
1167 | int bcdValue = v[x];
1168 | memory.write(bcdValue / 100, index);
1169 | memory.write((bcdValue % 100) / 10, index + 1);
1170 | memory.write((bcdValue % 100) % 10, index + 2);
1171 | lastOpDesc = "BCD V" + toHex(x, 1) + " (" + bcdValue + ")";
1172 | }
1173 |
1174 | /**
1175 | * Fx3A - Pitch Vx
1176 | * Loads the value from register x into the pitch register.
1177 | */
1178 | protected void loadPitch() {
1179 | int x = (operand & 0x0F00) >> 8;
1180 | pitch = v[x];
1181 | playbackRate = 4000 * Math.pow(2.0, (((float) pitch - 64.0) / 48.0));
1182 | lastOpDesc = "PITCH V" + toHex(x, 1) + " (" + v[x] + ")";
1183 | }
1184 |
1185 | /**
1186 | * Fn55 - STOR [I]
1187 | * Store the V registers in the memory pointed to by the index
1188 | * register.
1189 | */
1190 | protected void storeRegistersInMemory() {
1191 | int n = (operand & 0x0F00) >> 8;
1192 | for (int counter = 0; counter <= n; counter++) {
1193 | memory.write(v[counter], index + counter);
1194 | }
1195 | if (!indexQuirks) {
1196 | index += n + 1;
1197 | }
1198 | lastOpDesc = "STOR " + toHex(n, 1);
1199 | }
1200 |
1201 | /**
1202 | * Fn65 - LOAD V, I
1203 | * Read the V registers from the memory pointed to by the index
1204 | * register.
1205 | */
1206 | protected void readRegistersFromMemory() {
1207 | int n = (operand & 0x0F00) >> 8;
1208 | for (int counter = 0; counter <= n; counter++) {
1209 | v[counter] = memory.read(index + counter);
1210 | }
1211 | if (!indexQuirks) {
1212 | index += n + 1;
1213 | }
1214 | lastOpDesc = "READ " + toHex(n, 1);
1215 | }
1216 |
1217 | /**
1218 | * Fn75 - STORRPL n
1219 | * Stores the values from the V registers into the RPL registers.
1220 | */
1221 | protected void storeRegistersInRPL() {
1222 | int n = (operand & 0x0F00) >> 8;
1223 | System.arraycopy(v, 0, rpl, 0, n + 1);
1224 | lastOpDesc = "STORRPL " + toHex(n, 1);
1225 | }
1226 |
1227 | /**
1228 | * Fn85 - READRPL n
1229 | * Reads the values from the RPL registers back into the V registers.
1230 | */
1231 | protected void readRegistersFromRPL() {
1232 | int n = (operand & 0x0F00) >> 8;
1233 | System.arraycopy(rpl, 0, v, 0, n + 1);
1234 | lastOpDesc = "READRPL " + toHex(n, 1);
1235 | }
1236 |
1237 | /**
1238 | * Reset the CPU by blanking out all registers, and resetting the stack
1239 | * pointer and program counter to their starting values.
1240 | */
1241 | public void reset() {
1242 | v = new short[NUM_REGISTERS];
1243 | rpl = new short[NUM_REGISTERS];
1244 | pc = PROGRAM_COUNTER_START;
1245 | stack = STACK_POINTER_START;
1246 | index = 0;
1247 | delay = 0;
1248 | sound = 0;
1249 | pitch = 64;
1250 | playbackRate = 4000.0;
1251 | bitplane = 1;
1252 | if (screen != null) {
1253 | screen.clearScreen(bitplane);
1254 | }
1255 | awaitingKeypress = false;
1256 | audioPatternBuffer = new int[16];
1257 | soundPlaying = false;
1258 | tickCounter = 0;
1259 | }
1260 |
1261 | /**
1262 | * Decrement the delay timer and the sound timer if they are not zero.
1263 | */
1264 | private void decrementTimers() {
1265 | delay -= (delay != 0) ? (short) 1 : (short) 0;
1266 | sound -= (sound != 0) ? (short) 1 : (short) 0;
1267 |
1268 | if ((sound > 0) && (!soundPlaying)) {
1269 | if (generatedClip != null) {
1270 | generatedClip.loop(Clip.LOOP_CONTINUOUSLY);
1271 | soundPlaying = true;
1272 | }
1273 | }
1274 |
1275 | if ((sound == 0) && soundPlaying) {
1276 | if (generatedClip != null) {
1277 | generatedClip.stop();
1278 | soundPlaying = false;
1279 | }
1280 | }
1281 | }
1282 |
1283 | /**
1284 | * Turns on extended mode for the CPU.
1285 | */
1286 | protected void enableExtendedMode() {
1287 | screen.setExtendedScreenMode();
1288 | mode = MODE_EXTENDED;
1289 | }
1290 |
1291 | /**
1292 | * Turns on extended mode for the CPU.
1293 | */
1294 | private void disableExtendedMode() {
1295 | screen.setNormalScreenMode();
1296 | mode = MODE_NORMAL;
1297 | }
1298 |
1299 | /**
1300 | * 00Cn - SCROLL DOWN n
1301 | * Scrolls the screen down by the specified number of pixels.
1302 | *
1303 | * @param operand the operand to parse
1304 | */
1305 | private void scrollDown(int operand) {
1306 | int numPixels = operand & 0xF;
1307 | screen.scrollDown(numPixels, bitplane);
1308 | lastOpDesc = "Scroll Down " + numPixels;
1309 | }
1310 |
1311 | /**
1312 | * 00Dn - SCROLL UP n
1313 | * Scrolls the screen up by the specified number of pixels.
1314 | *
1315 | * @param operand the operand to parse
1316 | */
1317 | private void scrollUp(int operand) {
1318 | int numPixels = operand & 0xF;
1319 | screen.scrollUp(numPixels, bitplane);
1320 | lastOpDesc = "Scroll Up " + numPixels;
1321 | }
1322 |
1323 | /**
1324 | * Based on a playback rate specified by the XO Chip pitch, generate
1325 | * an audio waveform from the 16-byte audio_pattern_buffer. It converts
1326 | * the 16-bytes pattern into 128 separate bits. The bits are then used to fill
1327 | * a sample buffer. The sample buffer is filled by resampling the 128-bit
1328 | * pattern at the specified frequency. The sample buffer is then repeated
1329 | * until it is at least MIN_AUDIO_SAMPLES long. Playback (if currently
1330 | * happening) is stopped, the new waveform is loaded, and then playback
1331 | * is starts again (if the emulator had previously been playing a sound).
1332 | */
1333 | private void calculateAudioWaveform() throws Exception {
1334 | // Convert the 16-byte value into an array of 128-bit samples
1335 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
1336 | for (int x = 0; x < 16; x++) {
1337 | int audioByte = audioPatternBuffer[x];
1338 | int bufferMask = 0x80;
1339 | for (int y = 0; y < 8; y++) {
1340 | outputStream.write((audioByte & bufferMask) > 0 ? 127 : 0);
1341 | bufferMask = bufferMask >> 1;
1342 | }
1343 | }
1344 | outputStream.flush();
1345 | byte [] workingBuffer = outputStream.toByteArray();
1346 | outputStream.close();
1347 |
1348 | // Generate the initial re-sampled buffer
1349 | float position = 0.0f;
1350 | float step = (float) (playbackRate / AUDIO_PLAYBACK_RATE);
1351 | outputStream = new ByteArrayOutputStream();
1352 | while (position < 128.0f) {
1353 | outputStream.write(workingBuffer[(int) position]);
1354 | position += step;
1355 | }
1356 | outputStream.flush();
1357 | workingBuffer = outputStream.toByteArray();
1358 | outputStream.close();
1359 |
1360 | // Generate a final audio buffer that is at least MIN_AUDIO_SAMPLES long
1361 | int minCopies = MIN_AUDIO_SAMPLES / workingBuffer.length;
1362 | outputStream = new ByteArrayOutputStream();
1363 | for (int currentCopy = 0; currentCopy < minCopies; currentCopy++) {
1364 | outputStream.write(workingBuffer, 0, workingBuffer.length);
1365 | }
1366 | outputStream.flush();
1367 | workingBuffer = outputStream.toByteArray();
1368 | outputStream.close();
1369 |
1370 | // If there is an existing sound clip, stop it and close it
1371 | if (generatedClip != null) {
1372 | generatedClip.flush();
1373 | generatedClip.stop();
1374 | generatedClip.close();
1375 | }
1376 |
1377 | // Generate a new clip from the working audio buffer
1378 | AudioFormat audioFormat = new AudioFormat(AUDIO_PLAYBACK_RATE, 8, 1, true, false);
1379 | generatedClip = AudioSystem.getClip();
1380 | generatedClip.addLineListener(event -> {
1381 | if (LineEvent.Type.STOP.equals(event.getType())) {
1382 | event.getLine().close();
1383 | }
1384 | });
1385 | generatedClip.open(audioFormat, workingBuffer, 0, workingBuffer.length);
1386 |
1387 | // If the sound should be playing, restart the sound
1388 | if (soundPlaying) {
1389 | generatedClip.loop(Clip.LOOP_CONTINUOUSLY);
1390 | }
1391 | }
1392 |
1393 | /**
1394 | * Return the string of the last operation that occurred.
1395 | *
1396 | * @return A string containing the last operation
1397 | */
1398 | protected String getOpShortDesc() {
1399 | return lastOpDesc;
1400 | }
1401 |
1402 | /**
1403 | * Return a String representation of the operand.
1404 | *
1405 | * @return A string containing the operand
1406 | */
1407 | protected String getOp() {
1408 | return toHex(operand, 4);
1409 | }
1410 |
1411 | /**
1412 | * Converts a number into a hex string containing the number of digits
1413 | * specified.
1414 | *
1415 | * @param number The number to convert to hex
1416 | * @param numDigits The number of digits to include
1417 | * @return The String representation of the hex value
1418 | */
1419 | protected static String toHex(int number, int numDigits) {
1420 | String format = "%0" + numDigits + "X";
1421 | return String.format(format, number);
1422 | }
1423 |
1424 | /**
1425 | * Returns a status line containing the contents of the Index, Delay Timer,
1426 | * Sound Timer, Program Counter, current operand value, and a short
1427 | * description of the last operation run.
1428 | *
1429 | * @return A String containing Index, Delay, Sound, PC, operand and op
1430 | */
1431 | public String cpuStatusLine1() {
1432 | return "I:" + toHex(index, 4) + " DT:" + toHex(delay, 2) + " ST:" +
1433 | toHex(sound, 2) + " PC:" + toHex(pc, 4) + " " +
1434 | getOp() + " " + getOpShortDesc();
1435 | }
1436 |
1437 | /**
1438 | * Returns a status line containing the values of the first 8 registers.
1439 | *
1440 | * @return A String containing the values of the first 8 registers
1441 | */
1442 | public String cpuStatusLine2() {
1443 | return "V0:" + toHex(v[0], 2) + " V1:" + toHex(v[1], 2) + " V2:" +
1444 | toHex(v[2], 2) + " V3:" + toHex(v[3], 2) + " V4:" +
1445 | toHex(v[4], 2) + " V5:" + toHex(v[5], 2) + " V6:" +
1446 | toHex(v[6], 2) + " V7:" + toHex(v[7], 2);
1447 | }
1448 |
1449 | /**
1450 | * Returns a status line containing the values of the last 8 registers.
1451 | *
1452 | * @return A String containing the values of the last 8 registers
1453 | */
1454 | public String cpuStatusLine3() {
1455 | return "V8:" + toHex(v[8], 2) + " V9:" + toHex(v[9], 2) + " VA:" +
1456 | toHex(v[10], 2) + " VB:" + toHex(v[11], 2) + " VC:" +
1457 | toHex(v[12], 2) + " VD:" + toHex(v[13], 2) + " VE:" +
1458 | toHex(v[14], 2) + " VF:" + toHex(v[15], 2);
1459 | }
1460 |
1461 | /**
1462 | * Stops CPU execution.
1463 | */
1464 | public void kill() {
1465 | }
1466 | }
1467 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/components/Emulator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2025 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.components;
6 |
7 | import ca.craigthomas.chip8java.emulator.common.IO;
8 | import ca.craigthomas.chip8java.emulator.listeners.*;
9 |
10 | import javax.swing.*;
11 | import java.awt.*;
12 | import java.awt.event.KeyEvent;
13 | import java.io.InputStream;
14 | import java.util.*;
15 | import java.util.Timer;
16 | import java.util.logging.Logger;
17 |
18 | /**
19 | * The main Emulator class.
20 | */
21 | public class Emulator
22 | {
23 | // The number of buffers to use for bit blitting
24 | private static final int DEFAULT_NUMBER_OF_BUFFERS = 2;
25 |
26 | // The default title for the emulator window
27 | private static final String DEFAULT_TITLE = "Yet Another Super Chip 8 Emulator";
28 |
29 | // The logger for the class
30 | private final static Logger LOGGER = Logger.getLogger(Emulator.class.getName());
31 |
32 | // The font file for the Chip 8
33 | private static final String FONT_FILE = "FONTS.chip8";
34 |
35 | // The Chip8 components
36 | private CentralProcessingUnit cpu;
37 | private Screen screen;
38 | private Keyboard keyboard;
39 | private Memory memory;
40 |
41 | // Emulator window and frame elements
42 | private JMenuBar menuBar;
43 | private Canvas canvas;
44 | private JFrame container;
45 |
46 | // The current state of the emulator and associated tasks
47 | private volatile EmulatorState state;
48 | private int cpuCycleTime;
49 | private Timer timer;
50 | private TimerTask timerTask;
51 |
52 | /**
53 | * Convenience constructor that sets the emulator running with a 1x
54 | * screen scale, a cycle time of 0, a null rom, and trace mode off.
55 | */
56 | public Emulator() {
57 | this(1, 0, null, false, "#000000", "#666666", "#BBBBBB", "#FFFFFF", false, false, false, false, false);
58 | }
59 |
60 | /**
61 | * Initializes an Emulator based on the parameters passed.
62 | *
63 | * @param scale the screen scaling to apply to the emulator window
64 | * @param maxTicks the maximum number of operations per second to execute
65 | * @param rom the rom filename to load
66 | * @param memSize4k whether to set memory size to 4k
67 | * @param color0 the bitplane 0 color
68 | * @param color1 the bitplane 1 color
69 | * @param color2 the bitplane 2 color
70 | * @param color3 the bitplane 3 color
71 | * @param shiftQuirks whether to enable shift quirks or not
72 | * @param logicQuirks whether to enable logic quirks or not
73 | * @param jumpQuirks whether to enable logic quirks or not
74 | * @param clipQuirks whether to enable clip quirks or not
75 | */
76 | public Emulator(
77 | int scale,
78 | int maxTicks,
79 | String rom,
80 | boolean memSize4k,
81 | String color0,
82 | String color1,
83 | String color2,
84 | String color3,
85 | boolean shiftQuirks,
86 | boolean logicQuirks,
87 | boolean jumpQuirks,
88 | boolean indexQuirks,
89 | boolean clipQuirks
90 | ) {
91 | if (color0.length() != 6) {
92 | System.out.println("color_0 parameter must be 6 characters long");
93 | System.exit(1);
94 | }
95 |
96 | if (color1.length() != 6) {
97 | System.out.println("color_1 parameter must be 6 characters long");
98 | System.exit(1);
99 | }
100 |
101 | if (color2.length() != 6) {
102 | System.out.println("color_2 parameter must be 6 characters long");
103 | System.exit(1);
104 | }
105 |
106 | if (color3.length() != 6) {
107 | System.out.println("color_3 parameter must be 6 characters long");
108 | System.exit(1);
109 | }
110 |
111 | Color converted_color0 = null;
112 | try {
113 | converted_color0 = Color.decode("#" + color0);
114 | } catch (NumberFormatException e) {
115 | System.out.println("color_0 parameter could not be decoded (" + e.getMessage() +")");
116 | System.exit(1);
117 | }
118 |
119 | Color converted_color1 = null;
120 | try {
121 | converted_color1 = Color.decode("#" + color1);
122 | } catch (NumberFormatException e) {
123 | System.out.println("color_1 parameter could not be decoded (" + e.getMessage() +")");
124 | System.exit(1);
125 | }
126 |
127 | Color converted_color2 = null;
128 | try {
129 | converted_color2 = Color.decode("#" + color2);
130 | } catch (NumberFormatException e) {
131 | System.out.println("color_2 parameter could not be decoded (" + e.getMessage() +")");
132 | System.exit(1);
133 | }
134 |
135 | Color converted_color3 = null;
136 | try {
137 | converted_color3 = Color.decode("#" + color3);
138 | } catch (NumberFormatException e) {
139 | System.out.println("color_3 parameter could not be decoded (" + e.getMessage() +")");
140 | System.exit(1);
141 | }
142 |
143 | keyboard = new Keyboard();
144 | memory = new Memory(memSize4k);
145 | screen = new Screen(scale, converted_color0, converted_color1, converted_color2, converted_color3);
146 | cpu = new CentralProcessingUnit(memory, keyboard, screen);
147 | cpu.setShiftQuirks(shiftQuirks);
148 | cpu.setLogicQuirks(logicQuirks);
149 | cpu.setJumpQuirks(jumpQuirks);
150 | cpu.setIndexQuirks(indexQuirks);
151 | cpu.setClipQuirks(clipQuirks);
152 | cpu.setMaxTicks(maxTicks);
153 |
154 | // Load the font file into memory
155 | InputStream fontFileStream = IO.openInputStreamFromResource(FONT_FILE);
156 | if (!memory.loadStreamIntoMemory(fontFileStream, 0)) {
157 | LOGGER.severe("Could not load font file");
158 | kill();
159 | }
160 | IO.closeStream(fontFileStream);
161 |
162 | // Attempt to load specified ROM file
163 | setPaused();
164 | if (rom != null) {
165 | InputStream romFileStream = IO.openInputStream(rom);
166 | if (!memory.loadStreamIntoMemory(romFileStream,
167 | CentralProcessingUnit.PROGRAM_COUNTER_START)) {
168 | LOGGER.severe("Could not load ROM file [" + rom + "]");
169 | } else {
170 | setRunning();
171 | }
172 | IO.closeStream(romFileStream);
173 | }
174 |
175 | // Initialize the screen
176 | initEmulatorJFrame();
177 | start();
178 | }
179 |
180 | /**
181 | * Starts the main emulator loop running. Fires at the rate of 60Hz,
182 | * will repaint the screen and listen for any debug key presses.
183 | */
184 | public void start() {
185 | timer = new Timer();
186 | timerTask = new TimerTask() {
187 | public void run() {
188 | refreshScreen();
189 | }
190 | };
191 | timer.scheduleAtFixedRate(timerTask, 0L, 17L);
192 |
193 | while (state != EmulatorState.KILLED) {
194 | if (state != EmulatorState.PAUSED) {
195 | if (!cpu.isAwaitingKeypress()) {
196 | cpu.fetchIncrementExecute();
197 | } else {
198 | cpu.decodeKeypressAndContinue();
199 | }
200 | }
201 |
202 | if (keyboard.getRawKeyPressed() == Keyboard.CHIP8_QUIT) {
203 | break;
204 | }
205 | }
206 | kill();
207 | System.exit(0);
208 | }
209 |
210 | /**
211 | * Returns the main frame for the emulator.
212 | *
213 | * @return the JFrame containing the emulator
214 | */
215 | public JFrame getEmulatorFrame() {
216 | return container;
217 | }
218 |
219 | public CentralProcessingUnit getCPU() {
220 | return this.cpu;
221 | }
222 |
223 | public Memory getMemory() {
224 | return memory;
225 | }
226 |
227 | /**
228 | * Initializes the JFrame that the emulator will use to draw onto. Will set up the menu system and
229 | * link the action listeners to the menu items. Returns the JFrame that contains all of the emulator
230 | * screen elements.
231 | */
232 | private void initEmulatorJFrame() {
233 | container = new JFrame(DEFAULT_TITLE);
234 | menuBar = new JMenuBar();
235 |
236 | // File menu
237 | JMenu fileMenu = new JMenu("File");
238 | fileMenu.setMnemonic(KeyEvent.VK_F);
239 |
240 | JMenuItem openFile = new JMenuItem("Open", KeyEvent.VK_O);
241 | openFile.addActionListener(new OpenROMFileActionListener(this));
242 | fileMenu.add(openFile);
243 | fileMenu.addSeparator();
244 |
245 | JMenuItem quitFile = new JMenuItem("Quit", KeyEvent.VK_Q);
246 | quitFile.addActionListener(new QuitActionListener(this));
247 | fileMenu.add(quitFile);
248 | menuBar.add(fileMenu);
249 |
250 | // CPU menu
251 | JMenu cpuMenu = new JMenu("CPU");
252 | cpuMenu.setMnemonic(KeyEvent.VK_C);
253 |
254 | // Reset CPU menu item
255 | JMenuItem resetCPU = new JMenuItem("Reset", KeyEvent.VK_R);
256 | resetCPU.addActionListener(new ResetMenuItemActionListener(cpu));
257 | cpuMenu.add(resetCPU);
258 | cpuMenu.addSeparator();
259 | menuBar.add(cpuMenu);
260 |
261 | attachCanvas();
262 | }
263 |
264 | /**
265 | * Generates the canvas of the appropriate size and attaches it to the
266 | * main jFrame for the emulator.
267 | */
268 | private void attachCanvas() {
269 | int scaleFactor = screen.getScale();
270 | int scaledWidth = Screen.WIDTH * scaleFactor;
271 | int scaledHeight = Screen.HEIGHT * scaleFactor;
272 |
273 | JPanel panel = (JPanel) container.getContentPane();
274 | panel.removeAll();
275 | panel.setPreferredSize(new Dimension(scaledWidth, scaledHeight));
276 | panel.setLayout(null);
277 |
278 | canvas = new Canvas();
279 | canvas.setBounds(0, 0, scaledWidth, scaledHeight);
280 | canvas.setIgnoreRepaint(true);
281 |
282 | panel.add(canvas);
283 |
284 | container.setJMenuBar(menuBar);
285 | container.pack();
286 | container.setResizable(false);
287 | container.setVisible(true);
288 | container.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
289 | canvas.createBufferStrategy(DEFAULT_NUMBER_OF_BUFFERS);
290 | canvas.setFocusable(true);
291 | canvas.requestFocus();
292 |
293 | canvas.addKeyListener(keyboard);
294 | }
295 |
296 | /**
297 | * Will redraw the contents of the screen to the emulator window. Optionally, if
298 | * isInTraceMode is True, will also draw the contents of the overlayScreen to the screen.
299 | */
300 | private void refreshScreen() {
301 | Graphics2D graphics = (Graphics2D) canvas.getBufferStrategy().getDrawGraphics();
302 | graphics.drawImage(screen.getBuffer(), null, 0, 0);
303 | graphics.dispose();
304 | canvas.getBufferStrategy().show();
305 | }
306 |
307 | /**
308 | * Kills the CPU, any emulator based timers, and disposes the main
309 | * emulator JFrame before calling System.exit.
310 | */
311 | public void kill() {
312 | cpu.kill();
313 | timer.cancel();
314 | timer.purge();
315 | timerTask.cancel();
316 | dispose();
317 | state = EmulatorState.KILLED;
318 | }
319 |
320 | /**
321 | * Disposes of the main emulator JFrame.
322 | */
323 | public void dispose() {
324 | container.dispose();
325 | }
326 |
327 | /**
328 | * Sets the emulator running.
329 | */
330 | public void setRunning() {
331 | state = EmulatorState.RUNNING;
332 | }
333 |
334 | /**
335 | * Pauses the emulator.
336 | */
337 | public void setPaused() {
338 | state = EmulatorState.PAUSED;
339 | }
340 | }
341 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/components/EmulatorState.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2019 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.components;
6 |
7 | public enum EmulatorState
8 | {
9 | PAUSED, RUNNING, KILLED
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/components/Keyboard.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2024 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.components;
6 |
7 | import java.awt.event.KeyAdapter;
8 | import java.awt.event.KeyEvent;
9 |
10 | /**
11 | * The Keyboard class listens for keypress events and translates them into their
12 | * equivalent Chip8 key values, or will flag that a debug key was pressed.
13 | */
14 | public class Keyboard extends KeyAdapter
15 | {
16 | // Map from a keypress event to key values
17 | public static final int[] keycodeMap = {
18 | KeyEvent.VK_X, // 0x0
19 | KeyEvent.VK_1, // 0x1
20 | KeyEvent.VK_2, // 0x2
21 | KeyEvent.VK_3, // 0x3
22 | KeyEvent.VK_Q, // 0x4
23 | KeyEvent.VK_W, // 0x5
24 | KeyEvent.VK_E, // 0x6
25 | KeyEvent.VK_A, // 0x7
26 | KeyEvent.VK_S, // 0x8
27 | KeyEvent.VK_D, // 0x9
28 | KeyEvent.VK_Z, // 0xA
29 | KeyEvent.VK_C, // 0xB
30 | KeyEvent.VK_4, // 0xC
31 | KeyEvent.VK_R, // 0xD
32 | KeyEvent.VK_F, // 0xE
33 | KeyEvent.VK_V, // 0xF
34 | };
35 |
36 | public boolean [] keypressMap = {
37 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false
38 | };
39 |
40 | // The current key being pressed, -1 if no key
41 | protected int currentKeyPressed = -1;
42 |
43 | // Stores the last raw key keypress
44 | protected int rawKeyPressed;
45 |
46 | // The key to quit the emulator
47 | protected static final int CHIP8_QUIT = KeyEvent.VK_ESCAPE;
48 |
49 | public Keyboard() {}
50 |
51 | @Override
52 | public void keyPressed(KeyEvent e) {
53 | rawKeyPressed = e.getKeyCode();
54 | for (int x = 0; x < 16; x++) {
55 | if (rawKeyPressed == keycodeMap[x]) {
56 | keypressMap[x] = true;
57 | }
58 | }
59 | currentKeyPressed = mapKeycodeToChip8Key(rawKeyPressed);
60 | }
61 |
62 | @Override
63 | public void keyReleased(KeyEvent e) {
64 | rawKeyPressed = e.getKeyCode();
65 | for (int x = 0; x < 16; x++) {
66 | if (rawKeyPressed == keycodeMap[x]) {
67 | keypressMap[x] = false;
68 | }
69 | }
70 | }
71 |
72 | /**
73 | * Map a keycode value to a Chip 8 key value. See sKeycodeMap definition. Will
74 | * return -1 if no Chip8 key was pressed. In the case of multiple keys being
75 | * pressed simultaneously, will return the first one that it finds in the
76 | * keycode mapping object.
77 | *
78 | * @param keycode The code representing the key that was just pressed
79 | * @return The Chip 8 key value for the specified keycode
80 | */
81 | public int mapKeycodeToChip8Key(int keycode) {
82 | for (int i = 0; i < keycodeMap.length; i++) {
83 | if (keycodeMap[i] == keycode) {
84 | return i;
85 | }
86 | }
87 | return -1;
88 | }
89 |
90 | /**
91 | * Return the current key being pressed.
92 | *
93 | * @return The Chip8 key value being pressed
94 | */
95 | public int getCurrentKey() {
96 | return currentKeyPressed;
97 | }
98 |
99 | /**
100 | * Returns true if the specified key in the keymap is currently reported
101 | * as being pressed.
102 | *
103 | * @param key the key number in the keymap to check for
104 | * @return true if the key is pressed
105 | */
106 | public boolean isKeyPressed(int key) {
107 | if (key >= 0 && key < 16) {
108 | return keypressMap[key];
109 | }
110 | return false;
111 | }
112 |
113 | /**
114 | * Returns the currently pressed debug key. Will return 0 if no debug key was
115 | * pressed.
116 | *
117 | * @return the value of the currently pressed debug key.
118 | */
119 | public int getRawKeyPressed() {
120 | return rawKeyPressed;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/components/Memory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2018 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.components;
6 |
7 | import ca.craigthomas.chip8java.emulator.common.IO;
8 |
9 | import java.io.*;
10 |
11 | /**
12 | * Emulates the memory associated with a Chip 8 computer. Note - due to the
13 | * fact that Java does not have a native unsigned byte, all memory values are
14 | * stored as shorts instead. While having a separate class for memory access
15 | * may seem to be unwarranted, other architectures used to perform memory
16 | * mapped I/O. Since the I/O routines were accessed through memory addresses,
17 | * it makes more sense to have a separate class responsible for all memory.
18 | *
19 | * @author Craig Thomas
20 | */
21 | public class Memory
22 | {
23 | // Acceptable memory sizes
24 | public static final int MEMORY_4K = 4096;
25 | public static final int MEMORY_64K = 65536;
26 |
27 | // The internal storage array for the emulator's memory
28 | protected short[] memory;
29 |
30 | // The total size of emulator memory
31 | private int size;
32 |
33 | /**
34 | * Alternate constructor for the memory object. The memory object will default to
35 | * 64K.
36 | */
37 | public Memory() {
38 | this(false);
39 | }
40 |
41 | /**
42 | * Default constructor for the memory object. The user must set the
43 | * maximum size of the memory upon creation.
44 | *
45 | * @param memorySize4k if True, will set the maximum memory size to 4K, otherwise 64k
46 | */
47 | public Memory(boolean memorySize4k) {
48 | this.size = (memorySize4k) ? MEMORY_4K : MEMORY_64K;
49 | this.memory = new short[size];
50 | }
51 |
52 | /**
53 | * Reads a single byte value from memory.
54 | *
55 | * @param location The memory location to read from
56 | * @return The value read from memory
57 | */
58 | public short read(int location) {
59 | if (location > size) {
60 | throw new IllegalArgumentException("location must be less than memory size");
61 | }
62 |
63 | if (location < 0) {
64 | throw new IllegalArgumentException("location must be 0 or larger");
65 | }
66 |
67 | return (short) (memory[location] & 0xFF);
68 | }
69 |
70 | /**
71 | * Writes a single byte to memory.
72 | *
73 | * @param value The value to write to memory
74 | * @param location The memory location to write to
75 | */
76 | public void write(int value, int location) {
77 | if (location > size) {
78 | throw new IllegalArgumentException("location must be less than memory size");
79 | }
80 |
81 | if (location < 0) {
82 | throw new IllegalArgumentException("location must be 0 or larger");
83 | }
84 |
85 | memory[location] = (short) (value & 0xFF);
86 | }
87 |
88 | /**
89 | * Returns the size of memory allocated to the emulator.
90 | *
91 | * @return the memory size in bytes
92 | */
93 | public int getSize() {
94 | return size;
95 | }
96 |
97 | /**
98 | * Load a file full of bytes into emulator memory.
99 | *
100 | * @param stream The open stream to read from
101 | * @param offset The memory location to start loading the file into
102 | */
103 | public boolean loadStreamIntoMemory(InputStream stream, int offset) {
104 | return IO.copyStreamToShortArray(stream, memory, offset);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/components/Screen.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2024 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.components;
6 |
7 | import java.awt.*;
8 | import java.awt.image.BufferedImage;
9 |
10 | /**
11 | * A class to emulate a Chip 8 Screen. The original Chip 8 screen was 64 x 32.
12 | * This class creates a simple AWT canvas with a single back buffer to store the
13 | * current state of the Chip 8 screen. Two colors are used on the Chip 8 - the
14 | * foreColor
and the backColor
. The former is used
15 | * when turning pixels on, while the latter is used to turn pixels off.
16 | *
17 | * @author Craig Thomas
18 | */
19 | public class Screen
20 | {
21 | public static final int WIDTH = 128;
22 | public static final int HEIGHT = 64;
23 |
24 | public static final int SCREEN_MODE_NORMAL = 0;
25 | public static final int SCREEN_MODE_EXTENDED = 1;
26 |
27 | private final int scale;
28 |
29 | // The current screen mode
30 | private int screenMode;
31 |
32 | // The colors used for drawing on bitplanes
33 | private final Color color0;
34 | private final Color color1;
35 | private final Color color2;
36 | private final Color color3;
37 |
38 | // Create a back buffer to store image information
39 | protected BufferedImage backBuffer;
40 |
41 | /**
42 | * A constructor for a Chip8Screen. This is a convenience constructor that
43 | * will fill in default values for the scale and bitplane colors.
44 | */
45 | public Screen() {
46 | this(1);
47 | }
48 |
49 | /**
50 | * A constructor for a Chip8Screen. This is a convenience constructor that
51 | * will allow the caller to set the scale factor alone.
52 | *
53 | * @param scale The scale factor for the screen
54 | */
55 | public Screen(int scale) {
56 | this(scale, Color.black, Color.decode("#FF33CC"), Color.decode("#33CCFF"), Color.white);
57 | }
58 |
59 | /**
60 | * The main constructor for a Chip8Screen. This constructor allows for full
61 | * customization of the Chip8Screen object.
62 | *
63 | * @param scale the scale factor for the new screen
64 | * @param color0 the color for bitplane 0
65 | * @param color1 the color for bitplane 1
66 | * @param color2 the color for bitplane 2
67 | * @param color3 the color for bitplane 3
68 | */
69 | public Screen(int scale, Color color0, Color color1, Color color2, Color color3) {
70 | this.scale = scale;
71 | this.color0 = color0;
72 | this.color1 = color1;
73 | this.color2 = color2;
74 | this.color3 = color3;
75 | this.screenMode = SCREEN_MODE_NORMAL;
76 | this.createBackBuffer();
77 | }
78 |
79 | /**
80 | * Generates the BufferedImage that will act as the back buffer for the
81 | * screen. Flags the Screen state as having changed.
82 | */
83 | private void createBackBuffer() {
84 | backBuffer = new BufferedImage(WIDTH * scale, HEIGHT * scale, BufferedImage.TYPE_4BYTE_ABGR);
85 | }
86 |
87 | /**
88 | * Returns the color for the specified bitplane.
89 | *
90 | * @param bitplane The bitplane color to return
91 | */
92 | Color getBitplaneColor(int bitplane) {
93 | if (bitplane == 0) {
94 | return color0;
95 | }
96 |
97 | if (bitplane == 1) {
98 | return color1;
99 | }
100 |
101 | if (bitplane == 2) {
102 | return color2;
103 | }
104 |
105 | return color3;
106 | }
107 |
108 | /**
109 | * Low level routine to draw a pixel to the screen. Takes into account the
110 | * scaling factor applied to the screen. The top-left corner of the screen
111 | * is at coordinate (0, 0).
112 | *
113 | * @param x The x coordinate to place the pixel
114 | * @param y The y coordinate to place the pixel
115 | * @param color The Color of the pixel to draw
116 | */
117 | private void drawPixelPrimitive(int x, int y, Color color) {
118 | int mode_scale = (screenMode == SCREEN_MODE_EXTENDED) ? 1 : 2;
119 | Graphics2D graphics = backBuffer.createGraphics();
120 | graphics.setColor(color);
121 | graphics.fillRect(
122 | x * scale * mode_scale,
123 | y * scale * mode_scale,
124 | scale * mode_scale,
125 | scale * mode_scale);
126 | graphics.dispose();
127 | }
128 |
129 | /**
130 | * Returns true
if the pixel at the location (x, y) is
131 | * currently on.
132 | *
133 | * @param x The x coordinate of the pixel to check
134 | * @param y The y coordinate of the pixel to check
135 | * @param bitplane the bitplane to check
136 | * @return Returns true
if the pixel (x, y) is turned on
137 | */
138 | public boolean getPixel(int x, int y, int bitplane) {
139 | if (bitplane == 0) {
140 | return false;
141 | }
142 |
143 | Color bitplaneColor = getBitplaneColor(bitplane);
144 | int modeScale = (screenMode == SCREEN_MODE_EXTENDED) ? 1 : 2;
145 | int xScale = x * modeScale * scale;
146 | int yScale = y * modeScale * scale;
147 | Color color = new Color(backBuffer.getRGB(xScale, yScale), true);
148 | return color.equals(bitplaneColor) || color.equals(color3);
149 | }
150 |
151 | /**
152 | * Turn a pixel on or off at the specified location.
153 | *
154 | * @param x The x coordinate to place the pixel
155 | * @param y The y coordinate to place the pixel
156 | * @param turnOn Turns the pixel on at location x, y if true
157 | * @param bitplane the bitplane to draw to
158 | */
159 | public void drawPixel(int x, int y, boolean turnOn, int bitplane) {
160 | if (bitplane == 0) {
161 | return;
162 | }
163 |
164 | int otherBitplane = (bitplane == 1) ? 2 : 1;
165 | boolean otherPixelOn = getPixel(x, y, otherBitplane);
166 |
167 | Color drawColor = getBitplaneColor(0);
168 |
169 | if (turnOn && otherPixelOn) {
170 | drawColor = getBitplaneColor(3);
171 | }
172 | if (turnOn && !otherPixelOn) {
173 | drawColor = getBitplaneColor(bitplane);
174 | }
175 | if (!turnOn && otherPixelOn) {
176 | drawColor = getBitplaneColor(otherBitplane);
177 | }
178 | drawPixelPrimitive(x, y, drawColor);
179 | }
180 |
181 | /**
182 | * Clears the screen. Note that the caller must call
183 | * updateScreen
to flush the back buffer to the screen.
184 | *
185 | * @param bitplane The bitplane to clear
186 | */
187 | public void clearScreen(int bitplane) {
188 | if (bitplane == 0) {
189 | return;
190 | }
191 |
192 | if (bitplane == 3) {
193 | Graphics2D graphics = backBuffer.createGraphics();
194 | graphics.setColor(getBitplaneColor(0));
195 | graphics.fillRect(0, 0, WIDTH * scale, HEIGHT * scale);
196 | graphics.dispose();
197 | return;
198 | }
199 |
200 | int maxX = getWidth();
201 | int maxY = getHeight();
202 | for (int x = 0; x < maxX; x++) {
203 | for (int y = 0; y < maxY; y++) {
204 | drawPixel(x, y, false, bitplane);
205 | }
206 | }
207 | }
208 |
209 | /**
210 | * Scrolls the screen 4 pixels to the right.
211 | *
212 | * @param bitplane the bitplane to scroll
213 | */
214 | public void scrollRight(int bitplane) {
215 | if (bitplane == 0) {
216 | return;
217 | }
218 |
219 | int modeScale = (screenMode == SCREEN_MODE_EXTENDED) ? 1 : 2;
220 |
221 | int width = WIDTH * scale;
222 | int height = HEIGHT * scale;
223 | int right = scale * 4 * modeScale;
224 |
225 | if (bitplane == 3) {
226 | BufferedImage bufferedImage = copyImage(backBuffer.getSubimage(0, 0, width, height));
227 | Graphics2D graphics = backBuffer.createGraphics();
228 | graphics.setColor(color0);
229 | graphics.fillRect(0, 0, width, height);
230 | graphics.drawImage(bufferedImage, right, 0, null);
231 | graphics.dispose();
232 | return;
233 | }
234 |
235 | int maxX = getWidth();
236 | int maxY = getHeight();
237 |
238 | // Blank out any pixels in the right vertical lines that we will copy to
239 | for (int x = maxX - 4; x < maxX; x++) {
240 | for (int y = 0; y < maxY; y++) {
241 | drawPixel(x, y, false, bitplane);
242 | }
243 | }
244 |
245 | // Start copying pixels from the left to the right and shift by 4 pixels
246 | for (int x = maxX - 4 - 1; x > -1; x--) {
247 | for (int y = 0; y < maxY; y++) {
248 | boolean currentPixel = getPixel(x, y, bitplane);
249 | drawPixel(x, y, false, bitplane);
250 | drawPixel(x + 4, y, currentPixel, bitplane);
251 | }
252 | }
253 |
254 | // Blank out any pixels in the left 4 vertical lines
255 | for (int x = 0; x < 4; x++) {
256 | for (int y = 0; y < maxY; y++) {
257 | drawPixel(x, y, false, bitplane);
258 | }
259 | }
260 | }
261 |
262 | /**
263 | * Scrolls the screen 4 pixels to the left.
264 | *
265 | * @param bitplane the bitplane to scroll
266 | */
267 | public void scrollLeft(int bitplane) {
268 | if (bitplane == 0) {
269 | return;
270 | }
271 |
272 | int maxX = getWidth();
273 | int maxY = getHeight();
274 | int modeScale = (screenMode == SCREEN_MODE_EXTENDED) ? 1 : 2;
275 | int screenWidth = maxX * modeScale * scale;
276 | int screenHeight = maxY * modeScale * scale;
277 |
278 | // If bitplane 3 is selected, we can just do a fast copy instead of pixel by pixel
279 | if (bitplane == 3) {
280 | int left = -(scale * 4 * modeScale);
281 | BufferedImage bufferedImage = copyImage(backBuffer.getSubimage(0, 0, screenWidth, screenHeight));
282 | Graphics2D graphics = backBuffer.createGraphics();
283 | graphics.setColor(color0);
284 | graphics.fillRect(0, 0, screenWidth, screenHeight);
285 | graphics.drawImage(bufferedImage, left, 0, null);
286 | graphics.dispose();
287 | return;
288 | }
289 |
290 | // Blank out any pixels in the left 4 vertical lines we will copy to
291 | for (int x = 0; x < 4; x++) {
292 | for (int y = 0; y < maxY; y++) {
293 | drawPixel(x, y, false, bitplane);
294 | }
295 | }
296 |
297 | // Start copying pixels from the right to the left and shift by 4 pixels
298 | for (int x = 4; x < maxX; x++) {
299 | for (int y = 0; y < maxY; y++) {
300 | boolean currentPixel = getPixel(x, y, bitplane);
301 | drawPixel(x, y, false, bitplane);
302 | drawPixel(x - 4, y, currentPixel, bitplane);
303 | }
304 | }
305 |
306 | // Blank out any pixels in the right 4 vertical columns
307 | for (int x = maxX - 4; x < maxX; x++) {
308 | for (int y = 0; y < maxY; y++) {
309 | drawPixel(x, y, false, bitplane);
310 | }
311 | }
312 | }
313 |
314 | /**
315 | * Scrolls the screen down by the specified number of pixels.
316 | *
317 | * @param numPixels the number of pixels to scroll down
318 | * @param bitplane the bitplane to scroll
319 | */
320 | public void scrollDown(int numPixels, int bitplane) {
321 | if (bitplane == 0) {
322 | return;
323 | }
324 |
325 | int width = this.getWidth() * scale;
326 | int height = this.getHeight() * scale;
327 | int modeScale = (screenMode == SCREEN_MODE_EXTENDED) ? 1 : 2;
328 | int down = numPixels * scale * modeScale;
329 |
330 | // If bitplane 3 is selected, we can just do a fast copy instead of pixel by pixel
331 | if (bitplane == 3) {
332 | BufferedImage bufferedImage = copyImage(backBuffer.getSubimage(0, 0, width, height));
333 | Graphics2D graphics = backBuffer.createGraphics();
334 | graphics.setColor(color0);
335 | graphics.fillRect(0, 0, width, height);
336 | graphics.drawImage(bufferedImage, 0, down, null);
337 | graphics.dispose();
338 | return;
339 | }
340 |
341 | int maxX = getWidth();
342 | int maxY = getHeight();
343 |
344 | // Blank out any pixels in the bottom numPixels that we will copy to
345 | for (int x = 0; x < maxX; x++) {
346 | for (int y = maxY - numPixels; y < maxY; y++) {
347 | drawPixel(x, y, false, bitplane);
348 | }
349 | }
350 |
351 | // Start copying pixels from the top to the bottom and shift by numPixels
352 | for (int x = 0; x < maxX; x++) {
353 | for (int y = maxY - numPixels - 1; y > -1; y--) {
354 | boolean currentPixel = getPixel(x, y, bitplane);
355 | drawPixel(x, y, false, bitplane);
356 | drawPixel(x, y + numPixels, currentPixel, bitplane);
357 | }
358 | }
359 |
360 | // Blank out any pixels in the first numPixels horizontal lines
361 | for (int x = 0; x < maxX; x++) {
362 | for (int y = 0; y < numPixels; y++) {
363 | drawPixel(x, y, false, bitplane);
364 | }
365 | }
366 | }
367 |
368 | /**
369 | * Scrolls the screen up by numPixels.
370 | *
371 | * @param numPixels the number of pixels to scroll up
372 | * @param bitplane the bitplane to scroll
373 | */
374 | public void scrollUp(int numPixels, int bitplane) {
375 | if (bitplane == 0) {
376 | return;
377 | }
378 |
379 | int modeScale = (screenMode == SCREEN_MODE_EXTENDED) ? 1 : 2;
380 | int actualPixels = numPixels * modeScale * scale;
381 | int width = this.getWidth() * scale;
382 | int height = this.getHeight() * scale;
383 |
384 | // If bitplane 3 is selected, we can just do a fast copy instead of pixel by pixel
385 | if (bitplane == 3) {
386 | BufferedImage bufferedImage = copyImage(backBuffer.getSubimage(0, actualPixels, width, height - actualPixels));
387 | Graphics2D graphics = backBuffer.createGraphics();
388 | graphics.setColor(color0);
389 | graphics.fillRect(0, 0, width, height);
390 | graphics.drawImage(bufferedImage, 0, 0, null);
391 | graphics.dispose();
392 | return;
393 | }
394 |
395 | int maxX = getWidth();
396 | int maxY = getHeight();
397 |
398 | // Blank out any pixels in the top numPixels that we will copy to
399 | for (int x = 0; x < maxX; x++) {
400 | for (int y = 0; y < numPixels; y++) {
401 | drawPixel(x, y, false, bitplane);
402 | }
403 | }
404 |
405 | // Start copying pixels from the top to the bottom and shift up by numPixels
406 | for (int x = 0; x < maxX; x++) {
407 | for (int y = numPixels; y < maxY; y++) {
408 | boolean currentPixel = getPixel(x, y, bitplane);
409 | drawPixel(x, y, false, bitplane);
410 | drawPixel(x, y - numPixels, currentPixel, bitplane);
411 | }
412 | }
413 |
414 | // Blank out any piels in the bottom numPixels
415 | for (int x = 0; x < maxX; x++) {
416 | for (int y = maxY - numPixels; y < maxY; y++) {
417 | drawPixel(x, y, false, bitplane);
418 | }
419 | }
420 | }
421 |
422 | /**
423 | * Returns the height of the screen.
424 | *
425 | * @return The height of the screen in pixels
426 | */
427 | public int getHeight() {
428 | return (screenMode == SCREEN_MODE_EXTENDED) ? 64 : 32;
429 | }
430 |
431 | /**
432 | * Returns the width of the screen.
433 | *
434 | * @return The width of the screen
435 | */
436 | public int getWidth() {
437 | return (screenMode == SCREEN_MODE_EXTENDED) ? 128 : 64;
438 | }
439 |
440 | /**
441 | * Returns the scale of the screen.
442 | *
443 | * @return The scale factor of the screen
444 | */
445 | public int getScale() {
446 | return scale;
447 | }
448 |
449 | /**
450 | * Returns the BufferedImage that has the contents of the screen.
451 | *
452 | * @return the backBuffer for the screen
453 | */
454 | public BufferedImage getBuffer() {
455 | return backBuffer;
456 | }
457 |
458 | /**
459 | * Turns on the extended screen mode for the emulator (when operating
460 | * in Super Chip 8 mode). Flags the state of the emulator screen as
461 | * having been changed.
462 | */
463 | public void setExtendedScreenMode() {
464 | screenMode = SCREEN_MODE_EXTENDED;
465 | }
466 |
467 | /**
468 | * Turns on the normal screen mode for the emulator (when operating
469 | * in Super Chip 8 mode).
470 | */
471 | public void setNormalScreenMode() {
472 | screenMode = SCREEN_MODE_NORMAL;
473 | }
474 |
475 | /**
476 | * Generates a copy of the original back buffer.
477 | *
478 | * @param source the source to copy from
479 | * @return a BufferedImage that is a copy of the original source
480 | */
481 | private BufferedImage copyImage(BufferedImage source) {
482 | BufferedImage bufferedImage = new BufferedImage(source.getWidth(), source.getHeight(), source.getType());
483 | Graphics graphics = bufferedImage.getGraphics();
484 | graphics.drawImage(source, 0, 0, null);
485 | graphics.dispose();
486 | return bufferedImage;
487 | }
488 | }
489 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/listeners/OpenROMFileActionListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2019 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.listeners;
6 |
7 | import ca.craigthomas.chip8java.emulator.common.IO;
8 | import ca.craigthomas.chip8java.emulator.components.CentralProcessingUnit;
9 | import ca.craigthomas.chip8java.emulator.components.Emulator;
10 | import ca.craigthomas.chip8java.emulator.components.Memory;
11 |
12 | import javax.swing.*;
13 | import javax.swing.filechooser.FileFilter;
14 | import javax.swing.filechooser.FileNameExtensionFilter;
15 | import java.awt.event.ActionEvent;
16 | import java.awt.event.ActionListener;
17 | import java.io.File;
18 | import java.io.InputStream;
19 |
20 | /**
21 | * An ActionListener that will quit the emulator.
22 | */
23 | public class OpenROMFileActionListener implements ActionListener
24 | {
25 | private Emulator emulator;
26 |
27 | public OpenROMFileActionListener(Emulator emulator) {
28 | super();
29 | this.emulator = emulator;
30 | }
31 |
32 | @Override
33 | public void actionPerformed(ActionEvent e) {
34 | openROMFileDialog();
35 | }
36 |
37 | public void openROMFileDialog() {
38 | CentralProcessingUnit cpu = emulator.getCPU();
39 | Memory memory = emulator.getMemory();
40 | JFrame container = emulator.getEmulatorFrame();
41 | JFileChooser fileChooser = createFileChooser();
42 | if (fileChooser.showOpenDialog(container) == JFileChooser.APPROVE_OPTION) {
43 | InputStream inputStream = IO.openInputStream(fileChooser.getSelectedFile().toString());
44 | if (!memory.loadStreamIntoMemory(inputStream, CentralProcessingUnit.PROGRAM_COUNTER_START)) {
45 | JOptionPane.showMessageDialog(container, "Error reading file.", "File Read Problem",
46 | JOptionPane.ERROR_MESSAGE);
47 | emulator.setPaused();
48 | return;
49 | }
50 | cpu.reset();
51 | emulator.setRunning();
52 | }
53 | }
54 |
55 | public JFileChooser createFileChooser() {
56 | JFileChooser fileChooser = new JFileChooser();
57 | FileFilter filter1 = new FileNameExtensionFilter("CHIP8 Rom File (*.ch8)", "ch8");
58 | FileFilter filter2 = new FileNameExtensionFilter("Generic Rom File (*.rom)", "rom");
59 | fileChooser.setCurrentDirectory(new File("."));
60 | fileChooser.setDialogTitle("Open ROM file");
61 | fileChooser.setAcceptAllFileFilterUsed(true);
62 | fileChooser.setFileFilter(filter1);
63 | fileChooser.setFileFilter(filter2);
64 | return fileChooser;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/listeners/QuitActionListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2018 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.listeners;
6 |
7 | import ca.craigthomas.chip8java.emulator.components.Emulator;
8 |
9 | import java.awt.event.ActionEvent;
10 | import java.awt.event.ActionListener;
11 |
12 | /**
13 | * An ActionListener that will quit the emulator.
14 | */
15 | public class QuitActionListener implements ActionListener
16 | {
17 | private Emulator emulator;
18 |
19 | public QuitActionListener(Emulator emulator) {
20 | this.emulator = emulator;
21 | }
22 |
23 | @Override
24 | public void actionPerformed(ActionEvent e) {
25 | emulator.kill();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/listeners/ResetMenuItemActionListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2018 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.listeners;
6 |
7 | import ca.craigthomas.chip8java.emulator.components.CentralProcessingUnit;
8 |
9 | import java.awt.event.ActionEvent;
10 | import java.awt.event.ActionListener;
11 |
12 | /**
13 | * An ActionListener that will reset the specified CPU when triggered.
14 | */
15 | public class ResetMenuItemActionListener implements ActionListener
16 | {
17 | // The CPU that the ActionListener will reset when triggered
18 | private CentralProcessingUnit cpu;
19 |
20 | public ResetMenuItemActionListener(CentralProcessingUnit cpu) {
21 | super();
22 | this.cpu = cpu;
23 | }
24 |
25 | @Override
26 | public void actionPerformed(ActionEvent e) {
27 | cpu.reset();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/runner/Arguments.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2025 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.runner;
6 |
7 | import com.beust.jcommander.Parameter;
8 | import ca.craigthomas.chip8java.emulator.components.CentralProcessingUnit;
9 |
10 | /**
11 | * A data class that stores the arguments that may be passed to the emulator.
12 | */
13 | public class Arguments
14 | {
15 | @Parameter(description="ROM file")
16 | public String romFile;
17 |
18 | @Parameter(names={"--scale"}, description="scale factor")
19 | public Integer scale = 7;
20 |
21 | @Parameter(names={"--mem_size_4k"}, description="sets memory size to 4K (defaults to 64K)")
22 | public Boolean memSize4k = false;
23 |
24 | @Parameter(names={"--color_0"}, description="the hex color to use for the background (default=000000)", arity = 1)
25 | public String color0 = "000000";
26 |
27 | @Parameter(names={"--color_1"}, description="the hex color to use for bitplane 1 (default=FF33CC)", arity = 1)
28 | public String color1 = "FF33CC";
29 |
30 | @Parameter(names={"--color_2"}, description="the hex color to use for the bitplane 2 (default=33CCFF)", arity = 1)
31 | public String color2 = "33CCFF";
32 |
33 | @Parameter(names={"--color_3"}, description="the hex color to use for the bitplane 3 (default=FFFFFF)", arity = 1)
34 | public String color3 = "FFFFFF";
35 |
36 | @Parameter(names={"--shift_quirks"}, description="enable shift quirks")
37 | public Boolean shiftQuirks = false;
38 |
39 | @Parameter(names={"--logic_quirks"}, description="enable logic quirks")
40 | public Boolean logicQuirks = false;
41 |
42 | @Parameter(names={"--jump_quirks"}, description="enable jump quirks")
43 | public Boolean jumpQuirks = false;
44 |
45 | @Parameter(names={"--index_quirks"}, description="enable index quirks")
46 | public Boolean indexQuirks = false;
47 |
48 | @Parameter(names={"--clip_quirks"}, description="enable clip quirks")
49 | public Boolean clipQuirks = false;
50 |
51 | @Parameter(names={"--ticks"}, description="how many instructions per seconds are allowed")
52 | public int maxTicks = CentralProcessingUnit.DEFAULT_MAX_TICKS;
53 | }
--------------------------------------------------------------------------------
/src/main/java/ca/craigthomas/chip8java/emulator/runner/Runner.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2025 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.runner;
6 |
7 | import com.beust.jcommander.JCommander;
8 | import ca.craigthomas.chip8java.emulator.components.Emulator;
9 |
10 | /**
11 | * The main Emulator class for the Chip 8. The main
method will
12 | * attempt to parse any command line options passed to the emulator.
13 | *
14 | * @author Craig Thomas
15 | */
16 | public class Runner
17 | {
18 | /**
19 | * Runs the emulator with the specified command line options.
20 | *
21 | * @param argv the set of options passed to the emulator
22 | */
23 | public static void main(String[] argv) {
24 | Arguments args = new Arguments();
25 | JCommander jCommander = JCommander.newBuilder().addObject(args).build();
26 | jCommander.setProgramName("yac8e");
27 | jCommander.parse(argv);
28 |
29 | /* Create the emulator and start it running */
30 | Emulator emulator = new Emulator(
31 | args.scale,
32 | args.maxTicks,
33 | args.romFile,
34 | args.memSize4k,
35 | args.color0,
36 | args.color1,
37 | args.color2,
38 | args.color3,
39 | args.shiftQuirks,
40 | args.logicQuirks,
41 | args.jumpQuirks,
42 | args.indexQuirks,
43 | args.clipQuirks
44 | );
45 | emulator.start();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/resources/FONTS.chip8:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craigthomas/Chip8Java/b4f1debcd6f45a3ba5fae54bbe99c428a3c688bb/src/main/resources/FONTS.chip8
--------------------------------------------------------------------------------
/src/test/java/ca/craigthomas/chip8java/emulator/common/IOTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2019 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.common;
6 |
7 | import org.apache.commons.io.IOUtils;
8 | import org.junit.Test;
9 | import org.mockito.Mockito;
10 |
11 | import java.io.*;
12 |
13 | import static org.junit.Assert.*;
14 | import static org.mockito.Mockito.spy;
15 |
16 | public class IOTest
17 | {
18 | private static final String GOOD_STREAM_FILE = "test_stream_file.bin";
19 |
20 | @Test
21 | public void testOpenInputStreamFromResourceReturnsNullOnBadFile() {
22 | InputStream result = IO.openInputStreamFromResource("this_file_does_not_exist.bin");
23 | assertNull(result);
24 | }
25 |
26 | @Test
27 | public void testOpenInputStreamFromResourceReturnsNullOnNull() {
28 | InputStream result = IO.openInputStreamFromResource(null);
29 | assertNull(result);
30 | }
31 |
32 | @Test
33 | public void testOpenInputStreamFromResourceWorksCorrectly() throws IOException {
34 | InputStream stream = IO.openInputStreamFromResource(GOOD_STREAM_FILE);
35 | byte[] result = IOUtils.toByteArray(stream);
36 | byte[] expected = {0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74};
37 | assertArrayEquals(expected, result);
38 | }
39 |
40 | @Test
41 | public void testOpenInputStreamReturnsNotNull() {
42 | File resourceFile = new File(getClass().getClassLoader().getResource(GOOD_STREAM_FILE).getFile());
43 | InputStream result = IO.openInputStream(resourceFile.getPath());
44 | assertNotNull(result);
45 | }
46 |
47 | @Test
48 | public void testOpenInputStreamReturnsNullWithBadFilename() {
49 | InputStream result = IO.openInputStream("this_file_does_not_exist.bin");
50 | assertNull(result);
51 | }
52 |
53 | @Test
54 | public void testCloseStreamWorksCorrectly() throws IOException {
55 | ByteArrayOutputStream stream = spy(ByteArrayOutputStream.class);
56 | boolean result = IO.closeStream(stream);
57 | Mockito.verify(stream).close();
58 | assertTrue(result);
59 | }
60 |
61 | @Test
62 | public void testCloseStreamOnNullStreamDoesNotThrowException() {
63 | boolean result = IO.closeStream(null);
64 | assertFalse(result);
65 | }
66 |
67 | @Test
68 | public void testCopyStreamFailsWhenSourceIsNull() {
69 | short[] target = new short[14];
70 | assertFalse(IO.copyStreamToShortArray(null, target, 0));
71 | }
72 |
73 | @Test
74 | public void testCopyStreamFailsWhenTargetIsNull() {
75 | File resourceFile = new File(getClass().getClassLoader().getResource(GOOD_STREAM_FILE).getFile());
76 | InputStream stream = IO.openInputStream(resourceFile.getPath());
77 | assertFalse(IO.copyStreamToShortArray(stream, null, 0));
78 | }
79 |
80 | @Test
81 | public void testCopyStreamFailsWhenSourceBiggerThanTarget() {
82 | File resourceFile = new File(getClass().getClassLoader().getResource(GOOD_STREAM_FILE).getFile());
83 | InputStream stream = IO.openInputStream(resourceFile.getPath());
84 | short[] target = new short[2];
85 | assertFalse(IO.copyStreamToShortArray(stream, target, 0));
86 | }
87 |
88 | @Test
89 | public void testCopyStreamWorksCorrectly() {
90 | short[] expected = {0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74};
91 | File resourceFile = new File(getClass().getClassLoader().getResource(GOOD_STREAM_FILE).getFile());
92 | InputStream stream = IO.openInputStream(resourceFile.getPath());
93 | short[] target = new short[14];
94 | assertTrue(IO.copyStreamToShortArray(stream, target, 0));
95 | assertArrayEquals(expected, target);
96 | }
97 | }
--------------------------------------------------------------------------------
/src/test/java/ca/craigthomas/chip8java/emulator/components/KeyboardTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2024 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.components;
6 |
7 | import static org.junit.Assert.*;
8 | import static org.mockito.Mockito.*;
9 |
10 | import java.awt.event.KeyEvent;
11 |
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.mockito.Mockito;
15 |
16 | /**
17 | * Tests for the Chip8 Keyboard.
18 | */
19 | public class KeyboardTest
20 | {
21 | private Keyboard keyboard;
22 | private Emulator emulator;
23 | private KeyEvent event;
24 | private static final int KEY_NOT_IN_MAPPING = KeyEvent.VK_H;
25 |
26 | @Before
27 | public void setUp() {
28 | keyboard = new Keyboard();
29 | event = mock(KeyEvent.class);
30 | }
31 |
32 | @Test
33 | public void testMapKeycodeToChip8Key() {
34 | for (int index = 0; index < Keyboard.keycodeMap.length; index++) {
35 | assertEquals(index, keyboard.mapKeycodeToChip8Key(Keyboard.keycodeMap[index]));
36 | }
37 | }
38 |
39 | @Test
40 | public void testMapKeycodeToChip8KeyReturnsZeroOnInvalidKey() {
41 | assertEquals(-1, keyboard.mapKeycodeToChip8Key(KEY_NOT_IN_MAPPING));
42 | }
43 |
44 | @Test
45 | public void testCurrentKeyIsZeroWhenNoKeyPressed() {
46 | assertEquals(-1, keyboard.getCurrentKey());
47 | }
48 |
49 | @Test
50 | public void testGetDebugKey() {
51 | keyboard.rawKeyPressed = 1;
52 | assertEquals(1, keyboard.getRawKeyPressed());
53 | }
54 |
55 | @Test
56 | public void testKeyPressedWorksCorrectly() {
57 | when(event.getKeyCode()).thenReturn(KeyEvent.VK_2);
58 | keyboard.keyPressed(event);
59 | assertEquals(2, keyboard.currentKeyPressed);
60 | }
61 |
62 | @Test
63 | public void testKeyReleased() {
64 | when(event.getKeyCode()).thenReturn(KeyEvent.VK_2);
65 | keyboard.keyPressed(event);
66 | assertTrue(keyboard.isKeyPressed(2));
67 |
68 | keyboard.keyReleased(event);
69 | assertFalse(keyboard.isKeyPressed(2));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/test/java/ca/craigthomas/chip8java/emulator/components/MemoryTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2018 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.components;
6 |
7 | import static org.junit.Assert.*;
8 |
9 | import java.io.*;
10 | import java.util.Random;
11 |
12 | import org.junit.Before;
13 | import org.junit.Test;
14 |
15 | /**
16 | * Tests for the Memory module.
17 | */
18 | public class MemoryTest
19 | {
20 | private static final String TEST_ROM = "test.chip8";
21 | private Memory memory;
22 | private Random random;
23 |
24 | @Before
25 | public void setUp() {
26 | memory = new Memory();
27 | random = new Random();
28 | for (int location = 0; location < Memory.MEMORY_64K; location++) {
29 | memory.memory[location] = (short) (random.nextInt(Short.MAX_VALUE + 1) & 0xFF);
30 | }
31 | }
32 |
33 | private void closeStream(InputStream stream) {
34 | try {
35 | stream.close();
36 | } catch (Exception e) {
37 | fail("Failed to close the specified stream");
38 | }
39 | }
40 |
41 | @Test
42 | public void testMemorySets64KonDefault() {
43 | assertEquals(65536, memory.getSize());
44 | }
45 |
46 | @Test
47 | public void testMemorySets4KWhenSpecified() {
48 | memory = new Memory(true);
49 | assertEquals(4096, memory.getSize());
50 | }
51 |
52 | @Test
53 | public void testMemoryReadWorksCorrectly() {
54 | for (int location = 0; location < Memory.MEMORY_64K; location++) {
55 | assertEquals(memory.memory[location], memory.read(location));
56 | }
57 | }
58 |
59 | @Test
60 | public void testMemoryWriteWorksCorrectly() {
61 | for (int location = 0; location < Memory.MEMORY_64K; location++) {
62 | short value = (short) (random.nextInt(Short.MAX_VALUE + 1) & 0xFF);
63 | memory.write(value, location);
64 | assertEquals(value, memory.memory[location]);
65 | }
66 | }
67 |
68 | @Test(expected=IllegalArgumentException.class)
69 | public void testMemoryReadThrowsExceptionWhenLocationOutOfBounds() {
70 | memory.read(65537);
71 | }
72 |
73 | @Test(expected=IllegalArgumentException.class)
74 | public void testMemoryReadThrowsExceptionWhenLocationNegative() {
75 | memory.read(-16384);
76 | }
77 |
78 | @Test(expected=IllegalArgumentException.class)
79 | public void testMemoryWriteThrowsExceptionWhenLocationOutOfBounds() {
80 | memory.write(0, 65537);
81 | }
82 |
83 | @Test(expected=IllegalArgumentException.class)
84 | public void testMemoryWriteThrowsExceptionWhenLocationNegative() {
85 | memory.write(0, -16384);
86 | }
87 |
88 | @Test
89 | public void testLoadRomIntoMemoryReturnsTrueOnGoodFilename() {
90 | InputStream inputStream = getClass().getClassLoader().getResourceAsStream(TEST_ROM);
91 | assertTrue(memory.loadStreamIntoMemory(inputStream, 0x200));
92 | closeStream(inputStream);
93 | assertEquals(0x61, memory.read(0x200));
94 | assertEquals(0x62, memory.read(0x201));
95 | assertEquals(0x63, memory.read(0x202));
96 | assertEquals(0x64, memory.read(0x203));
97 | assertEquals(0x65, memory.read(0x204));
98 | assertEquals(0x66, memory.read(0x205));
99 | assertEquals(0x67, memory.read(0x206));
100 | }
101 |
102 | @Test
103 | public void testLoadStreamIntoMemoryReturnsFalseOnException() {
104 | assertFalse(memory.loadStreamIntoMemory(null, 0));
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/src/test/java/ca/craigthomas/chip8java/emulator/components/ScreenTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2018 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.components;
6 |
7 | import static org.junit.Assert.*;
8 |
9 | import java.awt.FontFormatException;
10 | import java.io.IOException;
11 |
12 | import org.junit.Before;
13 | import org.junit.Test;
14 |
15 | /**
16 | * Tests for the Chip8 Screen.
17 | */
18 | public class ScreenTest
19 | {
20 | private Screen screen;
21 |
22 | @Before
23 | public void setUp() throws IOException {
24 | screen = new Screen();
25 | }
26 |
27 | @Test
28 | public void testDefaultConstructorSetsCorrectWidth() {
29 | assertEquals(64, screen.getWidth());
30 | }
31 |
32 | @Test
33 | public void testDefaultConstructorSetsCorrectHeight() {
34 | assertEquals(32, screen.getHeight());
35 | }
36 |
37 | @Test
38 | public void testExtendedModeSetsCorrectWidth() {
39 | screen.setExtendedScreenMode();
40 | assertEquals(128, screen.getWidth());
41 | }
42 |
43 | @Test
44 | public void testExtendedModeSetsCorrectHeight() {
45 | screen.setExtendedScreenMode();
46 | assertEquals(64, screen.getHeight());
47 | }
48 |
49 | @Test
50 | public void testScaleFactorSetCorrectlyOnDefault() {
51 | assertEquals(1, screen.getScale());
52 | }
53 |
54 | @Test
55 | public void testScreenNoPixelsOnAtInit() {
56 | for (int xCoord = 0; xCoord < screen.getWidth(); xCoord++) {
57 | for (int yCoord = 0; yCoord < screen.getHeight(); yCoord++) {
58 | assertFalse(screen.getPixel(xCoord, yCoord, 1));
59 | }
60 | }
61 | }
62 |
63 | @Test
64 | public void testScreenTurningPixelsOnSetsGetPixel() {
65 | for (int x = 0; x < screen.getWidth(); x++) {
66 | for (int y = 0; y < screen.getHeight(); y++) {
67 | screen.drawPixel(x, y, true, 1);
68 | assertTrue(screen.getPixel(x, y, 1));
69 | }
70 | }
71 | }
72 |
73 | @Test
74 | public void testGetPixelOnBitplane0ReturnsFalse() {
75 | for (int x = 0; x < 64; x++) {
76 | for (int y = 0; y < 32; y++) {
77 | screen.drawPixel(x, y, true, 1);
78 | screen.drawPixel(x, y, true, 2);
79 | assertFalse(screen.getPixel(x, y, 0));
80 | }
81 | }
82 | }
83 |
84 | @Test
85 | public void testDrawPixelOnBitplane0DoesNothing() {
86 | for (int x = 0; x < 64; x++) {
87 | for (int y = 0; y < 32; y++) {
88 | screen.drawPixel(x, y, true, 0);
89 | assertFalse(screen.getPixel(x, y, 1));
90 | assertFalse(screen.getPixel(x, y, 2));
91 | }
92 | }
93 | }
94 |
95 | @Test
96 | public void testScreenTurningPixelsOffSetsPixelOff() {
97 | for (int x = 0; x < screen.getWidth(); x++) {
98 | for (int y = 0; y < screen.getHeight(); y++) {
99 | screen.drawPixel(x, y, true, 1);
100 | assertTrue(screen.getPixel(x, y, 1));
101 | screen.drawPixel(x, y, false, 1);
102 | assertFalse(screen.getPixel(x, y, 1));
103 | }
104 | }
105 | }
106 |
107 | @Test
108 | public void testWritePixelTurnsOnPixelOnBitplane1Only() {
109 | for (int x = 0; x < 64; x++) {
110 | for (int y = 0; y < 32; y++) {
111 | screen.drawPixel(x, y, true, 1);
112 | assertTrue(screen.getPixel(x, y, 1));
113 | assertFalse(screen.getPixel(x, y, 2));
114 | }
115 | }
116 | }
117 |
118 | @Test
119 | public void testWritePixelTurnsOnPixelOnBitplane2Only() {
120 | for (int x = 0; x < 64; x++) {
121 | for (int y = 0; y < 32; y++) {
122 | screen.drawPixel(x, y, true, 2);
123 | assertTrue(screen.getPixel(x, y, 2));
124 | assertFalse(screen.getPixel(x, y, 1));
125 | }
126 | }
127 | }
128 |
129 | @Test
130 | public void testWritePixelOnBitplane3TurnsOnPixelOnBitplane1And2() {
131 | for (int x = 0; x < 64; x++) {
132 | for (int y = 0; y < 32; y++) {
133 | screen.drawPixel(x, y, true, 3);
134 | assertTrue(screen.getPixel(x, y, 2));
135 | assertTrue(screen.getPixel(x, y, 1));
136 | }
137 | }
138 | }
139 |
140 | @Test
141 | public void testClearScreenOnBitplane0DoesNothingToBitplane1And2() {
142 | for (int x = 0; x < 64; x++) {
143 | for (int y = 0; y < 32; y++) {
144 | screen.drawPixel(x, y, true, 1);
145 | screen.drawPixel(x, y, true, 2);
146 | assertTrue(screen.getPixel(x, y, 1));
147 | assertTrue(screen.getPixel(x, y, 2));
148 | }
149 | }
150 | screen.clearScreen(0);
151 | for (int x = 0; x < 64; x++) {
152 | for (int y = 0; y < screen.getHeight(); y++) {
153 | assertTrue(screen.getPixel(x, y, 1));
154 | assertTrue(screen.getPixel(x, y, 2));
155 | }
156 | }
157 | }
158 |
159 | @Test
160 | public void testClearScreenSetsAllPixelsOffOnBitplane1() {
161 | for (int x = 0; x < 64; x++) {
162 | for (int y = 0; y < 32; y++) {
163 | screen.drawPixel(x, y, true, 1);
164 | assertTrue(screen.getPixel(x, y, 1));
165 | assertFalse(screen.getPixel(x, y, 2));
166 | }
167 | }
168 | screen.clearScreen(1);
169 | for (int x = 0; x < 64; x++) {
170 | for (int y = 0; y < screen.getHeight(); y++) {
171 | assertFalse(screen.getPixel(x, y, 1));
172 | }
173 | }
174 | }
175 |
176 | @Test
177 | public void testClearScreenSetsAllPixelsOffOnBitplane1OnlyWhenBothSet() {
178 | for (int x = 0; x < 64; x++) {
179 | for (int y = 0; y < 32; y++) {
180 | screen.drawPixel(x, y, true, 1);
181 | screen.drawPixel(x, y, true, 2);
182 | assertTrue(screen.getPixel(x, y, 1));
183 | assertTrue(screen.getPixel(x, y, 2));
184 | }
185 | }
186 | screen.clearScreen(1);
187 | for (int x = 0; x < 64; x++) {
188 | for (int y = 0; y < screen.getHeight(); y++) {
189 | assertFalse(screen.getPixel(x, y, 1));
190 | assertTrue(screen.getPixel(x, y, 2));
191 | }
192 | }
193 | }
194 |
195 | @Test
196 | public void testClearScreenSetsAllPixelsOffOnBitplane3WhenBothSet() {
197 | for (int x = 0; x < 64; x++) {
198 | for (int y = 0; y < 32; y++) {
199 | screen.drawPixel(x, y, true, 1);
200 | screen.drawPixel(x, y, true, 2);
201 | assertTrue(screen.getPixel(x, y, 1));
202 | assertTrue(screen.getPixel(x, y, 2));
203 | }
204 | }
205 | screen.clearScreen(3);
206 | for (int x = 0; x < 64; x++) {
207 | for (int y = 0; y < screen.getHeight(); y++) {
208 | assertFalse(screen.getPixel(x, y, 1));
209 | assertFalse(screen.getPixel(x, y, 2));
210 | }
211 | }
212 | }
213 |
214 | @Test
215 | public void testScaleFactorSetCorrectlyWithScaleConstructor() throws IOException {
216 | screen = new Screen(2);
217 | assertEquals(2, screen.getScale());
218 | }
219 |
220 | @Test(expected = IllegalArgumentException.class)
221 | public void testNegativeScaleFactorThrowsIllegalArgument()
222 | throws FontFormatException, IOException {
223 | screen = new Screen(-1);
224 | }
225 |
226 | @Test
227 | public void testScrollRightOnBitplane0DoesNothing() throws IOException {
228 | screen = new Screen(2);
229 | screen.drawPixel(0, 0, true, 1);
230 | screen.drawPixel(0, 0, true, 2);
231 | assertTrue(screen.getPixel(0, 0, 1));
232 | assertTrue(screen.getPixel(0, 0, 2));
233 | screen.scrollRight(0);
234 | assertTrue(screen.getPixel(0, 0, 1));
235 | assertFalse(screen.getPixel(1, 0, 1));
236 | assertFalse(screen.getPixel(2, 0, 1));
237 | assertFalse(screen.getPixel(3, 0, 1));
238 | assertFalse(screen.getPixel(4, 0, 1));
239 | assertTrue(screen.getPixel(0, 0, 2));
240 | assertFalse(screen.getPixel(1, 0, 2));
241 | assertFalse(screen.getPixel(2, 0, 2));
242 | assertFalse(screen.getPixel(3, 0, 2));
243 | assertFalse(screen.getPixel(4, 0, 2));
244 | }
245 |
246 | @Test
247 | public void testScrollRightBitplane1Only() throws IOException {
248 | screen = new Screen(2);
249 | screen.drawPixel(0, 0, true, 1);
250 | screen.drawPixel(0, 0, true, 2);
251 | screen.scrollRight(1);
252 | assertFalse(screen.getPixel(0, 0, 1));
253 | assertFalse(screen.getPixel(1, 0, 1));
254 | assertFalse(screen.getPixel(2, 0, 1));
255 | assertFalse(screen.getPixel(3, 0, 1));
256 | assertTrue(screen.getPixel(4, 0, 1));
257 | assertTrue(screen.getPixel(0, 0, 2));
258 | assertFalse(screen.getPixel(1, 0, 2));
259 | assertFalse(screen.getPixel(2, 0, 2));
260 | assertFalse(screen.getPixel(3, 0, 2));
261 | assertFalse(screen.getPixel(4, 0, 2));
262 | }
263 |
264 | @Test
265 | public void testScrollRightBitplane3() throws IOException {
266 | screen = new Screen(2);
267 | screen.drawPixel(0, 0, true, 1);
268 | screen.drawPixel(0, 0, true, 2);
269 | screen.scrollRight(3);
270 | assertFalse(screen.getPixel(0, 0, 1));
271 | assertFalse(screen.getPixel(1, 0, 1));
272 | assertFalse(screen.getPixel(2, 0, 1));
273 | assertFalse(screen.getPixel(3, 0, 1));
274 | assertTrue(screen.getPixel(4, 0, 1));
275 | assertFalse(screen.getPixel(0, 0, 2));
276 | assertFalse(screen.getPixel(1, 0, 2));
277 | assertFalse(screen.getPixel(2, 0, 2));
278 | assertFalse(screen.getPixel(3, 0, 2));
279 | assertTrue(screen.getPixel(4, 0, 2));
280 | }
281 |
282 | @Test
283 | public void testScrollLeft() throws IOException {
284 | screen = new Screen(2);
285 | screen.drawPixel(4, 0, true, 1);
286 | screen.scrollLeft(1);
287 | assertTrue(screen.getPixel(0, 0, 1));
288 | assertFalse(screen.getPixel(4, 0, 1));
289 | }
290 |
291 | @Test
292 | public void testScrollLeftOnBitplane0DoesNothing() throws IOException {
293 | screen = new Screen(2);
294 | screen.drawPixel(63, 0, true, 1);
295 | screen.drawPixel(63, 0, true, 2);
296 | assertTrue(screen.getPixel(63, 0, 1));
297 | assertTrue(screen.getPixel(63, 0, 2));
298 | screen.scrollLeft(0);
299 | assertTrue(screen.getPixel(63, 0, 1));
300 | assertFalse(screen.getPixel(62, 0, 1));
301 | assertFalse(screen.getPixel(61, 0, 1));
302 | assertFalse(screen.getPixel(60, 0, 1));
303 | assertFalse(screen.getPixel(59, 0, 1));
304 | assertTrue(screen.getPixel(63, 0, 2));
305 | assertFalse(screen.getPixel(62, 0, 2));
306 | assertFalse(screen.getPixel(61, 0, 2));
307 | assertFalse(screen.getPixel(60, 0, 2));
308 | assertFalse(screen.getPixel(59, 0, 2));
309 | }
310 |
311 | @Test
312 | public void testScrollLeftBitplane1Only() throws IOException {
313 | screen = new Screen(2);
314 | screen.drawPixel(63, 0, true, 1);
315 | screen.drawPixel(63, 0, true, 2);
316 | assertTrue(screen.getPixel(63, 0, 1));
317 | assertTrue(screen.getPixel(63, 0, 2));
318 | screen.scrollLeft(1);
319 | assertFalse(screen.getPixel(63, 0, 1));
320 | assertFalse(screen.getPixel(62, 0, 1));
321 | assertFalse(screen.getPixel(61, 0, 1));
322 | assertFalse(screen.getPixel(60, 0, 1));
323 | assertTrue(screen.getPixel(59, 0, 1));
324 | assertTrue(screen.getPixel(63, 0, 2));
325 | assertFalse(screen.getPixel(62, 0, 2));
326 | assertFalse(screen.getPixel(61, 0, 2));
327 | assertFalse(screen.getPixel(60, 0, 2));
328 | assertFalse(screen.getPixel(59, 0, 2));
329 | }
330 |
331 | @Test
332 | public void testScrollLeftBitplane3() throws IOException {
333 | screen = new Screen(2);
334 | screen.drawPixel(63, 0, true, 1);
335 | screen.drawPixel(63, 0, true, 2);
336 | assertTrue(screen.getPixel(63, 0, 1));
337 | assertTrue(screen.getPixel(63, 0, 2));
338 | screen.scrollLeft(3);
339 | assertFalse(screen.getPixel(63, 0, 1));
340 | assertFalse(screen.getPixel(62, 0, 1));
341 | assertFalse(screen.getPixel(61, 0, 1));
342 | assertFalse(screen.getPixel(60, 0, 1));
343 | assertTrue(screen.getPixel(59, 0, 1));
344 | assertFalse(screen.getPixel(63, 0, 2));
345 | assertFalse(screen.getPixel(62, 0, 2));
346 | assertFalse(screen.getPixel(61, 0, 2));
347 | assertFalse(screen.getPixel(60, 0, 2));
348 | assertTrue(screen.getPixel(59, 0, 2));
349 | }
350 |
351 | @Test
352 | public void testScrollDown() throws IOException {
353 | screen = new Screen(2);
354 | screen.drawPixel(0, 0, true, 1);
355 | screen.scrollDown(4, 1);
356 | assertTrue(screen.getPixel(0, 4, 1));
357 | assertFalse(screen.getPixel(0, 0, 1));
358 | }
359 |
360 | @Test
361 | public void testScrollDownBitplane0DoesNothing() throws IOException {
362 | screen = new Screen(2);
363 | screen.drawPixel(0, 0, true, 1);
364 | screen.drawPixel(0, 0, true, 2);
365 | screen.scrollDown(4, 0);
366 | assertTrue(screen.getPixel(0, 0, 1));
367 | assertTrue(screen.getPixel(0, 0, 2));
368 | }
369 |
370 | @Test
371 | public void testScrollUpBitplane0DoesNothing() throws IOException {
372 | screen = new Screen(2);
373 | screen.drawPixel(0, 1, true, 1);
374 | screen.drawPixel(0, 1, true, 2);
375 | assertTrue(screen.getPixel(0, 1, 1));
376 | assertTrue(screen.getPixel(0, 1, 2));
377 | screen.scrollUp(1, 0);
378 | assertFalse(screen.getPixel(0, 0, 1));
379 | assertFalse(screen.getPixel(0, 0, 2));
380 | assertTrue(screen.getPixel(0, 1, 1));
381 | assertTrue(screen.getPixel(0, 1, 2));
382 | }
383 |
384 | @Test
385 | public void testScrollDownBitplane1() throws IOException {
386 | screen = new Screen(2);
387 | screen.drawPixel(0, 0, true, 1);
388 | assertTrue(screen.getPixel(0, 0, 1));
389 | assertFalse(screen.getPixel(0, 0, 2));
390 | screen.scrollDown(1, 1);
391 | assertFalse(screen.getPixel(0, 0, 1));
392 | assertFalse(screen.getPixel(0, 0, 2));
393 | assertTrue(screen.getPixel(0, 1, 1));
394 | assertFalse(screen.getPixel(0, 1, 2));
395 | }
396 |
397 | @Test
398 | public void testScrollDownBitplane1BothPixelsActive() throws IOException {
399 | screen = new Screen(2);
400 | screen.drawPixel(0, 0, true, 1);
401 | screen.drawPixel(0, 0, true, 2);
402 | assertTrue(screen.getPixel(0, 0, 1));
403 | assertTrue(screen.getPixel(0, 0, 2));
404 | screen.scrollDown(1, 1);
405 | assertFalse(screen.getPixel(0, 0, 1));
406 | assertTrue(screen.getPixel(0, 0, 2));
407 | assertTrue(screen.getPixel(0, 1, 1));
408 | assertFalse(screen.getPixel(0, 1, 2));
409 | }
410 |
411 | @Test
412 | public void testScrollDownBitplane3BothPixelsActive() throws IOException {
413 | screen = new Screen(2);
414 | screen.drawPixel(0, 0, true, 1);
415 | screen.drawPixel(0, 0, true, 2);
416 | assertTrue(screen.getPixel(0, 0, 1));
417 | assertTrue(screen.getPixel(0, 0, 2));
418 | screen.scrollDown(1, 3);
419 | assertFalse(screen.getPixel(0, 0, 1));
420 | assertFalse(screen.getPixel(0, 0, 2));
421 | assertTrue(screen.getPixel(0, 1, 1));
422 | assertTrue(screen.getPixel(0, 1, 2));
423 | }
424 |
425 | @Test
426 | public void testScrollUpBitplane1() throws IOException {
427 | screen = new Screen(2);
428 | screen.drawPixel(0, 1, true, 1);
429 | assertTrue(screen.getPixel(0, 1, 1));
430 | assertFalse(screen.getPixel(0, 1, 2));
431 | screen.scrollUp(1, 1);
432 | assertTrue(screen.getPixel(0, 0, 1));
433 | assertFalse(screen.getPixel(0, 0, 2));
434 | assertFalse(screen.getPixel(0, 1, 1));
435 | assertFalse(screen.getPixel(0, 1, 2));
436 | }
437 |
438 | @Test
439 | public void testScrollUpBitplane1BothPixelsActive() throws IOException {
440 | screen = new Screen(2);
441 | screen.drawPixel(0, 1, true, 1);
442 | screen.drawPixel(0, 1, true, 2);
443 | assertTrue(screen.getPixel(0, 1, 1));
444 | assertTrue(screen.getPixel(0, 1, 2));
445 | screen.scrollUp(1, 1);
446 | assertTrue(screen.getPixel(0, 0, 1));
447 | assertFalse(screen.getPixel(0, 0, 2));
448 | assertFalse(screen.getPixel(0, 1, 1));
449 | assertTrue(screen.getPixel(0, 1, 2));
450 | }
451 |
452 | @Test
453 | public void testScrollRightBitplane0DoesNothing() throws IOException {
454 | screen = new Screen(2);
455 | screen.drawPixel(0, 1, true, 1);
456 | screen.drawPixel(0, 1, true, 2);
457 | assertTrue(screen.getPixel(0, 1, 1));
458 | assertTrue(screen.getPixel(0, 1, 2));
459 | screen.scrollRight(0);
460 | assertTrue(screen.getPixel(0, 1, 1));
461 | assertFalse(screen.getPixel(1, 1, 1));
462 | assertFalse(screen.getPixel(2, 1, 1));
463 | assertFalse(screen.getPixel(3, 1, 1));
464 | assertFalse(screen.getPixel(4, 1, 1));
465 | assertTrue(screen.getPixel(0, 1, 2));
466 | assertFalse(screen.getPixel(1, 1, 2));
467 | assertFalse(screen.getPixel(2, 1, 2));
468 | assertFalse(screen.getPixel(3, 1, 2));
469 | assertFalse(screen.getPixel(4, 1, 2));
470 | }
471 |
472 | @Test
473 | public void testScrollRightBitplane1() throws IOException {
474 | screen = new Screen(2);
475 | screen.drawPixel(0, 1, true, 1);
476 | screen.drawPixel(0, 1, true, 2);
477 | assertTrue(screen.getPixel(0, 1, 1));
478 | assertTrue(screen.getPixel(0, 1, 2));
479 | screen.scrollRight(1);
480 | assertFalse(screen.getPixel(0, 1, 1));
481 | assertFalse(screen.getPixel(1, 1, 1));
482 | assertFalse(screen.getPixel(2, 1, 1));
483 | assertFalse(screen.getPixel(3, 1, 1));
484 | assertTrue(screen.getPixel(4, 1, 1));
485 | assertTrue(screen.getPixel(0, 1, 2));
486 | assertFalse(screen.getPixel(1, 1, 2));
487 | assertFalse(screen.getPixel(2, 1, 2));
488 | assertFalse(screen.getPixel(3, 1, 2));
489 | assertFalse(screen.getPixel(4, 1, 2));
490 | }
491 |
492 | @Test
493 | public void testScrollUpBitplane3BothPixelsActive() throws IOException {
494 | screen = new Screen(2);
495 | screen.drawPixel(0, 1, true, 1);
496 | screen.drawPixel(0, 1, true, 2);
497 | assertTrue(screen.getPixel(0, 1, 1));
498 | assertTrue(screen.getPixel(0, 1, 2));
499 | screen.scrollUp(1, 3);
500 | assertTrue(screen.getPixel(0, 0, 1));
501 | assertTrue(screen.getPixel(0, 0, 2));
502 | assertFalse(screen.getPixel(0, 1, 1));
503 | assertFalse(screen.getPixel(0, 1, 2));
504 | }
505 |
506 | @Test
507 | public void testGetBackBufferWorksCorrectly() {
508 | screen = new Screen(2);
509 | assertEquals(screen.backBuffer, screen.getBuffer());
510 | }
511 | }
512 |
--------------------------------------------------------------------------------
/src/test/java/ca/craigthomas/chip8java/emulator/listeners/OpenROMFileActionListenerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2019 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.listeners;
6 |
7 | import ca.craigthomas.chip8java.emulator.components.CentralProcessingUnit;
8 | import ca.craigthomas.chip8java.emulator.components.Emulator;
9 | import ca.craigthomas.chip8java.emulator.components.Memory;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 |
13 | import javax.swing.*;
14 | import java.awt.event.ActionEvent;
15 | import java.io.File;
16 |
17 | import static org.mockito.Mockito.*;
18 |
19 | public class OpenROMFileActionListenerTest
20 | {
21 | private OpenROMFileActionListener listenerSpy;
22 | private ActionEvent mockItemEvent;
23 | private JFileChooser fileChooser;
24 |
25 | @Before
26 | public void setUp() {
27 | Emulator emulator = mock(Emulator.class);
28 | Memory memory = mock(Memory.class);
29 | CentralProcessingUnit cpu = mock(CentralProcessingUnit.class);
30 | when(memory.loadStreamIntoMemory(any(), anyInt())).thenReturn(true);
31 |
32 | fileChooser = mock(JFileChooser.class);
33 | when(fileChooser.getSelectedFile()).thenReturn(new File("test.chip8"));
34 |
35 | OpenROMFileActionListener listener = new OpenROMFileActionListener(emulator);
36 | listenerSpy = spy(listener);
37 | ButtonModel buttonModel = mock(ButtonModel.class);
38 | when(buttonModel.isSelected()).thenReturn(true);
39 | AbstractButton button = mock(AbstractButton.class);
40 | when(button.getModel()).thenReturn(buttonModel);
41 | mockItemEvent = mock(ActionEvent.class);
42 | when(mockItemEvent.getSource()).thenReturn(button);
43 | when(listenerSpy.createFileChooser()).thenReturn(fileChooser);
44 | when(emulator.getMemory()).thenReturn(memory);
45 | when(emulator.getCPU()).thenReturn(cpu);
46 | }
47 |
48 | @Test
49 | public void testOpenMenuItemActionListenerShowsWhenClicked() {
50 | listenerSpy.actionPerformed(mockItemEvent);
51 | listenerSpy.actionPerformed(mockItemEvent);
52 | verify(listenerSpy, times(2)).createFileChooser();
53 | verify(fileChooser, times(2)).showOpenDialog(any());
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/java/ca/craigthomas/chip8java/emulator/listeners/QuitActionListenerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2019 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.listeners;
6 |
7 | import ca.craigthomas.chip8java.emulator.components.Emulator;
8 | import org.junit.Before;
9 | import org.junit.Test;
10 |
11 | import javax.swing.*;
12 | import java.awt.event.ActionEvent;
13 |
14 | import static org.mockito.Mockito.*;
15 |
16 | public class QuitActionListenerTest
17 | {
18 | private QuitActionListener listenerSpy;
19 | private ActionEvent mockItemEvent;
20 | private Emulator emulator;
21 |
22 | @Before
23 | public void setUp() {
24 | emulator = mock(Emulator.class);
25 |
26 | QuitActionListener listener = new QuitActionListener(emulator);
27 | listenerSpy = spy(listener);
28 | ButtonModel buttonModel = mock(ButtonModel.class);
29 | when(buttonModel.isSelected()).thenReturn(true);
30 | AbstractButton button = mock(AbstractButton.class);
31 | when(button.getModel()).thenReturn(buttonModel);
32 | mockItemEvent = mock(ActionEvent.class);
33 | when(mockItemEvent.getSource()).thenReturn(button);
34 | }
35 |
36 | @Test
37 | public void testQuitActionListenerShowsWhenClicked() {
38 | listenerSpy.actionPerformed(mockItemEvent);
39 | verify(emulator, times(1)).kill();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/test/java/ca/craigthomas/chip8java/emulator/listeners/ResetMenuItemActionListenerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013-2018 Craig Thomas
3 | * This project uses an MIT style license - see LICENSE for details.
4 | */
5 | package ca.craigthomas.chip8java.emulator.listeners;
6 |
7 | import ca.craigthomas.chip8java.emulator.components.CentralProcessingUnit;
8 | import org.junit.Before;
9 | import org.junit.Test;
10 | import org.mockito.Mockito;
11 | import org.mockito.Spy;
12 |
13 | import javax.swing.*;
14 | import java.awt.event.ActionEvent;
15 |
16 | import static org.mockito.Mockito.mock;
17 | import static org.mockito.Mockito.times;
18 | import static org.mockito.Mockito.verify;
19 |
20 | /**
21 | * Tests for the PauseMenuItemListener.
22 | */
23 | public class ResetMenuItemActionListenerTest
24 | {
25 | private @Spy CentralProcessingUnit cpu;
26 | private ResetMenuItemActionListener resetMenuItemActionListener;
27 | private ActionEvent mockItemEvent;
28 |
29 | @Before
30 | public void setUp() {
31 | cpu = mock(CentralProcessingUnit.class);
32 | resetMenuItemActionListener = new ResetMenuItemActionListener(cpu);
33 | ButtonModel buttonModel = mock(ButtonModel.class);
34 | Mockito.when(buttonModel.isSelected()).thenReturn(true).thenReturn(false);
35 | AbstractButton button = mock(AbstractButton.class);
36 | Mockito.when(button.getModel()).thenReturn(buttonModel);
37 | mockItemEvent = mock(ActionEvent.class);
38 | Mockito.when(mockItemEvent.getSource()).thenReturn(button);
39 | }
40 |
41 | @Test
42 | public void testCPUResetWhenItemActionListenerTriggered() {
43 | resetMenuItemActionListener.actionPerformed(mockItemEvent);
44 | verify(cpu, times(1)).reset();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/resources/test.chip8:
--------------------------------------------------------------------------------
1 | abcdefg
--------------------------------------------------------------------------------
/src/test/resources/test_stream_file.bin:
--------------------------------------------------------------------------------
1 | This is a test
--------------------------------------------------------------------------------