├── .clang-format ├── .clang-format-ignore ├── .github └── workflows │ ├── compile.yml │ └── pr.yaml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake └── Modules │ └── FindSFML.cmake ├── include ├── APU │ ├── APU.h │ ├── Constants.h │ ├── DMC.h │ ├── Divider.h │ ├── FrameCounter.h │ ├── Noise.h │ ├── Pulse.h │ ├── PulseUnits.h │ ├── Timer.h │ ├── Triangle.h │ ├── Units.h │ └── spsc.hpp ├── AudioPlayer.h ├── CPU.h ├── CPUOpcodes.h ├── Cartridge.h ├── Controller.h ├── Emulator.h ├── IRQ.h ├── Log.h ├── MainBus.h ├── Mapper.h ├── MapperAxROM.h ├── MapperCNROM.h ├── MapperColorDreams.h ├── MapperGxROM.h ├── MapperMMC3.h ├── MapperNROM.h ├── MapperSxROM.h ├── MapperUxROM.h ├── PPU.h ├── PaletteColors.h ├── PictureBus.h └── VirtualScreen.h ├── keybindings.conf ├── main.cpp ├── src ├── APU │ ├── APU.cpp │ ├── DMC.cpp │ ├── FrameCounter.cpp │ ├── Noise.cpp │ ├── Pulse.cpp │ ├── Triangle.cpp │ └── Units.cpp ├── AudioPlayer.cpp ├── CPU.cpp ├── Cartridge.cpp ├── Controller.cpp ├── Emulator.cpp ├── KeybindingsParser.cpp ├── Log.cpp ├── MainBus.cpp ├── Mapper.cpp ├── MapperAxROM.cpp ├── MapperCNROM.cpp ├── MapperColorDreams.cpp ├── MapperGxROM.cpp ├── MapperMMC3.cpp ├── MapperNROM.cpp ├── MapperSxROM.cpp ├── MapperUxROM.cpp ├── PPU.cpp ├── PictureBus.cpp └── VirtualScreen.cpp ├── test └── audio.cpp └── vendor └── miniaudio ├── miniaudio.c └── miniaudio.h /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | # We'll use defaults from the LLVM style, but with 4 columns indentation. 3 | BasedOnStyle: Mozilla 4 | IndentWidth: 4 5 | --- 6 | Language: Cpp 7 | IndentCaseLabels: false 8 | AccessModifierOffset: -4 9 | AlignConsecutiveAssignments: 10 | Enabled: true 11 | AcrossEmptyLines: true 12 | AcrossComments: true 13 | AlignCompound: true 14 | AlignFunctionPointers: true 15 | AlignConsecutiveDeclarations: 16 | Enabled: true 17 | AcrossEmptyLines: true 18 | AcrossComments: true 19 | ColumnLimit: 120 20 | SpaceBeforeCpp11BracedList: true 21 | BraceWrapping: 22 | IndentBraces: false 23 | BreakBeforeBraces: Allman 24 | Standard: c++11 25 | BreakAfterReturnType: Automatic 26 | PointerAlignment: Left 27 | BreakBeforeBinaryOperators: None 28 | SpaceBeforeParens: ControlStatements 29 | BreakConstructorInitializers: BeforeComma 30 | --- 31 | -------------------------------------------------------------------------------- /.clang-format-ignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build_linux: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 1 14 | 15 | - name: Install dependencies 16 | run: | 17 | sudo apt update 18 | sudo apt install -y libsfml-dev 19 | 20 | - name: Build 21 | run: | 22 | mkdir build/ && cd build/ 23 | cmake -DCMAKE_BUILD_TYPE=Release .. 24 | make -j4 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | name: SimpleNES-linux 28 | path: build/SimpleNES 29 | 30 | 31 | build_macos: 32 | runs-on: macos-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | with: 36 | fetch-depth: 1 37 | 38 | - name: Install dependencies 39 | run: | 40 | brew install sfml@2 41 | brew link --force --overwrite sfml@2 42 | 43 | - name: Build 44 | run: | 45 | mkdir build/ && cd build/ 46 | cmake -DCMAKE_BUILD_TYPE=Release .. 47 | make -j4 48 | 49 | - uses: actions/upload-artifact@v4 50 | with: 51 | name: SimpleNES-macos 52 | path: build/SimpleNES 53 | 54 | build_windows: 55 | runs-on: windows-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | with: 59 | fetch-depth: 1 60 | 61 | - name: Install dependencies 62 | run: | 63 | $manifest = @' 64 | { 65 | "dependencies": [ "sfml" ], 66 | "builtin-baseline": "df6921c0b6cdf95269a4a3093e267b59e4bf0f5b", 67 | "overrides": [ 68 | { "name": "sfml", "version": "2.6.2" } 69 | ] 70 | } 71 | '@ 72 | Add-Content ./vcpkg.json $manifest 73 | vcpkg install 74 | 75 | - name: Build 76 | run: | 77 | mkdir build/ && cd build/ 78 | cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake .. 79 | cmake --build . --config Release 80 | 81 | - uses: actions/upload-artifact@v4 82 | with: 83 | name: SimpleNES-win.exe 84 | path: build/Release/SimpleNES.exe 85 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build_linux: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 1 14 | 15 | - name: Install dependencies 16 | run: | 17 | sudo apt update 18 | sudo apt install -y libsfml-dev 19 | 20 | - name: Build 21 | run: | 22 | mkdir build/ && cd build/ 23 | cmake -DCMAKE_BUILD_TYPE=Debug .. 24 | make -j4 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.layout 2 | *.cbp 3 | *.depend 4 | *.kdev4 5 | .kdev* 6 | build/ 7 | .cache/ 8 | compile_commands.json 9 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | project(SimpleNES) 3 | 4 | if(NOT CMAKE_BUILD_TYPE) 5 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING 6 | "Choose the type of build, options are: Debug Release." 7 | FORCE) 8 | endif(NOT CMAKE_BUILD_TYPE) 9 | 10 | set(BUILD_STATIC FALSE CACHE STRING "Set this to link external libraries statically") 11 | 12 | if(CMAKE_COMPILER_IS_GNUCXX OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") 13 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wextra -g") 14 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2") 15 | #set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address") 16 | #set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address") 17 | endif() 18 | 19 | # Add directory containing FindSFML.cmake to module path 20 | set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules/;${CMAKE_MODULE_PATH};${CMAKE_SOURCE_DIR}") 21 | 22 | # Add sources 23 | file(GLOB SOURCES 24 | "${PROJECT_SOURCE_DIR}/main.cpp" 25 | "${PROJECT_SOURCE_DIR}/src/*.cpp" 26 | "${PROJECT_SOURCE_DIR}/src/APU/*.cpp" 27 | ) 28 | 29 | file(GLOB VENDOR_SOURCES 30 | "${PROJECT_SOURCE_DIR}/vendor/miniaudio/*.c" 31 | ) 32 | 33 | # Copy keybindings.conf 34 | file(COPY keybindings.conf DESTINATION .) 35 | 36 | # Will add __FILENAME__ macros for all source files, which is the filename without full find_path 37 | # Courtesy of SO 38 | function(define_file_basename_for_sources targetname) 39 | get_target_property(source_files "${targetname}" SOURCES) 40 | foreach(sourcefile ${source_files}) 41 | # Get source file's current list of compile definitions. 42 | get_property(defs SOURCE "${sourcefile}" 43 | PROPERTY COMPILE_DEFINITIONS) 44 | # Add the FILE_BASENAME=filename compile definition to the list. 45 | get_filename_component(basename "${sourcefile}" NAME) 46 | list(APPEND defs "__FILENAME__=\"${basename}\"") 47 | # Set the updated compile definitions on the source file. 48 | set_property( 49 | SOURCE "${sourcefile}" 50 | PROPERTY COMPILE_DEFINITIONS ${defs}) 51 | endforeach() 52 | endfunction() 53 | 54 | # Specify include Directory 55 | include_directories( 56 | "${PROJECT_SOURCE_DIR}/include" 57 | "${PROJECT_SOURCE_DIR}/vendor/miniaudio/" 58 | ) 59 | 60 | # Set static if BUILD_STATIC is set 61 | if (BUILD_STATIC) 62 | set(SFML_STATIC_LIBRARIES TRUE) 63 | # Link libgcc and libstc++ statically as well 64 | if(CMAKE_COMPILER_IS_GNUCXX) 65 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libstdc++ -static-libgcc") 66 | endif() 67 | endif() 68 | 69 | # Find SFML 70 | if (SFML_OS_WINDOWS AND SFML_COMPILER_MSVC) 71 | find_package(SFML 2 COMPONENTS main audio graphics window system REQUIRED) 72 | else() 73 | find_package(SFML 2 COMPONENTS audio graphics window system REQUIRED) 74 | endif() 75 | 76 | if(SFML_FOUND) 77 | include_directories(${SFML_INCLUDE_DIR}) 78 | else() 79 | set(SFML_ROOT "" CACHE PATH "SFML top-level directory") 80 | message("\nSFML directory not found. Set SFML_ROOT to SFML's top-level path (containing \"include\" and \"lib\" directories).") 81 | message("Make sure the SFML libraries with the same configuration (Release/Debug, Static/Dynamic) exist.\n") 82 | endif() 83 | 84 | # generate compile commands for clangd 85 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 86 | 87 | add_executable(SimpleNES ${SOURCES} ${VENDOR_SOURCES}) 88 | target_link_libraries(SimpleNES PRIVATE ${SFML_LIBRARIES} ${SFML_DEPENDENCIES}) 89 | 90 | set_property(TARGET SimpleNES PROPERTY CXX_STANDARD 11) 91 | set_property(TARGET SimpleNES PROPERTY CXX_STANDARD_REQUIRED ON) 92 | 93 | target_link_libraries(SimpleNES) 94 | define_file_basename_for_sources(SimpleNES) 95 | 96 | install(TARGETS SimpleNES RUNTIME DESTINATION bin) 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SimpleNES 2 | ============= 3 | 4 | 5 | An NES emulator written in C++ for nothing but fun. 6 | 7 | Roughly 50-60% of games should work (ie. games that use either no mapper or mappers 1, 2, 3 and experimental support for 4, 7, 66 and 11). 8 | 9 | 10 | 11 | 12 | Examples of games that have been tested to run (but NOT limited to): 13 | 14 | (USA/Japan or World versions only i.e. NTSC compatible) 15 | 16 | * Super Mario Bros. 17 | * Contra 18 | * Adventure Island 19 | * Ninja Gaiden 20 | * Wrecking Crew 21 | * Megaman and Megaman 2 22 | * Mario Bros. 23 | * Donky Kong and Donkey Kong Jr. 24 | * Battle City 25 | * Paperboy 26 | * Legend of Zelda 27 | * Pacman 28 | * Tennis 29 | * Excitebike 30 | * Nightmare Elm Street 31 | * Cabal 32 | * Battletoads 33 | * Arch Rivals 34 | * etc... 35 | 36 | 37 | Screenshots 38 | ------------------------ 39 | ![Screenshot 1](http://amhndu.github.io/screenshots/nes1.png) 40 | ![Screenshot 2](http://amhndu.github.io/screenshots/nes2.png) 41 | ![Screenshot 3](http://amhndu.github.io/screenshots/nes3.png) 42 | ![Screenshot 4](http://amhndu.github.io/screenshots/nes4.png) 43 | ![Screenshot 5](http://amhndu.github.io/screenshots/nes5.png) 44 | ![Screenshot 6](http://amhndu.github.io/screenshots/nes6.png) 45 | ![Screenshot 6](http://amhndu.github.io/screenshots/nes7.png) 46 | ![Screenshot 6](http://amhndu.github.io/screenshots/nes8.png) 47 | 48 | Videos 49 | ------------ 50 | **OUTDATED**(missing audio) [YouTube Playlist](https://www.youtube.com/playlist?list=PLiULt7qySWt2VbHTkvIt9kYPMPcWt01qN) 51 | 52 | 53 | Compiling 54 | ----------- 55 | 56 | You need: 57 | * [SFML 2.*](#installing-sfml) development headers and library 58 | * C++11 compliant compiler 59 | * [CMake](https://cgold.readthedocs.io/en/latest/first-step/installation.html) build system 60 | 61 | Compiling is straight forward with cmake, just run cmake on the project directory with CMAKE_BUILD_TYPE=Release 62 | and you'll get Makefile or equivalent for your platform, with which you can compile the emulator 63 | 64 | For e.g., on Linux/OS X/FreeBSD: 65 | ``` 66 | $ git clone https://github.com/amhndu/SimpleNES 67 | $ cd SimpleNES 68 | $ mkdir build/ && cd build/ 69 | $ cmake -DCMAKE_BUILD_TYPE=Release .. 70 | $ make -j4 # Replace 4 with however many cores you have to spare 71 | ``` 72 | 73 | If SFML is installed on a non-standard location, specify SFML_ROOT as a cmake variable, e.g. on **Arch Linux**, after installing sfml2 from AUR: 74 | ``` 75 | $ cd SimpleNES && mkdir build/ && cd build/ 76 | $ cmake -DCMAKE_BUILD_TYPE=Release -DSFML_ROOT=/opt/sfml2 .. 77 | $ make -j8 78 | ``` 79 | See also: [compile.yaml](https://github.com/amhndu/SimpleNES/blob/master/.github/workflows/compile.yml) for platform specific instructions 80 | 81 | Download SimpleNES 82 | ----------------- 83 | 1. Download an executable based on your platform from the latest run on [Github Actions](https://github.com/amhndu/SimpleNES/actions) 84 | 2. Install [sfml](#installing-sfml) 85 | 86 | 87 | Installing SFML 88 | ----------------- 89 | * Windows: `vcpkg install sfml`. Alterntaively, download from [SFML](https://www.sfml-dev.org/download/sfml/2.6.2/). See: [compile.yaml](https://github.com/amhndu/SimpleNES/blob/master/.github/workflows/compile.yml) for instructions on pinning sfml to version 2 90 | * Debian/Ubuntu/derivates: `sudo apt install -y libsfml-dev` 91 | * Arch/etc: `yay -S sfml2` 92 | * MacOS: `brew install sfml@2 && brew link sfml@2` 93 | 94 | 95 | Running 96 | ----------------- 97 | 98 | Just pass the path to a .nes image like 99 | 100 | ``` 101 | $ ./SimpleNES ~/Games/SuperMarioBros.nes 102 | ``` 103 | To set size of the window, 104 | ``` 105 | $ ./SimpleNES -w 600 ~/Games/Contra.nes 106 | ``` 107 | For supported command line options, try 108 | ``` 109 | $ ./SimpleNES -h 110 | SimpleNES is a simple NES emulator. 111 | It can run off .nes images. 112 | Set keybindings with keybindings.conf 113 | 114 | Usage: SimpleNES [options] rom-path 115 | 116 | Options: 117 | -h, --help Print this help text and exit 118 | --mute-audio Mute audio 119 | -s, --scale Set video scale. Default: 3. 120 | Scale of 1 corresponds to 256x240 121 | -w, --width Set the width of the emulation screen (height is 122 | set automatically to fit the aspect ratio) 123 | -H, --height Set the height of the emulation screen (width is 124 | set automatically to fit the aspect ratio) 125 | This option is mutually exclusive to --width 126 | -C, --conf Set the keybindings file's path. The default 127 | keybindings file is keybindings.conf. 128 | 129 | ``` 130 | 131 | Controller 132 | ----------------- 133 | 134 | Keybindings can be configured with keybindings.conf 135 | 136 | 137 | Default keybindings: 138 | 139 | **Player 1** 140 | 141 | Button | Mapped to 142 | --------------|------------- 143 | Start | Return/Enter 144 | Select | Right Shift 145 | A | J 146 | B | K 147 | Up | W 148 | Down | S 149 | Left | A 150 | Right | D 151 | 152 | 153 | **Player 2** 154 | 155 | Button | Mapped to 156 | --------------|------------- 157 | Start | Numpad9 158 | Select | Numpad8 159 | A | Numpad5 160 | B | Numpad6 161 | Up | Up 162 | Down | Down 163 | Left | Left 164 | Right | Right 165 | 166 | -------------------------------------------------------------------------------- /cmake/Modules/FindSFML.cmake: -------------------------------------------------------------------------------- 1 | # This script locates the SFML library 2 | # ------------------------------------ 3 | # 4 | # Usage 5 | # ----- 6 | # 7 | # When you try to locate the SFML libraries, you must specify which modules you want to use (system, window, graphics, network, audio, main). 8 | # If none is given, the SFML_LIBRARIES variable will be empty and you'll end up linking to nothing. 9 | # example: 10 | # find_package(SFML COMPONENTS graphics window system) // find the graphics, window and system modules 11 | # 12 | # You can enforce a specific version, either MAJOR.MINOR or only MAJOR. 13 | # If nothing is specified, the version won't be checked (i.e. any version will be accepted). 14 | # example: 15 | # find_package(SFML COMPONENTS ...) // no specific version required 16 | # find_package(SFML 2 COMPONENTS ...) // any 2.x version 17 | # find_package(SFML 2.4 COMPONENTS ...) // version 2.4 or greater 18 | # 19 | # By default, the dynamic libraries of SFML will be found. To find the static ones instead, 20 | # you must set the SFML_STATIC_LIBRARIES variable to TRUE before calling find_package(SFML ...). 21 | # Since you have to link yourself all the SFML dependencies when you link it statically, the following 22 | # additional variables are defined: SFML_XXX_DEPENDENCIES and SFML_DEPENDENCIES (see their detailed 23 | # description below). 24 | # In case of static linking, the SFML_STATIC macro will also be defined by this script. 25 | # example: 26 | # set(SFML_STATIC_LIBRARIES TRUE) 27 | # find_package(SFML 2 COMPONENTS network system) 28 | # 29 | # On Mac OS X if SFML_STATIC_LIBRARIES is not set to TRUE then by default CMake will search for frameworks unless 30 | # CMAKE_FIND_FRAMEWORK is set to "NEVER" for example. Please refer to CMake documentation for more details. 31 | # Moreover, keep in mind that SFML frameworks are only available as release libraries unlike dylibs which 32 | # are available for both release and debug modes. 33 | # 34 | # If SFML is not installed in a standard path, you can use the SFML_ROOT CMake (or environment) variable 35 | # to tell CMake where SFML is. 36 | # 37 | # Output 38 | # ------ 39 | # 40 | # This script defines the following variables: 41 | # - For each specified module XXX (system, window, graphics, network, audio, main): 42 | # - SFML_XXX_LIBRARY_DEBUG: the name of the debug library of the xxx module (set to SFML_XXX_LIBRARY_RELEASE is no debug version is found) 43 | # - SFML_XXX_LIBRARY_RELEASE: the name of the release library of the xxx module (set to SFML_XXX_LIBRARY_DEBUG is no release version is found) 44 | # - SFML_XXX_LIBRARY: the name of the library to link to for the xxx module (includes both debug and optimized names if necessary) 45 | # - SFML_XXX_FOUND: true if either the debug or release library of the xxx module is found 46 | # - SFML_XXX_DEPENDENCIES: the list of libraries the module depends on, in case of static linking 47 | # - SFML_LIBRARIES: the list of all libraries corresponding to the required modules 48 | # - SFML_FOUND: true if all the required modules are found 49 | # - SFML_INCLUDE_DIR: the path where SFML headers are located (the directory containing the SFML/Config.hpp file) 50 | # - SFML_DEPENDENCIES: the list of libraries SFML depends on, in case of static linking 51 | # 52 | # example: 53 | # find_package(SFML 2 COMPONENTS system window graphics audio REQUIRED) 54 | # include_directories(${SFML_INCLUDE_DIR}) 55 | # add_executable(myapp ...) 56 | # target_link_libraries(myapp ${SFML_LIBRARIES}) 57 | 58 | # define the SFML_STATIC macro if static build was chosen 59 | if(SFML_STATIC_LIBRARIES) 60 | add_definitions(-DSFML_STATIC) 61 | endif() 62 | 63 | # define the list of search paths for headers and libraries 64 | set(FIND_SFML_PATHS 65 | ${SFML_ROOT} 66 | $ENV{SFML_ROOT} 67 | ~/Library/Frameworks 68 | /Library/Frameworks 69 | /usr/local 70 | /usr 71 | /sw 72 | /opt/local 73 | /opt/csw 74 | /opt) 75 | 76 | # find the SFML include directory 77 | find_path(SFML_INCLUDE_DIR SFML/Config.hpp 78 | PATH_SUFFIXES include 79 | PATHS ${FIND_SFML_PATHS}) 80 | 81 | # check the version number 82 | set(SFML_VERSION_OK TRUE) 83 | if(SFML_FIND_VERSION AND SFML_INCLUDE_DIR) 84 | # extract the major and minor version numbers from SFML/Config.hpp 85 | # we have to handle framework a little bit differently: 86 | if("${SFML_INCLUDE_DIR}" MATCHES "SFML.framework") 87 | set(SFML_CONFIG_HPP_INPUT "${SFML_INCLUDE_DIR}/Headers/Config.hpp") 88 | else() 89 | set(SFML_CONFIG_HPP_INPUT "${SFML_INCLUDE_DIR}/SFML/Config.hpp") 90 | endif() 91 | FILE(READ "${SFML_CONFIG_HPP_INPUT}" SFML_CONFIG_HPP_CONTENTS) 92 | STRING(REGEX REPLACE ".*#define SFML_VERSION_MAJOR ([0-9]+).*" "\\1" SFML_VERSION_MAJOR "${SFML_CONFIG_HPP_CONTENTS}") 93 | STRING(REGEX REPLACE ".*#define SFML_VERSION_MINOR ([0-9]+).*" "\\1" SFML_VERSION_MINOR "${SFML_CONFIG_HPP_CONTENTS}") 94 | STRING(REGEX REPLACE ".*#define SFML_VERSION_PATCH ([0-9]+).*" "\\1" SFML_VERSION_PATCH "${SFML_CONFIG_HPP_CONTENTS}") 95 | if (NOT "${SFML_VERSION_PATCH}" MATCHES "^[0-9]+$") 96 | set(SFML_VERSION_PATCH 0) 97 | endif() 98 | math(EXPR SFML_REQUESTED_VERSION "${SFML_FIND_VERSION_MAJOR} * 10000 + ${SFML_FIND_VERSION_MINOR} * 100 + ${SFML_FIND_VERSION_PATCH}") 99 | 100 | # if we could extract them, compare with the requested version number 101 | if (SFML_VERSION_MAJOR) 102 | # transform version numbers to an integer 103 | math(EXPR SFML_VERSION "${SFML_VERSION_MAJOR} * 10000 + ${SFML_VERSION_MINOR} * 100 + ${SFML_VERSION_PATCH}") 104 | 105 | # compare them 106 | if(SFML_VERSION LESS SFML_REQUESTED_VERSION) 107 | set(SFML_VERSION_OK FALSE) 108 | endif() 109 | else() 110 | # SFML version is < 2.0 111 | if (SFML_REQUESTED_VERSION GREATER 10900) 112 | set(SFML_VERSION_OK FALSE) 113 | set(SFML_VERSION_MAJOR 1) 114 | set(SFML_VERSION_MINOR x) 115 | set(SFML_VERSION_PATCH x) 116 | endif() 117 | endif() 118 | endif() 119 | 120 | # find the requested modules 121 | set(SFML_FOUND TRUE) # will be set to false if one of the required modules is not found 122 | foreach(FIND_SFML_COMPONENT ${SFML_FIND_COMPONENTS}) 123 | string(TOLOWER ${FIND_SFML_COMPONENT} FIND_SFML_COMPONENT_LOWER) 124 | string(TOUPPER ${FIND_SFML_COMPONENT} FIND_SFML_COMPONENT_UPPER) 125 | set(FIND_SFML_COMPONENT_NAME sfml-${FIND_SFML_COMPONENT_LOWER}) 126 | 127 | # no suffix for sfml-main, it is always a static library 128 | if(FIND_SFML_COMPONENT_LOWER STREQUAL "main") 129 | # release library 130 | find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE 131 | NAMES ${FIND_SFML_COMPONENT_NAME} 132 | PATH_SUFFIXES lib64 lib 133 | PATHS ${FIND_SFML_PATHS}) 134 | 135 | # debug library 136 | find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG 137 | NAMES ${FIND_SFML_COMPONENT_NAME}-d 138 | PATH_SUFFIXES lib64 lib 139 | PATHS ${FIND_SFML_PATHS}) 140 | else() 141 | # static release library 142 | find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_RELEASE 143 | NAMES ${FIND_SFML_COMPONENT_NAME}-s 144 | PATH_SUFFIXES lib64 lib 145 | PATHS ${FIND_SFML_PATHS}) 146 | 147 | # static debug library 148 | find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_DEBUG 149 | NAMES ${FIND_SFML_COMPONENT_NAME}-s-d 150 | PATH_SUFFIXES lib64 lib 151 | PATHS ${FIND_SFML_PATHS}) 152 | 153 | # dynamic release library 154 | find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_RELEASE 155 | NAMES ${FIND_SFML_COMPONENT_NAME} 156 | PATH_SUFFIXES lib64 lib 157 | PATHS ${FIND_SFML_PATHS}) 158 | 159 | # dynamic debug library 160 | find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_DEBUG 161 | NAMES ${FIND_SFML_COMPONENT_NAME}-d 162 | PATH_SUFFIXES lib64 lib 163 | PATHS ${FIND_SFML_PATHS}) 164 | 165 | # choose the entries that fit the requested link type 166 | if(SFML_STATIC_LIBRARIES) 167 | if(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_RELEASE) 168 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_RELEASE}) 169 | endif() 170 | if(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_DEBUG) 171 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_DEBUG}) 172 | endif() 173 | else() 174 | if(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_RELEASE) 175 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_RELEASE}) 176 | endif() 177 | if(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_DEBUG) 178 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_DEBUG}) 179 | endif() 180 | endif() 181 | endif() 182 | 183 | if (SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG OR SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE) 184 | # library found 185 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_FOUND TRUE) 186 | 187 | # if both are found, set SFML_XXX_LIBRARY to contain both 188 | if (SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG AND SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE) 189 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY debug ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG} 190 | optimized ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE}) 191 | endif() 192 | 193 | # if only one debug/release variant is found, set the other to be equal to the found one 194 | if (SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG AND NOT SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE) 195 | # debug and not release 196 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG}) 197 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG}) 198 | endif() 199 | if (SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE AND NOT SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG) 200 | # release and not debug 201 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE}) 202 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE}) 203 | endif() 204 | else() 205 | # library not found 206 | set(SFML_FOUND FALSE) 207 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_FOUND FALSE) 208 | set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY "") 209 | set(FIND_SFML_MISSING "${FIND_SFML_MISSING} SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY") 210 | endif() 211 | 212 | # mark as advanced 213 | MARK_AS_ADVANCED(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY 214 | SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE 215 | SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG 216 | SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_RELEASE 217 | SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_DEBUG 218 | SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_RELEASE 219 | SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_DEBUG) 220 | 221 | # add to the global list of libraries 222 | set(SFML_LIBRARIES ${SFML_LIBRARIES} "${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY}") 223 | endforeach() 224 | 225 | # in case of static linking, we must also define the list of all the dependencies of SFML libraries 226 | if(SFML_STATIC_LIBRARIES) 227 | 228 | # detect the OS 229 | if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") 230 | set(FIND_SFML_OS_WINDOWS 1) 231 | elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux") 232 | set(FIND_SFML_OS_LINUX 1) 233 | elseif(${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") 234 | set(FIND_SFML_OS_FREEBSD 1) 235 | elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") 236 | set(FIND_SFML_OS_MACOSX 1) 237 | endif() 238 | 239 | # start with an empty list 240 | set(SFML_DEPENDENCIES) 241 | set(FIND_SFML_DEPENDENCIES_NOTFOUND) 242 | 243 | # macro that searches for a 3rd-party library 244 | macro(find_sfml_dependency output friendlyname) 245 | # No lookup in environment variables (PATH on Windows), as they may contain wrong library versions 246 | find_library(${output} NAMES ${ARGN} PATHS ${FIND_SFML_PATHS} PATH_SUFFIXES lib NO_SYSTEM_ENVIRONMENT_PATH) 247 | if(${${output}} STREQUAL "${output}-NOTFOUND") 248 | unset(output) 249 | set(FIND_SFML_DEPENDENCIES_NOTFOUND "${FIND_SFML_DEPENDENCIES_NOTFOUND} ${friendlyname}") 250 | endif() 251 | endmacro() 252 | 253 | # sfml-system 254 | list(FIND SFML_FIND_COMPONENTS "system" FIND_SFML_SYSTEM_COMPONENT) 255 | if(NOT ${FIND_SFML_SYSTEM_COMPONENT} EQUAL -1) 256 | 257 | # update the list -- these are only system libraries, no need to find them 258 | if(FIND_SFML_OS_LINUX OR FIND_SFML_OS_FREEBSD OR FIND_SFML_OS_MACOSX) 259 | set(SFML_SYSTEM_DEPENDENCIES "pthread") 260 | endif() 261 | if(FIND_SFML_OS_LINUX) 262 | set(SFML_SYSTEM_DEPENDENCIES ${SFML_SYSTEM_DEPENDENCIES} "rt") 263 | endif() 264 | if(FIND_SFML_OS_WINDOWS) 265 | set(SFML_SYSTEM_DEPENDENCIES "winmm") 266 | endif() 267 | set(SFML_DEPENDENCIES ${SFML_SYSTEM_DEPENDENCIES} ${SFML_DEPENDENCIES}) 268 | endif() 269 | 270 | # sfml-network 271 | list(FIND SFML_FIND_COMPONENTS "network" FIND_SFML_NETWORK_COMPONENT) 272 | if(NOT ${FIND_SFML_NETWORK_COMPONENT} EQUAL -1) 273 | 274 | # update the list -- these are only system libraries, no need to find them 275 | if(FIND_SFML_OS_WINDOWS) 276 | set(SFML_NETWORK_DEPENDENCIES "ws2_32") 277 | endif() 278 | set(SFML_DEPENDENCIES ${SFML_NETWORK_DEPENDENCIES} ${SFML_DEPENDENCIES}) 279 | endif() 280 | 281 | # sfml-window 282 | list(FIND SFML_FIND_COMPONENTS "window" FIND_SFML_WINDOW_COMPONENT) 283 | if(NOT ${FIND_SFML_WINDOW_COMPONENT} EQUAL -1) 284 | 285 | # find libraries 286 | if(FIND_SFML_OS_LINUX OR FIND_SFML_OS_FREEBSD) 287 | find_sfml_dependency(X11_LIBRARY "X11" X11) 288 | find_sfml_dependency(LIBXCB_LIBRARIES "XCB" xcb libxcb) 289 | find_sfml_dependency(X11_XCB_LIBRARY "X11-xcb" X11-xcb libX11-xcb) 290 | find_sfml_dependency(XCB_RANDR_LIBRARY "xcb-randr" xcb-randr libxcb-randr) 291 | find_sfml_dependency(XCB_IMAGE_LIBRARY "xcb-image" xcb-image libxcb-image) 292 | endif() 293 | 294 | if(FIND_SFML_OS_LINUX) 295 | find_sfml_dependency(UDEV_LIBRARIES "UDev" udev libudev) 296 | endif() 297 | 298 | # update the list 299 | if(FIND_SFML_OS_WINDOWS) 300 | set(SFML_WINDOW_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} "opengl32" "winmm" "gdi32") 301 | elseif(FIND_SFML_OS_LINUX) 302 | set(SFML_WINDOW_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} "GL" ${X11_LIBRARY} ${LIBXCB_LIBRARIES} ${X11_XCB_LIBRARY} ${XCB_RANDR_LIBRARY} ${XCB_IMAGE_LIBRARY} ${UDEV_LIBRARIES}) 303 | elseif(FIND_SFML_OS_FREEBSD) 304 | set(SFML_WINDOW_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} "GL" ${X11_LIBRARY} ${LIBXCB_LIBRARIES} ${X11_XCB_LIBRARY} ${XCB_RANDR_LIBRARY} ${XCB_IMAGE_LIBRARY} "usbhid") 305 | elseif(FIND_SFML_OS_MACOSX) 306 | set(SFML_WINDOW_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} "-framework OpenGL -framework Foundation -framework AppKit -framework IOKit -framework Carbon") 307 | endif() 308 | set(SFML_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} ${SFML_DEPENDENCIES}) 309 | endif() 310 | 311 | # sfml-graphics 312 | list(FIND SFML_FIND_COMPONENTS "graphics" FIND_SFML_GRAPHICS_COMPONENT) 313 | if(NOT ${FIND_SFML_GRAPHICS_COMPONENT} EQUAL -1) 314 | 315 | # find libraries 316 | find_sfml_dependency(FREETYPE_LIBRARY "FreeType" freetype) 317 | find_sfml_dependency(JPEG_LIBRARY "libjpeg" jpeg) 318 | 319 | # update the list 320 | set(SFML_GRAPHICS_DEPENDENCIES ${FREETYPE_LIBRARY} ${JPEG_LIBRARY}) 321 | set(SFML_DEPENDENCIES ${SFML_GRAPHICS_DEPENDENCIES} ${SFML_DEPENDENCIES}) 322 | endif() 323 | 324 | # sfml-audio 325 | list(FIND SFML_FIND_COMPONENTS "audio" FIND_SFML_AUDIO_COMPONENT) 326 | if(NOT ${FIND_SFML_AUDIO_COMPONENT} EQUAL -1) 327 | 328 | # find libraries 329 | find_sfml_dependency(OPENAL_LIBRARY "OpenAL" openal openal32) 330 | find_sfml_dependency(OGG_LIBRARY "Ogg" ogg) 331 | find_sfml_dependency(VORBIS_LIBRARY "Vorbis" vorbis) 332 | find_sfml_dependency(VORBISFILE_LIBRARY "VorbisFile" vorbisfile) 333 | find_sfml_dependency(VORBISENC_LIBRARY "VorbisEnc" vorbisenc) 334 | find_sfml_dependency(FLAC_LIBRARY "FLAC" FLAC) 335 | 336 | # update the list 337 | set(SFML_AUDIO_DEPENDENCIES ${OPENAL_LIBRARY} ${FLAC_LIBRARY} ${VORBISENC_LIBRARY} ${VORBISFILE_LIBRARY} ${VORBIS_LIBRARY} ${OGG_LIBRARY}) 338 | set(SFML_DEPENDENCIES ${SFML_DEPENDENCIES} ${SFML_AUDIO_DEPENDENCIES}) 339 | endif() 340 | 341 | endif() 342 | 343 | # handle errors 344 | if(NOT SFML_VERSION_OK) 345 | # SFML version not ok 346 | set(FIND_SFML_ERROR "SFML found but version too low (requested: ${SFML_FIND_VERSION}, found: ${SFML_VERSION_MAJOR}.${SFML_VERSION_MINOR}.${SFML_VERSION_PATCH})") 347 | set(SFML_FOUND FALSE) 348 | elseif(SFML_STATIC_LIBRARIES AND FIND_SFML_DEPENDENCIES_NOTFOUND) 349 | set(FIND_SFML_ERROR "SFML found but some of its dependencies are missing (${FIND_SFML_DEPENDENCIES_NOTFOUND})") 350 | set(SFML_FOUND FALSE) 351 | elseif(NOT SFML_FOUND) 352 | # include directory or library not found 353 | set(FIND_SFML_ERROR "Could NOT find SFML (missing: ${FIND_SFML_MISSING})") 354 | endif() 355 | if (NOT SFML_FOUND) 356 | if(SFML_FIND_REQUIRED) 357 | # fatal error 358 | message(FATAL_ERROR ${FIND_SFML_ERROR}) 359 | elseif(NOT SFML_FIND_QUIETLY) 360 | # error but continue 361 | message("${FIND_SFML_ERROR}") 362 | endif() 363 | endif() 364 | 365 | # handle success 366 | if(SFML_FOUND AND NOT SFML_FIND_QUIETLY) 367 | message(STATUS "Found SFML ${SFML_VERSION_MAJOR}.${SFML_VERSION_MINOR}.${SFML_VERSION_PATCH} in ${SFML_INCLUDE_DIR}") 368 | endif() 369 | -------------------------------------------------------------------------------- /include/APU/APU.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "APU/DMC.h" 4 | #include "APU/FrameCounter.h" 5 | #include "APU/Noise.h" 6 | #include "APU/Pulse.h" 7 | #include "APU/Timer.h" 8 | #include "APU/Triangle.h" 9 | #include "APU/spsc.hpp" 10 | #include "AudioPlayer.h" 11 | #include "IRQ.h" 12 | 13 | namespace sn 14 | { 15 | class APU 16 | { 17 | public: 18 | Pulse pulse1 { Pulse::Type::Pulse1 }; 19 | Pulse pulse2 { Pulse::Type::Pulse2 }; 20 | Triangle triangle; 21 | Noise noise; 22 | DMC dmc; 23 | 24 | FrameCounter frame_counter; 25 | 26 | public: 27 | APU(AudioPlayer& player, IRQHandle& irq, std::function dmcDma) 28 | : dmc(irq, dmcDma) 29 | , frame_counter(setup_frame_counter(irq)) 30 | , audio_queue(player.audio_queue) 31 | , sampling_timer(nanoseconds(int64_t(1e9) / int64_t(player.output_sample_rate))) 32 | { 33 | } 34 | 35 | // clock at the same frequency as the cpu 36 | void step(); 37 | 38 | void writeRegister(Address addr, Byte value); 39 | Byte readStatus(); 40 | 41 | private: 42 | FrameCounter setup_frame_counter(IRQHandle& irq); 43 | bool divideByTwo = false; 44 | 45 | spsc::RingBuffer& audio_queue; 46 | Timer sampling_timer; 47 | }; 48 | 49 | } 50 | -------------------------------------------------------------------------------- /include/APU/Constants.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace sn 6 | { 7 | using namespace std::chrono; 8 | 9 | const float max_volume_f = static_cast(0xF); 10 | const int max_volume = 0xF; 11 | 12 | // NES CPU clock period 13 | const auto cpu_clock_period_ns = nanoseconds(559); 14 | const auto cpu_clock_period_s = duration_cast>(cpu_clock_period_ns); 15 | // The apu is clocked every second cpu period 16 | const auto apu_clock_period_ns = cpu_clock_period_ns * 2; 17 | const auto apu_clock_period_s = duration_cast>(apu_clock_period_ns); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /include/APU/DMC.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "APU/Divider.h" 4 | #include "Cartridge.h" 5 | #include "IRQ.h" 6 | #include 7 | 8 | namespace sn 9 | { 10 | 11 | struct DMC 12 | { 13 | bool irqEnable = false; 14 | bool loop = false; 15 | 16 | int volume = 0; 17 | 18 | bool change_enabled = false; 19 | Divider change_rate { 0 }; 20 | 21 | Address sample_begin = 0; 22 | int sample_length = 0; 23 | 24 | int remaining_bytes = 0; 25 | Address current_address = 0; 26 | 27 | Byte sample_buffer = 0; 28 | 29 | int shifter = 0; 30 | int remaining_bits = 0; 31 | bool silenced = false; 32 | 33 | bool interrupt = false; 34 | 35 | void set_irq_enable(bool enable); 36 | void set_rate(int idx); 37 | void control(bool enable); 38 | void clear_interrupt(); 39 | 40 | DMC(IRQHandle& irq, std::function dma) 41 | : irq(irq) 42 | , dma(dma) 43 | { 44 | } 45 | 46 | // Clocked at the cpu freq 47 | void clock(); 48 | 49 | Byte sample() const; 50 | 51 | bool has_more_samples() const { return remaining_bytes > 0; } 52 | 53 | private: 54 | // Load sample and return if it was succesfully loaded 55 | bool load_sample(); 56 | int pop_delta(); 57 | IRQHandle& irq; 58 | std::function dma; 59 | }; 60 | 61 | } 62 | -------------------------------------------------------------------------------- /include/APU/Divider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sn 4 | { 5 | // Modeled after NES timers; which have a period of (t+1) and count from t -> t-1 -> .. -> 0 -> t -> t-1 -> ... 0 -> ... 6 | struct Divider 7 | { 8 | public: 9 | explicit Divider(int period) 10 | : period(period) 11 | { 12 | } 13 | 14 | bool clock() 15 | { 16 | if (counter == 0) 17 | { 18 | counter = period; 19 | return true; 20 | } 21 | 22 | counter -= 1; 23 | return false; 24 | } 25 | 26 | void set_period(int p) { period = p; } 27 | 28 | void reset() { counter = period; } 29 | 30 | int get_period() const { return period; } 31 | 32 | private: 33 | int period = 0; 34 | int counter = 0; 35 | }; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /include/APU/FrameCounter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IRQ.h" 4 | #include 5 | #include 6 | 7 | namespace sn 8 | { 9 | struct FrameClockable 10 | { 11 | // will be called every quarter frame (including half frames) 12 | virtual void quarter_frame_clock() {}; 13 | // will be called every half frame 14 | virtual void half_frame_clock() {}; 15 | }; 16 | 17 | struct FrameCounter 18 | { 19 | constexpr static int Q1 = 7457; 20 | constexpr static int Q2 = 14913; 21 | constexpr static int Q3 = 22371; 22 | constexpr static int Q4 = 29829; 23 | constexpr static int preQ4 = Q4 - 1; 24 | constexpr static int postQ4 = Q4 + 1; 25 | constexpr static int seq4step_length = postQ4; 26 | 27 | constexpr static int Q5 = 37281; 28 | constexpr static int seq5step_length = Q5 + 1; 29 | 30 | std::vector> frame_slots; 31 | 32 | enum Mode 33 | { 34 | Seq4Step = 0, 35 | Seq5Step = 1, 36 | } mode = Seq4Step; 37 | int counter = 0; 38 | bool interrupt_inhibit = false; 39 | 40 | IRQHandle& irq; 41 | bool frame_interrupt = false; 42 | 43 | FrameCounter(std::vector> slots, IRQHandle& irq) 44 | : frame_slots(slots) 45 | , irq(irq) 46 | { 47 | } 48 | 49 | void clearFrameInterrupt(); 50 | void clock(); 51 | void reset(Mode m, bool irq_inhibit); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /include/APU/Noise.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "APU/Divider.h" 4 | #include "APU/Units.h" 5 | #include "Cartridge.h" 6 | 7 | namespace sn 8 | { 9 | 10 | struct Noise 11 | { 12 | Volume volume; 13 | LengthCounter length_counter; 14 | Divider divider { 0 }; 15 | 16 | enum Mode : bool 17 | { 18 | Bit1 = 0, 19 | BIt6 = 1, 20 | } mode = Bit1; 21 | int period = 0; 22 | int shift_register = 1; 23 | 24 | void set_period_from_table(int idx); 25 | 26 | // Clocked at the cpu freq 27 | void clock(); 28 | 29 | Byte sample() const; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /include/APU/Pulse.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "APU/Divider.h" 4 | #include "APU/PulseUnits.h" 5 | #include "APU/Units.h" 6 | #include "Cartridge.h" 7 | 8 | namespace sn 9 | { 10 | 11 | struct Pulse 12 | { 13 | Volume volume; 14 | LengthCounter length_counter; 15 | 16 | uint seq_idx { 0 }; 17 | PulseDuty::Type seq_type { PulseDuty::Type::SEQ_50 }; 18 | Divider sequencer { 0 }; 19 | int period = 0; 20 | 21 | enum class Type 22 | { 23 | Pulse1 = 1, 24 | Pulse2 = 2, 25 | } type; 26 | 27 | Pulse(Type type) 28 | : type(type) 29 | , sweep(*this, type == Type::Pulse1) 30 | { 31 | } 32 | 33 | Sweep sweep; 34 | 35 | void set_period(int p); 36 | 37 | // Clocked at half the cpu freq 38 | void clock(); 39 | 40 | Byte sample() const; 41 | }; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /include/APU/PulseUnits.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "APU/Divider.h" 4 | #include "APU/FrameCounter.h" 5 | #include "Cartridge.h" 6 | 7 | namespace sn 8 | { 9 | 10 | struct Pulse; 11 | 12 | struct PulseDuty 13 | { 14 | enum class Type 15 | { 16 | SEQ_12_5 = 0, 17 | SEQ_25 = 1, 18 | SEQ_50 = 2, 19 | SEQ_25_INV = 3, 20 | }; 21 | static const int Count = 4; 22 | static const int Length = 8; 23 | 24 | static inline bool active(Type cycle, int idx) 25 | { 26 | const bool _sequences[] { 27 | 0, 0, 0, 0, 0, 0, 0, 1, // 12.5% 28 | 0, 0, 0, 0, 0, 0, 1, 1, // 25% 29 | 0, 0, 0, 0, 1, 1, 1, 1, // 50% 30 | 1, 1, 1, 1, 1, 1, 0, 0, // 25% negated 31 | }; 32 | return _sequences[static_cast(cycle) * Length + idx]; 33 | } 34 | }; 35 | 36 | struct Sweep : public FrameClockable 37 | { 38 | Pulse& pulse; 39 | 40 | int period = 0; 41 | bool enabled = false; 42 | bool reload = false; 43 | bool negate = false; 44 | sn::Byte shift = 0; 45 | bool ones_complement = false; 46 | 47 | Divider divider { 0 }; 48 | 49 | Sweep(Pulse& pulse, bool ones_complement) 50 | : pulse(pulse) 51 | , ones_complement(ones_complement) 52 | { 53 | } 54 | 55 | void half_frame_clock() override; 56 | 57 | static bool is_muted(int current, int target) { return current < 8 || target > 0x7FF; } 58 | 59 | int calculate_target(int current) const; 60 | }; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /include/APU/Timer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace sn 5 | { 6 | using namespace std::chrono; 7 | 8 | struct Timer 9 | { 10 | 11 | public: 12 | explicit Timer(nanoseconds period) 13 | : period(period) 14 | { 15 | } 16 | const nanoseconds period; 17 | 18 | // clock the timer and return number of periods elapsed 19 | int clock(nanoseconds elapsed) 20 | { 21 | leftover += elapsed; 22 | if (leftover < elapsed) 23 | { 24 | return 0; 25 | } 26 | 27 | auto cycles = leftover / period; 28 | leftover = leftover % period; 29 | return cycles; 30 | } 31 | 32 | private: 33 | nanoseconds leftover = nanoseconds(0); 34 | }; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /include/APU/Triangle.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "APU/Divider.h" 3 | #include "APU/Units.h" 4 | #include "Cartridge.h" 5 | #include 6 | 7 | namespace sn 8 | { 9 | 10 | struct Triangle 11 | { 12 | LengthCounter length_counter; 13 | LinearCounter linear_counter; 14 | 15 | std::uint32_t seq_idx { 0 }; 16 | Divider sequencer { 0 }; 17 | int period = 0; 18 | 19 | void set_period(int p); 20 | 21 | // Clocked at the cpu freq 22 | void clock(); 23 | 24 | Byte sample() const; 25 | 26 | int volume() const; 27 | }; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /include/APU/Units.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "APU/Constants.h" 7 | #include "APU/Divider.h" 8 | #include "APU/FrameCounter.h" 9 | 10 | namespace sn 11 | { 12 | 13 | struct LengthCounter : public FrameClockable 14 | { 15 | void set_enable(bool new_value); 16 | bool is_enabled() const { return enabled; } 17 | 18 | void set_from_table(std::size_t index); 19 | void half_frame_clock() override; 20 | bool muted() const; 21 | 22 | bool halt = false; 23 | 24 | bool enabled = false; 25 | int counter = 0; 26 | }; 27 | 28 | struct LinearCounter : public FrameClockable 29 | { 30 | void set_linear(int new_value); 31 | void quarter_frame_clock() override; 32 | 33 | bool reload = false; 34 | int reloadValue = 0; 35 | bool control = true; 36 | 37 | int counter = 0; 38 | }; 39 | 40 | struct Volume : public FrameClockable 41 | { 42 | void quarter_frame_clock() override; 43 | 44 | int get() const; 45 | 46 | Divider divider { 0 }; 47 | std::uint32_t fixedVolumeOrPeriod = max_volume; 48 | std::uint32_t decayVolume = max_volume; 49 | bool constantVolume = true; 50 | bool isLooping = false; 51 | bool shouldStart = false; 52 | }; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /include/APU/spsc.hpp: -------------------------------------------------------------------------------- 1 | // Adapted from Boost.Lockfree to work without other boost dependencies 2 | // 3 | // lock-free single-producer/single-consumer ringbuffer 4 | // this algorithm is implemented in various projects (linux kernel) 5 | // 6 | // Copyright (C) 2009-2013, 2022 Tim Blechmann, Amish K. Naidu 7 | // 8 | // Distributed under the Boost Software License, Version 1.0. (See 9 | // accompanying file LICENSE_1_0.txt or copy at 10 | // http://www.boost.org/LICENSE_1_0.txt) 11 | 12 | #pragma once 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace spsc 20 | { 21 | 22 | using std::size_t; 23 | 24 | // RingBuffer assumes value is trivially destructible 25 | // Only works with single producer and single consumer threads. 26 | // 27 | // Thread safety: 28 | // * During push, write-index is stored with memory order release *after* storage[write_index] is written to, ensuring 29 | // the storage writes are visible in pop due to Release-Acquire ordering 30 | // * write-index only moves forward *upto* the `read_index_`, so during a pop, it is safe to extract values from the 31 | // storage, since push can only affect the empty area 32 | // * read-index is stored using release ordering to ensure it is only updated after the full pop operation is finished 33 | template 34 | class RingBuffer 35 | { 36 | static_assert(std::is_trivially_destructible::value, 37 | "expecting a simple (trivially_destructible) type in the ring buffer"); 38 | 39 | private: 40 | const size_t max_size; 41 | std::atomic write_index_; 42 | std::atomic read_index_; 43 | 44 | std::vector storage; 45 | 46 | RingBuffer(RingBuffer const&) = delete; 47 | RingBuffer& operator=(RingBuffer const&) = delete; 48 | 49 | public: 50 | explicit RingBuffer(int capacity) 51 | : max_size(capacity) 52 | , write_index_(0) 53 | , read_index_(0) 54 | { 55 | storage.resize(capacity); 56 | } 57 | 58 | static size_t next_index(size_t arg, size_t max_size) 59 | { 60 | size_t ret = arg + 1; 61 | while (ret >= max_size) 62 | ret -= max_size; 63 | return ret; 64 | } 65 | 66 | /** Push a value into the ring-buffer 67 | * 68 | * \returns If push was successful (it could fail if the queue was full) 69 | * \note Must be called from a single writer thread 70 | * */ 71 | bool push(T const& t) 72 | { 73 | const size_t write_index = write_index_.load(std::memory_order_relaxed); // only written from push thread 74 | const size_t next = next_index(write_index, max_size); 75 | 76 | if (next == read_index_.load(std::memory_order_acquire)) 77 | return false; /* RingBuffer is full */ 78 | 79 | storage[write_index] = t; 80 | 81 | write_index_.store(next, std::memory_order_release); 82 | 83 | return true; 84 | } 85 | 86 | /** Pop values into a output buffer 87 | * 88 | * \returns Number of values read/popped 89 | * \note Must be called from a single reader thread 90 | * */ 91 | size_t pop(T* output_buffer, size_t output_count) 92 | { 93 | const size_t write_index = write_index_.load(std::memory_order_acquire); 94 | const size_t read_index = read_index_.load(std::memory_order_relaxed); // only written from pop thread 95 | 96 | size_t avail; 97 | if (write_index >= read_index) 98 | { 99 | avail = write_index - read_index; 100 | } 101 | else 102 | { 103 | avail = write_index + max_size - read_index; 104 | } 105 | 106 | if (avail == 0) 107 | { 108 | return 0; 109 | } 110 | 111 | output_count = std::min(output_count, avail); 112 | 113 | size_t new_read_index = read_index + output_count; 114 | 115 | if (read_index + output_count >= max_size) 116 | { 117 | // copy data in two sections 118 | const size_t count0 = max_size - read_index; 119 | const size_t count1 = output_count - count0; 120 | 121 | std::copy(storage.begin() + read_index, storage.end(), output_buffer); 122 | std::copy(storage.begin(), storage.begin() + count1, output_buffer + count0); 123 | 124 | new_read_index -= max_size; 125 | } 126 | else 127 | { 128 | std::copy(storage.begin() + read_index, storage.begin() + (read_index + output_count), output_buffer); 129 | if (new_read_index == max_size) 130 | { 131 | new_read_index = 0; 132 | } 133 | } 134 | 135 | read_index_.store(new_read_index, std::memory_order_release); 136 | return output_count; 137 | } 138 | 139 | /** reset the RingBuffer 140 | * 141 | * \note Not thread-safe 142 | * */ 143 | void reset() 144 | { 145 | write_index_.store(0, std::memory_order_relaxed); 146 | read_index_.store(0, std::memory_order_release); 147 | } 148 | 149 | /** Check if the RingBuffer is empty 150 | * 151 | * \return true, if the RingBuffer is empty, false otherwise 152 | * \note Due to the concurrent nature of the RingBuffer the result may be inaccurate. 153 | * */ 154 | bool empty() { return size() == 0; } 155 | 156 | /** Get the current RingBuffer size 157 | * 158 | * \return Size of the buffer 159 | * \note Due to the concurrent nature of the RingBuffer the result may be inaccurate 160 | * */ 161 | std::size_t size() 162 | { 163 | const size_t write_index = write_index_.load(std::memory_order_relaxed); 164 | const size_t read_index = read_index_.load(std::memory_order_relaxed); 165 | 166 | size_t avail; 167 | if (write_index >= read_index) 168 | { 169 | avail = write_index - read_index; 170 | } 171 | else 172 | { 173 | avail = write_index + max_size - read_index; 174 | } 175 | 176 | return avail; 177 | } 178 | 179 | std::size_t capacity() { return max_size; } 180 | }; 181 | 182 | } /* namespace lockfree */ 183 | -------------------------------------------------------------------------------- /include/AudioPlayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "APU/spsc.hpp" 7 | 8 | namespace sn 9 | { 10 | 11 | const std::chrono::milliseconds callback_period_ms { 100 }; 12 | 13 | struct CallbackData 14 | { 15 | spsc::RingBuffer& ring_buffer; 16 | ma_resampler* resampler; 17 | std::vector input_frames_buffer; 18 | bool mute; 19 | int remaining_buffer_rounds; 20 | }; 21 | 22 | // Receives input at a fixed sample rate from the audio queue and uses miniaudio to resample and output to audiodevice 23 | // 24 | // Why not SFML? SFML's SoundStream introduces additional buffers and has it's own polling mechanism which introduces 25 | // extra lag. Effectively using it would mean relying on it's implementation-specific behaviour Using miniaudio is 26 | // simpler as we just need to implement one audio callback 27 | class AudioPlayer 28 | { 29 | public: 30 | const int output_sample_rate = ma_standard_sample_rate_44100; 31 | 32 | AudioPlayer(int input_rate) 33 | : input_sample_rate(input_rate) 34 | , audio_queue(4 * input_rate * 35 | (callback_period_ms.count() / 100)) // big enough to keep 4 callback's worth of samples 36 | , cb_data { audio_queue, &resampler, {}, false, 1 } 37 | { 38 | } 39 | ~AudioPlayer(); 40 | 41 | bool start(); 42 | void mute(); 43 | 44 | const int input_sample_rate; 45 | // ONLY safe for 1 writer and 1 reader 46 | spsc::RingBuffer audio_queue; 47 | 48 | private: 49 | CallbackData cb_data; 50 | 51 | bool initialized = false; 52 | ma_device_config deviceConfig; 53 | ma_device device; 54 | ma_resampler resampler; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /include/CPU.h: -------------------------------------------------------------------------------- 1 | #ifndef CPU_H 2 | #define CPU_H 3 | #include "CPUOpcodes.h" 4 | #include "IRQ.h" 5 | #include "MainBus.h" 6 | #include 7 | 8 | namespace sn 9 | { 10 | 11 | class CPU; 12 | class IRQHandler : public IRQHandle 13 | { 14 | int bit; 15 | CPU& cpu; 16 | 17 | public: 18 | IRQHandler(int bit, CPU& cpu) 19 | : bit(bit) 20 | , cpu(cpu) 21 | { 22 | } 23 | 24 | void release() override; 25 | void pull() override; 26 | }; 27 | 28 | class CPU 29 | { 30 | 31 | public: 32 | CPU(MainBus& mem); 33 | 34 | void step(); 35 | void reset(); 36 | void reset(Address start_addr); 37 | void log(); 38 | 39 | Address getPC() { return r_PC; } 40 | void skipOAMDMACycles(); 41 | void skipDMCDMACycles(); 42 | 43 | void nmiInterrupt(); 44 | 45 | IRQHandle& createIRQHandler(); 46 | void setIRQPulldown(int bit, bool state); 47 | 48 | private: 49 | void interruptSequence(InterruptType type); 50 | 51 | // Instructions are split into five sets to make decoding easier. 52 | // These functions return true if they succeed 53 | bool executeImplied(Byte opcode); 54 | bool executeBranch(Byte opcode); 55 | bool executeType0(Byte opcode); 56 | bool executeType1(Byte opcode); 57 | bool executeType2(Byte opcode); 58 | 59 | Address readAddress(Address addr); 60 | 61 | void pushStack(Byte value); 62 | Byte pullStack(); 63 | 64 | // If a and b are in different pages, increases the m_SkipCycles by 1 65 | void skipPageCrossCycle(Address a, Address b); 66 | void setZN(Byte value); 67 | 68 | int m_skipCycles; 69 | int m_cycles; 70 | 71 | // Registers 72 | Address r_PC; 73 | Byte r_SP; 74 | Byte r_A; 75 | Byte r_X; 76 | Byte r_Y; 77 | 78 | // Status flags. 79 | // Is storing them in one byte better ? 80 | bool f_C; 81 | bool f_Z; 82 | bool f_I; 83 | bool f_D; 84 | bool f_V; 85 | bool f_N; 86 | 87 | bool m_pendingNMI; 88 | 89 | bool isPendingIRQ() const { return !f_I && m_irqPulldowns != 0; }; 90 | 91 | MainBus& m_bus; 92 | 93 | // Each bit is assigned to an IRQ handler. 94 | // If any bits are set, it means the irq must be triggered 95 | int m_irqPulldowns = 0; 96 | std::list m_irqHandlers; 97 | }; 98 | 99 | }; 100 | #endif // CPU_H 101 | -------------------------------------------------------------------------------- /include/CPUOpcodes.h: -------------------------------------------------------------------------------- 1 | #ifndef CPUOPCODES_H_INCLUDED 2 | #define CPUOPCODES_H_INCLUDED 3 | 4 | namespace sn 5 | { 6 | const auto InstructionModeMask = 0x3; 7 | 8 | const auto OperationMask = 0xe0; 9 | const auto OperationShift = 5; 10 | 11 | const auto AddrModeMask = 0x1c; 12 | const auto AddrModeShift = 2; 13 | 14 | const auto BranchInstructionMask = 0x1f; 15 | const auto BranchInstructionMaskResult = 0x10; 16 | const auto BranchConditionMask = 0x20; 17 | const auto BranchOnFlagShift = 6; 18 | 19 | const auto NMIVector = 0xfffa; 20 | const auto ResetVector = 0xfffc; 21 | const auto IRQVector = 0xfffe; 22 | 23 | enum BranchOnFlag 24 | { 25 | Negative, 26 | Overflow, 27 | Carry, 28 | Zero 29 | }; 30 | 31 | enum Operation1 32 | { 33 | ORA, 34 | AND, 35 | EOR, 36 | ADC, 37 | STA, 38 | LDA, 39 | CMP, 40 | SBC, 41 | }; 42 | 43 | enum AddrMode1 44 | { 45 | IndexedIndirectX, 46 | ZeroPage, 47 | Immediate, 48 | Absolute, 49 | IndirectY, 50 | IndexedX, 51 | AbsoluteY, 52 | AbsoluteX, 53 | }; 54 | 55 | enum Operation2 56 | { 57 | ASL, 58 | ROL, 59 | LSR, 60 | ROR, 61 | STX, 62 | LDX, 63 | DEC, 64 | INC, 65 | }; 66 | 67 | enum AddrMode2 68 | { 69 | Immediate_, 70 | ZeroPage_, 71 | Accumulator, 72 | Absolute_, 73 | Indexed = 5, 74 | AbsoluteIndexed = 7, 75 | }; 76 | 77 | enum Operation0 78 | { 79 | BIT = 1, 80 | STY = 4, 81 | LDY, 82 | CPY, 83 | CPX, 84 | }; 85 | 86 | enum OperationImplied 87 | { 88 | NOP = 0xea, 89 | BRK = 0x00, 90 | JSR = 0x20, 91 | RTI = 0x40, 92 | RTS = 0x60, 93 | 94 | JMP = 0x4C, 95 | JMPI = 0x6C, // JMP Indirect 96 | 97 | PHP = 0x08, 98 | PLP = 0x28, 99 | PHA = 0x48, 100 | PLA = 0x68, 101 | 102 | DEY = 0x88, 103 | DEX = 0xca, 104 | TAY = 0xa8, 105 | INY = 0xc8, 106 | INX = 0xe8, 107 | 108 | CLC = 0x18, 109 | SEC = 0x38, 110 | CLI = 0x58, 111 | SEI = 0x78, 112 | TYA = 0x98, 113 | CLV = 0xb8, 114 | CLD = 0xd8, 115 | SED = 0xf8, 116 | 117 | TXA = 0x8a, 118 | TXS = 0x9a, 119 | TAX = 0xaa, 120 | TSX = 0xba, 121 | }; 122 | 123 | enum InterruptType 124 | { 125 | IRQ, 126 | NMI, 127 | BRK_ 128 | }; 129 | 130 | // 0 implies unused opcode 131 | static const int OperationCycles[0x100] = { 132 | // clang-format off 133 | 7, 6, 0, 0, 0, 3, 5, 0, 3, 2, 2, 0, 0, 4, 6, 0, 134 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 135 | 6, 6, 0, 0, 3, 3, 5, 0, 4, 2, 2, 0, 4, 4, 6, 0, 136 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 137 | 6, 6, 0, 0, 0, 3, 5, 0, 3, 2, 2, 0, 3, 4, 6, 0, 138 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 139 | 6, 6, 0, 0, 0, 3, 5, 0, 4, 2, 2, 0, 5, 4, 6, 0, 140 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 141 | 0, 6, 0, 0, 3, 3, 3, 0, 2, 0, 2, 0, 4, 4, 4, 0, 142 | 2, 6, 0, 0, 4, 4, 4, 0, 2, 5, 2, 0, 0, 5, 0, 0, 143 | 2, 6, 2, 0, 3, 3, 3, 0, 2, 2, 2, 0, 4, 4, 4, 0, 144 | 2, 5, 0, 0, 4, 4, 4, 0, 2, 4, 2, 0, 4, 4, 4, 0, 145 | 2, 6, 0, 0, 3, 3, 5, 0, 2, 2, 2, 0, 4, 4, 6, 0, 146 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 147 | 2, 6, 0, 0, 3, 3, 5, 0, 2, 2, 2, 2, 4, 4, 6, 0, 148 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 149 | // clang-format on 150 | }; 151 | }; 152 | 153 | #endif // CPUOPCODES_H_INCLUDED 154 | -------------------------------------------------------------------------------- /include/Cartridge.h: -------------------------------------------------------------------------------- 1 | #ifndef CARTRIDGE_H 2 | #define CARTRIDGE_H 3 | #include 4 | #include 5 | #include 6 | 7 | namespace sn 8 | { 9 | using Byte = std::uint8_t; 10 | using Address = std::uint16_t; 11 | 12 | class Cartridge 13 | { 14 | public: 15 | Cartridge(); 16 | bool loadFromFile(std::string path); 17 | const std::vector& getROM(); 18 | const std::vector& getVROM(); 19 | Byte getMapper(); 20 | Byte getNameTableMirroring(); 21 | bool hasExtendedRAM(); 22 | 23 | private: 24 | std::vector m_PRG_ROM; 25 | std::vector m_CHR_ROM; 26 | Byte m_nameTableMirroring; 27 | Byte m_mapperNumber; 28 | bool m_extendedRAM; 29 | bool m_chrRAM; 30 | }; 31 | 32 | }; 33 | 34 | #endif // CARTRIDGE_H 35 | -------------------------------------------------------------------------------- /include/Controller.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLLER_H 2 | #define CONTROLLER_H 3 | #include 4 | #include 5 | #include 6 | 7 | namespace sn 8 | { 9 | using Byte = std::uint8_t; 10 | class Controller 11 | { 12 | public: 13 | Controller(); 14 | enum Buttons 15 | { 16 | A, 17 | B, 18 | Select, 19 | Start, 20 | Up, 21 | Down, 22 | Left, 23 | Right, 24 | TotalButtons, 25 | }; 26 | 27 | void strobe(Byte b); 28 | Byte read(); 29 | void setKeyBindings(const std::vector& keys); 30 | 31 | private: 32 | bool m_strobe; 33 | unsigned int m_keyStates; 34 | 35 | std::vector m_keyBindings; 36 | }; 37 | } 38 | 39 | #endif // CONTROLLER_H 40 | -------------------------------------------------------------------------------- /include/Emulator.h: -------------------------------------------------------------------------------- 1 | #ifndef EMULATOR_H 2 | #define EMULATOR_H 3 | #include 4 | #include 5 | 6 | #include "APU/APU.h" 7 | #include "AudioPlayer.h" 8 | #include "CPU.h" 9 | #include "Controller.h" 10 | #include "MainBus.h" 11 | #include "PPU.h" 12 | #include "PictureBus.h" 13 | 14 | namespace sn 15 | { 16 | using TimePoint = std::chrono::high_resolution_clock::time_point; 17 | using Duration = std::chrono::high_resolution_clock::duration; 18 | 19 | const int NESVideoWidth = ScanlineVisibleDots; 20 | const int NESVideoHeight = VisibleScanlines; 21 | 22 | class Emulator 23 | { 24 | public: 25 | Emulator(); 26 | void run(std::string rom_path); 27 | void setVideoWidth(int width); 28 | void setVideoHeight(int height); 29 | void setVideoScale(float scale); 30 | void setKeys(std::vector& p1, std::vector& p2); 31 | void muteAudio(); 32 | 33 | private: 34 | void OAMDMA(Byte page); 35 | Byte DMCDMA(Address addr); 36 | 37 | CPU m_cpu; 38 | 39 | AudioPlayer m_audioPlayer; 40 | 41 | PictureBus m_pictureBus; 42 | PPU m_ppu; 43 | APU m_apu; 44 | Cartridge m_cartridge; 45 | std::unique_ptr m_mapper; 46 | 47 | Controller m_controller1, m_controller2; 48 | 49 | MainBus m_bus; 50 | 51 | sf::RenderWindow m_window; 52 | VirtualScreen m_emulatorScreen; 53 | float m_screenScale; 54 | 55 | TimePoint m_lastWakeup; 56 | 57 | Duration m_elapsedTime; 58 | }; 59 | } 60 | #endif // EMULATOR_H 61 | -------------------------------------------------------------------------------- /include/IRQ.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sn 4 | { 5 | 6 | class IRQHandle 7 | { 8 | public: 9 | virtual void pull() = 0; 10 | virtual void release() = 0; 11 | }; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /include/Log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #ifndef __FILENAME__ 11 | #define __FILENAME__ __FILE__ 12 | #endif 13 | 14 | #define LOG(level) \ 15 | if (level > sn::Log::get().getLevel()) \ 16 | ; \ 17 | else \ 18 | sn::Log::get().getStream() << sn::log_timestamp << '[' << __FILENAME__ << ":" << std::dec << __LINE__ << "] " 19 | 20 | #define LOG_CPU \ 21 | if (sn::CpuTrace != sn::Log::get().getLevel()) \ 22 | ; \ 23 | else \ 24 | sn::Log::get().getCpuTraceStream() 25 | 26 | #define VAR_PRINT(x) " \033[0;31m" << #x << "\033[0m=" << x 27 | 28 | namespace sn 29 | { 30 | inline std::ostream& log_timestamp(std::ostream& out) 31 | { 32 | auto timestamp_ms = 33 | std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) 34 | .count(); 35 | return out << '[' << timestamp_ms << ']'; 36 | } 37 | 38 | enum Level 39 | { 40 | None, 41 | Error, 42 | Info, 43 | InfoVerbose, 44 | ApuTrace, 45 | CpuTrace 46 | }; 47 | class Log 48 | { 49 | public: 50 | ~Log(); 51 | void setLogStream(std::ostream& stream); 52 | void setCpuTraceStream(std::ostream& stream); 53 | Log& setLevel(Level level); 54 | Level getLevel(); 55 | 56 | std::ostream& getStream(); 57 | std::ostream& getCpuTraceStream(); 58 | 59 | static Log& get(); 60 | 61 | private: 62 | Level m_logLevel; 63 | std::ostream* m_logStream; 64 | std::ostream* m_cpuTrace; 65 | }; 66 | 67 | // Courtesy of http://wordaligned.org/articles/cpp-streambufs#toctee-streams 68 | class TeeBuf : public std::streambuf 69 | { 70 | public: 71 | // Construct a streambuf which tees output to both input 72 | // streambufs. 73 | TeeBuf(std::streambuf* sb1, std::streambuf* sb2); 74 | 75 | private: 76 | // This tee buffer has no buffer. So every character "overflows" 77 | // and can be put directly into the teed buffers. 78 | virtual int overflow(int c); 79 | // Sync both teed buffers. 80 | virtual int sync(); 81 | 82 | private: 83 | std::streambuf* m_sb1; 84 | std::streambuf* m_sb2; 85 | }; 86 | 87 | class TeeStream : public std::ostream 88 | { 89 | public: 90 | // Construct an ostream which tees output to the supplied 91 | // ostreams. 92 | TeeStream(std::ostream& o1, std::ostream& o2); 93 | 94 | private: 95 | TeeBuf m_tbuf; 96 | }; 97 | 98 | }; 99 | #endif // LOG_H 100 | -------------------------------------------------------------------------------- /include/MainBus.h: -------------------------------------------------------------------------------- 1 | #ifndef MEMORY_H 2 | #define MEMORY_H 3 | #include "APU/APU.h" 4 | #include "Cartridge.h" 5 | #include "Controller.h" 6 | #include "Mapper.h" 7 | #include "PPU.h" 8 | #include 9 | #include 10 | 11 | namespace sn 12 | { 13 | class MainBus 14 | { 15 | public: 16 | enum Register 17 | { 18 | PPU_CTRL = 0x2000, 19 | PPU_MASK, // = 0x2001, 20 | PPU_STATUS, // = 0x2002, 21 | OAM_ADDR, // = 0x2003, 22 | OAM_DATA, // = 0x2004, 23 | PPU_SCROL, // = 0x2005, 24 | PPU_ADDR, // = 0x2006, 25 | PPU_DATA, // = 0x2007, 26 | 27 | // 0x2008 - 0x3fff mirrors of 0x2000 - 0x2007 28 | 29 | APU_REGISTER_START = 0x4000, 30 | APU_REGISTER_END = 0x4013, 31 | 32 | OAM_DMA = 0x4014, 33 | 34 | APU_CONTROL_AND_STATUS = 0x4015, 35 | 36 | JOY1 = 0x4016, 37 | JOY2_AND_FRAME_CONTROL = 0x4017, 38 | }; 39 | 40 | MainBus(PPU& ppu, APU& apu, Controller& ctrl1, Controller& ctrl2, std::function dma); 41 | Byte read(Address addr); 42 | void write(Address addr, Byte value); 43 | bool setMapper(Mapper* mapper); 44 | const Byte* getPagePtr(Byte page); 45 | 46 | private: 47 | std::vector m_RAM; 48 | std::vector m_extRAM; 49 | std::function m_dmaCallback; 50 | Mapper* m_mapper; 51 | PPU& m_ppu; 52 | APU& m_apu; 53 | Controller& m_controller1; 54 | Controller& m_controller2; 55 | }; 56 | }; 57 | 58 | #endif // MEMORY_H 59 | -------------------------------------------------------------------------------- /include/Mapper.h: -------------------------------------------------------------------------------- 1 | #ifndef MAPPER_H 2 | #define MAPPER_H 3 | #include "Cartridge.h" 4 | #include "IRQ.h" 5 | #include 6 | #include 7 | 8 | namespace sn 9 | { 10 | enum NameTableMirroring 11 | { 12 | Horizontal = 0, 13 | Vertical = 1, 14 | FourScreen = 8, 15 | OneScreenLower, 16 | OneScreenHigher, 17 | }; 18 | 19 | class Mapper 20 | { 21 | public: 22 | enum Type 23 | { 24 | NROM = 0, 25 | SxROM = 1, 26 | UxROM = 2, 27 | CNROM = 3, 28 | MMC3 = 4, 29 | AxROM = 7, 30 | ColorDreams = 11, 31 | GxROM = 66, 32 | }; 33 | 34 | Mapper(Cartridge& cart, Type t) 35 | : m_cartridge(cart) 36 | , m_type(t) {}; 37 | virtual ~Mapper() = default; 38 | virtual void writePRG(Address addr, Byte value) = 0; 39 | virtual Byte readPRG(Address addr) = 0; 40 | 41 | virtual Byte readCHR(Address addr) = 0; 42 | virtual void writeCHR(Address addr, Byte value) = 0; 43 | 44 | virtual NameTableMirroring getNameTableMirroring(); 45 | 46 | bool inline hasExtendedRAM() { return m_cartridge.hasExtendedRAM(); } 47 | 48 | virtual void scanlineIRQ() {} 49 | 50 | static std::unique_ptr createMapper(Type mapper_t, 51 | Cartridge& cart, 52 | IRQHandle& irq, 53 | std::function mirroring_cb); 54 | 55 | protected: 56 | Cartridge& m_cartridge; 57 | Type m_type; 58 | }; 59 | } 60 | 61 | #endif // MAPPER_H 62 | -------------------------------------------------------------------------------- /include/MapperAxROM.h: -------------------------------------------------------------------------------- 1 | #include "Mapper.h" 2 | #include "PictureBus.h" 3 | 4 | namespace sn 5 | { 6 | class MapperAxROM : public Mapper 7 | { 8 | public: 9 | MapperAxROM(Cartridge& cart, std::function mirroring_cb); 10 | 11 | void writePRG(Address address, Byte value); 12 | Byte readPRG(Address address); 13 | 14 | Byte readCHR(Address address); 15 | void writeCHR(Address address, Byte value); 16 | 17 | NameTableMirroring getNameTableMirroring(); 18 | 19 | private: 20 | NameTableMirroring m_mirroring; 21 | 22 | std::function m_mirroringCallback; 23 | uint32_t m_prgBank; 24 | std::vector m_characterRAM; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /include/MapperCNROM.h: -------------------------------------------------------------------------------- 1 | #ifndef MAPPERCNROM_H 2 | #define MAPPERCNROM_H 3 | #include "Mapper.h" 4 | 5 | namespace sn 6 | { 7 | class MapperCNROM : public Mapper 8 | { 9 | public: 10 | MapperCNROM(Cartridge& cart); 11 | void writePRG(Address addr, Byte value); 12 | Byte readPRG(Address addr); 13 | 14 | Byte readCHR(Address addr); 15 | void writeCHR(Address addr, Byte value); 16 | 17 | private: 18 | bool m_oneBank; 19 | 20 | Address m_selectCHR; 21 | }; 22 | } 23 | #endif // MAPPERCNROM_H 24 | -------------------------------------------------------------------------------- /include/MapperColorDreams.h: -------------------------------------------------------------------------------- 1 | #ifndef MAPPERCOLORDREAMS_H_INCLUDED 2 | #define MAPPERCOLORDREAMS_H_INCLUDED 3 | 4 | #include "Mapper.h" 5 | 6 | namespace sn 7 | { 8 | class MapperColorDreams : public Mapper 9 | { 10 | public: 11 | MapperColorDreams(Cartridge& cart, std::function mirroring_cb); 12 | NameTableMirroring getNameTableMirroring(); 13 | void writePRG(Address address, Byte value); 14 | Byte readPRG(Address address); 15 | 16 | Byte readCHR(Address address); 17 | void writeCHR(Address address, Byte value); 18 | 19 | private: 20 | NameTableMirroring m_mirroring; 21 | uint32_t prgbank; 22 | uint32_t chrbank; 23 | std::function m_mirroringCallback; 24 | }; 25 | } 26 | 27 | #endif // MAPPERCOLORDREAMS_H_INCLUDED 28 | -------------------------------------------------------------------------------- /include/MapperGxROM.h: -------------------------------------------------------------------------------- 1 | #include "Mapper.h" 2 | 3 | namespace sn 4 | { 5 | class MapperGxROM : public Mapper 6 | { 7 | public: 8 | MapperGxROM(Cartridge& cart, std::function mirroring_cb); 9 | NameTableMirroring getNameTableMirroring(); 10 | void writePRG(Address address, Byte value); 11 | Byte readPRG(Address address); 12 | 13 | Byte readCHR(Address address); 14 | void writeCHR(Address address, Byte value); 15 | Byte prgbank; 16 | Byte chrbank; 17 | 18 | private: 19 | NameTableMirroring m_mirroring; 20 | 21 | std::vector m_characterRAM; 22 | std::function m_mirroringCallback; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /include/MapperMMC3.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "IRQ.h" 3 | #include "Mapper.h" 4 | #include 5 | 6 | namespace sn 7 | { 8 | 9 | class MapperMMC3 : public Mapper 10 | { 11 | public: 12 | MapperMMC3(Cartridge& cart, IRQHandle& irq, std::function mirroring_cb); 13 | 14 | Byte readPRG(Address addr); 15 | void writePRG(Address addr, Byte value); 16 | 17 | NameTableMirroring getNameTableMirroring(); 18 | Byte readCHR(Address addr); 19 | void writeCHR(Address addr, Byte value); 20 | 21 | void scanlineIRQ(); 22 | 23 | private: 24 | // Control variables 25 | uint32_t m_targetRegister; 26 | bool m_prgBankMode; 27 | bool m_chrInversion; 28 | 29 | uint32_t m_bankRegister[8]; 30 | 31 | bool m_irqEnabled; 32 | Byte m_irqCounter; 33 | Byte m_irqLatch; 34 | bool m_irqReloadPending; 35 | 36 | std::vector m_prgRam; 37 | std::vector m_mirroringRam; 38 | const Byte* m_prgBank0; 39 | const Byte* m_prgBank1; 40 | const Byte* m_prgBank2; 41 | const Byte* m_prgBank3; 42 | 43 | std::array m_chrBanks; 44 | 45 | NameTableMirroring m_mirroring; 46 | std::function m_mirroringCallback; 47 | IRQHandle& m_irq; 48 | }; 49 | 50 | } // namespace sn 51 | -------------------------------------------------------------------------------- /include/MapperNROM.h: -------------------------------------------------------------------------------- 1 | #ifndef MAPPERNROM_H 2 | #define MAPPERNROM_H 3 | #include "Mapper.h" 4 | 5 | namespace sn 6 | { 7 | class MapperNROM : public Mapper 8 | { 9 | public: 10 | MapperNROM(Cartridge& cart); 11 | void writePRG(Address addr, Byte value); 12 | Byte readPRG(Address addr); 13 | 14 | Byte readCHR(Address addr); 15 | void writeCHR(Address addr, Byte value); 16 | 17 | private: 18 | bool m_oneBank; 19 | bool m_usesCharacterRAM; 20 | 21 | std::vector m_characterRAM; 22 | }; 23 | } 24 | #endif // MAPPERNROM_H 25 | -------------------------------------------------------------------------------- /include/MapperSxROM.h: -------------------------------------------------------------------------------- 1 | #ifndef MAPPERSXROM_H 2 | #define MAPPERSXROM_H 3 | #include "Mapper.h" 4 | 5 | namespace sn 6 | { 7 | 8 | class MapperSxROM : public Mapper 9 | { 10 | public: 11 | MapperSxROM(Cartridge& cart, std::function mirroring_cb); 12 | void writePRG(Address addr, Byte value); 13 | Byte readPRG(Address addr); 14 | 15 | Byte readCHR(Address addr); 16 | void writeCHR(Address addr, Byte value); 17 | 18 | NameTableMirroring getNameTableMirroring(); 19 | 20 | private: 21 | void calculatePRGPointers(); 22 | 23 | std::function m_mirroringCallback; 24 | NameTableMirroring m_mirroing; 25 | 26 | bool m_usesCharacterRAM; 27 | int m_modeCHR; 28 | int m_modePRG; 29 | 30 | Byte m_tempRegister; 31 | int m_writeCounter; 32 | 33 | Byte m_regPRG; 34 | Byte m_regCHR0; 35 | Byte m_regCHR1; 36 | 37 | const Byte* m_firstBankPRG; 38 | const Byte* m_secondBankPRG; 39 | 40 | int m_firstBankCHRIdx; 41 | int m_secondBankCHRIdx; 42 | 43 | std::vector m_characterRAM; 44 | }; 45 | } 46 | #endif // MAPPERSXROM_H 47 | -------------------------------------------------------------------------------- /include/MapperUxROM.h: -------------------------------------------------------------------------------- 1 | #ifndef MAPPERUXROM_H 2 | #define MAPPERUXROM_H 3 | #include "Mapper.h" 4 | 5 | namespace sn 6 | { 7 | class MapperUxROM : public Mapper 8 | { 9 | public: 10 | MapperUxROM(Cartridge& cart); 11 | void writePRG(Address addr, Byte value); 12 | Byte readPRG(Address addr); 13 | 14 | Byte readCHR(Address addr); 15 | void writeCHR(Address addr, Byte value); 16 | 17 | private: 18 | bool m_usesCharacterRAM; 19 | 20 | const Byte* m_lastBankPtr; 21 | Address m_selectPRG; 22 | 23 | std::vector m_characterRAM; 24 | }; 25 | } 26 | #endif // MAPPERUXROM_H 27 | -------------------------------------------------------------------------------- /include/PPU.h: -------------------------------------------------------------------------------- 1 | #ifndef PPU_H 2 | #define PPU_H 3 | #include "PaletteColors.h" 4 | #include "PictureBus.h" 5 | #include "VirtualScreen.h" 6 | #include 7 | 8 | namespace sn 9 | { 10 | const int ScanlineCycleLength = 341; 11 | const int ScanlineEndCycle = 340; 12 | const int VisibleScanlines = 240; 13 | const int ScanlineVisibleDots = 256; 14 | const int FrameEndScanline = 261; 15 | 16 | const int AttributeOffset = 0x3C0; 17 | 18 | class PPU 19 | { 20 | public: 21 | PPU(PictureBus& bus, VirtualScreen& screen); 22 | void step(); 23 | void reset(); 24 | 25 | void setInterruptCallback(std::function cb); 26 | 27 | void doDMA(const Byte* page_ptr); 28 | 29 | // Callbacks mapped to CPU address space 30 | // Addresses written to by the program 31 | void control(Byte ctrl); 32 | void setMask(Byte mask); 33 | void setOAMAddress(Byte addr); 34 | void setDataAddress(Byte addr); 35 | void setScroll(Byte scroll); 36 | void setData(Byte data); 37 | // Read by the program 38 | Byte getStatus(); 39 | Byte getData(); 40 | Byte getOAMData(); 41 | void setOAMData(Byte value); 42 | 43 | private: 44 | Byte readOAM(Byte addr); 45 | void writeOAM(Byte addr, Byte value); 46 | Byte read(Address addr); 47 | PictureBus& m_bus; 48 | VirtualScreen& m_screen; 49 | 50 | std::function m_vblankCallback; 51 | 52 | std::vector m_spriteMemory; 53 | 54 | std::vector m_scanlineSprites; 55 | 56 | enum State 57 | { 58 | PreRender, 59 | Render, 60 | PostRender, 61 | VerticalBlank 62 | } m_pipelineState; 63 | int m_cycle; 64 | int m_scanline; 65 | bool m_evenFrame; 66 | 67 | bool m_vblank; 68 | bool m_sprZeroHit; 69 | bool m_spriteOverflow; 70 | 71 | // Registers 72 | Address m_dataAddress; 73 | Address m_tempAddress; 74 | Byte m_fineXScroll; 75 | bool m_firstWrite; 76 | Byte m_dataBuffer; 77 | 78 | Byte m_spriteDataAddress; 79 | 80 | // Setup flags and variables 81 | bool m_longSprites; 82 | bool m_generateInterrupt; 83 | 84 | bool m_greyscaleMode; 85 | bool m_showSprites; 86 | bool m_showBackground; 87 | bool m_hideEdgeSprites; 88 | bool m_hideEdgeBackground; 89 | 90 | enum CharacterPage 91 | { 92 | Low, 93 | High, 94 | } m_bgPage, 95 | m_sprPage; 96 | 97 | Address m_dataAddrIncrement; 98 | 99 | std::vector> m_pictureBuffer; 100 | }; 101 | } 102 | 103 | #endif // PPU_H 104 | -------------------------------------------------------------------------------- /include/PaletteColors.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Colors in RGBA (8 bit colors) 4 | const sf::Uint32 colors[] = { 5 | 0x666666ff, 0x002a88ff, 0x1412a7ff, 0x3b00a4ff, 0x5c007eff, 0x6e0040ff, 0x6c0600ff, 0x561d00ff, 6 | 0x333500ff, 0x0b4800ff, 0x005200ff, 0x004f08ff, 0x00404dff, 0x000000ff, 0x000000ff, 0x000000ff, 7 | 0xadadadff, 0x155fd9ff, 0x4240ffff, 0x7527feff, 0xa01accff, 0xb71e7bff, 0xb53120ff, 0x994e00ff, 8 | 0x6b6d00ff, 0x388700ff, 0x0c9300ff, 0x008f32ff, 0x007c8dff, 0x000000ff, 0x000000ff, 0x000000ff, 9 | 0xfffeffff, 0x64b0ffff, 0x9290ffff, 0xc676ffff, 0xf36affff, 0xfe6eccff, 0xfe8170ff, 0xea9e22ff, 10 | 0xbcbe00ff, 0x88d800ff, 0x5ce430ff, 0x45e082ff, 0x48cddeff, 0x4f4f4fff, 0x000000ff, 0x000000ff, 11 | 0xfffeffff, 0xc0dfffff, 0xd3d2ffff, 0xe8c8ffff, 0xfbc2ffff, 0xfec4eaff, 0xfeccc5ff, 0xf7d8a5ff, 12 | 0xe4e594ff, 0xcfef96ff, 0xbdf4abff, 0xb3f3ccff, 0xb5ebf2ff, 0xb8b8b8ff, 0x000000ff, 0x000000ff, 13 | }; 14 | -------------------------------------------------------------------------------- /include/PictureBus.h: -------------------------------------------------------------------------------- 1 | #ifndef PICTUREBUS_H 2 | #define PICTUREBUS_H 3 | #include "Cartridge.h" 4 | #include "Mapper.h" 5 | #include 6 | 7 | namespace sn 8 | { 9 | class PictureBus 10 | { 11 | public: 12 | PictureBus(); 13 | Byte read(Address addr); 14 | void write(Address addr, Byte value); 15 | 16 | bool setMapper(Mapper* mapper); 17 | Byte readPalette(Byte paletteAddr); 18 | void updateMirroring(); 19 | void scanlineIRQ(); 20 | 21 | private: 22 | std::size_t NameTable0, NameTable1, NameTable2, NameTable3; // indices where they start in RAM vector 23 | 24 | std::vector m_palette; 25 | 26 | std::vector m_RAM; 27 | Mapper* m_mapper; 28 | }; 29 | } 30 | #endif // PICTUREBUS_H 31 | -------------------------------------------------------------------------------- /include/VirtualScreen.h: -------------------------------------------------------------------------------- 1 | #ifndef VIRTUALSCREEN_H 2 | #define VIRTUALSCREEN_H 3 | #include 4 | 5 | namespace sn 6 | { 7 | class VirtualScreen : public sf::Drawable 8 | { 9 | public: 10 | void create(unsigned int width, unsigned int height, float pixel_size, sf::Color color); 11 | void setPixel(std::size_t x, std::size_t y, sf::Color color); 12 | 13 | private: 14 | void draw(sf::RenderTarget& target, sf::RenderStates states) const; 15 | 16 | sf::Vector2u m_screenSize; 17 | float m_pixelSize; // virtual pixel size in real pixels 18 | sf::VertexArray m_vertices; 19 | }; 20 | }; 21 | #endif // VIRTUALSCREEN_H 22 | -------------------------------------------------------------------------------- /keybindings.conf: -------------------------------------------------------------------------------- 1 | [Player1] 2 | A = J 3 | B = K 4 | Select = RShift 5 | Start = Return 6 | Up = W 7 | Down = S 8 | Left = A 9 | Right = D 10 | 11 | [Player2] 12 | A = Numpad5 13 | B = Numpad6 14 | Select = Numpad8 15 | Start = Numpad9 16 | Up = Up 17 | Down = Down 18 | Left = Left 19 | Right = Right 20 | 21 | 22 | # Available Keys: 23 | # A, 24 | # B, 25 | # C, 26 | # D, 27 | # E, 28 | # F, 29 | # G, 30 | # H, 31 | # I, 32 | # J, 33 | # K, 34 | # L, 35 | # M, 36 | # N, 37 | # O, 38 | # P, 39 | # Q, 40 | # R, 41 | # S, 42 | # T, 43 | # U, 44 | # V, 45 | # W, 46 | # X, 47 | # Y, 48 | # Z, 49 | # Num0, 50 | # Num1, 51 | # Num2, 52 | # Num3, 53 | # Num4, 54 | # Num5, 55 | # Num6, 56 | # Num7, 57 | # Num8, 58 | # Num9, 59 | # Escape, 60 | # LControl, 61 | # LShift, 62 | # LAlt, 63 | # LSystem, 64 | # RControl, 65 | # RShift, 66 | # RAlt, 67 | # RSystem, 68 | # Menu, 69 | # LBracket, 70 | # RBracket, 71 | # SemiColon, 72 | # Comma, 73 | # Period, 74 | # Quote, 75 | # Slash, 76 | # BackSlash, 77 | # Tilde, 78 | # Equal, 79 | # Dash, 80 | # Space, 81 | # Return, 82 | # BackSpace, 83 | # Tab, 84 | # PageUp, 85 | # PageDown, 86 | # End, 87 | # Home, 88 | # Insert, 89 | # Delete, 90 | # Add, 91 | # Subtract, 92 | # Multiply, 93 | # Divide, 94 | # Left, 95 | # Right, 96 | # Up, 97 | # Down, 98 | # Numpad0, 99 | # Numpad1, 100 | # Numpad2, 101 | # Numpad3, 102 | # Numpad4, 103 | # Numpad5, 104 | # Numpad6, 105 | # Numpad7, 106 | # Numpad8, 107 | # Numpad9, 108 | # F1, 109 | # F2, 110 | # F3, 111 | # F4, 112 | # F5, 113 | # F6, 114 | # F7, 115 | # F8, 116 | # F9, 117 | # F10, 118 | # F11, 119 | # F12, 120 | # F13, 121 | # F14, 122 | # F15, 123 | # Pause 124 | 125 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "Emulator.h" 2 | #include "Log.h" 3 | #include 4 | #include 5 | 6 | namespace sn 7 | { 8 | void parseControllerConf(std::string filepath, std::vector& p1, std::vector& p2); 9 | } 10 | 11 | int main(int argc, char** argv) 12 | { 13 | std::ofstream logFile("simplenes.log"), cpuTraceFile; 14 | sn::TeeStream logTee(logFile, std::cout); 15 | 16 | if (logFile.is_open() && logFile.good()) 17 | sn::Log::get().setLogStream(logTee); 18 | else 19 | sn::Log::get().setLogStream(std::cout); 20 | 21 | sn::Log::get().setLevel(sn::Info); 22 | 23 | std::string path; 24 | std::string keybindingsPath = "keybindings.conf"; 25 | 26 | // Default keybindings 27 | std::vector p1 { sf::Keyboard::J, sf::Keyboard::K, sf::Keyboard::RShift, sf::Keyboard::Return, 28 | sf::Keyboard::W, sf::Keyboard::S, sf::Keyboard::A, sf::Keyboard::D }, 29 | p2 { sf::Keyboard::Numpad5, sf::Keyboard::Numpad6, sf::Keyboard::Numpad8, sf::Keyboard::Numpad9, 30 | sf::Keyboard::Up, sf::Keyboard::Down, sf::Keyboard::Left, sf::Keyboard::Right }; 31 | sn::Emulator emulator; 32 | 33 | for (int i = 1; i < argc; ++i) 34 | { 35 | std::string arg(argv[i]); 36 | if (arg == "-h" || arg == "--help") 37 | { 38 | std::cout << "SimpleNES is a simple NES emulator.\n" 39 | << "It can run .nes images.\n" 40 | << "Set keybindings with keybindings.conf\n\n" 41 | << "Usage: SimpleNES [options] rom-path\n\n" 42 | << "Options:\n" 43 | << "-h, --help Print this help text and exit\n" 44 | << "--mute-audio Mute audio\n" 45 | << "-s, --scale Set video scale. Default: 3.\n" 46 | << " Scale of 1 corresponds to " << sn::NESVideoWidth << "x" 47 | << sn::NESVideoHeight << std::endl 48 | << "-w, --width Set the width of the emulation screen (height is\n" 49 | << " set automatically to fit the aspect ratio)\n" 50 | << "-H, --height Set the height of the emulation screen (width is\n" 51 | << " set automatically to fit the aspect ratio)\n" 52 | << " This option is mutually exclusive to --width\n" 53 | << "-C, --conf Set the keybindings file's path. The default \n" 54 | << " keybindings file is keybindings.conf.\n" 55 | << std::endl; 56 | return 0; 57 | } 58 | else if (arg == "--log-cpu") 59 | { 60 | sn::Log::get().setLevel(sn::CpuTrace); 61 | cpuTraceFile.open("sn.cpudump"); 62 | sn::Log::get().setCpuTraceStream(cpuTraceFile); 63 | LOG(sn::Info) << "CPU logging set." << std::endl; 64 | } 65 | else if (arg == "--mute-audio") 66 | { 67 | emulator.muteAudio(); 68 | LOG(sn::Info) << "Audio muted." << std::endl; 69 | } 70 | else if (arg == "-s" || arg == "--scale") 71 | { 72 | float scale; 73 | std::stringstream ss; 74 | if (i + 1 < argc && ss << argv[i + 1] && ss >> scale) 75 | emulator.setVideoScale(scale); 76 | else 77 | LOG(sn::Error) << "Setting scale from argument failed" << std::endl; 78 | ++i; 79 | } 80 | else if (std::strcmp(argv[i], "-w") == 0 || std::strcmp(argv[i], "--width") == 0) 81 | { 82 | int width; 83 | std::stringstream ss; 84 | if (i + 1 < argc && ss << argv[i + 1] && ss >> width) 85 | emulator.setVideoWidth(width); 86 | else 87 | LOG(sn::Error) << "Setting width from argument failed" << std::endl; 88 | ++i; 89 | } 90 | else if (std::strcmp(argv[i], "-H") == 0 || std::strcmp(argv[i], "--height") == 0) 91 | { 92 | int height; 93 | std::stringstream ss; 94 | if (i + 1 < argc && ss << argv[i + 1] && ss >> height) 95 | emulator.setVideoHeight(height); 96 | else 97 | LOG(sn::Error) << "Setting height from argument failed" << std::endl; 98 | ++i; 99 | } 100 | else if (std::strcmp(argv[i], "-C") == 0 || std::strcmp(argv[i], "--conf") == 0) 101 | { 102 | if (i + 1 < argc) 103 | keybindingsPath = argv[i + 1]; 104 | else 105 | LOG(sn::Error) << "Setting keybindings.conf's path from argument failed" << std::endl; 106 | ++i; 107 | } 108 | else if (argv[i][0] != '-') 109 | path = argv[i]; 110 | else 111 | std::cerr << "Unrecognized argument: " << argv[i] << std::endl; 112 | } 113 | 114 | if (path.empty()) 115 | { 116 | std::cout << "Argument required: ROM path" << std::endl; 117 | return 1; 118 | } 119 | 120 | sn::parseControllerConf(std::move(keybindingsPath), p1, p2); 121 | emulator.setKeys(p1, p2); 122 | emulator.run(path); 123 | return 0; 124 | } 125 | -------------------------------------------------------------------------------- /src/APU/APU.cpp: -------------------------------------------------------------------------------- 1 | #include "APU/APU.h" 2 | #include "APU/FrameCounter.h" 3 | #include "APU/Pulse.h" 4 | #include "APU/spsc.hpp" 5 | #include "Cartridge.h" 6 | #include "Log.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace std::chrono; 14 | 15 | namespace sn 16 | { 17 | enum Register 18 | { 19 | APU_SQ1_VOL = 0x4000, 20 | APU_SQ1_SWEEP = 0x4001, 21 | APU_SQ1_LO = 0x4002, 22 | APU_SQ1_HI = 0x4003, 23 | 24 | APU_SQ2_VOL = 0x4004, 25 | APU_SQ2_SWEEP = 0x4005, 26 | APU_SQ2_LO = 0x4006, 27 | APU_SQ2_HI = 0x4007, 28 | 29 | APU_TRI_LINEAR = 0x4008, 30 | // unused - 0x4009 31 | APU_TRI_LO = 0x400a, 32 | APU_TRI_HI = 0x400b, 33 | 34 | APU_NOISE_VOL = 0x400c, 35 | // unused - 0x400d 36 | APU_NOISE_LO = 0x400e, 37 | APU_NOISE_HI = 0x400f, 38 | 39 | APU_DMC_FREQ = 0x4010, 40 | APU_DMC_RAW = 0x4011, 41 | APU_DMC_START = 0x4012, 42 | APU_DMC_LEN = 0x4013, 43 | 44 | APU_CONTROL = 0x4015, 45 | 46 | APU_FRAME_CONTROL = 0x4017, 47 | }; 48 | 49 | float mix(Byte pulse1, Byte pulse2, Byte triangle, Byte noise, Byte dmc) 50 | { 51 | float pulse1out = static_cast(pulse1); // 0-15 52 | float pulse2out = static_cast(pulse2); // 0-15 53 | 54 | float pulse_out = 0; 55 | if (pulse1out + pulse2out != 0) 56 | { 57 | pulse_out = 95.88 / ((8128.0 / (pulse1out + pulse2out)) + 100.0); 58 | } 59 | 60 | float tnd_out = 0; 61 | float triangleout = static_cast(triangle); // 0-15 62 | float noiseout = static_cast(noise); // 0-15 63 | float dmcout = static_cast(dmc); // 0-127 64 | 65 | if (triangleout + noiseout + dmcout != 0) 66 | { 67 | float tnd_sum = (triangleout / 8227.0) + (noiseout / 12241.0) + (dmcout / 22638.0); 68 | tnd_out = 159.79 / (1.0 / tnd_sum + 100.0); 69 | } 70 | 71 | return pulse_out + tnd_out; 72 | } 73 | 74 | void APU::step() 75 | { 76 | noise.clock(); 77 | dmc.clock(); 78 | triangle.clock(); 79 | if (divideByTwo) 80 | { 81 | frame_counter.clock(); 82 | pulse1.clock(); 83 | pulse2.clock(); 84 | 85 | audio_queue.push(mix(pulse1.sample(), pulse2.sample(), triangle.sample(), noise.sample(), dmc.sample())); 86 | } 87 | divideByTwo = !divideByTwo; 88 | } 89 | 90 | void APU::writeRegister(Address addr, Byte value) 91 | { 92 | switch (addr) 93 | { 94 | case APU_SQ1_VOL: 95 | pulse1.volume.fixedVolumeOrPeriod = value & 0xf; 96 | pulse1.volume.constantVolume = value & (1 << 4); 97 | pulse1.volume.isLooping = pulse1.length_counter.halt = value & (1 << 5); 98 | pulse1.seq_type = static_cast(value >> 6); 99 | LOG(CpuTrace) << "APU_SQ1_VOL " << std::hex << +value << std::dec << std::boolalpha 100 | << VAR_PRINT(pulse1.volume.fixedVolumeOrPeriod) << VAR_PRINT(pulse1.volume.constantVolume) 101 | << VAR_PRINT(pulse1.length_counter.halt) << VAR_PRINT(int(pulse1.seq_type)) << std::endl; 102 | break; 103 | 104 | case APU_SQ1_SWEEP: 105 | pulse1.sweep.enabled = value & (1 << 7); 106 | pulse1.sweep.period = (value >> 4) & 0x7; 107 | pulse1.sweep.negate = value & (1 << 3); 108 | pulse1.sweep.shift = value & 0x7; 109 | pulse1.sweep.reload = true; 110 | LOG(ApuTrace) << "APU_SQ1_SWEEP " << std::hex << +value << std::dec << std::boolalpha 111 | << VAR_PRINT(pulse1.sweep.enabled) << VAR_PRINT(pulse1.sweep.period) 112 | << VAR_PRINT(pulse1.sweep.negate) << +VAR_PRINT(+pulse1.sweep.shift) << std::endl; 113 | break; 114 | 115 | case APU_SQ1_LO: 116 | { 117 | int new_period = (pulse1.period & 0xff00) | value; 118 | pulse1.set_period(new_period); 119 | LOG(ApuTrace) << "APU_SQ1_LO " << std::hex << +value << std::dec << std::endl; 120 | break; 121 | } 122 | 123 | case APU_SQ1_HI: 124 | { 125 | int new_period = (pulse1.period & 0x00ff) | ((value & 0x7) << 8); 126 | pulse1.length_counter.set_from_table(value >> 3); 127 | pulse1.seq_idx = 0; 128 | pulse1.volume.shouldStart = true; 129 | pulse1.set_period(new_period); 130 | LOG(ApuTrace) << "APU_SQ1_HI " << std::hex << +value << std::dec << VAR_PRINT(pulse1.period) 131 | << VAR_PRINT(pulse1.seq_idx) << VAR_PRINT(pulse1.length_counter.counter) << std::endl; 132 | break; 133 | } 134 | 135 | case APU_SQ2_VOL: 136 | pulse2.volume.fixedVolumeOrPeriod = value & 0xf; 137 | pulse2.volume.constantVolume = value & (1 << 4); 138 | pulse2.volume.isLooping = pulse2.length_counter.halt = value & (1 << 5); 139 | pulse2.seq_type = static_cast(value >> 6); 140 | LOG(CpuTrace) << "APU_SQ2_VOL " << std::hex << +value << std::dec << std::boolalpha 141 | << VAR_PRINT(pulse2.volume.fixedVolumeOrPeriod) << VAR_PRINT(pulse2.volume.constantVolume) 142 | << VAR_PRINT(pulse2.length_counter.halt) << VAR_PRINT(int(pulse2.seq_type)) << std::endl; 143 | break; 144 | 145 | case APU_SQ2_SWEEP: 146 | pulse2.sweep.enabled = value & (1 << 7); 147 | pulse2.sweep.period = (value >> 4) & 0x7; 148 | pulse2.sweep.negate = value & (1 << 3); 149 | pulse2.sweep.shift = value & 0x7; 150 | pulse2.sweep.reload = true; 151 | LOG(CpuTrace) << "APU_SQ2_SWEEP " << std::hex << +value << std::dec << std::boolalpha 152 | << VAR_PRINT(pulse2.sweep.enabled) << VAR_PRINT(pulse2.sweep.period) 153 | << VAR_PRINT(pulse2.sweep.negate) << +VAR_PRINT(pulse2.sweep.shift) << std::endl; 154 | break; 155 | 156 | case APU_SQ2_LO: 157 | { 158 | int new_period = (pulse2.period & 0xff00) | value; 159 | LOG(CpuTrace) << "APU_SQ2_LO " << std::hex << +value << std::dec << std::endl; 160 | pulse2.set_period(new_period); 161 | break; 162 | } 163 | 164 | case APU_SQ2_HI: 165 | { 166 | int new_period = (pulse2.period & 0x00ff) | ((value & 0x7) << 8); 167 | pulse2.length_counter.set_from_table(value >> 3); 168 | pulse2.seq_idx = 0; 169 | pulse2.set_period(new_period); 170 | pulse2.volume.shouldStart = true; 171 | LOG(CpuTrace) << "APU_SQ2_HI " << std::hex << +value << std::dec << VAR_PRINT(pulse2.period) 172 | << VAR_PRINT(pulse2.seq_idx) << VAR_PRINT(pulse2.length_counter.counter) << std::endl; 173 | break; 174 | } 175 | 176 | case APU_TRI_LINEAR: 177 | triangle.linear_counter.set_linear(value & 0x7f); 178 | triangle.linear_counter.reload = true; 179 | // same bit is used for both the length counter half and linear counter control 180 | triangle.linear_counter.control = triangle.length_counter.halt = 1 >> 7; 181 | LOG(CpuTrace) << "APU_TRI_LINEAR " << std::hex << +value << std::dec 182 | << VAR_PRINT(triangle.linear_counter.reloadValue) << std::boolalpha 183 | << VAR_PRINT(triangle.length_counter.halt) << VAR_PRINT(triangle.linear_counter.control) 184 | << VAR_PRINT(triangle.length_counter.counter) << std::endl; 185 | break; 186 | 187 | case APU_TRI_LO: 188 | { 189 | int new_period = (triangle.period & 0xff00) | value; 190 | triangle.set_period(new_period); 191 | LOG(CpuTrace) << "APU_TRI_LOW " << std::hex << +value << std::dec << std::endl; 192 | break; 193 | } 194 | 195 | case APU_TRI_HI: 196 | { 197 | int new_period = (triangle.period & 0x00ff) | ((value & 0x7) << 8); 198 | triangle.length_counter.set_from_table(value >> 3); 199 | triangle.set_period(new_period); 200 | triangle.linear_counter.reload = true; 201 | LOG(CpuTrace) << "APU_TRI_HI " << std::hex << +value << std::dec << VAR_PRINT(triangle.period) 202 | << VAR_PRINT(triangle.seq_idx) << VAR_PRINT(triangle.length_counter.counter) 203 | << VAR_PRINT(triangle.linear_counter.reloadValue) << std::endl; 204 | break; 205 | } 206 | 207 | case APU_NOISE_VOL: 208 | noise.volume.fixedVolumeOrPeriod = value & 0xf; 209 | noise.volume.constantVolume = value & (1 << 4); 210 | noise.volume.isLooping = noise.length_counter.halt = value & (1 << 5); 211 | LOG(CpuTrace) << "APU_NOISE_VOL " << std::hex << +value << std::dec << std::boolalpha 212 | << VAR_PRINT(noise.volume.fixedVolumeOrPeriod) << VAR_PRINT(noise.volume.constantVolume) 213 | << VAR_PRINT(noise.length_counter.halt) << VAR_PRINT(int(noise.shift_register)) << std::endl; 214 | break; 215 | 216 | case APU_NOISE_LO: 217 | noise.mode = static_cast(value & (1 << 7)); 218 | noise.set_period_from_table(value & 0xf); 219 | LOG(CpuTrace) << "APU_NOISE_LO " << std::hex << +value << std::dec << std::boolalpha << VAR_PRINT(noise.mode) 220 | << VAR_PRINT(noise.period) << std::endl; 221 | break; 222 | 223 | case APU_NOISE_HI: 224 | noise.length_counter.set_from_table(value >> 3); 225 | noise.volume.divider.reset(); 226 | LOG(CpuTrace) << "APU_NOISE_HI " << std::hex << +value << std::dec << std::boolalpha 227 | << VAR_PRINT(noise.length_counter.counter) << std::endl; 228 | break; 229 | 230 | case APU_DMC_FREQ: 231 | dmc.irqEnable = value >> 7; 232 | dmc.loop = value >> 6; 233 | dmc.set_rate(value & 0xf); 234 | LOG(CpuTrace) << "APU_DMC_FREQ" << std::hex << +value << std::dec << std::boolalpha << VAR_PRINT(dmc.irqEnable) 235 | << VAR_PRINT(dmc.loop) << VAR_PRINT(dmc.change_rate.get_period()) << std::endl; 236 | break; 237 | 238 | case APU_DMC_RAW: 239 | dmc.volume = value & 0x7f; 240 | LOG(CpuTrace) << "APU_DMC_RAW " << VAR_PRINT(+dmc.volume) << std::endl; 241 | break; 242 | 243 | case APU_DMC_START: 244 | dmc.sample_begin = 0xc000 | (value << 6); 245 | LOG(CpuTrace) << "APU_DMC_START " << VAR_PRINT(dmc.sample_begin) << std::endl; 246 | break; 247 | 248 | case APU_DMC_LEN: 249 | dmc.sample_length = (value << 4) | 1; 250 | LOG(CpuTrace) << "APU_DMC_LEN " << VAR_PRINT(dmc.sample_length) << std::endl; 251 | break; 252 | 253 | case APU_CONTROL: 254 | pulse1.length_counter.set_enable(value & 0x1); 255 | pulse2.length_counter.set_enable(value & 0x2); 256 | triangle.length_counter.set_enable(value & 0x4); 257 | noise.length_counter.set_enable(value & 0x8); 258 | dmc.control(value & 0x10); 259 | LOG(CpuTrace) << "APU_CONTROL" << std::boolalpha << VAR_PRINT(pulse1.length_counter.is_enabled()) 260 | << VAR_PRINT(pulse1.length_counter.counter) << VAR_PRINT(pulse2.length_counter.is_enabled()) 261 | << VAR_PRINT(pulse1.length_counter.counter) << VAR_PRINT(triangle.length_counter.is_enabled()) 262 | << VAR_PRINT(triangle.length_counter.counter) << VAR_PRINT(noise.length_counter.is_enabled()) 263 | << VAR_PRINT(dmc.change_enabled) << std::endl; 264 | break; 265 | 266 | case APU_FRAME_CONTROL: 267 | frame_counter.reset(static_cast(value >> 7), value >> 6); 268 | LOG(ApuTrace) << "APU_FRAME_CONTROL " << VAR_PRINT(+value) << VAR_PRINT(frame_counter.mode) 269 | << VAR_PRINT(frame_counter.interrupt_inhibit) << std::endl; 270 | break; 271 | } 272 | } 273 | 274 | Byte APU::readStatus() 275 | { 276 | bool last_frame_interrupt = frame_counter.frame_interrupt; 277 | frame_counter.clearFrameInterrupt(); 278 | bool dmc_interrupt = dmc.interrupt; 279 | dmc.clear_interrupt(); 280 | LOG(CpuTrace) << "APU_STATUS" << std::endl; 281 | return ((!pulse1.length_counter.muted()) << 0 | (!pulse2.length_counter.muted()) << 1 | 282 | (!triangle.length_counter.muted()) << 2 | (!noise.length_counter.muted()) << 3 | 283 | (!dmc.has_more_samples()) << 4 | last_frame_interrupt << 6 | dmc_interrupt << 7); 284 | } 285 | 286 | FrameCounter APU::setup_frame_counter(IRQHandle& irq) 287 | { 288 | return FrameCounter( 289 | { 290 | std::ref(pulse1.volume), 291 | std::ref(pulse1.sweep), 292 | std::ref(pulse1.length_counter), 293 | 294 | std::ref(pulse2.volume), 295 | std::ref(pulse2.sweep), 296 | std::ref(pulse2.length_counter), 297 | 298 | std::ref(triangle.length_counter), 299 | std::ref(triangle.linear_counter), 300 | 301 | std::ref(noise.volume), 302 | std::ref(noise.length_counter), 303 | }, 304 | irq); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/APU/DMC.cpp: -------------------------------------------------------------------------------- 1 | #include "APU/DMC.h" 2 | #include "APU/Divider.h" 3 | #include "Cartridge.h" 4 | #include 5 | #include 6 | #include 7 | 8 | namespace sn 9 | { 10 | 11 | void DMC::set_irq_enable(bool enable) 12 | { 13 | irqEnable = enable; 14 | if (!irqEnable) 15 | { 16 | interrupt = false; 17 | irq.release(); 18 | } 19 | } 20 | 21 | void DMC::control(bool enable) 22 | { 23 | change_enabled = enable; 24 | if (!enable) 25 | { 26 | remaining_bytes = 0; 27 | } 28 | else if (remaining_bytes == 0) 29 | { 30 | // restart 31 | current_address = sample_begin; 32 | remaining_bytes = sample_length; 33 | } 34 | } 35 | 36 | void DMC::set_rate(int idx) 37 | { 38 | const static int rate[] { 428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54 }; 39 | 40 | change_rate.set_period(rate[idx]); 41 | change_rate.reset(); 42 | } 43 | 44 | void DMC::clear_interrupt() 45 | { 46 | irq.release(); 47 | interrupt = false; 48 | } 49 | 50 | Byte DMC::sample() const 51 | { 52 | return volume; 53 | } 54 | 55 | void DMC::clock() 56 | { 57 | if (!change_enabled) 58 | { 59 | return; 60 | } 61 | 62 | if (!change_rate.clock()) 63 | { 64 | return; 65 | } 66 | 67 | int delta = pop_delta(); 68 | if (silenced) 69 | { 70 | return; 71 | } 72 | 73 | if (delta == 1 && volume <= 125) 74 | { 75 | volume += 2; 76 | } 77 | else if (delta == 0 && volume >= 2) 78 | { 79 | volume -= 2; 80 | } 81 | } 82 | 83 | int DMC::pop_delta() 84 | { 85 | if (remaining_bits == 0) 86 | { 87 | remaining_bits = 8; 88 | 89 | if (load_sample()) 90 | { 91 | shifter = sample_buffer; 92 | silenced = false; 93 | } 94 | else 95 | { 96 | silenced = true; 97 | } 98 | } 99 | else 100 | { 101 | --remaining_bits; 102 | } 103 | 104 | int rv = shifter & 0x1; 105 | shifter >>= 1; 106 | return rv; 107 | } 108 | 109 | bool DMC::load_sample() 110 | { 111 | if (remaining_bytes == 0) 112 | { 113 | if (!loop) 114 | { 115 | if (irqEnable) 116 | { 117 | interrupt = true; 118 | irq.pull(); 119 | } 120 | 121 | return false; 122 | } 123 | 124 | current_address = sample_begin; 125 | remaining_bytes = sample_length; 126 | } 127 | else 128 | { 129 | remaining_bytes -= 1; 130 | } 131 | 132 | sample_buffer = dma(current_address); 133 | 134 | if (current_address == 0xffff) 135 | { 136 | current_address = 0x8000; 137 | } 138 | else 139 | { 140 | current_address += 1; 141 | } 142 | return true; 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/APU/FrameCounter.cpp: -------------------------------------------------------------------------------- 1 | #include "APU/FrameCounter.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | 7 | void FrameCounter::clearFrameInterrupt() 8 | { 9 | if (frame_interrupt) 10 | { 11 | frame_interrupt = false; 12 | irq.release(); 13 | } 14 | }; 15 | 16 | void FrameCounter::reset(Mode m, bool irq_inhibit) 17 | { 18 | mode = m; 19 | interrupt_inhibit = irq_inhibit; 20 | // TODO: delay reset by 3-4 cycles? 21 | if (interrupt_inhibit) 22 | { 23 | clearFrameInterrupt(); 24 | } 25 | if (mode == Seq5Step) 26 | { 27 | for (FrameClockable& c : frame_slots) 28 | { 29 | // clock envelopes & triangle's linear counter 30 | c.quarter_frame_clock(); 31 | // clock length counter & sweep units 32 | c.half_frame_clock(); 33 | } 34 | } 35 | } 36 | 37 | // clocked at apu freq (half the cpu freq) 38 | void FrameCounter::clock() 39 | { 40 | counter += 1; 41 | 42 | switch (counter) 43 | { 44 | case Q1: 45 | for (FrameClockable& c : frame_slots) 46 | { 47 | // clock envelopes & triangle's linear counter 48 | c.quarter_frame_clock(); 49 | } 50 | LOG(CpuTrace) << "framecounter: Q1 clock" << std::endl; 51 | break; 52 | case Q2: 53 | for (FrameClockable& c : frame_slots) 54 | { 55 | // clock envelopes & triangle's linear counter 56 | c.quarter_frame_clock(); 57 | // clock length counter & sweep units 58 | c.half_frame_clock(); 59 | } 60 | LOG(CpuTrace) << "framecounter: Q2 clock" << std::endl; 61 | break; 62 | case Q3: 63 | for (FrameClockable& c : frame_slots) 64 | { 65 | // clock envelopes & triangle's linear counter 66 | c.quarter_frame_clock(); 67 | } 68 | LOG(CpuTrace) << "framecounter: Q3 clock" << std::endl; 69 | break; 70 | case Q4: 71 | // only 4-step 72 | if (mode != Seq4Step) 73 | { 74 | break; 75 | } 76 | for (FrameClockable& c : frame_slots) 77 | { 78 | // clock envelopes & triangle's linear counter 79 | c.quarter_frame_clock(); 80 | // clock length counter & sweep units 81 | c.half_frame_clock(); 82 | } 83 | LOG(CpuTrace) << "framecounter: Q4 clock" << std::endl; 84 | // set frame irq if not inhibit 85 | if (!interrupt_inhibit) 86 | { 87 | irq.pull(); 88 | frame_interrupt = true; 89 | } 90 | break; 91 | case Q5: 92 | // only 5-step 93 | if (mode != Seq5Step) 94 | { 95 | break; 96 | } 97 | for (FrameClockable& c : frame_slots) 98 | { 99 | // clock envelopes & triangle's linear counter 100 | c.quarter_frame_clock(); 101 | // clock length counter & sweep units 102 | c.half_frame_clock(); 103 | } 104 | LOG(CpuTrace) << "framecounter: Q5 clock" << std::endl; 105 | break; 106 | }; 107 | 108 | if ((mode == Seq4Step && counter == seq4step_length) || (/* mode == Seq5Step && */ counter == seq5step_length)) 109 | { 110 | counter = 0; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/APU/Noise.cpp: -------------------------------------------------------------------------------- 1 | #include "APU/Noise.h" 2 | #include "APU/Divider.h" 3 | #include "Cartridge.h" 4 | #include 5 | #include 6 | #include 7 | 8 | namespace sn 9 | { 10 | 11 | void Noise::set_period_from_table(int idx) 12 | { 13 | const static int periods[] { 14 | 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068, 15 | }; 16 | 17 | divider.set_period(periods[idx]); 18 | } 19 | 20 | void Noise::clock() 21 | { 22 | if (!divider.clock()) 23 | { 24 | return; 25 | } 26 | 27 | bool feedback_input1 = (shift_register & 0x2) ? mode == Bit1 : (shift_register & 0x40); 28 | bool feedback_input2 = (shift_register & 0x1); 29 | 30 | bool feedback = feedback_input1 != feedback_input2; 31 | 32 | shift_register = shift_register >> 1 | (feedback << 14); 33 | } 34 | 35 | Byte Noise::sample() const 36 | { 37 | if (length_counter.muted()) 38 | { 39 | return 0; 40 | } 41 | 42 | if (shift_register & 0x1) 43 | { 44 | return 0; 45 | } 46 | 47 | return volume.get(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/APU/Pulse.cpp: -------------------------------------------------------------------------------- 1 | #include "APU/Constants.h" 2 | #include "Cartridge.h" 3 | #include "Log.h" 4 | 5 | #include "APU/Divider.h" 6 | #include "APU/Pulse.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace sn 14 | { 15 | 16 | /***** Pulse *****/ 17 | 18 | inline int calc_note_freq(int period, int seq_length, nanoseconds clock_period) 19 | { 20 | return 1e9 / ((float)clock_period.count() * period * seq_length); 21 | } 22 | 23 | std::string freq_to_note(double freq) 24 | { 25 | std::vector notes = { "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#" }; 26 | 27 | double note_number_double = 12 * std::log2(freq / 440.0) + 49; 28 | int note_number = static_cast(std::round(note_number_double)); 29 | 30 | int note_index = (note_number - 1) % notes.size(); 31 | if (note_index < 0) 32 | { // Handle negative modulo result in C++ 33 | note_index += notes.size(); 34 | } 35 | std::string note = notes[note_index]; 36 | 37 | int octave = (note_number + 8) / notes.size(); 38 | 39 | return note + std::to_string(octave); 40 | } 41 | 42 | void Pulse::set_period(int p) 43 | { 44 | period = p; 45 | sequencer.set_period(period); 46 | 47 | if (sn::Log::get().getLevel() <= ApuTrace) 48 | { 49 | auto note_freq = calc_note_freq(period, 8, apu_clock_period_ns); 50 | auto note = freq_to_note(note_freq); 51 | LOG(ApuTrace) << "PULSE" << (int)type << " RELEAD: " VAR_PRINT(period) << std::hex << VAR_PRINT(note) 52 | << std::dec << std::endl; 53 | } 54 | } 55 | 56 | // Clocked at half the cpu freq 57 | void Pulse::clock() 58 | { 59 | if (sequencer.clock()) 60 | { 61 | // NES counts downwards in sequencer 62 | seq_idx = (8 + (seq_idx - 1)) % 8; 63 | } 64 | } 65 | 66 | Byte Pulse::sample() const 67 | { 68 | if (length_counter.muted()) 69 | { 70 | return 0; 71 | } 72 | 73 | // TODO: cache the target to avoid recalculation? 74 | if (sweep.is_muted(period, sweep.calculate_target(period))) 75 | { 76 | return 0; 77 | } 78 | 79 | if (!PulseDuty::active(seq_type, seq_idx)) 80 | { 81 | return 0; 82 | } 83 | 84 | return volume.get(); 85 | } 86 | 87 | /***** Sweep *****/ 88 | 89 | void Sweep::half_frame_clock() 90 | { 91 | if (reload) 92 | { 93 | divider.set_period(period); 94 | reload = false; 95 | return; 96 | } 97 | 98 | if (!enabled) 99 | { 100 | return; 101 | } 102 | 103 | if (!divider.clock()) 104 | { 105 | return; 106 | } 107 | 108 | if (shift > 0) 109 | { 110 | int current = pulse.period; 111 | int target = calculate_target(current); 112 | if (!is_muted(pulse.period, target)) 113 | { 114 | LOG(ApuTrace) << "Sweep" << (int)pulse.type << " update " << VAR_PRINT(+current) << VAR_PRINT(+target) 115 | << VAR_PRINT(+shift) << VAR_PRINT((current >> shift)) << std::endl; 116 | pulse.set_period(target); 117 | } 118 | else 119 | { 120 | LOG(ApuTrace) << "Sweep" << (int)pulse.type << " skip " << VAR_PRINT(+current) << VAR_PRINT(+target) 121 | << std::endl; 122 | } 123 | } 124 | } 125 | 126 | int Sweep::calculate_target(int current) const 127 | { 128 | const auto amt = current >> shift; 129 | if (!negate) 130 | { 131 | return current + amt; 132 | } 133 | 134 | if (ones_complement) 135 | { 136 | return std::max(0, current - amt - 1); 137 | } 138 | 139 | return std::max(0, current - amt); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/APU/Triangle.cpp: -------------------------------------------------------------------------------- 1 | #include "APU/Triangle.h" 2 | #include "APU/Divider.h" 3 | #include "APU/Units.h" 4 | #include "Cartridge.h" 5 | #include "Log.h" 6 | #include 7 | #include 8 | #include 9 | 10 | namespace sn 11 | { 12 | 13 | inline int calc_note_freq(int period, int seq_length, nanoseconds clock_period) 14 | { 15 | return 1e9 / ((float)clock_period.count() * period * seq_length); 16 | } 17 | 18 | void Triangle::set_period(int p) 19 | { 20 | period = p; 21 | sequencer.set_period(p); 22 | } 23 | 24 | // Clocked at the cpu freq 25 | void Triangle::clock() 26 | { 27 | if (length_counter.muted()) 28 | { 29 | return; 30 | } 31 | 32 | if (linear_counter.counter == 0) 33 | { 34 | return; 35 | } 36 | 37 | if (sequencer.clock()) 38 | { 39 | seq_idx = (seq_idx + 1) % 32; 40 | } 41 | } 42 | 43 | Byte Triangle::sample() const 44 | { 45 | return volume(); 46 | } 47 | 48 | int Triangle::volume() const 49 | { 50 | // const static int sequence[] { 51 | // // clang-format off 52 | // 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 53 | // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 54 | // // clang-format on 55 | // }; 56 | // return sequence[seq_idx]; 57 | 58 | if (seq_idx < 16) 59 | { 60 | return 15 - seq_idx; 61 | } 62 | else 63 | { 64 | return seq_idx - 16; 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/APU/Units.cpp: -------------------------------------------------------------------------------- 1 | #include "APU/Units.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | 7 | /***** LengthCounter *****/ 8 | 9 | void LengthCounter::set_enable(bool new_value) 10 | { 11 | enabled = new_value; 12 | 13 | if (!enabled) 14 | { 15 | counter = 0; 16 | } 17 | } 18 | 19 | void LengthCounter::set_from_table(std::size_t index) 20 | { 21 | const static int length_table[] = { 22 | 10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 23 | 12, 16, 24, 18, 48, 20, 96, 22, 192, 24, 72, 26, 16, 28, 32, 30, 24 | }; 25 | 26 | if (!enabled) 27 | { 28 | return; 29 | } 30 | 31 | counter = length_table[index]; 32 | } 33 | 34 | void LengthCounter::half_frame_clock() 35 | { 36 | if (halt) 37 | { 38 | return; 39 | } 40 | 41 | if (counter == 0) 42 | { 43 | return; 44 | } 45 | 46 | --counter; 47 | } 48 | 49 | bool LengthCounter::muted() const 50 | { 51 | return !enabled || counter == 0; 52 | } 53 | 54 | /***** LinearCounter ****/ 55 | void LinearCounter::set_linear(int new_value) 56 | { 57 | reloadValue = new_value; 58 | } 59 | 60 | void LinearCounter::quarter_frame_clock() 61 | { 62 | if (reload) 63 | { 64 | counter = reloadValue; 65 | if (!control) 66 | { 67 | reload = false; 68 | } 69 | } 70 | 71 | if (counter == 0) 72 | { 73 | return; 74 | } 75 | 76 | --counter; 77 | } 78 | 79 | /***** Volume *****/ 80 | 81 | void Volume::quarter_frame_clock() 82 | { 83 | if (shouldStart) 84 | { 85 | shouldStart = false; 86 | decayVolume = max_volume; 87 | divider.set_period(fixedVolumeOrPeriod); 88 | return; 89 | } 90 | 91 | if (!divider.clock()) 92 | { 93 | return; 94 | } 95 | 96 | if (decayVolume > 0) 97 | { 98 | --decayVolume; 99 | } 100 | else if (isLooping) 101 | { 102 | decayVolume = max_volume; 103 | } 104 | } 105 | 106 | int Volume::get() const 107 | { 108 | if (constantVolume) 109 | { 110 | return fixedVolumeOrPeriod; 111 | } 112 | 113 | return decayVolume; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/AudioPlayer.cpp: -------------------------------------------------------------------------------- 1 | #include "AudioPlayer.h" 2 | #include "Log.h" 3 | #include "miniaudio.h" 4 | 5 | namespace sn 6 | { 7 | void data_callback(ma_device* device, 8 | void* output, 9 | [[maybe_unused]] const void* input, 10 | ma_uint32 required_output_frame_count) 11 | { 12 | if (device->pUserData == nullptr) 13 | { 14 | return; 15 | } 16 | 17 | CallbackData& cb_data = *(CallbackData*)device->pUserData; 18 | 19 | if (cb_data.mute) 20 | { 21 | return; 22 | } 23 | 24 | if (cb_data.remaining_buffer_rounds-- > 0) 25 | { 26 | LOG(sn::Info) << "skipping buffer round" << std::endl; 27 | return; 28 | } 29 | 30 | ma_uint64 input_frames = 0; 31 | ma_result result = 32 | ma_resampler_get_required_input_frame_count(cb_data.resampler, required_output_frame_count, &input_frames); 33 | if (result != MA_SUCCESS) 34 | { 35 | input_frames = required_output_frame_count * cb_data.resampler->sampleRateIn / cb_data.resampler->sampleRateOut; 36 | LOG(sn::Error) << "ma_resampler_get_required_input_frame_count failed: " << result << std::endl; 37 | } 38 | 39 | cb_data.input_frames_buffer.resize(input_frames); 40 | ma_uint64 input_frames_avail = cb_data.ring_buffer.pop(cb_data.input_frames_buffer.data(), input_frames); 41 | 42 | ma_uint64 output_frame_count64 = required_output_frame_count; 43 | ma_resampler_process_pcm_frames(cb_data.resampler, 44 | reinterpret_cast(cb_data.input_frames_buffer.data()), 45 | &input_frames_avail, 46 | output, 47 | &output_frame_count64); 48 | if (result != MA_SUCCESS) 49 | { 50 | LOG(sn::Error) << "resampling failed; errorcode=" << result << std::endl; 51 | } 52 | } 53 | 54 | bool AudioPlayer::start() 55 | { 56 | deviceConfig = ma_device_config_init(ma_device_type_playback); 57 | deviceConfig.playback.format = ma_format_f32; 58 | deviceConfig.playback.channels = 1; 59 | deviceConfig.sampleRate = output_sample_rate; 60 | deviceConfig.dataCallback = data_callback; 61 | deviceConfig.pUserData = &cb_data; 62 | deviceConfig.periodSizeInMilliseconds = callback_period_ms.count(); 63 | 64 | auto result = ma_device_init(NULL, &deviceConfig, &device); 65 | if (result != MA_SUCCESS) 66 | { 67 | LOG(Error) << "Failed to open playback device: error code = " << result << std::endl; 68 | return false; 69 | } 70 | 71 | result = ma_device_start(&device); 72 | if (result != MA_SUCCESS) 73 | { 74 | LOG(Error) << "Failed to start playback device: error code = " << result << std::endl; 75 | ma_device_uninit(&device); 76 | return false; 77 | } 78 | 79 | ma_resampler_config config = ma_resampler_config_init(ma_format_f32, 80 | deviceConfig.playback.channels, 81 | input_sample_rate, 82 | output_sample_rate, 83 | ma_resample_algorithm_linear); 84 | result = ma_resampler_init(&config, nullptr, &resampler); 85 | if (result != MA_SUCCESS) 86 | { 87 | LOG(Error) << "Failed to start playback device: error code = " << result << std::endl; 88 | ma_device_uninit(&device); 89 | return false; 90 | } 91 | 92 | initialized = true; 93 | return true; 94 | } 95 | 96 | AudioPlayer::~AudioPlayer() 97 | { 98 | if (!initialized) 99 | { 100 | return; 101 | } 102 | 103 | ma_device_uninit(&device); 104 | } 105 | 106 | void AudioPlayer::mute() 107 | { 108 | cb_data.mute = true; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/CPU.cpp: -------------------------------------------------------------------------------- 1 | #include "CPU.h" 2 | #include "CPUOpcodes.h" 3 | #include "Log.h" 4 | #include 5 | 6 | namespace sn 7 | { 8 | CPU::CPU(MainBus& mem) 9 | : m_pendingNMI(false) 10 | , m_bus(mem) 11 | { 12 | } 13 | 14 | void CPU::reset() 15 | { 16 | reset(readAddress(ResetVector)); 17 | } 18 | 19 | void CPU::reset(Address start_addr) 20 | { 21 | m_skipCycles = m_cycles = 0; 22 | r_A = r_X = r_Y = 0; 23 | f_I = true; 24 | f_C = f_D = f_N = f_V = f_Z = false; 25 | r_PC = start_addr; 26 | r_SP = 0xfd; // documented startup state 27 | } 28 | 29 | void CPU::nmiInterrupt() 30 | { 31 | m_pendingNMI = true; 32 | } 33 | 34 | IRQHandle& CPU::createIRQHandler() 35 | { 36 | int bit = 1 << m_irqHandlers.size(); 37 | m_irqHandlers.emplace_back(IRQHandler { bit, *this }); 38 | return m_irqHandlers.back(); 39 | } 40 | 41 | void IRQHandler::release() 42 | { 43 | cpu.setIRQPulldown(bit, false); 44 | } 45 | 46 | void IRQHandler::pull() 47 | { 48 | cpu.setIRQPulldown(bit, true); 49 | } 50 | 51 | void CPU::setIRQPulldown(int bit, bool state) 52 | { 53 | int mask = ~(1 << bit); 54 | m_irqPulldowns = (m_irqPulldowns & mask) | state; 55 | }; 56 | 57 | void CPU::interruptSequence(InterruptType type) 58 | { 59 | if (f_I && type != NMI && type != BRK_) 60 | return; 61 | 62 | if (type == BRK_) // Add one if BRK, a quirk of 6502 63 | ++r_PC; 64 | 65 | pushStack(r_PC >> 8); 66 | pushStack(r_PC); 67 | 68 | Byte flags = f_N << 7 | f_V << 6 | 1 << 5 | // unused bit, supposed to be always 1 69 | (type == BRK_) << 4 | // B flag set if BRK 70 | f_D << 3 | f_I << 2 | f_Z << 1 | f_C; 71 | pushStack(flags); 72 | 73 | f_I = true; 74 | 75 | switch (type) 76 | { 77 | case IRQ: 78 | case BRK_: 79 | r_PC = readAddress(IRQVector); 80 | break; 81 | case NMI: 82 | r_PC = readAddress(NMIVector); 83 | break; 84 | } 85 | 86 | // Interrupt sequence takes 7 87 | m_skipCycles += 7; 88 | } 89 | 90 | void CPU::pushStack(Byte value) 91 | { 92 | m_bus.write(0x100 | r_SP, value); 93 | --r_SP; // Hardware stacks grow downward! 94 | } 95 | 96 | Byte CPU::pullStack() 97 | { 98 | return m_bus.read(0x100 | ++r_SP); 99 | } 100 | 101 | void CPU::setZN(Byte value) 102 | { 103 | f_Z = !value; 104 | f_N = value & 0x80; 105 | } 106 | 107 | void CPU::skipPageCrossCycle(Address a, Address b) 108 | { 109 | // Page is determined by the high byte 110 | if ((a & 0xff00) != (b & 0xff00)) 111 | m_skipCycles += 1; 112 | } 113 | 114 | void CPU::skipOAMDMACycles() 115 | { 116 | m_skipCycles += 513; // 256 read + 256 write + 1 dummy read 117 | m_skipCycles += (m_cycles & 1); //+1 if on odd cycle 118 | } 119 | 120 | void CPU::skipDMCDMACycles() 121 | { 122 | // Cycles to skip depends on alignment and what not, but we keep it simple and just wait 3 on average 123 | m_skipCycles += 3; 124 | } 125 | 126 | void CPU::step() 127 | { 128 | ++m_cycles; 129 | 130 | if (m_skipCycles-- > 1) 131 | return; 132 | 133 | m_skipCycles = 0; 134 | 135 | // NMI has higher priority, check for it first 136 | if (m_pendingNMI) 137 | { 138 | interruptSequence(NMI); 139 | m_pendingNMI = false; 140 | return; 141 | } 142 | else if (isPendingIRQ()) 143 | { 144 | interruptSequence(IRQ); 145 | return; 146 | } 147 | 148 | int psw = f_N << 7 | f_V << 6 | 1 << 5 | f_D << 3 | f_I << 2 | f_Z << 1 | f_C; 149 | LOG_CPU << std::hex << std::setfill('0') << std::uppercase << std::setw(4) << +r_PC << " " << std::setw(2) 150 | << +m_bus.read(r_PC) << " " 151 | << "A:" << std::setw(2) << +r_A << " " 152 | << "X:" << std::setw(2) << +r_X << " " 153 | << "Y:" << std::setw(2) << +r_Y << " " 154 | << "P:" << std::setw(2) << psw << " " 155 | << "SP:" << std::setw(2) << +r_SP << /*std::endl;*/ " " << "CYC:" << std::setw(3) << std::setfill(' ') 156 | << std::dec << ((m_cycles - 1) * 3) % 341 << std::endl; 157 | 158 | Byte opcode = m_bus.read(r_PC++); 159 | 160 | auto CycleLength = OperationCycles[opcode]; 161 | 162 | // Using short-circuit evaluation, call the other function only if the first failed 163 | // ExecuteImplied must be called first and ExecuteBranch must be before ExecuteType0 164 | if (CycleLength && (executeImplied(opcode) || executeBranch(opcode) || executeType1(opcode) || 165 | executeType2(opcode) || executeType0(opcode))) 166 | { 167 | m_skipCycles += CycleLength; 168 | // m_cycles %= 340; //compatibility with Nintendulator log 169 | // m_skipCycles = 0; //for TESTING 170 | } 171 | else 172 | { 173 | LOG(Error) << "Unrecognized opcode: " << std::hex << +opcode << std::endl; 174 | } 175 | } 176 | 177 | bool CPU::executeImplied(Byte opcode) 178 | { 179 | switch (static_cast(opcode)) 180 | { 181 | case NOP: 182 | break; 183 | case BRK: 184 | interruptSequence(BRK_); 185 | break; 186 | case JSR: 187 | // Push address of next instruction - 1, thus r_PC + 1 instead of r_PC + 2 188 | // since r_PC and r_PC + 1 are address of subroutine 189 | pushStack(static_cast((r_PC + 1) >> 8)); 190 | pushStack(static_cast(r_PC + 1)); 191 | r_PC = readAddress(r_PC); 192 | break; 193 | case RTS: 194 | r_PC = pullStack(); 195 | r_PC |= pullStack() << 8; 196 | ++r_PC; 197 | break; 198 | case RTI: 199 | { 200 | Byte flags = pullStack(); 201 | f_N = flags & 0x80; 202 | f_V = flags & 0x40; 203 | f_D = flags & 0x8; 204 | f_I = flags & 0x4; 205 | f_Z = flags & 0x2; 206 | f_C = flags & 0x1; 207 | } 208 | r_PC = pullStack(); 209 | r_PC |= pullStack() << 8; 210 | break; 211 | case JMP: 212 | r_PC = readAddress(r_PC); 213 | break; 214 | case JMPI: 215 | { 216 | Address location = readAddress(r_PC); 217 | // 6502 has a bug such that the when the vector of anindirect address begins at the last byte of a page, 218 | // the second byte is fetched from the beginning of that page rather than the beginning of the next 219 | // Recreating here: 220 | Address Page = location & 0xff00; 221 | r_PC = m_bus.read(location) | m_bus.read(Page | ((location + 1) & 0xff)) << 8; 222 | } 223 | break; 224 | case PHP: 225 | { 226 | Byte flags = f_N << 7 | f_V << 6 | 1 << 5 | // supposed to always be 1 227 | 1 << 4 | // PHP pushes with the B flag as 1, no matter what 228 | f_D << 3 | f_I << 2 | f_Z << 1 | f_C; 229 | pushStack(flags); 230 | } 231 | break; 232 | case PLP: 233 | { 234 | Byte flags = pullStack(); 235 | f_N = flags & 0x80; 236 | f_V = flags & 0x40; 237 | f_D = flags & 0x8; 238 | f_I = flags & 0x4; 239 | f_Z = flags & 0x2; 240 | f_C = flags & 0x1; 241 | } 242 | break; 243 | case PHA: 244 | pushStack(r_A); 245 | break; 246 | case PLA: 247 | r_A = pullStack(); 248 | setZN(r_A); 249 | break; 250 | case DEY: 251 | --r_Y; 252 | setZN(r_Y); 253 | break; 254 | case DEX: 255 | --r_X; 256 | setZN(r_X); 257 | break; 258 | case TAY: 259 | r_Y = r_A; 260 | setZN(r_Y); 261 | break; 262 | case INY: 263 | ++r_Y; 264 | setZN(r_Y); 265 | break; 266 | case INX: 267 | ++r_X; 268 | setZN(r_X); 269 | break; 270 | case CLC: 271 | f_C = false; 272 | break; 273 | case SEC: 274 | f_C = true; 275 | break; 276 | case CLI: 277 | f_I = false; 278 | break; 279 | case SEI: 280 | f_I = true; 281 | break; 282 | case CLD: 283 | f_D = false; 284 | break; 285 | case SED: 286 | f_D = true; 287 | break; 288 | case TYA: 289 | r_A = r_Y; 290 | setZN(r_A); 291 | break; 292 | case CLV: 293 | f_V = false; 294 | break; 295 | case TXA: 296 | r_A = r_X; 297 | setZN(r_A); 298 | break; 299 | case TXS: 300 | r_SP = r_X; 301 | break; 302 | case TAX: 303 | r_X = r_A; 304 | setZN(r_X); 305 | break; 306 | case TSX: 307 | r_X = r_SP; 308 | setZN(r_X); 309 | break; 310 | default: 311 | return false; 312 | }; 313 | return true; 314 | } 315 | 316 | bool CPU::executeBranch(Byte opcode) 317 | { 318 | if ((opcode & BranchInstructionMask) == BranchInstructionMaskResult) 319 | { 320 | // branch is initialized to the condition required (for the flag specified later) 321 | bool branch = opcode & BranchConditionMask; 322 | 323 | // set branch to true if the given condition is met by the given flag 324 | // We use xnor here, it is true if either both operands are true or false 325 | switch (opcode >> BranchOnFlagShift) 326 | { 327 | case Negative: 328 | branch = !(branch ^ f_N); 329 | break; 330 | case Overflow: 331 | branch = !(branch ^ f_V); 332 | break; 333 | case Carry: 334 | branch = !(branch ^ f_C); 335 | break; 336 | case Zero: 337 | branch = !(branch ^ f_Z); 338 | break; 339 | default: 340 | return false; 341 | } 342 | 343 | if (branch) 344 | { 345 | int8_t offset = m_bus.read(r_PC++); 346 | // skip 1 cycle since branch is taken 347 | ++m_skipCycles; 348 | auto newPC = static_cast
(r_PC + offset); 349 | // skip 1 additional cycle if page is crossed 350 | skipPageCrossCycle(r_PC, newPC); 351 | r_PC = newPC; 352 | } 353 | else 354 | ++r_PC; 355 | return true; 356 | } 357 | return false; 358 | } 359 | 360 | bool CPU::executeType1(Byte opcode) 361 | { 362 | if ((opcode & InstructionModeMask) == 0x1) 363 | { 364 | Address location = 0; // Location of the operand, could be in RAM 365 | auto op = static_cast((opcode & OperationMask) >> OperationShift); 366 | switch (static_cast((opcode & AddrModeMask) >> AddrModeShift)) 367 | { 368 | case IndexedIndirectX: 369 | { 370 | Byte zero_addr = r_X + m_bus.read(r_PC++); 371 | // Addresses wrap in zero page mode, thus pass through a mask 372 | location = m_bus.read(zero_addr & 0xff) | m_bus.read((zero_addr + 1) & 0xff) << 8; 373 | } 374 | break; 375 | case ZeroPage: 376 | location = m_bus.read(r_PC++); 377 | break; 378 | case Immediate: 379 | location = r_PC++; 380 | break; 381 | case Absolute: 382 | location = readAddress(r_PC); 383 | r_PC += 2; 384 | break; 385 | case IndirectY: 386 | { 387 | Byte zero_addr = m_bus.read(r_PC++); 388 | location = m_bus.read(zero_addr & 0xff) | m_bus.read((zero_addr + 1) & 0xff) << 8; 389 | if (op != STA) 390 | skipPageCrossCycle(location, location + r_Y); 391 | location += r_Y; 392 | } 393 | break; 394 | case IndexedX: 395 | // Address wraps around in the zero page 396 | location = (m_bus.read(r_PC++) + r_X) & 0xff; 397 | break; 398 | case AbsoluteY: 399 | location = readAddress(r_PC); 400 | r_PC += 2; 401 | if (op != STA) 402 | skipPageCrossCycle(location, location + r_Y); 403 | location += r_Y; 404 | break; 405 | case AbsoluteX: 406 | location = readAddress(r_PC); 407 | r_PC += 2; 408 | if (op != STA) 409 | skipPageCrossCycle(location, location + r_X); 410 | location += r_X; 411 | break; 412 | default: 413 | return false; 414 | } 415 | 416 | switch (op) 417 | { 418 | case ORA: 419 | r_A |= m_bus.read(location); 420 | setZN(r_A); 421 | break; 422 | case AND: 423 | r_A &= m_bus.read(location); 424 | setZN(r_A); 425 | break; 426 | case EOR: 427 | r_A ^= m_bus.read(location); 428 | setZN(r_A); 429 | break; 430 | case ADC: 431 | { 432 | Byte operand = m_bus.read(location); 433 | std::uint16_t sum = r_A + operand + f_C; 434 | // Carry forward or UNSIGNED overflow 435 | f_C = sum & 0x100; 436 | // SIGNED overflow, would only happen if the sign of sum is 437 | // different from BOTH the operands 438 | f_V = (r_A ^ sum) & (operand ^ sum) & 0x80; 439 | r_A = static_cast(sum); 440 | setZN(r_A); 441 | } 442 | break; 443 | case STA: 444 | m_bus.write(location, r_A); 445 | break; 446 | case LDA: 447 | r_A = m_bus.read(location); 448 | setZN(r_A); 449 | break; 450 | case SBC: 451 | { 452 | // High carry means "no borrow", thus negate and subtract 453 | std::uint16_t subtrahend = m_bus.read(location), diff = r_A - subtrahend - !f_C; 454 | // if the ninth bit is 1, the resulting number is negative => borrow => low carry 455 | f_C = !(diff & 0x100); 456 | // Same as ADC, except instead of the subtrahend, 457 | // substitute with it's one complement 458 | f_V = (r_A ^ diff) & (~subtrahend ^ diff) & 0x80; 459 | r_A = diff; 460 | setZN(diff); 461 | } 462 | break; 463 | case CMP: 464 | { 465 | std::uint16_t diff = r_A - m_bus.read(location); 466 | f_C = !(diff & 0x100); 467 | setZN(diff); 468 | } 469 | break; 470 | default: 471 | return false; 472 | } 473 | return true; 474 | } 475 | return false; 476 | } 477 | 478 | bool CPU::executeType2(Byte opcode) 479 | { 480 | if ((opcode & InstructionModeMask) == 2) 481 | { 482 | Address location = 0; 483 | auto op = static_cast((opcode & OperationMask) >> OperationShift); 484 | auto addr_mode = static_cast((opcode & AddrModeMask) >> AddrModeShift); 485 | switch (addr_mode) 486 | { 487 | case Immediate_: 488 | location = r_PC++; 489 | break; 490 | case ZeroPage_: 491 | location = m_bus.read(r_PC++); 492 | break; 493 | case Accumulator: 494 | break; 495 | case Absolute_: 496 | location = readAddress(r_PC); 497 | r_PC += 2; 498 | break; 499 | case Indexed: 500 | { 501 | location = m_bus.read(r_PC++); 502 | Byte index; 503 | if (op == LDX || op == STX) 504 | index = r_Y; 505 | else 506 | index = r_X; 507 | // The mask wraps address around zero page 508 | location = (location + index) & 0xff; 509 | } 510 | break; 511 | case AbsoluteIndexed: 512 | { 513 | location = readAddress(r_PC); 514 | r_PC += 2; 515 | Byte index; 516 | if (op == LDX || op == STX) 517 | index = r_Y; 518 | else 519 | index = r_X; 520 | skipPageCrossCycle(location, location + index); 521 | location += index; 522 | } 523 | break; 524 | default: 525 | return false; 526 | } 527 | 528 | std::uint16_t operand = 0; 529 | switch (op) 530 | { 531 | case ASL: 532 | case ROL: 533 | if (addr_mode == Accumulator) 534 | { 535 | auto prev_C = f_C; 536 | f_C = r_A & 0x80; 537 | r_A <<= 1; 538 | // If Rotating, set the bit-0 to the the previous carry 539 | r_A = r_A | (prev_C && (op == ROL)); 540 | setZN(r_A); 541 | } 542 | else 543 | { 544 | auto prev_C = f_C; 545 | operand = m_bus.read(location); 546 | f_C = operand & 0x80; 547 | operand = operand << 1 | (prev_C && (op == ROL)); 548 | setZN(operand); 549 | m_bus.write(location, operand); 550 | } 551 | break; 552 | case LSR: 553 | case ROR: 554 | if (addr_mode == Accumulator) 555 | { 556 | auto prev_C = f_C; 557 | f_C = r_A & 1; 558 | r_A >>= 1; 559 | // If Rotating, set the bit-7 to the previous carry 560 | r_A = r_A | (prev_C && (op == ROR)) << 7; 561 | setZN(r_A); 562 | } 563 | else 564 | { 565 | auto prev_C = f_C; 566 | operand = m_bus.read(location); 567 | f_C = operand & 1; 568 | operand = operand >> 1 | (prev_C && (op == ROR)) << 7; 569 | setZN(operand); 570 | m_bus.write(location, operand); 571 | } 572 | break; 573 | case STX: 574 | m_bus.write(location, r_X); 575 | break; 576 | case LDX: 577 | r_X = m_bus.read(location); 578 | setZN(r_X); 579 | break; 580 | case DEC: 581 | { 582 | auto tmp = m_bus.read(location) - 1; 583 | setZN(tmp); 584 | m_bus.write(location, tmp); 585 | } 586 | break; 587 | case INC: 588 | { 589 | auto tmp = m_bus.read(location) + 1; 590 | setZN(tmp); 591 | m_bus.write(location, tmp); 592 | } 593 | break; 594 | default: 595 | return false; 596 | } 597 | return true; 598 | } 599 | return false; 600 | } 601 | 602 | bool CPU::executeType0(Byte opcode) 603 | { 604 | if ((opcode & InstructionModeMask) == 0x0) 605 | { 606 | Address location = 0; 607 | switch (static_cast((opcode & AddrModeMask) >> AddrModeShift)) 608 | { 609 | case Immediate_: 610 | location = r_PC++; 611 | break; 612 | case ZeroPage_: 613 | location = m_bus.read(r_PC++); 614 | break; 615 | case Absolute_: 616 | location = readAddress(r_PC); 617 | r_PC += 2; 618 | break; 619 | case Indexed: 620 | // Address wraps around in the zero page 621 | location = (m_bus.read(r_PC++) + r_X) & 0xff; 622 | break; 623 | case AbsoluteIndexed: 624 | location = readAddress(r_PC); 625 | r_PC += 2; 626 | skipPageCrossCycle(location, location + r_X); 627 | location += r_X; 628 | break; 629 | default: 630 | return false; 631 | } 632 | std::uint16_t operand = 0; 633 | switch (static_cast((opcode & OperationMask) >> OperationShift)) 634 | { 635 | case BIT: 636 | operand = m_bus.read(location); 637 | f_Z = !(r_A & operand); 638 | f_V = operand & 0x40; 639 | f_N = operand & 0x80; 640 | break; 641 | case STY: 642 | m_bus.write(location, r_Y); 643 | break; 644 | case LDY: 645 | r_Y = m_bus.read(location); 646 | setZN(r_Y); 647 | break; 648 | case CPY: 649 | { 650 | std::uint16_t diff = r_Y - m_bus.read(location); 651 | f_C = !(diff & 0x100); 652 | setZN(diff); 653 | } 654 | break; 655 | case CPX: 656 | { 657 | std::uint16_t diff = r_X - m_bus.read(location); 658 | f_C = !(diff & 0x100); 659 | setZN(diff); 660 | } 661 | break; 662 | default: 663 | return false; 664 | } 665 | 666 | return true; 667 | } 668 | return false; 669 | } 670 | 671 | Address CPU::readAddress(Address addr) 672 | { 673 | return m_bus.read(addr) | m_bus.read(addr + 1) << 8; 674 | } 675 | 676 | }; 677 | -------------------------------------------------------------------------------- /src/Cartridge.cpp: -------------------------------------------------------------------------------- 1 | #include "Cartridge.h" 2 | #include "Log.h" 3 | #include "Mapper.h" 4 | #include 5 | #include 6 | 7 | namespace sn 8 | { 9 | Cartridge::Cartridge() 10 | : m_nameTableMirroring(0) 11 | , m_mapperNumber(0) 12 | , m_extendedRAM(false) 13 | { 14 | } 15 | const std::vector& Cartridge::getROM() 16 | { 17 | return m_PRG_ROM; 18 | } 19 | 20 | const std::vector& Cartridge::getVROM() 21 | { 22 | return m_CHR_ROM; 23 | } 24 | 25 | Byte Cartridge::getMapper() 26 | { 27 | return m_mapperNumber; 28 | } 29 | 30 | Byte Cartridge::getNameTableMirroring() 31 | { 32 | return m_nameTableMirroring; 33 | } 34 | 35 | bool Cartridge::hasExtendedRAM() 36 | { 37 | // Some ROMs don't have this set correctly, plus there's no particular reason to disable it. 38 | return true; 39 | } 40 | 41 | bool Cartridge::loadFromFile(std::string path) 42 | { 43 | std::ifstream romFile(path, std::ios_base::binary | std::ios_base::in); 44 | if (!romFile) 45 | { 46 | LOG(Error) << "Could not open ROM file from path: " << path << std::endl; 47 | return false; 48 | } 49 | 50 | std::vector header; 51 | LOG(Info) << "Reading ROM from path: " << path << std::endl; 52 | 53 | // Header 54 | header.resize(0x10); 55 | if (!romFile.read(reinterpret_cast(&header[0]), 0x10)) 56 | { 57 | LOG(Error) << "Reading iNES header failed." << std::endl; 58 | return false; 59 | } 60 | if (std::string { &header[0], &header[4] } != "NES\x1A") 61 | { 62 | LOG(Error) << "Not a valid iNES image. Magic number: " << std::hex << header[0] << " " << header[1] << " " 63 | << header[2] << " " << int(header[3]) << std::endl 64 | << "Valid magic number : N E S 1a" << std::endl; 65 | return false; 66 | } 67 | 68 | LOG(Info) << "Reading header, it dictates: \n"; 69 | 70 | Byte banks = header[4]; 71 | LOG(Info) << "16KB PRG-ROM Banks: " << +banks << std::endl; 72 | if (!banks) 73 | { 74 | LOG(Error) << "ROM has no PRG-ROM banks. Loading ROM failed." << std::endl; 75 | return false; 76 | } 77 | 78 | Byte vbanks = header[5]; 79 | LOG(Info) << "8KB CHR-ROM Banks: " << +vbanks << std::endl; 80 | 81 | if (header[6] & 0x8) 82 | { 83 | m_nameTableMirroring = NameTableMirroring::FourScreen; 84 | LOG(Info) << "Name Table Mirroring: " << "FourScreen" << std::endl; 85 | } 86 | else 87 | { 88 | m_nameTableMirroring = header[6] & 0x1; 89 | LOG(Info) << "Name Table Mirroring: " << (m_nameTableMirroring == 0 ? "Horizontal" : "Vertical") << std::endl; 90 | } 91 | 92 | m_mapperNumber = ((header[6] >> 4) & 0xf) | (header[7] & 0xf0); 93 | LOG(Info) << "Mapper #: " << +m_mapperNumber << std::endl; 94 | 95 | m_extendedRAM = header[6] & 0x2; 96 | LOG(Info) << "Extended (CPU) RAM: " << std::boolalpha << m_extendedRAM << std::endl; 97 | 98 | if (header[6] & 0x4) 99 | { 100 | LOG(Error) << "Trainer is not supported." << std::endl; 101 | return false; 102 | } 103 | 104 | if ((header[0xA] & 0x3) == 0x2 || (header[0xA] & 0x1)) 105 | { 106 | LOG(Error) << "PAL ROM not supported." << std::endl; 107 | return false; 108 | } 109 | else 110 | LOG(Info) << "ROM is NTSC compatible.\n"; 111 | 112 | // PRG-ROM 16KB banks 113 | m_PRG_ROM.resize(0x4000 * banks); 114 | if (!romFile.read(reinterpret_cast(&m_PRG_ROM[0]), 0x4000 * banks)) 115 | { 116 | LOG(Error) << "Reading PRG-ROM from image file failed." << std::endl; 117 | return false; 118 | } 119 | 120 | // CHR-ROM 8KB banks 121 | if (vbanks) 122 | { 123 | m_CHR_ROM.resize(0x2000 * vbanks); 124 | if (!romFile.read(reinterpret_cast(&m_CHR_ROM[0]), 0x2000 * vbanks)) 125 | { 126 | LOG(Error) << "Reading CHR-ROM from image file failed." << std::endl; 127 | return false; 128 | } 129 | } 130 | else 131 | LOG(Info) << "Cartridge with CHR-RAM." << std::endl; 132 | return true; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Controller.cpp: -------------------------------------------------------------------------------- 1 | #include "Controller.h" 2 | 3 | namespace sn 4 | { 5 | Controller::Controller() 6 | : m_keyStates(0) 7 | , m_keyBindings(TotalButtons) 8 | { 9 | // m_keyBindings[A] = sf::Keyboard::J; 10 | // m_keyBindings[B] = sf::Keyboard::K; 11 | // m_keyBindings[Select] = sf::Keyboard::RShift; 12 | // m_keyBindings[Start] = sf::Keyboard::Return; 13 | // m_keyBindings[Up] = sf::Keyboard::W; 14 | // m_keyBindings[Down] = sf::Keyboard::S; 15 | // m_keyBindings[Left] = sf::Keyboard::A; 16 | // m_keyBindings[Right] = sf::Keyboard::D; 17 | } 18 | 19 | void Controller::setKeyBindings(const std::vector& keys) 20 | { 21 | m_keyBindings = keys; 22 | } 23 | 24 | void Controller::strobe(Byte b) 25 | { 26 | m_strobe = (b & 1); 27 | if (!m_strobe) 28 | { 29 | m_keyStates = 0; 30 | int shift = 0; 31 | for (int button = A; button < TotalButtons; ++button) 32 | { 33 | m_keyStates |= (sf::Keyboard::isKeyPressed(m_keyBindings[static_cast(button)]) << shift); 34 | ++shift; 35 | } 36 | } 37 | } 38 | 39 | Byte Controller::read() 40 | { 41 | Byte ret; 42 | if (m_strobe) 43 | ret = sf::Keyboard::isKeyPressed(m_keyBindings[A]); 44 | else 45 | { 46 | ret = (m_keyStates & 1); 47 | m_keyStates >>= 1; 48 | } 49 | return ret | 0x40; 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/Emulator.cpp: -------------------------------------------------------------------------------- 1 | #include "Emulator.h" 2 | #include "APU/Constants.h" 3 | #include "Log.h" 4 | 5 | #include 6 | 7 | namespace sn 8 | { 9 | using std::chrono::high_resolution_clock; 10 | 11 | Emulator::Emulator() 12 | : m_cpu(m_bus) 13 | , m_audioPlayer(static_cast(1.0 / apu_clock_period_s.count())) 14 | , m_ppu(m_pictureBus, m_emulatorScreen) 15 | , m_apu(m_audioPlayer, m_cpu.createIRQHandler(), [&](Address addr) { return DMCDMA(addr); }) 16 | , m_bus(m_ppu, m_apu, m_controller1, m_controller2, [&](Byte b) { OAMDMA(b); }) 17 | , m_screenScale(3.f) 18 | , m_lastWakeup() 19 | { 20 | m_ppu.setInterruptCallback([&]() { m_cpu.nmiInterrupt(); }); 21 | } 22 | 23 | void Emulator::run(std::string rom_path) 24 | { 25 | if (!m_cartridge.loadFromFile(rom_path)) 26 | return; 27 | 28 | m_mapper = Mapper::createMapper(static_cast(m_cartridge.getMapper()), 29 | m_cartridge, 30 | m_cpu.createIRQHandler(), 31 | [&]() { m_pictureBus.updateMirroring(); }); 32 | if (!m_mapper) 33 | { 34 | LOG(Error) << "Creating Mapper failed. Probably unsupported." << std::endl; 35 | return; 36 | } 37 | 38 | if (!m_bus.setMapper(m_mapper.get()) || !m_pictureBus.setMapper(m_mapper.get())) 39 | { 40 | return; 41 | } 42 | 43 | m_cpu.reset(); 44 | m_ppu.reset(); 45 | 46 | m_window.create(sf::VideoMode(NESVideoWidth * m_screenScale, NESVideoHeight * m_screenScale), 47 | "SimpleNES", 48 | sf::Style::Titlebar | sf::Style::Close); 49 | m_window.setVerticalSyncEnabled(true); 50 | m_emulatorScreen.create(NESVideoWidth, NESVideoHeight, m_screenScale, sf::Color::White); 51 | 52 | m_lastWakeup = high_resolution_clock::now(); 53 | m_elapsedTime = m_lastWakeup - m_lastWakeup; 54 | 55 | m_audioPlayer.start(); 56 | 57 | sf::Event event; 58 | bool focus = true, pause = false; 59 | while (m_window.isOpen()) 60 | { 61 | while (m_window.pollEvent(event)) 62 | { 63 | if (event.type == sf::Event::Closed || 64 | (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape)) 65 | { 66 | m_window.close(); 67 | return; 68 | } 69 | else if (event.type == sf::Event::GainedFocus) 70 | { 71 | focus = true; 72 | const auto now = high_resolution_clock::now(); 73 | LOG(Info) << "Gained focus. Removing " << (now - m_lastWakeup).count() << "ns from timers" << std::endl; 74 | m_lastWakeup = now; 75 | } 76 | else if (event.type == sf::Event::LostFocus) 77 | { 78 | focus = false; 79 | LOG(Info) << "Losing focus; paused." << std::endl; 80 | } 81 | else if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F2) 82 | { 83 | pause = !pause; 84 | if (!pause) 85 | { 86 | const auto now = high_resolution_clock::now(); 87 | LOG(Info) << "Unpaused. Removing " << (now - m_lastWakeup).count() << "ns from timers" << std::endl; 88 | m_lastWakeup = now; 89 | } 90 | else 91 | { 92 | LOG(Info) << "Paused." << std::endl; 93 | } 94 | } 95 | else if (pause && event.type == sf::Event::KeyReleased && event.key.code == sf::Keyboard::F3) 96 | { 97 | for (int i = 0; i < 29781; ++i) // Around one frame 98 | { 99 | // PPU 100 | m_ppu.step(); 101 | m_ppu.step(); 102 | m_ppu.step(); 103 | // CPU 104 | m_cpu.step(); 105 | // APU 106 | m_apu.step(); 107 | } 108 | } 109 | else if (focus && event.type == sf::Event::KeyReleased && event.key.code == sf::Keyboard::F4) 110 | { 111 | Log::get().setLevel(Info); 112 | } 113 | else if (focus && event.type == sf::Event::KeyReleased && event.key.code == sf::Keyboard::F5) 114 | { 115 | Log::get().setLevel(InfoVerbose); 116 | } 117 | } 118 | 119 | if (focus && !pause) 120 | { 121 | const auto now = high_resolution_clock::now(); 122 | m_elapsedTime += now - m_lastWakeup; 123 | m_lastWakeup = now; 124 | 125 | while (m_elapsedTime > cpu_clock_period_ns) 126 | { 127 | // PPU 128 | m_ppu.step(); 129 | m_ppu.step(); 130 | m_ppu.step(); 131 | // CPU 132 | m_cpu.step(); 133 | // APU 134 | m_apu.step(); 135 | 136 | m_elapsedTime -= cpu_clock_period_ns; 137 | } 138 | 139 | m_window.draw(m_emulatorScreen); 140 | m_window.display(); 141 | } 142 | else 143 | { 144 | sf::sleep(sf::milliseconds(1000 / 60)); 145 | } 146 | } 147 | } 148 | 149 | void Emulator::OAMDMA(Byte page) 150 | { 151 | m_cpu.skipOAMDMACycles(); 152 | auto page_ptr = m_bus.getPagePtr(page); 153 | if (page_ptr != nullptr) 154 | { 155 | m_ppu.doDMA(page_ptr); 156 | } 157 | else 158 | { 159 | LOG(Error) << "Can't get pageptr for DMA" << std::endl; 160 | } 161 | } 162 | 163 | Byte Emulator::DMCDMA(Address addr) 164 | { 165 | m_cpu.skipDMCDMACycles(); 166 | return m_bus.read(addr); 167 | }; 168 | 169 | void Emulator::setVideoHeight(int height) 170 | { 171 | m_screenScale = height / float(NESVideoHeight); 172 | LOG(Info) << "Scale: " << m_screenScale << " set. Screen: " << int(NESVideoWidth * m_screenScale) << "x" 173 | << int(NESVideoHeight * m_screenScale) << std::endl; 174 | } 175 | 176 | void Emulator::setVideoWidth(int width) 177 | { 178 | m_screenScale = width / float(NESVideoWidth); 179 | LOG(Info) << "Scale: " << m_screenScale << " set. Screen: " << int(NESVideoWidth * m_screenScale) << "x" 180 | << int(NESVideoHeight * m_screenScale) << std::endl; 181 | } 182 | void Emulator::setVideoScale(float scale) 183 | { 184 | m_screenScale = scale; 185 | LOG(Info) << "Scale: " << m_screenScale << " set. Screen: " << int(NESVideoWidth * m_screenScale) << "x" 186 | << int(NESVideoHeight * m_screenScale) << std::endl; 187 | } 188 | 189 | void Emulator::setKeys(std::vector& p1, std::vector& p2) 190 | { 191 | m_controller1.setKeyBindings(p1); 192 | m_controller2.setKeyBindings(p2); 193 | } 194 | 195 | void Emulator::muteAudio() 196 | { 197 | m_audioPlayer.mute(); 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /src/KeybindingsParser.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Controller.h" 8 | #include "Log.h" 9 | 10 | namespace sn 11 | { 12 | // trim from start (construct new string) 13 | inline std::string ltrim(const std::string& str) 14 | { 15 | std::string s(str); 16 | s.erase(s.begin(), std::find_if_not(s.begin(), s.end(), std::isspace)); 17 | return s; 18 | } 19 | 20 | // trim from end (construct new string) 21 | inline std::string rtrim(const std::string& str) 22 | { 23 | std::string s(str); 24 | s.erase(std::find_if_not(s.rbegin(), s.rend(), std::isspace).base(), s.end()); 25 | return s; 26 | } 27 | 28 | void parseControllerConf(std::string filepath, std::vector& p1, std::vector& p2) 29 | { 30 | const std::string buttonStrings[] = { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" }; 31 | const std::string keys[] = { 32 | "A", "B", "C", "D", "E", "F", "G", "H", "I", 33 | "J", "K", "L", "M", "N", "O", "P", "Q", "R", 34 | "S", "T", "U", "V", "W", "X", "Y", "Z", "Num0", 35 | "Num1", "Num2", "Num3", "Num4", "Num5", "Num6", "Num7", "Num8", "Num9", 36 | "Escape", "LControl", "LShift", "LAlt", "LSystem", "RControl", "RShift", "RAlt", "RSystem", 37 | "Menu", "LBracket", "RBracket", "SemiColon", "Comma", "Period", "Quote", "Slash", "BackSlash", 38 | "Tilde", "Equal", "Dash", "Space", "Return", "BackSpace", "Tab", "PageUp", "PageDown", 39 | "End", "Home", "Insert", "Delete", "Add", "Subtract", "Multiply", "Divide", "Left", 40 | "Right", "Up", "Down", "Numpad0", "Numpad1", "Numpad2", "Numpad3", "Numpad4", "Numpad5", 41 | "Numpad6", "Numpad7", "Numpad8", "Numpad9", "F1", "F2", "F3", "F4", "F5", 42 | "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", 43 | "F15", "Pause" 44 | }; 45 | 46 | std::ifstream file(filepath); 47 | std::string line; 48 | enum 49 | { 50 | Player1, 51 | Player2, 52 | None 53 | } state = None; 54 | unsigned int line_no = 0; 55 | while (std::getline(file, line)) 56 | { 57 | line = rtrim(ltrim(line)); 58 | if (line[0] == '#' || line.empty()) 59 | continue; 60 | else if (line == "[Player1]") 61 | { 62 | state = Player1; 63 | } 64 | else if (line == "[Player2]") 65 | { 66 | state = Player2; 67 | } 68 | else if (state == Player1 || state == Player2) 69 | { 70 | auto divider = line.find("="); 71 | auto it = 72 | std::find(std::begin(buttonStrings), std::end(buttonStrings), ltrim(rtrim(line.substr(0, divider)))), 73 | it2 = std::find(std::begin(keys), std::end(keys), ltrim(rtrim(line.substr(divider + 1)))); 74 | if (it == std::end(buttonStrings) || it2 == std::end(keys)) 75 | { 76 | LOG(Error) << "Invalid key in configuration file at Line " << line_no << std::endl; 77 | continue; 78 | } 79 | auto i = std::distance(std::begin(buttonStrings), it); 80 | auto key = std::distance(std::begin(keys), it2); 81 | (state == Player1 ? p1 : p2)[i] = static_cast(key); 82 | } 83 | else 84 | LOG(Error) << "Invalid line in key configuration at Line " << line_no << std::endl; 85 | 86 | ++line_no; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Log.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | 3 | namespace sn 4 | { 5 | Log::~Log() {} 6 | 7 | Log& Log::get() 8 | { 9 | static Log instance; 10 | return instance; 11 | } 12 | 13 | std::ostream& Log::getCpuTraceStream() 14 | { 15 | return *m_cpuTrace; 16 | } 17 | std::ostream& Log::getStream() 18 | { 19 | return *m_logStream; 20 | } 21 | 22 | void Log::setLogStream(std::ostream& stream) 23 | { 24 | m_logStream = &stream; 25 | } 26 | 27 | void Log::setCpuTraceStream(std::ostream& stream) 28 | { 29 | m_cpuTrace = &stream; 30 | } 31 | 32 | Log& Log::setLevel(Level level) 33 | { 34 | m_logLevel = level; 35 | return *this; 36 | } 37 | 38 | Level Log::getLevel() 39 | { 40 | return m_logLevel; 41 | } 42 | 43 | TeeBuf::TeeBuf(std::streambuf* sb1, std::streambuf* sb2) 44 | : m_sb1(sb1) 45 | , m_sb2(sb2) 46 | { 47 | } 48 | int TeeBuf::overflow(int c) 49 | { 50 | if (c == EOF) 51 | { 52 | return !EOF; 53 | } 54 | else 55 | { 56 | int const r1 = m_sb1->sputc(c); 57 | int const r2 = m_sb2->sputc(c); 58 | return r1 == EOF || r2 == EOF ? EOF : c; 59 | } 60 | } 61 | 62 | int TeeBuf::sync() 63 | { 64 | int const r1 = m_sb1->pubsync(); 65 | int const r2 = m_sb2->pubsync(); 66 | return r1 == 0 && r2 == 0 ? 0 : -1; 67 | } 68 | 69 | TeeStream::TeeStream(std::ostream& o1, std::ostream& o2) 70 | : std::ostream(&m_tbuf) 71 | , m_tbuf(o1.rdbuf(), o2.rdbuf()) 72 | { 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/MainBus.cpp: -------------------------------------------------------------------------------- 1 | #include "MainBus.h" 2 | #include "Cartridge.h" 3 | #include "Log.h" 4 | #include 5 | 6 | namespace sn 7 | { 8 | MainBus::MainBus(PPU& ppu, APU& apu, Controller& ctrl1, Controller& ctrl2, std::function dma) 9 | : m_RAM(0x800, 0) 10 | , m_dmaCallback(dma) 11 | , m_mapper(nullptr) 12 | , m_ppu(ppu) 13 | , m_apu(apu) 14 | , m_controller1(ctrl1) 15 | , m_controller2(ctrl2) 16 | { 17 | } 18 | 19 | Address normalize_mirror(Address addr) 20 | { 21 | if (addr >= MainBus::PPU_CTRL && addr < MainBus::APU_REGISTER_START) 22 | { 23 | // 0x2008 - 0x3fff are mirrors of 0x2000 - 0x2007 24 | return addr & 0x2007; 25 | } 26 | 27 | // no mirroring 28 | return addr; 29 | } 30 | 31 | Byte MainBus::read(Address addr) 32 | { 33 | if (addr < 0x2000) 34 | { 35 | return m_RAM[addr & 0x7ff]; 36 | } 37 | else if (addr < 0x4020) // memory-mapped registers 38 | { 39 | addr = normalize_mirror(addr); 40 | switch (addr) 41 | { 42 | case PPU_STATUS: 43 | return m_ppu.getStatus(); 44 | break; 45 | case PPU_DATA: 46 | return m_ppu.getData(); 47 | break; 48 | case JOY1: 49 | return m_controller1.read(); 50 | break; 51 | case JOY2_AND_FRAME_CONTROL: 52 | return m_controller2.read(); 53 | break; 54 | case OAM_DATA: 55 | return m_ppu.getOAMData(); 56 | break; 57 | case APU_CONTROL_AND_STATUS: 58 | return m_apu.readStatus(); 59 | break; 60 | default: 61 | LOG(InfoVerbose) << "Read access attempt at: " << std::hex << +addr << std::endl; 62 | return 0; 63 | break; 64 | } 65 | } 66 | else if (addr < 0x6000) 67 | { 68 | LOG(InfoVerbose) << "Expansion ROM read attempted. This is currently unsupported" << std::endl; 69 | return 0; 70 | } 71 | else if (addr < 0x8000) 72 | { 73 | if (m_mapper->hasExtendedRAM()) 74 | { 75 | return m_extRAM[addr - 0x6000]; 76 | } 77 | 78 | return 0; 79 | } 80 | else 81 | { 82 | return m_mapper->readPRG(addr); 83 | } 84 | } 85 | 86 | void MainBus::write(Address addr, Byte value) 87 | { 88 | if (addr < 0x2000) 89 | { 90 | m_RAM[addr & 0x7ff] = value; 91 | } 92 | else if (addr < 0x4020) // memory-mapped registers 93 | { 94 | addr = normalize_mirror(addr); 95 | switch (addr) 96 | { 97 | case PPU_CTRL: 98 | m_ppu.control(value); 99 | break; 100 | case PPU_MASK: 101 | m_ppu.setMask(value); 102 | break; 103 | case OAM_ADDR: 104 | m_ppu.setOAMAddress(value); 105 | break; 106 | case OAM_DATA: 107 | m_ppu.setOAMData(value); 108 | break; 109 | case PPU_ADDR: 110 | m_ppu.setDataAddress(value); 111 | break; 112 | case PPU_SCROL: 113 | m_ppu.setScroll(value); 114 | break; 115 | case PPU_DATA: 116 | m_ppu.setData(value); 117 | break; 118 | case OAM_DMA: 119 | m_dmaCallback(value); 120 | break; 121 | case JOY1: 122 | m_controller1.strobe(value); 123 | m_controller2.strobe(value); 124 | break; 125 | 126 | case APU_CONTROL_AND_STATUS: 127 | case JOY2_AND_FRAME_CONTROL: 128 | m_apu.writeRegister(addr, value); 129 | break; 130 | 131 | default: 132 | if (addr >= APU_REGISTER_START && addr <= APU_REGISTER_END) 133 | { 134 | m_apu.writeRegister(addr, value); 135 | } 136 | else 137 | { 138 | LOG(InfoVerbose) << "Write access attempt at: " << std::hex << +addr << std::endl; 139 | } 140 | break; 141 | } 142 | } 143 | else if (addr < 0x6000) 144 | { 145 | LOG(InfoVerbose) << "Expansion ROM access attempted. This is currently unsupported" << std::endl; 146 | } 147 | else if (addr < 0x8000) 148 | { 149 | if (m_mapper->hasExtendedRAM()) 150 | { 151 | m_extRAM[addr - 0x6000] = value; 152 | } 153 | } 154 | else 155 | { 156 | m_mapper->writePRG(addr, value); 157 | } 158 | } 159 | 160 | const Byte* MainBus::getPagePtr(Byte page) 161 | { 162 | Address addr = page << 8; 163 | if (addr < 0x2000) 164 | { 165 | return &m_RAM[addr & 0x7ff]; 166 | } 167 | else if (addr < 0x4020) 168 | { 169 | LOG(Error) << "Register address memory pointer access attempt" << std::endl; 170 | } 171 | else if (addr < 0x6000) 172 | { 173 | LOG(Error) << "Expansion ROM access attempted, which is unsupported" << std::endl; 174 | } 175 | else if (addr < 0x8000) 176 | { 177 | if (m_mapper->hasExtendedRAM()) 178 | { 179 | return &m_extRAM[addr - 0x6000]; 180 | } 181 | } 182 | else 183 | { 184 | LOG(Error) << "Unexpected DMA request: " << std::hex << "0x" << +addr << " (" << +page << ")" << std::dec 185 | << std::endl; 186 | } 187 | return nullptr; 188 | } 189 | 190 | bool MainBus::setMapper(Mapper* mapper) 191 | { 192 | m_mapper = mapper; 193 | 194 | if (!mapper) 195 | { 196 | LOG(Error) << "Mapper pointer is nullptr" << std::endl; 197 | return false; 198 | } 199 | 200 | if (mapper->hasExtendedRAM()) 201 | m_extRAM.resize(0x2000); 202 | 203 | return true; 204 | } 205 | }; 206 | -------------------------------------------------------------------------------- /src/Mapper.cpp: -------------------------------------------------------------------------------- 1 | #include "Mapper.h" 2 | #include "MapperAxROM.h" 3 | #include "MapperCNROM.h" 4 | #include "MapperColorDreams.h" 5 | #include "MapperGxROM.h" 6 | #include "MapperMMC3.h" 7 | #include "MapperNROM.h" 8 | #include "MapperSxROM.h" 9 | #include "MapperUxROM.h" 10 | 11 | namespace sn 12 | { 13 | NameTableMirroring Mapper::getNameTableMirroring() 14 | { 15 | return static_cast(m_cartridge.getNameTableMirroring()); 16 | } 17 | 18 | std::unique_ptr Mapper::createMapper(Mapper::Type mapper_t, 19 | sn::Cartridge& cart, 20 | IRQHandle& irq, 21 | std::function mirroring_cb) 22 | { 23 | std::unique_ptr ret(nullptr); 24 | switch (mapper_t) 25 | { 26 | case NROM: 27 | ret.reset(new MapperNROM(cart)); 28 | break; 29 | case SxROM: 30 | ret.reset(new MapperSxROM(cart, mirroring_cb)); 31 | break; 32 | case UxROM: 33 | ret.reset(new MapperUxROM(cart)); 34 | break; 35 | case CNROM: 36 | ret.reset(new MapperCNROM(cart)); 37 | break; 38 | case MMC3: 39 | ret.reset(new MapperMMC3(cart, irq, mirroring_cb)); 40 | break; 41 | case AxROM: 42 | ret.reset(new MapperAxROM(cart, mirroring_cb)); 43 | break; 44 | case ColorDreams: 45 | ret.reset(new MapperColorDreams(cart, mirroring_cb)); 46 | break; 47 | case GxROM: 48 | ret.reset(new MapperGxROM(cart, mirroring_cb)); 49 | break; 50 | default: 51 | break; 52 | } 53 | return ret; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/MapperAxROM.cpp: -------------------------------------------------------------------------------- 1 | #include "MapperAxROM.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | MapperAxROM::MapperAxROM(Cartridge& cart, std::function mirroring_cb) 7 | : Mapper(cart, Mapper::AxROM) 8 | , m_mirroring(OneScreenLower) 9 | , m_mirroringCallback(mirroring_cb) 10 | , m_prgBank(0) 11 | { 12 | if (cart.getROM().size() >= 0x8000) 13 | { 14 | LOG(Info) << "Using PRG-ROM OK" << std::endl; 15 | } 16 | if (cart.getVROM().size() == 0) 17 | { 18 | m_characterRAM.resize(0x2000); 19 | LOG(Info) << "Uses Character RAM OK" << std::endl; 20 | } 21 | } 22 | 23 | Byte MapperAxROM::readPRG(Address address) 24 | { 25 | if (address >= 0x8000) 26 | { 27 | 28 | return m_cartridge.getROM()[m_prgBank * 0x8000 + (address & 0x7FFF)]; 29 | } 30 | 31 | return 0; 32 | } 33 | 34 | void MapperAxROM::writePRG(Address address, Byte value) 35 | { 36 | if (address >= 0x8000) 37 | { 38 | m_prgBank = value & 0x07; 39 | m_mirroring = (value & 0x10) ? OneScreenHigher : OneScreenLower; 40 | m_mirroringCallback(); 41 | } 42 | } 43 | 44 | NameTableMirroring MapperAxROM::getNameTableMirroring() 45 | { 46 | return m_mirroring; 47 | } 48 | 49 | Byte MapperAxROM::readCHR(Address address) 50 | { 51 | if (address < 0x2000) 52 | { 53 | return m_characterRAM[address]; 54 | } 55 | 56 | return 0; 57 | } 58 | 59 | void MapperAxROM::writeCHR(Address address, Byte value) 60 | { 61 | if (address < 0x2000) 62 | { 63 | m_characterRAM[address] = value; 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/MapperCNROM.cpp: -------------------------------------------------------------------------------- 1 | #include "MapperCNROM.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | MapperCNROM::MapperCNROM(Cartridge& cart) 7 | : Mapper(cart, Mapper::CNROM) 8 | , m_selectCHR(0) 9 | { 10 | if (cart.getROM().size() == 0x4000) // 1 bank 11 | { 12 | m_oneBank = true; 13 | } 14 | else // 2 banks 15 | { 16 | m_oneBank = false; 17 | } 18 | } 19 | 20 | Byte MapperCNROM::readPRG(Address addr) 21 | { 22 | if (!m_oneBank) 23 | return m_cartridge.getROM()[addr - 0x8000]; 24 | else // mirrored 25 | return m_cartridge.getROM()[(addr - 0x8000) & 0x3fff]; 26 | } 27 | 28 | void MapperCNROM::writePRG(Address, Byte value) 29 | { 30 | m_selectCHR = value & 0x3; 31 | } 32 | 33 | Byte MapperCNROM::readCHR(Address addr) 34 | { 35 | return m_cartridge.getVROM()[addr | (m_selectCHR << 13)]; 36 | } 37 | 38 | void MapperCNROM::writeCHR(Address addr, Byte) 39 | { 40 | LOG(Info) << "Read-only CHR memory write attempt at " << std::hex << addr << std::endl; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/MapperColorDreams.cpp: -------------------------------------------------------------------------------- 1 | #include "MapperColorDreams.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | 7 | MapperColorDreams::MapperColorDreams(Cartridge& cart, std::function mirroring_cb) 8 | : Mapper(cart, Mapper::ColorDreams) 9 | , m_mirroring(Vertical) 10 | , m_mirroringCallback(mirroring_cb) 11 | { 12 | } 13 | 14 | Byte MapperColorDreams::readPRG(Address address) 15 | { 16 | if (address >= 0x8000) 17 | { 18 | return m_cartridge.getROM()[(prgbank * 0x8000) + (address & 0x7fff)]; 19 | } 20 | return 0; 21 | } 22 | 23 | void MapperColorDreams::writePRG(Address address, Byte value) 24 | { 25 | if (address >= 0x8000) 26 | { 27 | prgbank = ((value >> 0) & 0x3); 28 | chrbank = ((value >> 4) & 0xF); 29 | } 30 | } 31 | 32 | Byte MapperColorDreams::readCHR(Address address) 33 | { 34 | if (address <= 0x1FFF) 35 | { 36 | return m_cartridge.getVROM()[(chrbank * 0x2000) + address]; 37 | } 38 | 39 | return 0; 40 | } 41 | 42 | NameTableMirroring MapperColorDreams::getNameTableMirroring() 43 | { 44 | return m_mirroring; 45 | } 46 | 47 | void MapperColorDreams::writeCHR(Address, Byte) {} 48 | } 49 | -------------------------------------------------------------------------------- /src/MapperGxROM.cpp: -------------------------------------------------------------------------------- 1 | #include "MapperGxROM.h" 2 | #include "Log.h" 3 | namespace sn 4 | { 5 | 6 | MapperGxROM::MapperGxROM(Cartridge& cart, std::function mirroring_cb) 7 | : Mapper(cart, Mapper::GxROM) 8 | , m_mirroring(Vertical) 9 | , m_mirroringCallback(mirroring_cb) 10 | { 11 | } 12 | 13 | Byte MapperGxROM::readPRG(Address address) 14 | { 15 | if (address >= 0x8000) 16 | { 17 | return m_cartridge.getROM()[(prgbank * 0x8000) + (address & 0x7fff)]; 18 | } 19 | 20 | return 0; 21 | } 22 | 23 | void MapperGxROM::writePRG(Address address, Byte value) 24 | { 25 | if (address >= 0x8000) 26 | { 27 | prgbank = ((value & 0x30) >> 4); 28 | chrbank = (value & 0x3); 29 | m_mirroring = Vertical; 30 | } 31 | m_mirroringCallback(); 32 | } 33 | 34 | Byte MapperGxROM::readCHR(Address address) 35 | { 36 | if (address <= 0x1FFF) 37 | { 38 | return m_cartridge.getVROM()[chrbank * 0x2000 + address]; 39 | } 40 | return 0; 41 | } 42 | 43 | NameTableMirroring MapperGxROM::getNameTableMirroring() 44 | { 45 | return m_mirroring; 46 | } 47 | 48 | void MapperGxROM::writeCHR(Address, Byte) 49 | { 50 | LOG(Info) << "not expecting writes here"; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/MapperMMC3.cpp: -------------------------------------------------------------------------------- 1 | #include "MapperMMC3.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | MapperMMC3::MapperMMC3(Cartridge& cart, IRQHandle& irq, std::function mirroring_cb) 7 | : Mapper(cart, Mapper::MMC3) 8 | , m_targetRegister(0) 9 | , m_prgBankMode(false) 10 | , m_chrInversion(false) 11 | , m_bankRegister {} 12 | , m_irqEnabled(false) 13 | , m_irqCounter(0) 14 | , m_irqLatch(0) 15 | , m_irqReloadPending(false) 16 | , m_prgRam(32 * 1024) 17 | , m_mirroringRam(4 * 1024) 18 | , m_mirroring(Horizontal) 19 | , m_mirroringCallback(mirroring_cb) 20 | , m_irq(irq) 21 | { 22 | m_prgBank0 = &cart.getROM()[cart.getROM().size() - 0x4000]; 23 | m_prgBank1 = &cart.getROM()[cart.getROM().size() - 0x2000]; 24 | m_prgBank2 = &cart.getROM()[cart.getROM().size() - 0x4000]; 25 | m_prgBank3 = &cart.getROM()[cart.getROM().size() - 0x2000]; 26 | 27 | for (auto& bank : m_chrBanks) 28 | { 29 | bank = cart.getVROM().size() - 0x400; 30 | } 31 | m_chrBanks[0] = cart.getVROM().size() - 0x800; 32 | m_chrBanks[3] = cart.getVROM().size() - 0x800; 33 | } 34 | 35 | Byte MapperMMC3::readPRG(Address addr) 36 | { 37 | if (addr >= 0x6000 && addr <= 0x7FFF) 38 | { 39 | return m_prgRam[addr & 0x1fff]; 40 | } 41 | 42 | if (addr >= 0x8000 && addr <= 0x9FFF) 43 | { 44 | return *(m_prgBank0 + (addr & 0x1fff)); 45 | } 46 | 47 | if (addr >= 0xA000 && addr <= 0xBFFF) 48 | { 49 | return *(m_prgBank1 + (addr & 0x1fff)); 50 | } 51 | 52 | if (addr >= 0xC000 && addr <= 0xDFFF) 53 | { 54 | return *(m_prgBank2 + (addr & 0x1fff)); 55 | } 56 | 57 | if (addr >= 0xE000) 58 | { 59 | return *(m_prgBank3 + (addr & 0x1fff)); 60 | } 61 | 62 | return 0; 63 | } 64 | 65 | Byte MapperMMC3::readCHR(Address addr) 66 | { 67 | if (addr < 0x1fff) 68 | { 69 | // select 1kb bank 70 | const auto bankSelect = addr >> 10; 71 | // get the configured base address for the bank 72 | const auto baseAddress = m_chrBanks[bankSelect]; 73 | const auto offset = addr & 0x3ff; 74 | return m_cartridge.getVROM()[baseAddress + offset]; 75 | } 76 | else if (addr <= 0x2fff) 77 | { 78 | return m_mirroringRam[addr - 0x2000]; 79 | } 80 | 81 | return 0; 82 | } 83 | 84 | void MapperMMC3::writePRG(Address addr, Byte value) 85 | { 86 | 87 | if (addr >= 0x6000 && addr <= 0x7FFF) 88 | { 89 | m_prgRam[addr & 0x1FFF] = value; 90 | } 91 | 92 | else if (addr >= 0x8000 && addr <= 0x9FFF) 93 | { 94 | // Bank Select 95 | if (!(addr & 0x01)) 96 | { 97 | m_targetRegister = value & 0x7; 98 | m_prgBankMode = value & 0x40; 99 | m_chrInversion = value & 0x80; 100 | } 101 | else 102 | { 103 | m_bankRegister[m_targetRegister] = value; 104 | 105 | if (m_chrInversion == 0) 106 | { 107 | // Add 0xfe mask to ignore lowest bit 108 | m_chrBanks[0] = (m_bankRegister[0] & 0xFE) * 0x0400; 109 | m_chrBanks[1] = (m_bankRegister[0] & 0xFE) * 0x0400 + 0x0400; 110 | m_chrBanks[2] = (m_bankRegister[1] & 0xFE) * 0x0400; 111 | m_chrBanks[3] = (m_bankRegister[1] & 0xFE) * 0x0400 + 0x0400; 112 | m_chrBanks[4] = m_bankRegister[2] * 0x0400; 113 | m_chrBanks[5] = m_bankRegister[3] * 0x0400; 114 | m_chrBanks[6] = m_bankRegister[4] * 0x0400; 115 | m_chrBanks[7] = m_bankRegister[5] * 0x0400; 116 | } 117 | else if (m_chrInversion == 1) 118 | { 119 | m_chrBanks[0] = m_bankRegister[2] * 0x0400; 120 | m_chrBanks[1] = m_bankRegister[3] * 0x0400; 121 | m_chrBanks[2] = m_bankRegister[4] * 0x0400; 122 | m_chrBanks[3] = m_bankRegister[5] * 0x0400; 123 | m_chrBanks[4] = (m_bankRegister[0] & 0xFE) * 0x0400; 124 | m_chrBanks[5] = (m_bankRegister[0] & 0xFE) * 0x0400 + 0x0400; 125 | m_chrBanks[6] = (m_bankRegister[1] & 0xFE) * 0x0400; 126 | m_chrBanks[7] = (m_bankRegister[1] & 0xFE) * 0x0400 + 0x0400; 127 | } 128 | 129 | if (m_prgBankMode == 0) 130 | { 131 | // ignore top two bits for R6 / R7 using 0x3F 132 | m_prgBank0 = &m_cartridge.getROM()[(m_bankRegister[6] & 0x3F) * 0x2000]; 133 | m_prgBank1 = &m_cartridge.getROM()[(m_bankRegister[7] & 0x3F) * 0x2000]; 134 | m_prgBank2 = &m_cartridge.getROM()[m_cartridge.getROM().size() - 0x4000]; 135 | m_prgBank3 = &m_cartridge.getROM()[m_cartridge.getROM().size() - 0x2000]; 136 | } 137 | else if (m_prgBankMode == 1) 138 | { 139 | m_prgBank0 = &m_cartridge.getROM()[m_cartridge.getROM().size() - 0x4000]; 140 | m_prgBank1 = &m_cartridge.getROM()[(m_bankRegister[7] & 0x3F) * 0x2000]; 141 | m_prgBank2 = &m_cartridge.getROM()[(m_bankRegister[6] & 0x3F) * 0x2000]; 142 | m_prgBank3 = &m_cartridge.getROM()[m_cartridge.getROM().size() - 0x2000]; 143 | } 144 | } 145 | } 146 | else if (addr >= 0xA000 && addr <= 0xBFFF) 147 | { 148 | if (!(addr & 0x01)) 149 | { 150 | // Mirroring 151 | if (m_cartridge.getNameTableMirroring() & 0x8) 152 | { 153 | m_mirroring = NameTableMirroring::FourScreen; 154 | } 155 | else if (value & 0x01) 156 | { 157 | m_mirroring = NameTableMirroring::Horizontal; 158 | } 159 | else 160 | { 161 | m_mirroring = NameTableMirroring::Vertical; 162 | } 163 | m_mirroringCallback(); 164 | } 165 | else 166 | { 167 | // PRG Ram Protect 168 | } 169 | } 170 | 171 | else if (addr >= 0xC000 && addr <= 0xDFFF) 172 | { 173 | if (!(addr & 0x01)) 174 | { 175 | m_irqLatch = value; 176 | } 177 | else 178 | { 179 | m_irqCounter = 0; 180 | m_irqReloadPending = true; 181 | } 182 | } 183 | 184 | else if (addr >= 0xE000) 185 | { 186 | // enabled if odd address 187 | m_irqEnabled = (addr & 0x01) == 0x01; 188 | m_irq.release(); 189 | } 190 | } 191 | 192 | void MapperMMC3::writeCHR(Address addr, Byte value) 193 | { 194 | if (addr >= 0x2000 && addr <= 0x2fff) 195 | { 196 | m_mirroringRam[addr - 0x2000] = value; 197 | } 198 | } 199 | 200 | void MapperMMC3::scanlineIRQ() 201 | { 202 | bool zeroTransition = false; 203 | 204 | if (m_irqCounter == 0 || m_irqReloadPending) 205 | { 206 | m_irqCounter = m_irqLatch; 207 | // zeroTransition = m_irqReloadPending; 208 | m_irqReloadPending = false; 209 | } 210 | else 211 | { 212 | m_irqCounter--; 213 | zeroTransition = m_irqCounter == 0; 214 | } 215 | 216 | if (zeroTransition && m_irqEnabled) 217 | { 218 | m_irq.pull(); 219 | } 220 | } 221 | 222 | NameTableMirroring MapperMMC3::getNameTableMirroring() 223 | { 224 | return m_mirroring; 225 | } 226 | 227 | } // namespace sn 228 | -------------------------------------------------------------------------------- /src/MapperNROM.cpp: -------------------------------------------------------------------------------- 1 | #include "MapperNROM.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | MapperNROM::MapperNROM(Cartridge& cart) 7 | : Mapper(cart, Mapper::NROM) 8 | { 9 | if (cart.getROM().size() == 0x4000) // 1 bank 10 | { 11 | m_oneBank = true; 12 | } 13 | else // 2 banks 14 | { 15 | m_oneBank = false; 16 | } 17 | 18 | if (cart.getVROM().size() == 0) 19 | { 20 | m_usesCharacterRAM = true; 21 | m_characterRAM.resize(0x2000); 22 | LOG(Info) << "Uses character RAM" << std::endl; 23 | } 24 | else 25 | m_usesCharacterRAM = false; 26 | } 27 | 28 | Byte MapperNROM::readPRG(Address addr) 29 | { 30 | if (!m_oneBank) 31 | return m_cartridge.getROM()[addr - 0x8000]; 32 | else // mirrored 33 | return m_cartridge.getROM()[(addr - 0x8000) & 0x3fff]; 34 | } 35 | 36 | void MapperNROM::writePRG(Address addr, Byte value) 37 | { 38 | LOG(InfoVerbose) << "ROM memory write attempt at " << +addr << " to set " << +value << std::endl; 39 | } 40 | 41 | Byte MapperNROM::readCHR(Address addr) 42 | { 43 | if (m_usesCharacterRAM) 44 | return m_characterRAM[addr]; 45 | else 46 | return m_cartridge.getVROM()[addr]; 47 | } 48 | 49 | void MapperNROM::writeCHR(Address addr, Byte value) 50 | { 51 | if (m_usesCharacterRAM) 52 | m_characterRAM[addr] = value; 53 | else 54 | LOG(Info) << "Read-only CHR memory write attempt at " << std::hex << addr << std::endl; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/MapperSxROM.cpp: -------------------------------------------------------------------------------- 1 | #include "MapperSxROM.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | MapperSxROM::MapperSxROM(Cartridge& cart, std::function mirroring_cb) 7 | : Mapper(cart, Mapper::SxROM) 8 | , m_mirroringCallback(mirroring_cb) 9 | , m_mirroing(Horizontal) 10 | , m_modeCHR(0) 11 | , m_modePRG(3) 12 | , m_tempRegister(0) 13 | , m_writeCounter(0) 14 | , m_regPRG(0) 15 | , m_regCHR0(0) 16 | , m_regCHR1(0) 17 | , m_firstBankPRG(nullptr) 18 | , m_secondBankPRG(nullptr) 19 | , m_firstBankCHRIdx(0) 20 | , m_secondBankCHRIdx(0) 21 | { 22 | if (cart.getVROM().size() == 0) 23 | { 24 | m_usesCharacterRAM = true; 25 | m_characterRAM.resize(0x8000); 26 | LOG(Info) << "Uses character RAM" << std::endl; 27 | } 28 | else 29 | { 30 | LOG(Info) << "Using CHR-ROM" << std::endl; 31 | m_usesCharacterRAM = false; 32 | m_firstBankCHRIdx = 0; 33 | m_secondBankCHRIdx = 0x1000 * m_regCHR1; 34 | } 35 | 36 | m_firstBankPRG = &cart.getROM()[0]; // first bank 37 | m_secondBankPRG = &cart.getROM()[cart.getROM().size() - 0x4000 /*0x2000 * 0x0e*/]; // last bank 38 | } 39 | 40 | Byte MapperSxROM::readPRG(Address addr) 41 | { 42 | if (addr < 0xc000) 43 | return *(m_firstBankPRG + (addr & 0x3fff)); 44 | else 45 | return *(m_secondBankPRG + (addr & 0x3fff)); 46 | } 47 | 48 | NameTableMirroring MapperSxROM::getNameTableMirroring() 49 | { 50 | return m_mirroing; 51 | } 52 | 53 | void MapperSxROM::writePRG(Address addr, Byte value) 54 | { 55 | if (!(value & 0x80)) // if reset bit is NOT set 56 | { 57 | m_tempRegister = (m_tempRegister >> 1) | ((value & 1) << 4); 58 | ++m_writeCounter; 59 | 60 | if (m_writeCounter == 5) 61 | { 62 | if (addr <= 0x9fff) 63 | { 64 | switch (m_tempRegister & 0x3) 65 | { 66 | case 0: 67 | m_mirroing = OneScreenLower; 68 | break; 69 | case 1: 70 | m_mirroing = OneScreenHigher; 71 | break; 72 | case 2: 73 | m_mirroing = Vertical; 74 | break; 75 | case 3: 76 | m_mirroing = Horizontal; 77 | break; 78 | } 79 | m_mirroringCallback(); 80 | 81 | m_modeCHR = (m_tempRegister & 0x10) >> 4; 82 | m_modePRG = (m_tempRegister & 0xc) >> 2; 83 | calculatePRGPointers(); 84 | 85 | // Recalculate CHR pointers 86 | if (m_modeCHR == 0) // one 8KB bank 87 | { 88 | m_firstBankCHRIdx = 0x1000 * (m_regCHR0 | 1); // ignore last bit 89 | m_secondBankCHRIdx = m_firstBankCHRIdx + 0x1000; 90 | } 91 | else // two 4KB banks 92 | { 93 | m_firstBankCHRIdx = 0x1000 * m_regCHR0; 94 | m_secondBankCHRIdx = 0x1000 * m_regCHR1; 95 | } 96 | } 97 | else if (addr <= 0xbfff) // CHR Reg 0 98 | { 99 | m_regCHR0 = m_tempRegister; 100 | m_firstBankCHRIdx = 0x1000 * (m_tempRegister | (1 - m_modeCHR)); // OR 1 if 8KB 101 | // mode 102 | if (m_modeCHR == 0) 103 | m_secondBankCHRIdx = m_firstBankCHRIdx + 0x1000; 104 | } 105 | else if (addr <= 0xdfff) 106 | { 107 | m_regCHR1 = m_tempRegister; 108 | if (m_modeCHR == 1) 109 | m_secondBankCHRIdx = 0x1000 * m_tempRegister; 110 | } 111 | else 112 | { 113 | // TODO PRG-RAM 114 | if ((m_tempRegister & 0x10) == 0x10) 115 | { 116 | LOG(Info) << "PRG-RAM activated" << std::endl; 117 | } 118 | 119 | m_tempRegister &= 0xf; 120 | m_regPRG = m_tempRegister; 121 | calculatePRGPointers(); 122 | } 123 | 124 | m_tempRegister = 0; 125 | m_writeCounter = 0; 126 | } 127 | } 128 | else // reset 129 | { 130 | m_tempRegister = 0; 131 | m_writeCounter = 0; 132 | m_modePRG = 3; 133 | calculatePRGPointers(); 134 | } 135 | } 136 | 137 | void MapperSxROM::calculatePRGPointers() 138 | { 139 | if (m_modePRG <= 1) // 32KB changeable 140 | { 141 | // equivalent to multiplying 0x8000 * (m_regPRG >> 1) 142 | m_firstBankPRG = &m_cartridge.getROM()[0x4000 * (m_regPRG & ~1)]; 143 | m_secondBankPRG = m_firstBankPRG + 0x4000; // add 16KB 144 | } 145 | else if (m_modePRG == 2) // fix first switch second 146 | { 147 | m_firstBankPRG = &m_cartridge.getROM()[0]; 148 | m_secondBankPRG = m_firstBankPRG + 0x4000 * m_regPRG; 149 | } 150 | else // switch first fix second 151 | { 152 | m_firstBankPRG = &m_cartridge.getROM()[0x4000 * m_regPRG]; 153 | m_secondBankPRG = &m_cartridge.getROM()[m_cartridge.getROM().size() - 0x4000 /*0x2000 * 0x0e*/]; 154 | } 155 | } 156 | 157 | Byte MapperSxROM::readCHR(Address addr) 158 | { 159 | if (m_usesCharacterRAM) 160 | { 161 | if (addr < 0x1000) 162 | return m_characterRAM[m_firstBankCHRIdx + addr]; 163 | else 164 | return m_characterRAM[m_secondBankCHRIdx + (addr & 0xfff)]; 165 | } 166 | else 167 | { 168 | if (addr < 0x1000) 169 | return m_cartridge.getVROM()[m_firstBankCHRIdx + addr]; 170 | else 171 | return m_cartridge.getVROM()[m_secondBankCHRIdx + (addr & 0xfff)]; 172 | } 173 | } 174 | 175 | void MapperSxROM::writeCHR(Address addr, Byte value) 176 | { 177 | if (m_usesCharacterRAM) 178 | { 179 | if (addr < 0x1000) 180 | m_characterRAM[m_firstBankCHRIdx + addr] = value; 181 | else 182 | m_characterRAM[m_secondBankCHRIdx + (addr & 0xfff)] = value; 183 | } 184 | else 185 | { 186 | LOG(Info) << "Read-only CHR memory write attempt at " << std::hex << addr << std::endl; 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/MapperUxROM.cpp: -------------------------------------------------------------------------------- 1 | #include "MapperUxROM.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | MapperUxROM::MapperUxROM(Cartridge& cart) 7 | : Mapper(cart, Mapper::UxROM) 8 | , m_selectPRG(0) 9 | { 10 | if (cart.getVROM().size() == 0) 11 | { 12 | m_usesCharacterRAM = true; 13 | m_characterRAM.resize(0x2000); 14 | LOG(Info) << "Uses character RAM" << std::endl; 15 | } 16 | else 17 | m_usesCharacterRAM = false; 18 | 19 | m_lastBankPtr = &cart.getROM()[cart.getROM().size() - 0x4000]; // last - 16KB 20 | } 21 | 22 | Byte MapperUxROM::readPRG(Address addr) 23 | { 24 | if (addr < 0xc000) 25 | return m_cartridge.getROM()[((addr - 0x8000) & 0x3fff) | (m_selectPRG << 14)]; 26 | else 27 | return *(m_lastBankPtr + (addr & 0x3fff)); 28 | } 29 | 30 | void MapperUxROM::writePRG(Address, Byte value) 31 | { 32 | m_selectPRG = value; 33 | } 34 | 35 | Byte MapperUxROM::readCHR(Address addr) 36 | { 37 | if (m_usesCharacterRAM) 38 | return m_characterRAM[addr]; 39 | else 40 | return m_cartridge.getVROM()[addr]; 41 | } 42 | 43 | void MapperUxROM::writeCHR(Address addr, Byte value) 44 | { 45 | if (m_usesCharacterRAM) 46 | m_characterRAM[addr] = value; 47 | else 48 | LOG(Info) << "Read-only CHR memory write attempt at " << std::hex << addr << std::endl; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/PPU.cpp: -------------------------------------------------------------------------------- 1 | #include "PPU.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | PPU::PPU(PictureBus& bus, VirtualScreen& screen) 7 | : m_bus(bus) 8 | , m_screen(screen) 9 | , m_spriteMemory(64 * 4) 10 | , m_pictureBuffer(ScanlineVisibleDots, std::vector(VisibleScanlines, sf::Color::Magenta)) 11 | { 12 | } 13 | 14 | void PPU::reset() 15 | { 16 | m_longSprites = m_generateInterrupt = m_greyscaleMode = m_vblank = m_spriteOverflow = false; 17 | m_showBackground = m_showSprites = m_evenFrame = m_firstWrite = true; 18 | m_bgPage = m_sprPage = Low; 19 | m_dataAddress = m_cycle = m_scanline = m_spriteDataAddress = m_fineXScroll = m_tempAddress = 0; 20 | // m_baseNameTable = 0x2000; 21 | m_dataAddrIncrement = 1; 22 | m_pipelineState = PreRender; 23 | m_scanlineSprites.reserve(8); 24 | m_scanlineSprites.resize(0); 25 | } 26 | 27 | void PPU::setInterruptCallback(std::function cb) 28 | { 29 | m_vblankCallback = cb; 30 | } 31 | 32 | void PPU::step() 33 | { 34 | switch (m_pipelineState) 35 | { 36 | case PreRender: 37 | if (m_cycle == 1) 38 | m_vblank = m_sprZeroHit = false; 39 | else if (m_cycle == ScanlineVisibleDots + 2 && m_showBackground && m_showSprites) 40 | { 41 | // Set bits related to horizontal position 42 | m_dataAddress &= ~0x41f; // Unset horizontal bits 43 | m_dataAddress |= m_tempAddress & 0x41f; // Copy 44 | } 45 | else if (m_cycle > 280 && m_cycle <= 304 && m_showBackground && m_showSprites) 46 | { 47 | // Set vertical bits 48 | m_dataAddress &= ~0x7be0; // Unset bits related to horizontal 49 | m_dataAddress |= m_tempAddress & 0x7be0; // Copy 50 | } 51 | // if (m_cycle > 257 && m_cycle < 320) 52 | // m_spriteDataAddress = 0; 53 | // if rendering is on, every other frame is one cycle shorter 54 | if (m_cycle >= ScanlineEndCycle - (!m_evenFrame && m_showBackground && m_showSprites)) 55 | { 56 | m_pipelineState = Render; 57 | m_cycle = m_scanline = 0; 58 | } 59 | 60 | // add IRQ support for MMC3 61 | if (m_cycle == 260 && m_showBackground && m_showSprites) 62 | { 63 | m_bus.scanlineIRQ(); 64 | } 65 | break; 66 | case Render: 67 | if (m_cycle > 0 && m_cycle <= ScanlineVisibleDots) 68 | { 69 | Byte bgColor = 0, sprColor = 0; 70 | bool bgOpaque = false, sprOpaque = true; 71 | bool spriteForeground = false; 72 | 73 | int x = m_cycle - 1; 74 | int y = m_scanline; 75 | 76 | if (m_showBackground) 77 | { 78 | auto x_fine = (m_fineXScroll + x) % 8; 79 | if (!m_hideEdgeBackground || x >= 8) 80 | { 81 | // fetch tile 82 | auto addr = 0x2000 | (m_dataAddress & 0x0FFF); // mask off fine y 83 | // auto addr = 0x2000 + x / 8 + (y / 8) * (ScanlineVisibleDots / 8); 84 | Byte tile = read(addr); 85 | 86 | // fetch pattern 87 | // Each pattern occupies 16 bytes, so multiply by 16 88 | addr = (tile * 16) + ((m_dataAddress >> 12 /*y % 8*/) & 0x7); // Add fine y 89 | addr |= m_bgPage << 12; // set whether the pattern is in the high or low page 90 | // Get the corresponding bit determined by (8 - x_fine) from the right 91 | bgColor = (read(addr) >> (7 ^ x_fine)) & 1; // bit 0 of palette entry 92 | bgColor |= ((read(addr + 8) >> (7 ^ x_fine)) & 1) << 1; // bit 1 93 | 94 | bgOpaque = bgColor; // flag used to calculate final pixel with the sprite pixel 95 | 96 | // fetch attribute and calculate higher two bits of palette 97 | addr = 98 | 0x23C0 | (m_dataAddress & 0x0C00) | ((m_dataAddress >> 4) & 0x38) | ((m_dataAddress >> 2) & 0x07); 99 | auto attribute = read(addr); 100 | int shift = ((m_dataAddress >> 4) & 4) | (m_dataAddress & 2); 101 | // Extract and set the upper two bits for the color 102 | bgColor |= ((attribute >> shift) & 0x3) << 2; 103 | } 104 | // Increment/wrap coarse X 105 | if (x_fine == 7) 106 | { 107 | if ((m_dataAddress & 0x001F) == 31) // if coarse X == 31 108 | { 109 | m_dataAddress &= ~0x001F; // coarse X = 0 110 | m_dataAddress ^= 0x0400; // switch horizontal nametable 111 | } 112 | else 113 | { 114 | m_dataAddress += 1; // increment coarse X 115 | } 116 | } 117 | } 118 | 119 | if (m_showSprites && (!m_hideEdgeSprites || x >= 8)) 120 | { 121 | for (auto i : m_scanlineSprites) 122 | { 123 | Byte spr_x = m_spriteMemory[i * 4 + 3]; 124 | 125 | if (0 > x - spr_x || x - spr_x >= 8) 126 | continue; 127 | 128 | Byte spr_y = m_spriteMemory[i * 4 + 0] + 1, tile = m_spriteMemory[i * 4 + 1], 129 | attribute = m_spriteMemory[i * 4 + 2]; 130 | 131 | int length = (m_longSprites) ? 16 : 8; 132 | 133 | int x_shift = (x - spr_x) % 8, y_offset = (y - spr_y) % length; 134 | 135 | if ((attribute & 0x40) == 0) // If NOT flipping horizontally 136 | x_shift ^= 7; 137 | if ((attribute & 0x80) != 0) // IF flipping vertically 138 | y_offset ^= (length - 1); 139 | 140 | Address addr = 0; 141 | 142 | if (!m_longSprites) 143 | { 144 | addr = tile * 16 + y_offset; 145 | if (m_sprPage == High) 146 | addr += 0x1000; 147 | } 148 | else // 8x16 sprites 149 | { 150 | // bit-3 is one if it is the bottom tile of the sprite, multiply by two to get the next pattern 151 | y_offset = (y_offset & 7) | ((y_offset & 8) << 1); 152 | addr = (tile >> 1) * 32 + y_offset; 153 | addr |= (tile & 1) << 12; // Bank 0x1000 if bit-0 is high 154 | } 155 | 156 | sprColor |= (read(addr) >> (x_shift)) & 1; // bit 0 of palette entry 157 | sprColor |= ((read(addr + 8) >> (x_shift)) & 1) << 1; // bit 1 158 | 159 | if (!(sprOpaque = sprColor)) 160 | { 161 | sprColor = 0; 162 | continue; 163 | } 164 | 165 | sprColor |= 0x10; // Select sprite palette 166 | sprColor |= (attribute & 0x3) << 2; // bits 2-3 167 | 168 | spriteForeground = !(attribute & 0x20); 169 | 170 | // Sprite-0 hit detection 171 | if (!m_sprZeroHit && m_showBackground && i == 0 && sprOpaque && bgOpaque) 172 | { 173 | m_sprZeroHit = true; 174 | } 175 | 176 | break; // Exit the loop now since we've found the highest priority sprite 177 | } 178 | } 179 | 180 | Byte paletteAddr = bgColor; 181 | 182 | if ((!bgOpaque && sprOpaque) || (bgOpaque && sprOpaque && spriteForeground)) 183 | paletteAddr = sprColor; 184 | else if (!bgOpaque && !sprOpaque) 185 | paletteAddr = 0; 186 | // else bgColor 187 | 188 | m_pictureBuffer[x][y] = sf::Color(colors[m_bus.readPalette(paletteAddr)]); 189 | } 190 | else if (m_cycle == ScanlineVisibleDots + 1 && m_showBackground) 191 | { 192 | // Shamelessly copied from nesdev wiki 193 | if ((m_dataAddress & 0x7000) != 0x7000) // if fine Y < 7 194 | m_dataAddress += 0x1000; // increment fine Y 195 | else 196 | { 197 | m_dataAddress &= ~0x7000; // fine Y = 0 198 | int y = (m_dataAddress & 0x03E0) >> 5; // let y = coarse Y 199 | if (y == 29) 200 | { 201 | y = 0; // coarse Y = 0 202 | m_dataAddress ^= 0x0800; // switch vertical nametable 203 | } 204 | else if (y == 31) 205 | y = 0; // coarse Y = 0, nametable not switched 206 | else 207 | y += 1; // increment coarse Y 208 | m_dataAddress = (m_dataAddress & ~0x03E0) | (y << 5); 209 | // put coarse Y back into m_dataAddress 210 | } 211 | } 212 | else if (m_cycle == ScanlineVisibleDots + 2 && m_showBackground && m_showSprites) 213 | { 214 | // Copy bits related to horizontal position 215 | m_dataAddress &= ~0x41f; 216 | m_dataAddress |= m_tempAddress & 0x41f; 217 | } 218 | 219 | // if (m_cycle > 257 && m_cycle < 320) 220 | // m_spriteDataAddress = 0; 221 | 222 | // add IRQ support for MMC3 223 | if (m_cycle == 260 && m_showBackground && m_showSprites) 224 | { 225 | m_bus.scanlineIRQ(); 226 | } 227 | 228 | if (m_cycle >= ScanlineEndCycle) 229 | { 230 | // Find and index sprites that are on the next Scanline 231 | // This isn't where/when this indexing, actually copying in 2C02 is done 232 | // but (I think) it shouldn't hurt any games if this is done here 233 | 234 | m_scanlineSprites.resize(0); 235 | 236 | int range = 8; 237 | if (m_longSprites) 238 | { 239 | range = 16; 240 | } 241 | 242 | std::size_t j = 0; 243 | for (std::size_t i = m_spriteDataAddress / 4; i < 64; ++i) 244 | { 245 | auto diff = (m_scanline - m_spriteMemory[i * 4]); 246 | if (0 <= diff && diff < range) 247 | { 248 | if (j >= 8) 249 | { 250 | m_spriteOverflow = true; 251 | break; 252 | } 253 | m_scanlineSprites.push_back(i); 254 | ++j; 255 | } 256 | } 257 | 258 | ++m_scanline; 259 | m_cycle = 0; 260 | } 261 | 262 | if (m_scanline >= VisibleScanlines) 263 | m_pipelineState = PostRender; 264 | 265 | break; 266 | case PostRender: 267 | if (m_cycle >= ScanlineEndCycle) 268 | { 269 | ++m_scanline; 270 | m_cycle = 0; 271 | m_pipelineState = VerticalBlank; 272 | 273 | for (std::size_t x = 0; x < m_pictureBuffer.size(); ++x) 274 | { 275 | for (std::size_t y = 0; y < m_pictureBuffer[0].size(); ++y) 276 | { 277 | m_screen.setPixel(x, y, m_pictureBuffer[x][y]); 278 | } 279 | } 280 | } 281 | 282 | break; 283 | case VerticalBlank: 284 | if (m_cycle == 1 && m_scanline == VisibleScanlines + 1) 285 | { 286 | m_vblank = true; 287 | if (m_generateInterrupt) 288 | m_vblankCallback(); 289 | } 290 | 291 | if (m_cycle >= ScanlineEndCycle) 292 | { 293 | ++m_scanline; 294 | m_cycle = 0; 295 | } 296 | 297 | if (m_scanline >= FrameEndScanline) 298 | { 299 | m_pipelineState = PreRender; 300 | m_scanline = 0; 301 | m_evenFrame = !m_evenFrame; 302 | } 303 | 304 | break; 305 | default: 306 | LOG(Error) << "Well, this shouldn't have happened." << std::endl; 307 | } 308 | 309 | ++m_cycle; 310 | } 311 | 312 | Byte PPU::readOAM(Byte addr) 313 | { 314 | return m_spriteMemory[addr]; 315 | } 316 | 317 | void PPU::writeOAM(Byte addr, Byte value) 318 | { 319 | m_spriteMemory[addr] = value; 320 | } 321 | 322 | void PPU::doDMA(const Byte* page_ptr) 323 | { 324 | std::memcpy(m_spriteMemory.data() + m_spriteDataAddress, page_ptr, 256 - m_spriteDataAddress); 325 | if (m_spriteDataAddress) 326 | std::memcpy(m_spriteMemory.data(), page_ptr + (256 - m_spriteDataAddress), m_spriteDataAddress); 327 | } 328 | 329 | void PPU::control(Byte ctrl) 330 | { 331 | m_generateInterrupt = ctrl & 0x80; 332 | m_longSprites = ctrl & 0x20; 333 | m_bgPage = static_cast(!!(ctrl & 0x10)); 334 | m_sprPage = static_cast(!!(ctrl & 0x8)); 335 | if (ctrl & 0x4) 336 | m_dataAddrIncrement = 0x20; 337 | else 338 | m_dataAddrIncrement = 1; 339 | // m_baseNameTable = (ctrl & 0x3) * 0x400 + 0x2000; 340 | 341 | // Set the nametable in the temp address, this will be reflected in the data address during rendering 342 | m_tempAddress &= ~0xc00; // Unset 343 | m_tempAddress |= (ctrl & 0x3) << 10; // Set according to ctrl bits 344 | } 345 | 346 | void PPU::setMask(Byte mask) 347 | { 348 | m_greyscaleMode = mask & 0x1; 349 | m_hideEdgeBackground = !(mask & 0x2); 350 | m_hideEdgeSprites = !(mask & 0x4); 351 | m_showBackground = mask & 0x8; 352 | m_showSprites = mask & 0x10; 353 | } 354 | 355 | Byte PPU::getStatus() 356 | { 357 | Byte status = m_spriteOverflow << 5 | m_sprZeroHit << 6 | m_vblank << 7; 358 | // m_dataAddress = 0; 359 | 360 | // Reading status clears vblank! 361 | m_vblank = false; 362 | m_firstWrite = true; 363 | return status; 364 | } 365 | 366 | void PPU::setDataAddress(Byte addr) 367 | { 368 | // m_dataAddress = ((m_dataAddress << 8) & 0xff00) | addr; 369 | if (m_firstWrite) 370 | { 371 | m_tempAddress &= ~0xff00; // Unset the upper byte 372 | m_tempAddress |= (addr & 0x3f) << 8; 373 | m_firstWrite = false; 374 | } 375 | else 376 | { 377 | m_tempAddress &= ~0xff; // Unset the lower byte; 378 | m_tempAddress |= addr; 379 | m_dataAddress = m_tempAddress; 380 | m_firstWrite = true; 381 | } 382 | } 383 | 384 | Byte PPU::getData() 385 | { 386 | auto data = m_bus.read(m_dataAddress); 387 | m_dataAddress += m_dataAddrIncrement; 388 | 389 | // Reads are delayed by one byte/read when address is in this range 390 | if (m_dataAddress < 0x3f00) 391 | { 392 | // Return from the data buffer and store the current value in the buffer 393 | std::swap(data, m_dataBuffer); 394 | } 395 | 396 | return data; 397 | } 398 | 399 | Byte PPU::getOAMData() 400 | { 401 | return readOAM(m_spriteDataAddress); 402 | } 403 | 404 | void PPU::setData(Byte data) 405 | { 406 | m_bus.write(m_dataAddress, data); 407 | m_dataAddress += m_dataAddrIncrement; 408 | } 409 | 410 | void PPU::setOAMAddress(Byte addr) 411 | { 412 | m_spriteDataAddress = addr; 413 | } 414 | 415 | void PPU::setOAMData(Byte value) 416 | { 417 | writeOAM(m_spriteDataAddress++, value); 418 | } 419 | 420 | void PPU::setScroll(Byte scroll) 421 | { 422 | if (m_firstWrite) 423 | { 424 | m_tempAddress &= ~0x1f; 425 | m_tempAddress |= (scroll >> 3) & 0x1f; 426 | m_fineXScroll = scroll & 0x7; 427 | m_firstWrite = false; 428 | } 429 | else 430 | { 431 | m_tempAddress &= ~0x73e0; 432 | m_tempAddress |= ((scroll & 0x7) << 12) | ((scroll & 0xf8) << 2); 433 | m_firstWrite = true; 434 | } 435 | } 436 | 437 | Byte PPU::read(Address addr) 438 | { 439 | return m_bus.read(addr); 440 | } 441 | 442 | } 443 | -------------------------------------------------------------------------------- /src/PictureBus.cpp: -------------------------------------------------------------------------------- 1 | #include "PictureBus.h" 2 | #include "Log.h" 3 | 4 | namespace sn 5 | { 6 | 7 | PictureBus::PictureBus() 8 | : m_palette(0x20) 9 | , m_RAM(0x800) 10 | , m_mapper(nullptr) 11 | { 12 | } 13 | 14 | Byte PictureBus::read(Address addr) 15 | { 16 | // PictureBus is limited to 0x3fff 17 | addr = addr & 0x3fff; 18 | 19 | if (addr < 0x2000) 20 | { 21 | return m_mapper->readCHR(addr); 22 | } 23 | else if (addr <= 0x3eff) 24 | { 25 | const auto index = addr & 0x3ff; 26 | // Name tables upto 0x3000, then mirrored upto 3eff 27 | auto normalizedAddr = addr; 28 | if (addr >= 0x3000) 29 | { 30 | normalizedAddr -= 0x1000; 31 | } 32 | 33 | if (NameTable0 >= m_RAM.size()) 34 | return m_mapper->readCHR(normalizedAddr); 35 | else if (normalizedAddr < 0x2400) // NT0 36 | return m_RAM[NameTable0 + index]; 37 | else if (normalizedAddr < 0x2800) // NT1 38 | return m_RAM[NameTable1 + index]; 39 | else if (normalizedAddr < 0x2c00) // NT2 40 | return m_RAM[NameTable2 + index]; 41 | else /* if (normalizedAddr < 0x3000)*/ // NT3 42 | return m_RAM[NameTable3 + index]; 43 | } 44 | else if (addr <= 0x3fff) 45 | { 46 | auto paletteAddr = addr & 0x1f; 47 | return readPalette(paletteAddr); 48 | } 49 | return 0; 50 | } 51 | 52 | Byte PictureBus::readPalette(Byte paletteAddr) 53 | { 54 | // Addresses $3F10/$3F14/$3F18/$3F1C are mirrors of $3F00/$3F04/$3F08/$3F0C 55 | if (paletteAddr >= 0x10 && paletteAddr % 4 == 0) 56 | { 57 | paletteAddr = paletteAddr & 0xf; 58 | } 59 | return m_palette[paletteAddr]; 60 | } 61 | 62 | void PictureBus::write(Address addr, Byte value) 63 | { 64 | // PictureBus is limited to 0x3fff 65 | addr = addr & 0x3fff; 66 | 67 | if (addr < 0x2000) 68 | { 69 | m_mapper->writeCHR(addr, value); 70 | } 71 | else if (addr <= 0x3eff) 72 | { 73 | const auto index = addr & 0x3ff; 74 | // Name tables upto 0x3000, then mirrored upto 3eff 75 | auto normalizedAddr = addr; 76 | if (addr >= 0x3000) 77 | { 78 | normalizedAddr -= 0x1000; 79 | } 80 | 81 | if (NameTable0 >= m_RAM.size()) 82 | m_mapper->writeCHR(normalizedAddr, value); 83 | else if (normalizedAddr < 0x2400) // NT0 84 | m_RAM[NameTable0 + index] = value; 85 | else if (normalizedAddr < 0x2800) // NT1 86 | m_RAM[NameTable1 + index] = value; 87 | else if (normalizedAddr < 0x2c00) // NT2 88 | m_RAM[NameTable2 + index] = value; 89 | else // NT3 90 | m_RAM[NameTable3 + index] = value; 91 | } 92 | else if (addr <= 0x3fff) 93 | { 94 | auto palette = addr & 0x1f; 95 | // Addresses $3F10/$3F14/$3F18/$3F1C are mirrors of $3F00/$3F04/$3F08/$3F0C 96 | if (palette >= 0x10 && addr % 4 == 0) 97 | { 98 | palette = palette & 0xf; 99 | } 100 | m_palette[palette] = value; 101 | } 102 | } 103 | 104 | void PictureBus::updateMirroring() 105 | { 106 | switch (m_mapper->getNameTableMirroring()) 107 | { 108 | case Horizontal: 109 | NameTable0 = NameTable1 = 0; 110 | NameTable2 = NameTable3 = 0x400; 111 | LOG(InfoVerbose) << "Horizontal Name Table mirroring set. (Vertical Scrolling)" << std::endl; 112 | break; 113 | case Vertical: 114 | NameTable0 = NameTable2 = 0; 115 | NameTable1 = NameTable3 = 0x400; 116 | LOG(InfoVerbose) << "Vertical Name Table mirroring set. (Horizontal Scrolling)" << std::endl; 117 | break; 118 | case OneScreenLower: 119 | NameTable0 = NameTable1 = NameTable2 = NameTable3 = 0; 120 | LOG(InfoVerbose) << "Single Screen mirroring set with lower bank." << std::endl; 121 | break; 122 | case OneScreenHigher: 123 | NameTable0 = NameTable1 = NameTable2 = NameTable3 = 0x400; 124 | LOG(InfoVerbose) << "Single Screen mirroring set with higher bank." << std::endl; 125 | break; 126 | case FourScreen: 127 | NameTable0 = m_RAM.size(); 128 | LOG(InfoVerbose) << "FourScreen mirroring." << std::endl; 129 | break; 130 | default: 131 | NameTable0 = NameTable1 = NameTable2 = NameTable3 = 0; 132 | LOG(Error) << "Unsupported Name Table mirroring : " << m_mapper->getNameTableMirroring() << std::endl; 133 | } 134 | } 135 | 136 | bool PictureBus::setMapper(Mapper* mapper) 137 | { 138 | if (!mapper) 139 | { 140 | LOG(Error) << "Mapper argument is nullptr" << std::endl; 141 | return false; 142 | } 143 | 144 | m_mapper = mapper; 145 | updateMirroring(); 146 | return true; 147 | } 148 | 149 | void PictureBus::scanlineIRQ() 150 | { 151 | m_mapper->scanlineIRQ(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/VirtualScreen.cpp: -------------------------------------------------------------------------------- 1 | #include "VirtualScreen.h" 2 | 3 | namespace sn 4 | { 5 | void VirtualScreen::create(unsigned int w, unsigned int h, float pixel_size, sf::Color color) 6 | { 7 | m_vertices.resize(w * h * 6); 8 | m_screenSize = { w, h }; 9 | m_vertices.setPrimitiveType(sf::Triangles); 10 | m_pixelSize = pixel_size; 11 | for (std::size_t x = 0; x < w; ++x) 12 | { 13 | for (std::size_t y = 0; y < h; ++y) 14 | { 15 | auto index = (x * m_screenSize.y + y) * 6; 16 | sf::Vector2f coord2d(x * m_pixelSize, y * m_pixelSize); 17 | 18 | // Triangle-1 19 | // top-left 20 | m_vertices[index].position = coord2d; 21 | m_vertices[index].color = color; 22 | 23 | // top-right 24 | m_vertices[index + 1].position = coord2d + sf::Vector2f { m_pixelSize, 0 }; 25 | m_vertices[index + 1].color = color; 26 | 27 | // bottom-right 28 | m_vertices[index + 2].position = coord2d + sf::Vector2f { m_pixelSize, m_pixelSize }; 29 | m_vertices[index + 2].color = color; 30 | 31 | // Triangle-2 32 | // bottom-right 33 | m_vertices[index + 3].position = coord2d + sf::Vector2f { m_pixelSize, m_pixelSize }; 34 | m_vertices[index + 3].color = color; 35 | 36 | // bottom-left 37 | m_vertices[index + 4].position = coord2d + sf::Vector2f { 0, m_pixelSize }; 38 | m_vertices[index + 4].color = color; 39 | 40 | // top-left 41 | m_vertices[index + 5].position = coord2d; 42 | m_vertices[index + 5].color = color; 43 | } 44 | } 45 | } 46 | 47 | void VirtualScreen::setPixel(std::size_t x, std::size_t y, sf::Color color) 48 | { 49 | auto index = (x * m_screenSize.y + y) * 6; 50 | if (index >= m_vertices.getVertexCount()) 51 | return; 52 | 53 | sf::Vector2f coord2d(x * m_pixelSize, y * m_pixelSize); 54 | 55 | // Triangle-1 56 | // top-left 57 | m_vertices[index].color = color; 58 | 59 | // top-right 60 | m_vertices[index + 1].color = color; 61 | 62 | // bottom-right 63 | m_vertices[index + 2].color = color; 64 | 65 | // Triangle-2 66 | // bottom-right 67 | m_vertices[index + 3].color = color; 68 | 69 | // bottom-left 70 | m_vertices[index + 4].color = color; 71 | 72 | // top-left 73 | m_vertices[index + 5].color = color; 74 | } 75 | 76 | void VirtualScreen::draw(sf::RenderTarget& target, sf::RenderStates states) const 77 | { 78 | target.draw(m_vertices, states); 79 | } 80 | 81 | } --------------------------------------------------------------------------------