├── src
├── test
│ ├── resources
│ │ ├── test.chip8
│ │ └── test_stream_file.bin
│ └── java
│ │ └── ca
│ │ └── craigthomas
│ │ └── chip8java
│ │ └── emulator
│ │ ├── listeners
│ │ ├── QuitActionListenerTest.java
│ │ ├── ResetMenuItemActionListenerTest.java
│ │ └── OpenROMFileActionListenerTest.java
│ │ ├── components
│ │ ├── KeyboardTest.java
│ │ ├── MemoryTest.java
│ │ └── ScreenTest.java
│ │ └── common
│ │ └── IOTest.java
└── main
│ ├── resources
│ └── FONTS.chip8
│ └── java
│ └── ca
│ └── craigthomas
│ └── chip8java
│ └── emulator
│ ├── components
│ ├── EmulatorState.java
│ ├── Memory.java
│ ├── Keyboard.java
│ ├── Emulator.java
│ ├── Screen.java
│ └── CentralProcessingUnit.java
│ ├── listeners
│ ├── QuitActionListener.java
│ ├── ResetMenuItemActionListener.java
│ └── OpenROMFileActionListener.java
│ ├── runner
│ ├── Runner.java
│ └── Arguments.java
│ └── common
│ └── IO.java
├── .gitignore
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .github
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── gradle.yml
├── PULL_REQUEST_TEMPLATE.md
├── LICENSE
├── gradlew.bat
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── gradlew
└── README.md
/src/test/resources/test.chip8:
--------------------------------------------------------------------------------
1 | abcdefg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .gradle
3 | build
4 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'chip8java'
2 |
--------------------------------------------------------------------------------
/src/test/resources/test_stream_file.bin:
--------------------------------------------------------------------------------
1 | This is a test
--------------------------------------------------------------------------------
/src/main/resources/FONTS.chip8:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craigthomas/Chip8Java/HEAD/src/main/resources/FONTS.chip8
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/craigthomas/Chip8Java/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gradle"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | name: Build Test Coverage
2 | on: [push, pull_request]
3 | jobs:
4 | run:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v6
9 | - name: Setup JDK 17
10 | uses: actions/setup-java@v5
11 | with:
12 | java-version: '17'
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@v5
22 | env:
23 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Use the maximum available, or set MAX_FD != -1 to use that value.
89 | MAX_FD=maximum
90 |
91 | warn () {
92 | echo "$*"
93 | } >&2
94 |
95 | die () {
96 | echo
97 | echo "$*"
98 | echo
99 | exit 1
100 | } >&2
101 |
102 | # OS specific support (must be 'true' or 'false').
103 | cygwin=false
104 | msys=false
105 | darwin=false
106 | nonstop=false
107 | case "$( uname )" in #(
108 | CYGWIN* ) cygwin=true ;; #(
109 | Darwin* ) darwin=true ;; #(
110 | MSYS* | MINGW* ) msys=true ;; #(
111 | NONSTOP* ) nonstop=true ;;
112 | esac
113 |
114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
115 |
116 |
117 | # Determine the Java command to use to start the JVM.
118 | if [ -n "$JAVA_HOME" ] ; then
119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
120 | # IBM's JDK on AIX uses strange locations for the executables
121 | JAVACMD=$JAVA_HOME/jre/sh/java
122 | else
123 | JAVACMD=$JAVA_HOME/bin/java
124 | fi
125 | if [ ! -x "$JAVACMD" ] ; then
126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
127 |
128 | Please set the JAVA_HOME variable in your environment to match the
129 | location of your Java installation."
130 | fi
131 | else
132 | JAVACMD=java
133 | if ! command -v java >/dev/null 2>&1
134 | then
135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
136 |
137 | Please set the JAVA_HOME variable in your environment to match the
138 | location of your Java installation."
139 | fi
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 |
201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
203 |
204 | # Collect all arguments for the java command;
205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
206 | # shell script including quotes and variable substitutions, so put them in
207 | # double quotes to make sure that they get re-expanded; and
208 | # * put everything else in single quotes, so that it's not re-expanded.
209 |
210 | set -- \
211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
212 | -classpath "$CLASSPATH" \
213 | org.gradle.wrapper.GradleWrapperMain \
214 | "$@"
215 |
216 | # Stop when "xargs" is not available.
217 | if ! command -v xargs >/dev/null 2>&1
218 | then
219 | die "xargs is not available"
220 | fi
221 |
222 | # Use "xargs" to parse quoted args.
223 | #
224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
225 | #
226 | # In Bash we could simply go:
227 | #
228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
229 | # set -- "${ARGS[@]}" "$@"
230 | #
231 | # but POSIX shell has neither arrays nor command substitution, so instead we
232 | # post-process each arg (as a line of input to sed) to backslash-escape any
233 | # character that might be a shell metacharacter, then use eval to reverse
234 | # that process (while maintaining the separation between arguments), and wrap
235 | # the whole thing up as a single "set" statement.
236 | #
237 | # This will of course break if any of these variables contains a newline or
238 | # an unmatched quote.
239 | #
240 |
241 | eval "set -- $(
242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
243 | xargs -n1 |
244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
245 | tr '\n' ' '
246 | )" '"$@"'
247 |
248 | exec "$JAVACMD" "$@"
249 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Yet Another (Super) Chip 8 Emulator
2 |
3 | [](https://github.com/craigthomas/Chip8Java/actions)
4 | [](https://codecov.io/gh/craigthomas/Chip8Java)
5 | [](https://libraries.io/github/craigthomas/Chip8Java)
6 | [](https://github.com/craigthomas/Chip8Java/releases)
7 | [](https://github.com/craigthomas/Chip8Java/releases)
8 | [](https://opensource.org/licenses/MIT)
9 |
10 | An Octo compatible XO Chip, Super Chip, and Chip 8 emulator.
11 |
12 | ## Table of Contents
13 |
14 | 1. [What is it?](#what-is-it)
15 | 2. [License](#license)
16 | 3. [Compiling](#compiling)
17 | 4. [Running](#running)
18 | 1. [Requirements](#requirements)
19 | 2. [Starting the Emulator](#starting-the-emulator)
20 | 3. [Running a ROM](#running-a-rom)
21 | 4. [Screen Scale](#screen-scale)
22 | 5. [Instructions Per Second](#instructions-per-second)
23 | 6. [Quirks Modes](#quirks-modes)
24 | 1. [Shift Quirks](#shift-quirks)
25 | 2. [Index Quirks](#index-quirks)
26 | 3. [Jump Quirks](#jump-quirks)
27 | 4. [Clip Quirks](#clip-quirks)
28 | 5. [Logic Quirks](#logic-quirks)
29 | 7. [Memory Size](#memory-size)
30 | 8. [Colors](#colors)
31 | 5. [Customization](#customization)
32 | 1. [Keys](#keys)
33 | 2. [Debug Keys](#debug-keys)
34 | 6. [ROM Compatibility](#rom-compatibility)
35 | 7. [Third Party Licenses and Attributions](#third-party-licenses-and-attributions)
36 | 1. [JCommander](#jcommander)
37 | 2. [Apache Commons IO](#apache-commons-io)
38 |
39 | ## What is it?
40 |
41 | This project is a Chip 8 emulator written in Java. There are two other versions
42 | of the emulator written in different languages:
43 |
44 | * [Chip8Python](https://github.com/craigthomas/Chip8Python)
45 | * [Chip8C](https://github.com/craigthomas/Chip8C)
46 |
47 | The original goal of these projects was to learn how to code a simple emulator.
48 |
49 | In addition to supporting Chip 8 ROMs, the emulator also supports the
50 | [XO Chip](https://johnearnest.github.io/Octo/docs/XO-ChipSpecification.html) and [Super Chip](https://github.com/JohnEarnest/Octo/blob/gh-pages/docs/SuperChip.md) specifications. Note that while there
51 | are no special flags that are needed to run an XO Chip,
52 | Super Chip, or normal Chip 8 ROM, there are other compatibility flags that
53 | may need to be set for the ROM to run properly. See the [Quirks Modes](#quirks-modes)
54 | documentation below for more information.
55 |
56 |
57 | ## License
58 |
59 | This project makes use of an MIT license. Please see the file called
60 | LICENSE for more information. Note that this project may make use of other
61 | software that has separate license terms. See the section called `Third
62 | Party Licenses and Attributions` below for more information on those
63 | software components.
64 |
65 |
66 | ## Compiling
67 |
68 | To compile the project, you will need a Java Development Kit (JDK) version 17 or greater installed
69 | (note that these steps are only needed if you want to compile the software yourself - if you just
70 | want to run the emulator, see the [Running](#running) section below).
71 |
72 | 1. *For Linux* - the simplest way to install the JDK is to use OpenJDK:
73 |
74 | ```bash
75 | sudo apt update
76 | sudo apt install openjdk-17-jdk
77 | ```
78 |
79 | 2. *For Windows* - I recommend using Eclipse Temurin (formerly AdoptJDK) as the software
80 | is licensed under the GNU license version 2 with classpath exception. The latest
81 | JRE builds are available at [https://adoptium.net/en-GB/temurin/releases](https://adoptium.net/en-GB/temurin/releases)
82 | (make sure you select _JDK_ as the type you wish to download). The MSI method
83 | will download an installer that will download and can be run to install the
84 | JDK for you. Follow the prompts for more information. Note that this will also
85 | install the appropriate JRE as well.
86 |
87 |
88 | To build the project, switch to the root of the source directory, and
89 | type:
90 |
91 | ./gradlew build
92 |
93 | On Windows, switch to the root of the source directory, and type:
94 |
95 | gradlew.bat build
96 |
97 | The compiled JAR file will be placed in the `build/libs` directory, as a file called
98 | `emulator-2.0.2-all.jar`.
99 |
100 |
101 | ## Running
102 |
103 | The project needs several different packages installed in order to run the
104 | emulator properly. Please see the platform specific steps below for
105 | more information.
106 |
107 | ### Linux
108 |
109 | You will need to install the Java Runtime Environment (JRE) 17 or
110 | higher.
111 |
112 | 1. Java Runtime Environment (JRE) version 17 or higher. The simplest way to
113 | do this is to install _OpenJDK 17_ or higher. On Ubuntu or Debian systems, this can
114 | be done with :
115 |
116 | ```bash
117 | sudo apt update
118 | sudo apt install openjdk-17-jre
119 | ```
120 |
121 | 2. Check that installation was successful by typing:
122 |
123 | ```bash
124 | java -version
125 | ```
126 |
127 | ### Windows
128 |
129 | You will need to install the Java Runtime Environment (JRE) 17 or higher.
130 |
131 | 1. I recommend using Eclipse Temurin (formerly AdoptJDK) as the software
132 | is licensed under the GNU license version 2 with classpath exception. The latest
133 | JRE builds are available at [https://adoptium.net/en-GB/temurin/releases](https://adoptium.net/en-GB/temurin/releases)
134 | (make sure you select _JRE_ as the type you wish to download). The MSI method
135 | will download an installer that will download and can be run to install the
136 | JRE for you. Follow the prompts for more information.
137 |
138 |
139 | ### Starting the Emulator
140 |
141 | By default, the emulator can start up without a ROM loaded. Simply double-click
142 | the JAR file, or run it with the following command line:
143 |
144 | java -jar emulator-2.0.2-all.jar
145 |
146 | ### Running a ROM
147 |
148 | The command-line interface currently requires a single argument, which
149 | is the full path to a Chip 8 ROM:
150 |
151 | java -jar emulator-2.0.2-all.jar /path/to/rom/filename
152 |
153 | This will start the emulator with the specified ROM.
154 |
155 | ### Screen Scale
156 |
157 | The `--scale` switch will scale the size of the window (the original size
158 | at 1x scale is 64 x 32):
159 |
160 | java -jar emulator-2.0.2-all.jar /path/to/rom/filename --scale 10
161 |
162 | The command above will scale the window so that it is 10 times the normal
163 | size.
164 |
165 | ### Instructions Per Second
166 |
167 | The `--ticks` switch will limit the number of instructions per second that the
168 | emulator is allowed to run. By default, the value is set to 1,000. Minimum values
169 | are 200. Use this switch to adjust the running time of ROMs that execute too quickly.
170 | For Super Chip 8 or XO Chip 8 ROMs, you will probably want to execute more instructions
171 | per second. For simplicity, each instruction is assumed to take the same amount of time.
172 |
173 | ### Quirks Modes
174 |
175 | Over time, various extensions to the Chip8 mnemonics were developed, which
176 | resulted in an interesting fragmentation of the Chip8 language specification.
177 | As discussed in Octo's [Mastering SuperChip](https://github.com/JohnEarnest/Octo/blob/gh-pages/docs/SuperChip.md)
178 | documentation, one version of the SuperChip instruction set subtly changed
179 | the meaning of a few instructions from their original Chip8 definitions.
180 | This change went mostly unnoticed for many implementations of the Chip8
181 | language. Problems arose when people started writing programs using the
182 | updated language model - programs written for "pure" Chip8 ceased to
183 | function correctly on emulators making use of the altered specification.
184 |
185 | To address this issue, [Octo](https://github.com/JohnEarnest/Octo) implements
186 | a number of _quirks_ modes so that all Chip8 software can run correctly,
187 | regardless of which specification was used when developing the Chip8 program.
188 | This same approach is used here, such that there are several `quirks` flags
189 | that can be passed to the emulator at startup to force it to run with
190 | adjustments to the language specification.
191 |
192 | Additional quirks and their impacts on the running Chip8 interpreter are
193 | examined in great depth at Chromatophore's [HP48-Superchip](https://github.com/Chromatophore/HP48-Superchip)
194 | repository. Many thanks for this detailed explanation of various quirks
195 | found in the wild!
196 |
197 | #### Shift Quirks
198 |
199 | The `--shift_quirks` flag will change the way that register shift operations work.
200 | In the original language specification two registers were required: the
201 | destination register `x`, and the source register `y`. The source register `y`
202 | value was shifted one bit left or right, and stored in `x`. For example,
203 | shift left was defined as:
204 |
205 | Vx = Vy << 1
206 |
207 | However, with the updated language specification, the source and destination
208 | register are assumed to always be the same, thus the `y` register is ignored and
209 | instead the value is sourced from `x` as such:
210 |
211 | Vx = Vx << 1
212 |
213 | #### Index Quirks
214 |
215 | The `--index_quirks` flag controls whether post-increments are made to the index register
216 | following various register based operations. For load (`Fn65`) and store (`Fn55`) register
217 | operations, the original specification for the Chip8 language results in the index
218 | register being post-incremented by the number of registers stored. With the Super
219 | Chip8 specification, this behavior is not always adhered to. Setting `--index_quirks`
220 | will prevent the post-increment of the index register from occurring after either of these
221 | instructions.
222 |
223 |
224 | #### Jump Quirks
225 |
226 | The `--jump_quirks` controls how jumps to various addresses are made with the jump (`Bnnn`)
227 | instruction. In the original Chip8 language specification, the jump is made by taking the
228 | contents of register 0, and adding it to the encoded numeric value, such as:
229 |
230 | PC = V0 + nnn
231 |
232 | With the Super Chip8 specification, the highest 4 bits of the instruction encode the
233 | register to use (`Bxnn`) such. The behavior of `--jump_quirks` becomes:
234 |
235 | PC = Vx + nn
236 |
237 |
238 | #### Clip Quirks
239 |
240 | The `--clip_quirks` controls whether sprites are allowed to wrap around the display.
241 | By default, sprits will wrap around the borders of the screen. If turned on, then
242 | sprites will not be allowed to wrap.
243 |
244 |
245 | #### Logic Quirks
246 |
247 | The `--logic_quirks` controls whether the F register is cleared after logic operations
248 | such as AND, OR, and XOR. By default, F is left undefined following these operations.
249 | With the flag turned on, F will always be cleared.
250 |
251 | ### Memory Size
252 |
253 | The original specification of the Chip8 language defined a 4K memory size
254 | for the interpreter. The addition of the XO Chip extensions require a 64K
255 | memory size for the interpreter. By default, the interpreter will start w
256 | ith a 64K memory size, but this behavior can be controlled with the
257 | `--mem_size_4k` flag, which will start the emulator with 4K.
258 |
259 | ### Colors
260 |
261 | The original Chip8 language specification called for pixels to be turned
262 | on or off. It did not specify what color the pixel states had to be. The
263 | emulator lets the user specify what colors they want to use when the emulator
264 | is running. Color values are specified by using HTML hex values such as
265 | `AABBCC` without the leading `#`. There are currently 4 color values that can
266 | be set:
267 |
268 | * `--color_0` specifies the background color. This defaults to `000000`.
269 | * `--color_1` specifies bitplane 1 color. This defaults to `FF33CC`.
270 | * `--color_2` specifies bitplane 2 color. This defaults to `33CCFF`.
271 | * `--color_3` specifies bitplane 1 and 2 overlap color. This defaults to `FFFFFF`.
272 |
273 | For Chip8 and SuperChip 8 programs, only the background `color color_0`
274 | (for pixels turned off) and the bitplane 1 `color color_1` (for pixels turned
275 | on) are used. Only XO Chip programs will use `color_2` and `color_3` when
276 | the additional bitplanes are potentially used.
277 |
278 | ## Customization
279 |
280 | The file `components/Keyboard.java` contains several variables that can be
281 | changed to customize the operation of the emulator. The Chip 8 has 16 keys:
282 |
283 | ### Keys
284 |
285 | The original Chip 8 had a keypad with the numbered keys 0 - 9 and A - F (16
286 | keys in total). The original key configuration was as follows:
287 |
288 |
289 | | `1` | `2` | `3` | `C` |
290 | |-----|-----|-----|-----|
291 | | `4` | `5` | `6` | `D` |
292 | | `7` | `8` | `9` | `E` |
293 | | `A` | `0` | `B` | `F` |
294 |
295 | The Chip8Java emulator maps them to the following keyboard keys by default:
296 |
297 | | `1` | `2` | `3` | `4` |
298 | |-----|-----|-----|-----|
299 | | `Q` | `W` | `E` | `R` |
300 | | `A` | `S` | `D` | `F` |
301 | | `Z` | `X` | `C` | `V` |
302 |
303 |
304 | ### Debug Keys
305 |
306 | Pressing a debug key at any time will cause the emulator to enter into a
307 | different mode of operation. The debug keys are:
308 |
309 | | Keyboard Key | Effect |
310 | |:------------:|--------------------------------|
311 | | `ESC` | Quits the emulator |
312 |
313 | ## ROM Compatibility
314 |
315 | Here are the list of public domain ROMs and their current status with the emulator, along
316 | with links to public domain repositories where applicable.
317 |
318 | ### Chip 8 ROMs
319 |
320 | | ROM Name | Working | Flags |
321 | |:--------------------------------------------------------------------------------------------------|:------------------:|:-------------:|
322 | | [1D Cellular Automata](https://johnearnest.github.io/chip8Archive/play.html?p=1dcell) | :heavy_check_mark: | |
323 | | [8CE Attourny - Disc 1](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d1) | :heavy_check_mark: | |
324 | | [8CE Attourny - Disc 2](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d2) | :heavy_check_mark: | |
325 | | [8CE Attourny - Disc 3](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d3) | :heavy_check_mark: | |
326 | | [Bad Kaiju Ju](https://johnearnest.github.io/chip8Archive/play.html?p=BadKaiJuJu) | :heavy_check_mark: | |
327 | | [Br8kout](https://johnearnest.github.io/chip8Archive/play.html?p=br8kout) | :heavy_check_mark: | |
328 | | [Carbon8](https://johnearnest.github.io/chip8Archive/play.html?p=carbon8) | :heavy_check_mark: | |
329 | | [Cave Explorer](https://johnearnest.github.io/chip8Archive/play.html?p=caveexplorer) | :heavy_check_mark: | |
330 | | [Chipquarium](https://johnearnest.github.io/chip8Archive/play.html?p=chipquarium) | :heavy_check_mark: | |
331 | | [Danm8ku](https://johnearnest.github.io/chip8Archive/play.html?p=danm8ku) | :heavy_check_mark: | |
332 | | [down8](https://johnearnest.github.io/chip8Archive/play.html?p=down8) | :heavy_check_mark: | |
333 | | [Falling Ghosts](https://veganjay.itch.io/falling-ghosts) | :heavy_check_mark: | |
334 | | [Flight Runner](https://johnearnest.github.io/chip8Archive/play.html?p=flightrunner) | :heavy_check_mark: | |
335 | | [Fuse](https://johnearnest.github.io/chip8Archive/play.html?p=fuse) | :heavy_check_mark: | |
336 | | [Ghost Escape](https://johnearnest.github.io/chip8Archive/play.html?p=ghostEscape) | :heavy_check_mark: | |
337 | | [Glitch Ghost](https://johnearnest.github.io/chip8Archive/play.html?p=glitchGhost) | :heavy_check_mark: | |
338 | | [Horse World Online](https://johnearnest.github.io/chip8Archive/play.html?p=horseWorldOnline) | :heavy_check_mark: | |
339 | | [Invisible Man](https://mremerson.itch.io/invisible-man) | :heavy_check_mark: | `clip_quirks` |
340 | | [Knumber Knower](https://internet-janitor.itch.io/knumber-knower) | :heavy_check_mark: | |
341 | | [Masquer8](https://johnearnest.github.io/chip8Archive/play.html?p=masquer8) | :heavy_check_mark: | |
342 | | [Mastermind](https://johnearnest.github.io/chip8Archive/play.html?p=mastermind) | :x: | |
343 | | [Mini Lights Out](https://johnearnest.github.io/chip8Archive/play.html?p=mini-lights-out) | :heavy_check_mark: | |
344 | | [Octo: a Chip 8 Story](https://johnearnest.github.io/chip8Archive/play.html?p=octoachip8story) | :x: | |
345 | | [Octogon Trail](https://tarsi.itch.io/octogon-trail) | :question: | |
346 | | [Octojam 1 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam1title) | :heavy_check_mark: | |
347 | | [Octojam 2 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam2title) | :heavy_check_mark: | |
348 | | [Octojam 3 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam3title) | :heavy_check_mark: | |
349 | | [Octojam 4 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam4title) | :heavy_check_mark: | |
350 | | [Octojam 5 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam5title) | :heavy_check_mark: | |
351 | | [Octojam 6 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam6title) | :heavy_check_mark: | |
352 | | [Octojam 7 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam7title) | :heavy_check_mark: | |
353 | | [Octojam 8 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam8title) | :heavy_check_mark: | |
354 | | [Octojam 9 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam9title) | :heavy_check_mark: | |
355 | | [Octojam 10 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam10title) | :heavy_check_mark: | |
356 | | [Octo Rancher](https://johnearnest.github.io/chip8Archive/play.html?p=octorancher) | :question: | |
357 | | [Outlaw](https://johnearnest.github.io/chip8Archive/play.html?p=outlaw) | :heavy_check_mark: | |
358 | | [Pet Dog](https://johnearnest.github.io/chip8Archive/play.html?p=petdog) | :question: | |
359 | | [Piper](https://johnearnest.github.io/chip8Archive/play.html?p=piper) | :heavy_check_mark: | |
360 | | [Pumpkin "Dress" Up](https://johnearnest.github.io/chip8Archive/play.html?p=pumpkindressup) | :heavy_check_mark: | |
361 | | [RPS](https://johnearnest.github.io/chip8Archive/play.html?p=RPS) | :question: | |
362 | | [Slippery Slope](https://johnearnest.github.io/chip8Archive/play.html?p=slipperyslope) | :heavy_check_mark: | |
363 | | [Snek](https://johnearnest.github.io/chip8Archive/play.html?p=snek) | :heavy_check_mark: | |
364 | | [Space Jam](https://johnearnest.github.io/chip8Archive/play.html?p=spacejam) | :heavy_check_mark: | |
365 | | [Spock Paper Scissors](https://johnearnest.github.io/chip8Archive/play.html?p=spockpaperscissors) | :heavy_check_mark: | |
366 | | [Super Pong](https://johnearnest.github.io/chip8Archive/play.html?p=superpong) | :heavy_check_mark: | |
367 | | [Tank!](https://johnearnest.github.io/chip8Archive/play.html?p=tank) | :heavy_check_mark: | |
368 | | [TOMB STON TIPP](https://johnearnest.github.io/chip8Archive/play.html?p=tombstontipp) | :heavy_check_mark: | |
369 | | [WDL](https://johnearnest.github.io/chip8Archive/play.html?p=wdl) | :x: | |
370 |
371 | ### Super Chip ROMs
372 |
373 | | ROM Name | Working | Flags |
374 | |:---------------------------------------------------------------------------------------------|:------------------:|:-----:|
375 | | [Applejak](https://johnearnest.github.io/chip8Archive/play.html?p=applejak) | :x: | |
376 | | [Bulb](https://johnearnest.github.io/chip8Archive/play.html?p=bulb) | :x: | |
377 | | [Black Rainbow](https://johnearnest.github.io/chip8Archive/play.html?p=blackrainbow) | :heavy_check_mark: | |
378 | | [Chipcross](https://tobiasvl.itch.io/chipcross) | :heavy_check_mark: | |
379 | | [Chipolarium](https://tobiasvl.itch.io/chipolarium) | :heavy_check_mark: | |
380 | | [Collision Course](https://ninjaweedle.itch.io/collision-course) | :heavy_check_mark: | |
381 | | [Dodge](https://johnearnest.github.io/chip8Archive/play.html?p=dodge) | :x: | |
382 | | [DVN8](https://johnearnest.github.io/chip8Archive/play.html?p=DVN8) | :heavy_check_mark: | |
383 | | [Eaty the Alien](https://johnearnest.github.io/chip8Archive/play.html?p=eaty) | :heavy_check_mark: | |
384 | | [Grad School Simulator 2014](https://johnearnest.github.io/chip8Archive/play.html?p=gradsim) | :heavy_check_mark: | |
385 | | [Horsey Jump](https://johnearnest.github.io/chip8Archive/play.html?p=horseyJump) | :question: | |
386 | | [Knight](https://johnearnest.github.io/chip8Archive/play.html?p=knight) | :x: | |
387 | | [Mondri8](https://johnearnest.github.io/chip8Archive/play.html?p=mondrian) | :heavy_check_mark: | |
388 | | [Octopeg](https://johnearnest.github.io/chip8Archive/play.html?p=octopeg) | :heavy_check_mark: | |
389 | | [Octovore](https://johnearnest.github.io/chip8Archive/play.html?p=octovore) | :heavy_check_mark: | |
390 | | [Rocto](https://johnearnest.github.io/chip8Archive/play.html?p=rockto) | :heavy_check_mark: | |
391 | | [Sens8tion](https://johnearnest.github.io/chip8Archive/play.html?p=sens8tion) | :heavy_check_mark: | |
392 | | [Snake](https://johnearnest.github.io/chip8Archive/play.html?p=snake) | :heavy_check_mark: | |
393 | | [Squad](https://johnearnest.github.io/chip8Archive/play.html?p=squad) | :x: | |
394 | | [Sub-Terr8nia](https://johnearnest.github.io/chip8Archive/play.html?p=sub8) | :heavy_check_mark: | |
395 | | [Super Octogon](https://johnearnest.github.io/chip8Archive/play.html?p=octogon) | :heavy_check_mark: | |
396 | | [Super Square](https://johnearnest.github.io/chip8Archive/play.html?p=supersquare) | :heavy_check_mark: | |
397 | | [The Binding of COSMAC](https://johnearnest.github.io/chip8Archive/play.html?p=binding) | :heavy_check_mark: | |
398 | | [Turnover '77](https://johnearnest.github.io/chip8Archive/play.html?p=turnover77) | :heavy_check_mark: | |
399 |
400 | ### XO Chip ROMs
401 |
402 | | ROM Name | Working | Flags |
403 | |:------------------------------------------------------------------------------------------------------|:------------------:|:-----:|
404 | | [An Evening to Die For](https://johnearnest.github.io/chip8Archive/play.html?p=anEveningToDieFor) | :heavy_check_mark: | |
405 | | [Business Is Contagious](https://johnearnest.github.io/chip8Archive/play.html?p=businessiscontagious) | :heavy_check_mark: | |
406 | | [Chicken Scratch](https://johnearnest.github.io/chip8Archive/play.html?p=chickenScratch) | :heavy_check_mark: | |
407 | | [Civiliz8n](https://johnearnest.github.io/chip8Archive/play.html?p=civiliz8n) | :heavy_check_mark: | |
408 | | [Flutter By](https://johnearnest.github.io/chip8Archive/play.html?p=flutterby) | :heavy_check_mark: | |
409 | | [Into The Garlicscape](https://johnearnest.github.io/chip8Archive/play.html?p=garlicscape) | :heavy_check_mark: | |
410 | | [jub8 Song 1](https://johnearnest.github.io/chip8Archive/play.html?p=jub8-1) | :heavy_check_mark: | |
411 | | [jub8 Song 2](https://johnearnest.github.io/chip8Archive/play.html?p=jub8-2) | :heavy_check_mark: | |
412 | | [Kesha Was Biird](https://johnearnest.github.io/chip8Archive/play.html?p=keshaWasBiird) | :heavy_check_mark: | |
413 | | [Kesha Was Niinja](https://johnearnest.github.io/chip8Archive/play.html?p=keshaWasNiinja) | :heavy_check_mark: | |
414 | | [Octo paint](https://johnearnest.github.io/chip8Archive/play.html?p=octopaint) | :heavy_check_mark: | |
415 | | [Octo Party Mix!](https://johnearnest.github.io/chip8Archive/play.html?p=OctoPartyMix) | :heavy_check_mark: | |
416 | | [Octoma](https://johnearnest.github.io/chip8Archive/play.html?p=octoma) | :heavy_check_mark: | |
417 | | [Red October V](https://johnearnest.github.io/chip8Archive/play.html?p=redOctober) | :heavy_check_mark: | |
418 | | [Skyward](https://johnearnest.github.io/chip8Archive/play.html?p=skyward) | :heavy_check_mark: | |
419 | | [Spock Paper Scissors](https://johnearnest.github.io/chip8Archive/play.html?p=spockpaperscissors) | :heavy_check_mark: | |
420 | | [T8NKS](https://johnearnest.github.io/chip8Archive/play.html?p=t8nks) | :question: | |
421 | | [Tapeworm](https://tarsi.itch.io/tapeworm) | :heavy_check_mark: | |
422 | | [Truck Simul8or](https://johnearnest.github.io/chip8Archive/play.html?p=trucksimul8or) | :heavy_check_mark: | |
423 | | [SK8 H8 1988](https://johnearnest.github.io/chip8Archive/play.html?p=sk8) | :heavy_check_mark: | |
424 | | [Super NeatBoy](https://johnearnest.github.io/chip8Archive/play.html?p=superneatboy) | :heavy_check_mark: | |
425 | | [Wonky Pong](https://johnearnest.github.io/chip8Archive/play.html?p=wonkypong) | :heavy_check_mark: | |
426 |
427 | ## Third Party Licenses and Attributions
428 |
429 | ### JCommander
430 |
431 | This links to the JCommander library, which is licensed under the
432 | Apache License, Version 2.0. The license can be downloaded from
433 | http://www.apache.org/licenses/LICENSE-2.0.html. The source code for this
434 | software is available from [https://github.com/cbeust/jcommander](https://github.com/cbeust/jcommander)
435 |
436 | ### Apache Commons IO
437 |
438 | This links to the Apache Commons IO, which is licensed under the
439 | Apache License, Version 2.0. The license can be downloaded from
440 | http://www.apache.org/licenses/LICENSE-2.0.html. The source code for this
441 | software is available from http://commons.apache.org/io
--------------------------------------------------------------------------------
/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 | --------------------------------------------------------------------------------