├── .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 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/craigthomas/Chip8Java/gradle.yml?style=flat-square&branch=main)](https://github.com/craigthomas/Chip8Java/actions) 4 | [![Coverage Status](https://img.shields.io/codecov/c/gh/craigthomas/Chip8Java?style=flat-square)](https://codecov.io/gh/craigthomas/Chip8Java) 5 | [![Dependencies](https://img.shields.io/librariesio/github/craigthomas/Chip8Java?style=flat-square)](https://libraries.io/github/craigthomas/Chip8Java) 6 | [![Version](https://img.shields.io/github/release/craigthomas/Chip8Java?style=flat-square)](https://github.com/craigthomas/Chip8Java/releases) 7 | [![Downloads](https://img.shields.io/github/downloads/craigthomas/Chip8Java/total?style=flat-square)](https://github.com/craigthomas/Chip8Java/releases) 8 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](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 --------------------------------------------------------------------------------