diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2524c58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +### made using snippets from https://github.com/github/gitignore + +### C++ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +### CMake +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..92bb6f1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.5) + +# config project +project(sdgbc CXX) +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# enable debug symbols if no build type is specified +if(CMAKE_BUILD_TYPE STREQUAL "") + set(CMAKE_BUILD_TYPE Debug) +endif() + +# define local include dir +include_directories(include) + +# define executable sources +file(GLOB sdgbc_SOURCES + include/*.h + include/audio/*.h + include/debug/*.h + include/hw/*.h + include/hw/apu/*.h + include/hw/cart/*.h + include/hw/cpu/*.h + include/wxui/*.h + include/wxui/winrc/*.rc # windows resource files + include/wxui/xpm/*.h # icon xpms + include/wxui/xpm/*.xpm + + src/*.cpp + src/audio/*.cpp + src/debug/*.cpp + src/hw/*.cpp + src/hw/apu/*.cpp + src/hw/cart/*.cpp + src/hw/cpu/*.cpp + src/wxui/*.cpp +) + +add_executable(sdgbc WIN32 ${sdgbc_SOURCES}) + +# macro defs for msvc to disable unsafe warnings from wxWidgets headers +if(MSVC) + target_compile_definitions(sdgbc PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() + +# setup our modules path +set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH}) + +# if UNIX, find X11 and also find PkgConfig and use it to find GTK +if(UNIX) + find_package(X11 REQUIRED) + if(X11_FOUND) + message(STATUS "X11 found") + include_directories(${X11_INCLUDE_DIR}) + target_link_libraries(sdgbc ${X11_LIBRARIES}) + endif() + + find_package(PkgConfig REQUIRED) + if(PkgConfig_FOUND) + message(STATUS "PkgConfig found") + + pkg_check_modules(GTK REQUIRED gtk+-2.0) + if(GTK_FOUND) + message(STATUS "GTK found") + include_directories(${GTK_INCLUDE_DIRS}) + link_directories(${GTK_LIBRARY_DIRS}) + add_definitions(${GTK_CFLAGS_OTHER}) + target_link_libraries(sdgbc ${GTK_LIBRARIES}) + endif() + endif() +endif() + +# find wxWidgets +find_package(wxWidgets 3.0 REQUIRED core base) +if(wxWidgets_FOUND) + message(STATUS "wxWidgets found") + include(${wxWidgets_USE_FILE}) + target_link_libraries(sdgbc ${wxWidgets_LIBRARIES}) +endif() + +# find SFML +# NOTE: any version above 2.3.1 causes the program to crash due to an X11 +# incompatability for some reason +find_package(SFML 2.3.1 EXACT REQUIRED system window graphics audio) +if(SFML_FOUND) + message(STATUS "SFML found") + include_directories(${SFML_INCLUDE_DIR}) + target_link_libraries(sdgbc ${SFML_LIBRARIES}) +endif() + +# install target and extra pre-reqs +install(TARGETS sdgbc DESTINATION bin) +include(InstallRequiredSystemLibraries) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..efce366 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017-2018 Sean Dewar (seandewar @ Github) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..984bdc8 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# sdgbc + +A software emulator for the Nintendo Game Boy Color written in C++ using SFML +and wxWidgets. Available for Windows and Linux (tested using the MSVC and GCC +compilers). + +![img](https://github.com/seandewar/sdgbc/blob/master/docs/img/gui_pkmn_crystal.png?raw=true "sdgbc running Pokémon Crystal Version") +![img](https://github.com/seandewar/sdgbc/blob/master/docs/img/gui_toy_story_racer.png?raw=true "sdgbc running Toy Story Racer") +![img](https://github.com/seandewar/sdgbc/blob/master/docs/img/gui_loz_dx_dmg_mode.png?raw=true "sdgbc running The Legend of Zelda: Link's Awakening in DMG mode") + +This emulator can run most Game Boy and Game Boy Color games from their ROM dump +files to an acceptable level of accuracy, including those requiring MBC1, MBC2, MBC3 +(with minimal RTC) and MBC5 (without Rumble) support. + +sdgbc was developed as a final year BSc. computer science project at the University +of Leicester, UK. +A copy of the submitted dissertation and viva presentation slides can be +found in the `docs` directory. + + +## Building + +sdgbc is configured to be built using CMake (version 3.5 or above). The code relies +on some C++14 features, and thus, requires a C++14 standard-compliant compiler. + +The following instructions assume some familiarity with CMake. Running `cmake-gui` +or `ccmake` is recommended to easily follow these instructions: + + +### Building for Windows + +*The following has been tested using CMake 3.5, MSVC 14, wxWidgets 3.0.1 and SFML +2.4.1:* + +1. Launch CMake. +2. Set the source code directory to the sdgbc repository root directory. +3. Set the binary output directory to any directory you wish. +4. Create a `PATH` cache variable called `SFML_ROOT`. Set it to root directory of +your SFML installation. +5. Create a `PATH` cache variable called `wxWidgets_ROOT_DIR`. Set it to root +directory of your wxWidgets installation. +6. Create a `PATH` cache variable called `wxWidgets_LIB_DIR`. Set it to the +`lib\vc140_dll` directory of your wxWidgets installation. +7. Configure the project using the `Visual Studio 14 2015` generator. +8. Generate the project file. A `sdgbc.sln` Visual Studio solution file will be +created inside of the specified binary output directory. + +After opening the solution file in Visual Studio, press Ctrl+F5 to build and run +the software (without debugging). + + +### Building for Linux + +*The following has been tested using CMake 3.5, GCC 5.4, wxWidgets 3.0.1 and SFML +2.3.1:* +**(You may experience some X11 compatibility issues running the software with any +version of SFML greater than 2.3.1!)** + +1. Launch CMake. +2. Set the source code directory to the sdgbc repository root directory. +3. Set the binary output directory to any directory you wish. +4. Create a `PATH` cache variable called `SFML_ROOT`. Set it to root directory of +your SFML installation. +5. Configure the project using the `Unix Makefile` generator. +6. Generate the makefile. It will be created inside of the specified binary output +directory. + +Using a shell, change your current working directory to the binary output directory +and run `make`. +This can be done using sh or bash by running the following command: + +```bash +cd BINARY_DIR_PATH && make +``` + + +## Running + +The emulator can be ran in a GUI mode by simply opening the built `sdgbc` executable +file. The emulated joypad can be interacted with using the keyboard as follows: + +| Joypad Key | Keyboard Key | +| ---------- | ------------ | +| D-pad Keys | Arrow Keys | +| B | Z | +| A | X | +| Start | Return | +| Select | Shift | + +![img](https://github.com/seandewar/sdgbc/blob/master/docs/img/cpu_cmd.png?raw=true "sdgbc running with the --cpu-cmd command-line argument") + +Additionally, a command-line interface for debugging the emulated CPU is +available for use. It can be accessed by passing the `--cpu-cmd` argument to +the program. A list of commands can be found by executing the `help` command. + +Optionally, a path to a GB or GBC ROM file can be given via the command-line +arguments for both modes: +* `sdgbc ROM_PATH` *(for the main GUI mode)* +* `sdgbc --cpu-cmd ROM_PATH` *(for the command-line CPU debugger mode)* + + +## License + +The software is distributed using the MIT license as follows: + +``` +The MIT License (MIT) + +Copyright (c) 2017-2018 Sean Dewar (seandewar @ Github) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` diff --git a/cmake/modules/FindSFML.cmake b/cmake/modules/FindSFML.cmake new file mode 100644 index 0000000..7570dd5 --- /dev/null +++ b/cmake/modules/FindSFML.cmake @@ -0,0 +1,365 @@ +# This script locates the SFML library +# ------------------------------------ +# +# Usage +# ----- +# +# When you try to locate the SFML libraries, you must specify which modules you want to use (system, window, graphics, network, audio, main). +# If none is given, the SFML_LIBRARIES variable will be empty and you'll end up linking to nothing. +# example: +# find_package(SFML COMPONENTS graphics window system) # find the graphics, window and system modules +# +# You can enforce a specific version, either MAJOR.MINOR or only MAJOR. +# If nothing is specified, the version won't be checked (i.e. any version will be accepted). +# example: +# find_package(SFML COMPONENTS ...) # no specific version required +# find_package(SFML 2 COMPONENTS ...) # any 2.x version +# find_package(SFML 2.4 COMPONENTS ...) # version 2.4 or greater +# +# By default, the dynamic libraries of SFML will be found. To find the static ones instead, +# you must set the SFML_STATIC_LIBRARIES variable to TRUE before calling find_package(SFML ...). +# Since you have to link yourself all the SFML dependencies when you link it statically, the following +# additional variables are defined: SFML_XXX_DEPENDENCIES and SFML_DEPENDENCIES (see their detailed +# description below). +# In case of static linking, the SFML_STATIC macro will also be defined by this script. +# example: +# set(SFML_STATIC_LIBRARIES TRUE) +# find_package(SFML 2 COMPONENTS network system) +# +# On Mac OS X if SFML_STATIC_LIBRARIES is not set to TRUE then by default CMake will search for frameworks unless +# CMAKE_FIND_FRAMEWORK is set to "NEVER" for example. Please refer to CMake documentation for more details. +# Moreover, keep in mind that SFML frameworks are only available as release libraries unlike dylibs which +# are available for both release and debug modes. +# +# If SFML is not installed in a standard path, you can use the SFML_ROOT CMake (or environment) variable +# to tell CMake where SFML is. +# +# Output +# ------ +# +# This script defines the following variables: +# - For each specified module XXX (system, window, graphics, network, audio, main): +# - 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) +# - 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) +# - SFML_XXX_LIBRARY: the name of the library to link to for the xxx module (includes both debug and optimized names if necessary) +# - SFML_XXX_FOUND: true if either the debug or release library of the xxx module is found +# - SFML_XXX_DEPENDENCIES: the list of libraries the module depends on, in case of static linking +# - SFML_LIBRARIES: the list of all libraries corresponding to the required modules +# - SFML_FOUND: true if all the required modules are found +# - SFML_INCLUDE_DIR: the path where SFML headers are located (the directory containing the SFML/Config.hpp file) +# - SFML_DEPENDENCIES: the list of libraries SFML depends on, in case of static linking +# +# example: +# find_package(SFML 2 COMPONENTS system window graphics audio REQUIRED) +# include_directories(${SFML_INCLUDE_DIR}) +# add_executable(myapp ...) +# target_link_libraries(myapp ${SFML_LIBRARIES}) + +# define the SFML_STATIC macro if static build was chosen +if(SFML_STATIC_LIBRARIES) +add_definitions(-DSFML_STATIC) +endif() + +# define the list of search paths for headers and libraries +set(FIND_SFML_PATHS +${SFML_ROOT} +$ENV{SFML_ROOT} +~/Library/Frameworks +/Library/Frameworks +/usr/local +/usr +/sw +/opt/local +/opt/csw +/opt) + +# find the SFML include directory +find_path(SFML_INCLUDE_DIR SFML/Config.hpp + PATH_SUFFIXES include + PATHS ${FIND_SFML_PATHS}) + +# check the version number +set(SFML_VERSION_OK TRUE) +if(SFML_FIND_VERSION AND SFML_INCLUDE_DIR) +# extract the major and minor version numbers from SFML/Config.hpp +# we have to handle framework a little bit differently: +if("${SFML_INCLUDE_DIR}" MATCHES "SFML.framework") + set(SFML_CONFIG_HPP_INPUT "${SFML_INCLUDE_DIR}/Headers/Config.hpp") +else() + set(SFML_CONFIG_HPP_INPUT "${SFML_INCLUDE_DIR}/SFML/Config.hpp") +endif() +FILE(READ "${SFML_CONFIG_HPP_INPUT}" SFML_CONFIG_HPP_CONTENTS) +STRING(REGEX REPLACE ".*#define SFML_VERSION_MAJOR ([0-9]+).*" "\\1" SFML_VERSION_MAJOR "${SFML_CONFIG_HPP_CONTENTS}") +STRING(REGEX REPLACE ".*#define SFML_VERSION_MINOR ([0-9]+).*" "\\1" SFML_VERSION_MINOR "${SFML_CONFIG_HPP_CONTENTS}") +STRING(REGEX REPLACE ".*#define SFML_VERSION_PATCH ([0-9]+).*" "\\1" SFML_VERSION_PATCH "${SFML_CONFIG_HPP_CONTENTS}") +if (NOT "${SFML_VERSION_PATCH}" MATCHES "^[0-9]+$") + set(SFML_VERSION_PATCH 0) +endif() +math(EXPR SFML_REQUESTED_VERSION "${SFML_FIND_VERSION_MAJOR} * 10000 + ${SFML_FIND_VERSION_MINOR} * 100 + ${SFML_FIND_VERSION_PATCH}") + +# if we could extract them, compare with the requested version number +if (SFML_VERSION_MAJOR) + # transform version numbers to an integer + math(EXPR SFML_VERSION "${SFML_VERSION_MAJOR} * 10000 + ${SFML_VERSION_MINOR} * 100 + ${SFML_VERSION_PATCH}") + + # compare them + if(SFML_VERSION LESS SFML_REQUESTED_VERSION) + set(SFML_VERSION_OK FALSE) + endif() +else() + # SFML version is < 2.0 + if (SFML_REQUESTED_VERSION GREATER 10900) + set(SFML_VERSION_OK FALSE) + set(SFML_VERSION_MAJOR 1) + set(SFML_VERSION_MINOR x) + set(SFML_VERSION_PATCH x) + endif() +endif() +endif() + +# find the requested modules +set(SFML_FOUND TRUE) # will be set to false if one of the required modules is not found +foreach(FIND_SFML_COMPONENT ${SFML_FIND_COMPONENTS}) +string(TOLOWER ${FIND_SFML_COMPONENT} FIND_SFML_COMPONENT_LOWER) +string(TOUPPER ${FIND_SFML_COMPONENT} FIND_SFML_COMPONENT_UPPER) +set(FIND_SFML_COMPONENT_NAME sfml-${FIND_SFML_COMPONENT_LOWER}) + +# no suffix for sfml-main, it is always a static library +if(FIND_SFML_COMPONENT_LOWER STREQUAL "main") + # release library + find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE + NAMES ${FIND_SFML_COMPONENT_NAME} + PATH_SUFFIXES lib64 lib + PATHS ${FIND_SFML_PATHS}) + + # debug library + find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG + NAMES ${FIND_SFML_COMPONENT_NAME}-d + PATH_SUFFIXES lib64 lib + PATHS ${FIND_SFML_PATHS}) +else() + # static release library + find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_RELEASE + NAMES ${FIND_SFML_COMPONENT_NAME}-s + PATH_SUFFIXES lib64 lib + PATHS ${FIND_SFML_PATHS}) + + # static debug library + find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_DEBUG + NAMES ${FIND_SFML_COMPONENT_NAME}-s-d + PATH_SUFFIXES lib64 lib + PATHS ${FIND_SFML_PATHS}) + + # dynamic release library + find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_RELEASE + NAMES ${FIND_SFML_COMPONENT_NAME} + PATH_SUFFIXES lib64 lib + PATHS ${FIND_SFML_PATHS}) + + # dynamic debug library + find_library(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_DEBUG + NAMES ${FIND_SFML_COMPONENT_NAME}-d + PATH_SUFFIXES lib64 lib + PATHS ${FIND_SFML_PATHS}) + + # choose the entries that fit the requested link type + if(SFML_STATIC_LIBRARIES) + if(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_RELEASE) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_RELEASE}) + endif() + if(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_DEBUG) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_DEBUG}) + endif() + else() + if(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_RELEASE) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_RELEASE}) + endif() + if(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_DEBUG) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_DEBUG}) + endif() + endif() +endif() + +if (SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG OR SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE) + # library found + set(SFML_${FIND_SFML_COMPONENT_UPPER}_FOUND TRUE) + + # if both are found, set SFML_XXX_LIBRARY to contain both + if (SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG AND SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY debug ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG} + optimized ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE}) + endif() + + # if only one debug/release variant is found, set the other to be equal to the found one + if (SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG AND NOT SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE) + # debug and not release + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG}) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG}) + endif() + if (SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE AND NOT SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG) + # release and not debug + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE}) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY ${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE}) + endif() +else() + # library not found + set(SFML_FOUND FALSE) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_FOUND FALSE) + set(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY "") + set(FIND_SFML_MISSING "${FIND_SFML_MISSING} SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY") +endif() + +# mark as advanced +MARK_AS_ADVANCED(SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY + SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_RELEASE + SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DEBUG + SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_RELEASE + SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_STATIC_DEBUG + SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_RELEASE + SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY_DYNAMIC_DEBUG) + +# add to the global list of libraries +set(SFML_LIBRARIES ${SFML_LIBRARIES} "${SFML_${FIND_SFML_COMPONENT_UPPER}_LIBRARY}") +endforeach() + +# in case of static linking, we must also define the list of all the dependencies of SFML libraries +if(SFML_STATIC_LIBRARIES) + +# detect the OS +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + set(FIND_SFML_OS_WINDOWS 1) +elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux") + set(FIND_SFML_OS_LINUX 1) +elseif(${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") + set(FIND_SFML_OS_FREEBSD 1) +elseif(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + set(FIND_SFML_OS_MACOSX 1) +endif() + +# start with an empty list +set(SFML_DEPENDENCIES) +set(FIND_SFML_DEPENDENCIES_NOTFOUND) + +# macro that searches for a 3rd-party library +macro(find_sfml_dependency output friendlyname) + # No lookup in environment variables (PATH on Windows), as they may contain wrong library versions + find_library(${output} NAMES ${ARGN} PATHS ${FIND_SFML_PATHS} PATH_SUFFIXES lib NO_SYSTEM_ENVIRONMENT_PATH) + if(${${output}} STREQUAL "${output}-NOTFOUND") + unset(output) + set(FIND_SFML_DEPENDENCIES_NOTFOUND "${FIND_SFML_DEPENDENCIES_NOTFOUND} ${friendlyname}") + endif() +endmacro() + +# sfml-system +list(FIND SFML_FIND_COMPONENTS "system" FIND_SFML_SYSTEM_COMPONENT) +if(NOT ${FIND_SFML_SYSTEM_COMPONENT} EQUAL -1) + + # update the list -- these are only system libraries, no need to find them + if(FIND_SFML_OS_LINUX OR FIND_SFML_OS_FREEBSD OR FIND_SFML_OS_MACOSX) + set(SFML_SYSTEM_DEPENDENCIES "pthread") + endif() + if(FIND_SFML_OS_LINUX) + set(SFML_SYSTEM_DEPENDENCIES ${SFML_SYSTEM_DEPENDENCIES} "rt") + endif() + if(FIND_SFML_OS_WINDOWS) + set(SFML_SYSTEM_DEPENDENCIES "winmm") + endif() + set(SFML_DEPENDENCIES ${SFML_SYSTEM_DEPENDENCIES} ${SFML_DEPENDENCIES}) +endif() + +# sfml-network +list(FIND SFML_FIND_COMPONENTS "network" FIND_SFML_NETWORK_COMPONENT) +if(NOT ${FIND_SFML_NETWORK_COMPONENT} EQUAL -1) + + # update the list -- these are only system libraries, no need to find them + if(FIND_SFML_OS_WINDOWS) + set(SFML_NETWORK_DEPENDENCIES "ws2_32") + endif() + set(SFML_DEPENDENCIES ${SFML_NETWORK_DEPENDENCIES} ${SFML_DEPENDENCIES}) +endif() + +# sfml-window +list(FIND SFML_FIND_COMPONENTS "window" FIND_SFML_WINDOW_COMPONENT) +if(NOT ${FIND_SFML_WINDOW_COMPONENT} EQUAL -1) + + # find libraries + if(FIND_SFML_OS_LINUX OR FIND_SFML_OS_FREEBSD) + find_sfml_dependency(X11_LIBRARY "X11" X11) + find_sfml_dependency(XRANDR_LIBRARY "Xrandr" Xrandr) + endif() + + if(FIND_SFML_OS_LINUX) + find_sfml_dependency(UDEV_LIBRARIES "UDev" udev libudev) + endif() + + # update the list + if(FIND_SFML_OS_WINDOWS) + set(SFML_WINDOW_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} "opengl32" "winmm" "gdi32") + elseif(FIND_SFML_OS_LINUX) + set(SFML_WINDOW_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} "GL" ${X11_LIBRARY} ${XRANDR_LIBRARY} ${UDEV_LIBRARIES}) + elseif(FIND_SFML_OS_FREEBSD) + set(SFML_WINDOW_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} "GL" ${X11_LIBRARY} ${XRANDR_LIBRARY} "usbhid") + elseif(FIND_SFML_OS_MACOSX) + set(SFML_WINDOW_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} "-framework OpenGL -framework Foundation -framework AppKit -framework IOKit -framework Carbon") + endif() + set(SFML_DEPENDENCIES ${SFML_WINDOW_DEPENDENCIES} ${SFML_DEPENDENCIES}) +endif() + +# sfml-graphics +list(FIND SFML_FIND_COMPONENTS "graphics" FIND_SFML_GRAPHICS_COMPONENT) +if(NOT ${FIND_SFML_GRAPHICS_COMPONENT} EQUAL -1) + + # find libraries + find_sfml_dependency(FREETYPE_LIBRARY "FreeType" freetype) + find_sfml_dependency(JPEG_LIBRARY "libjpeg" jpeg) + + # update the list + set(SFML_GRAPHICS_DEPENDENCIES ${FREETYPE_LIBRARY} ${JPEG_LIBRARY}) + set(SFML_DEPENDENCIES ${SFML_GRAPHICS_DEPENDENCIES} ${SFML_DEPENDENCIES}) +endif() + +# sfml-audio +list(FIND SFML_FIND_COMPONENTS "audio" FIND_SFML_AUDIO_COMPONENT) +if(NOT ${FIND_SFML_AUDIO_COMPONENT} EQUAL -1) + + # find libraries + find_sfml_dependency(OPENAL_LIBRARY "OpenAL" openal openal32) + find_sfml_dependency(OGG_LIBRARY "Ogg" ogg) + find_sfml_dependency(VORBIS_LIBRARY "Vorbis" vorbis) + find_sfml_dependency(VORBISFILE_LIBRARY "VorbisFile" vorbisfile) + find_sfml_dependency(VORBISENC_LIBRARY "VorbisEnc" vorbisenc) + find_sfml_dependency(FLAC_LIBRARY "FLAC" FLAC) + + # update the list + set(SFML_AUDIO_DEPENDENCIES ${OPENAL_LIBRARY} ${FLAC_LIBRARY} ${VORBISENC_LIBRARY} ${VORBISFILE_LIBRARY} ${VORBIS_LIBRARY} ${OGG_LIBRARY}) + set(SFML_DEPENDENCIES ${SFML_DEPENDENCIES} ${SFML_AUDIO_DEPENDENCIES}) +endif() + +endif() + +# handle errors +if(NOT SFML_VERSION_OK) +# SFML version not ok +set(FIND_SFML_ERROR "SFML found but version too low (requested: ${SFML_FIND_VERSION}, found: ${SFML_VERSION_MAJOR}.${SFML_VERSION_MINOR}.${SFML_VERSION_PATCH})") +set(SFML_FOUND FALSE) +elseif(SFML_STATIC_LIBRARIES AND FIND_SFML_DEPENDENCIES_NOTFOUND) +set(FIND_SFML_ERROR "SFML found but some of its dependencies are missing (${FIND_SFML_DEPENDENCIES_NOTFOUND})") +set(SFML_FOUND FALSE) +elseif(NOT SFML_FOUND) +# include directory or library not found +set(FIND_SFML_ERROR "Could NOT find SFML (missing: ${FIND_SFML_MISSING})") +endif() +if (NOT SFML_FOUND) +if(SFML_FIND_REQUIRED) + # fatal error + message(FATAL_ERROR ${FIND_SFML_ERROR}) +elseif(NOT SFML_FIND_QUIETLY) + # error but continue + message("${FIND_SFML_ERROR}") +endif() +endif() + +# handle success +if(SFML_FOUND AND NOT SFML_FIND_QUIETLY) +message(STATUS "Found SFML ${SFML_VERSION_MAJOR}.${SFML_VERSION_MINOR}.${SFML_VERSION_PATCH} in ${SFML_INCLUDE_DIR}") +endif() diff --git a/docs/dissertation.docx b/docs/dissertation.docx new file mode 100644 index 0000000..7862188 Binary files /dev/null and b/docs/dissertation.docx differ diff --git a/docs/dissertation.pdf b/docs/dissertation.pdf new file mode 100644 index 0000000..87d09a3 Binary files /dev/null and b/docs/dissertation.pdf differ diff --git a/docs/img/cpu_cmd.png b/docs/img/cpu_cmd.png new file mode 100644 index 0000000..e4fbb90 Binary files /dev/null and b/docs/img/cpu_cmd.png differ diff --git a/docs/img/gui_loz_dx_dmg_mode.png b/docs/img/gui_loz_dx_dmg_mode.png new file mode 100644 index 0000000..2ff45de Binary files /dev/null and b/docs/img/gui_loz_dx_dmg_mode.png differ diff --git a/docs/img/gui_pkmn_crystal.png b/docs/img/gui_pkmn_crystal.png new file mode 100644 index 0000000..36471bd Binary files /dev/null and b/docs/img/gui_pkmn_crystal.png differ diff --git a/docs/img/gui_toy_story_racer.png b/docs/img/gui_toy_story_racer.png new file mode 100644 index 0000000..c875136 Binary files /dev/null and b/docs/img/gui_toy_story_racer.png differ diff --git a/docs/viva_presentation.pdf b/docs/viva_presentation.pdf new file mode 100644 index 0000000..8b5bcb4 Binary files /dev/null and b/docs/viva_presentation.pdf differ diff --git a/docs/viva_presentation.pptx b/docs/viva_presentation.pptx new file mode 100644 index 0000000..8271114 Binary files /dev/null and b/docs/viva_presentation.pptx differ diff --git a/include/audio/sfml_apu_out.h b/include/audio/sfml_apu_out.h new file mode 100644 index 0000000..4f31efe --- /dev/null +++ b/include/audio/sfml_apu_out.h @@ -0,0 +1,67 @@ +#ifndef SDGBC_SFML_APU_OUT_H_ +#define SDGBC_SFML_APU_OUT_H_ + +#include "hw/apu/apu.h" +#include "types.h" +#include +#include +#include +#include +#include + +// the maximum amount of samples that can be buffered at any one time. +// this doesn't include the amount of samples that are currently buffered by the +// audio driver for being played, just the samples that haven't been sent yet +constexpr std::size_t kMaxSampleBufferSize = 4096; + +// the minimum amount of samples required to update the audio stream. +// onGetData() will block until this many samples are acquired before sending +// data to the audio driver +constexpr std::size_t kMinStreamedSamples = 2048; + +using SampleBuffer = std::array; + +// NOTE: we privately inherit from sf::SoundStream to disallow external code +// from calling play(), stop() etc. manually. This is because our stream blocks +// until it receives enough data to send to the audio driver. Unless we unblock +// first, these methods will never return - StopStreaming() and StartStreaming() +// handles these cases properly +class SfmlApuSoundStream : private sf::SoundStream, public IApuOutput { + static_assert(kMaxSampleBufferSize % 2 == 0, + "kMaxSampleBufferSize must be a multiple of 2 (2 channels)"); + +public: + SfmlApuSoundStream(); + ~SfmlApuSoundStream(); + + void StartStreaming(); + void StopStreaming(); + + void AudioBufferSamples(i16 leftSample, i16 rightSample) override; + bool AudioIsMuted() const override; + + bool IsStreaming() const; + +private: + // we use double buffering for samples. + // this is because the sound thread will copy to the audio driver after + // onGetData() returns, which means unlocked sample writes after onGetData() + // can potentially affect affect the copy that gets sent to the audio device + std::array sampleBuffers_; + SampleBuffer* sampleFrontBuffer_; + SampleBuffer* sampleBackBuffer_; + // NOTE: only the back buffer can be accessed by the sound and emulation + // thread at the same time. this is possible during the swap, which is started + // by the sound thread in onGetData() + mutable std::mutex sampleBackBufferMutex_; + std::atomic samplesNextIdx_; + + mutable std::mutex streamWaitMutex_; + std::condition_variable streamWaitCondition_; + std::atomic isStreaming_; + + bool onGetData(Chunk& data) override; + void onSeek(sf::Time timeOffset) override; +}; + +#endif // SDGBC_SFML_APU_OUT_H_ diff --git a/include/debug/cpu_cmd_mode.h b/include/debug/cpu_cmd_mode.h new file mode 100644 index 0000000..f3f5c8e --- /dev/null +++ b/include/debug/cpu_cmd_mode.h @@ -0,0 +1,36 @@ +#ifndef SDGBC_CPU_CMD_MODE_H_ +#define SDGBC_CPU_CMD_MODE_H_ + +#include "debug/serial_stdout.h" +#include "hw/gbc.h" + +class CpuCmdMode { +public: + CpuCmdMode(); + + int Run(const std::string& romFilePath = {}); + +private: + Gbc gbc_; + SerialStdout serialStdout_; + + unsigned long long totalCpuSteps_, totalCpuCycles_; + bool autoResume_, updateGbcOnStep_; + + void ResetInputStream() const; + + std::string QueryUserCommand() const; + bool ExecuteUserCommand(const std::string& cmd); + RomLoadResult ExecuteLoadRomCommand(std::string romFilePath = {}); + void ExecuteViewMemoryAreaCommand(); + + void ExecuteStepCpuCommand(); + void StepCpu(unsigned int steps = 1); + + void PrintCpuStatus() const; + void PrintMemoryArea(u16 middleLoc, unsigned int linesAboveBelow) const; + void PrintMemoryRow(u16 loc, bool markLoc = false) const; + std::string GetCpuStatusString() const; +}; + +#endif // SDGBC_CPU_CMD_MODE_H_ diff --git a/include/debug/serial_stdout.h b/include/debug/serial_stdout.h new file mode 100644 index 0000000..d663041 --- /dev/null +++ b/include/debug/serial_stdout.h @@ -0,0 +1,20 @@ +#ifndef SDGBC_SERIAL_STDOUT_H_ +#define SDGBC_SERIAL_STDOUT_H_ + +#include "hw/serial.h" +#include + +// TODO: this could be generalized into using an ostream, but as we're not +// implementing serial properly (due to requirements), this is fine for now +class SerialStdout : public ISerialOutput { +public: + void SerialReset() override; + + void SerialWriteBit(bool bitSet) override; + void SerialOnByteWritten() override; + +private: + u8 outByte_; +}; + +#endif // SDGBC_SERIAL_STDOUT_H_ diff --git a/include/emulator.h b/include/emulator.h new file mode 100644 index 0000000..fcb6b26 --- /dev/null +++ b/include/emulator.h @@ -0,0 +1,112 @@ +#ifndef SDGBC_EMULATOR_H_ +#define SDGBC_EMULATOR_H_ + +#include "hw/gbc.h" +#include +#include +#include +#include +#include + +// VBlank happens at approx. 59.7275 Hz +using FrameDurationMillis = std::chrono::duration>; +constexpr FrameDurationMillis kFrameTime(1); + +// VBlank takes 70224 clock cycles in normal speed mode. +// 4.194304 MHz div 59.7275 = approx 70224 clock cycles +constexpr auto kNormalSpeedCyclesPerFrame = 70224u; + +// system is clocked at 4.194304 MHz in normal speed mode +constexpr auto kNormalSpeedClockRateHz = 4194304u; + +class Emulator { +public: + Emulator(); + explicit Emulator(const Emulator& other) = delete; + explicit Emulator(Emulator&& other) = delete; + ~Emulator(); + + Emulator& operator=(const Emulator& other) = delete; + Emulator& operator=(Emulator&& other) = delete; + + RomLoadResult LoadCartridgeRomFile(const std::string& filePath, + const std::string& fileName = {}); + + bool ExportCartridgeBatteryExtRam(const std::string& filePath) const; + bool ImportCartridgeBatteryExtRam(const std::string& filePath); + + void Reset(bool forceDmgMode = false); + + void SetVideoLcd(ILcd* lcd); + void SetApuOutput(IApuOutput* audioOut); + + void SetJoypadKeyState(JoypadKey key, bool pressed); + + void SetJoypadImpossibleInputsAllowed(bool val); + bool AreJoypadImpossibleInputsAllowed() const; + + void SetPaused(bool val); + bool IsPaused() const; + + void SetLimitFramerate(bool val); + bool IsLimitingFramerate() const; + + bool IsStarted() const; + + void SetApuMuteCh1(bool val); + bool IsApuCh1Muted() const; + + void SetApuMuteCh2(bool val); + bool IsApuCh2Muted() const; + + void SetApuMuteCh3(bool val); + bool IsApuCh3Muted() const; + + void SetApuMuteCh4(bool val); + bool IsApuCh4Muted() const; + + void SetVideoScanlineSpritesLimiterEnabled(bool val); + bool IsVideoScanlineSpritesLimiterEnabled() const; + + void SetVideoBgRenderEnabled(bool val); + bool IsVideoBgRenderEnabled() const; + + void SetVideoBgWindowRenderEnabled(bool val); + bool IsVideoBgWindowRenderEnabled() const; + + void SetVideoSpritesRenderEnabled(bool val); + bool IsVideoSpritesRenderEnabled() const; + + bool IsCartridgeRomLoaded() const; + const CartridgeExtMeta& GetCartridgeExtMeta() const; + std::string GetCartridgeRomFileName() const; + std::string GetCartridgeRomFilePath() const; + + bool IsInCgbMode() const; + +private: + Gbc gbc_; + + std::atomic isPaused_, isStarted_, limitFramerate_; + unsigned int normalSpeedFrameCycles_; + + std::thread emulationThread_; + // NOTE: access of some emulated hw properties (such as cartridge ROM info) + // does not require locking, as these properties are only ever mutated when + // emulation is not running, or aren't mutated by the emulation thread at + // all (such as APU channel mute status) + mutable std::mutex emulationMutex_; + + std::condition_variable emulationPauseCondition_; + mutable std::mutex emulationPauseMutex_; + + bool StartEmulation(); + void StopEmulation(); + + void EmulationLoop(); + void EmulateFrame(); + void PauseUntilNotify(); +}; + +#endif // SDGBC_EMULATOR_H_ diff --git a/include/hw/apu/apu.h b/include/hw/apu/apu.h new file mode 100644 index 0000000..056d633 --- /dev/null +++ b/include/hw/apu/apu.h @@ -0,0 +1,127 @@ +#ifndef SDGBC_APU_H_ +#define SDGBC_APU_H_ + +#include "hw/apu/apu_chan_noise.h" +#include "hw/apu/apu_chan_square.h" +#include "hw/apu/apu_chan_wave.h" +#include "types.h" +#include + +enum class ApuCh1Register { + SweepCtrl, + LengthLoadDutyCtrl, + EnvelopeCtrl, + FreqLoadLo, + LengthCtrlFreqLoadHi +}; + +enum class ApuCh2Register { + LengthLoadDutyCtrl, + EnvelopeCtrl, + FreqLoadLo, + LengthCtrlFreqLoadHi +}; + +enum class ApuCh3Register { + DacOnCtrl, + LengthLoad, + VolumeCtrl, + FreqLoadLo, + LengthCtrlFreqLoadHi +}; + +enum class ApuCh4Register { + LengthLoad, + EnvelopeCtrl, + PolynomialCtrl, + LengthCtrl +}; + +// CD-quality sound output rate +constexpr auto kApuOutputSampleRateHz = 44100u; + +class IApuOutput { +public: + virtual ~IApuOutput() = default; + + virtual void AudioBufferSamples(i16 leftSample, i16 rightSample) = 0; + virtual bool AudioIsMuted() const = 0; +}; + +class Cpu; + +class Apu { +public: + explicit Apu(const Cpu& cpu); + + void Reset(); + + void Update(unsigned int cycles); + + void SetApuOutput(IApuOutput* audioOut); + + void WriteWaveRam8(u8 loc, u8 val); + u8 ReadWaveRam8(u8 loc) const; + u8 GetWaveRamLastWritten8() const; + + void WriteCh1Register(ApuCh1Register reg, u8 val); + u8 ReadCh1Register(ApuCh1Register reg) const; + + void WriteCh2Register(ApuCh2Register reg, u8 val); + u8 ReadCh2Register(ApuCh2Register reg) const; + + void WriteCh3Register(ApuCh3Register reg, u8 val); + u8 ReadCh3Register(ApuCh3Register reg) const; + + void WriteCh4Register(ApuCh4Register reg, u8 val); + u8 ReadCh4Register(ApuCh4Register reg) const; + + void SetChannelCtrl(u8 val); + u8 GetChannelCtrl() const; + + void SetOutputCtrl(u8 val); + u8 GetOutputCtrl() const; + + void SetOnCtrl(u8 val); + u8 GetOnCtrl() const; + + void SetMuteCh1(bool val); + bool IsCh1Muted() const; + + void SetMuteCh2(bool val); + bool IsCh2Muted() const; + + void SetMuteCh3(bool val); + bool IsCh3Muted() const; + + void SetMuteCh4(bool val); + bool IsCh4Muted() const; + +private: + const Cpu& cpu_; + + IApuOutput* audioOut_; + + ApuSquareSweepChannel ch1_; + ApuSquareChannel ch2_; + ApuWaveChannel ch3_; + ApuNoiseChannel ch4_; + + bool muteCh1_, muteCh2_, muteCh3_, muteCh4_; + + unsigned int frameSeqCycles_, frameSeqStep_; + unsigned int outSampleCycles_; + + // sound control registers + u8 channelCtrl_, outCtrl_; + bool soundOn_; + + void UpdateFrameSequencerCycle(); + + void ZeroWriteAllRegisters(); + + // returns the samples for both the left and right channels + std::pair MixChannels() const; +}; + +#endif // SDGBC_APU_H_ diff --git a/include/hw/apu/apu_chan_base.h b/include/hw/apu/apu_chan_base.h new file mode 100644 index 0000000..bb0146a --- /dev/null +++ b/include/hw/apu/apu_chan_base.h @@ -0,0 +1,72 @@ +#ifndef SDGBC_APU_CHAN_BASE_H_ +#define SDGBC_APU_CHAN_BASE_H_ + +#include "hw/memory.h" +#include "types.h" + +constexpr u8 kApuChannelMaxOutputVolume = 15; +constexpr u16 kApuChannelMaxFreqLoad = 0x07ff; + +class ApuSoundChannelBase { +public: + virtual ~ApuSoundChannelBase() = default; + + virtual void Reset(); + virtual void Restart(); + + void UpdateFrequencyTimer(); + void UpdateLengthCounter(); + + u8 CalculateDacOutputVolume() const; + + void SetLengthCtrl(u8 val); + u8 GetLengthCtrl() const; + + bool IsEnabled() const; + +protected: + bool enabled_; + + bool lengthEnabled_; + u16 lengthCounter_; + + void ResetLengthCounter(u8 lengthSubtract = 0); + + void SetDacEnabled(bool val); + bool IsDacEnabled() const; + + virtual void UpdateFrequency() = 0; + + virtual u16 GetFrequencyTimerPeriod() const = 0; + virtual u16 GetMaxLength() const = 0; + + virtual u8 CalculateOutputVolume() const = 0; + +private: + bool dacEnabled_; + u16 freqTimer_; +}; + +class ApuEnvelopeChannelBase : public ApuSoundChannelBase { +public: + virtual ~ApuEnvelopeChannelBase() = default; + + virtual void Reset() override; + virtual void Restart() override; + + void UpdateEnvelope(); + + void SetEnvelopeCtrl(u8 val); + u8 GetEnvelopeCtrl() const; + +protected: + u8 envTimer_, envVolume_; + u8 envCtrl_; + + u8 ResetEnvelopeTimer(); // returns the new envelope period + void ResetEnvelopeVolume(); + + u16 GetMaxLength() const override; +}; + +#endif // SDGBC_APU_CHAN_BASE_H_ diff --git a/include/hw/apu/apu_chan_noise.h b/include/hw/apu/apu_chan_noise.h new file mode 100644 index 0000000..c964c29 --- /dev/null +++ b/include/hw/apu/apu_chan_noise.h @@ -0,0 +1,26 @@ +#ifndef SDGBC_APU_CHAN_NOISE_H_ +#define SDGBC_APU_CHAN_NOISE_H_ + +#include "hw/apu/apu_chan_base.h" + +class ApuNoiseChannel : public ApuEnvelopeChannelBase { +public: + void Reset() override; + void Restart() override; + + void SetLengthLoad(u8 val); + + void SetPolynomialCtrl(u8 val); + u8 GetPolynomialCtrl() const; + +private: + u8 polyCtrl_; + u16 linearShift_; + + void UpdateFrequency() override; + u8 CalculateOutputVolume() const override; + + u16 GetFrequencyTimerPeriod() const override; +}; + +#endif // SDGBC_APU_CHAN_NOISE_H_ diff --git a/include/hw/apu/apu_chan_square.h b/include/hw/apu/apu_chan_square.h new file mode 100644 index 0000000..9a988a2 --- /dev/null +++ b/include/hw/apu/apu_chan_square.h @@ -0,0 +1,55 @@ +#ifndef SDGBC_APU_CHAN_SQUARE_H_ +#define SDGBC_APU_CHAN_SQUARE_H_ + +#include "hw/apu/apu_chan_base.h" + +class ApuSquareChannel : public ApuEnvelopeChannelBase { +public: + virtual ~ApuSquareChannel() = default; + + virtual void Reset() override; + + void ResetDutyCounter(); + + void SetLengthLoadDutyCtrl(u8 val); + u8 GetLengthLoadDutyCtrl() const; + + void SetFreqLoadLo(u8 val); + void SetLengthCtrlFreqLoadHi(u8 val); + +protected: + u8 dutyNum_; + u8 dutyBitIdxCounter_; + + u16 freqLoad_; + + void UpdateFrequency() override; + u8 CalculateOutputVolume() const override; + + u16 GetFrequencyTimerPeriod() const override; +}; + +class ApuSquareSweepChannel : public ApuSquareChannel { +public: + void Reset() override; + void Restart() override; + + void UpdateSweep(); + + void SetSweepCtrl(u8 val); + u8 GetSweepCtrl() const; + +private: + u8 sweepTimer_; + u16 sweepShadow_; + bool sweepEnabled_; + u8 sweepCtrl_; + + u8 ResetSweepTimer(); // returns the new sweep period + void ResetSweepShadowFrequency(); + + u16 CalculateNewSweepFreq(); + u8 GetSweepShift() const; +}; + +#endif // SDGBC_APU_CHAN_SQUARE_H_ diff --git a/include/hw/apu/apu_chan_wave.h b/include/hw/apu/apu_chan_wave.h new file mode 100644 index 0000000..5dbdee3 --- /dev/null +++ b/include/hw/apu/apu_chan_wave.h @@ -0,0 +1,43 @@ +#ifndef SDGBC_APU_CHAN_WAVE_H_ +#define SDGBC_APU_CHAN_WAVE_H_ + +#include "hw/apu/apu_chan_base.h" + +class ApuWaveChannel : public ApuSoundChannelBase { +public: + void Reset() override; + void Restart() override; + + void ClearWaveRam(); + + void WriteWaveRam8(u8 loc, u8 val); + u8 ReadWaveRam8(u8 loc) const; + u8 GetWaveRamLastWritten8() const; + + void SetDacOnCtrl(u8 val); + u8 GetDacOnCtrl() const; + + void SetLengthLoad(u8 val); + + void SetVolumeCtrl(u8 val); + u8 GetVolumeCtrl() const; + + void SetFreqLoadLo(u8 val); + void SetLengthCtrlFreqLoadHi(u8 val); + +private: + u16 freqLoad_; + u8 volumeCode_; + + WaveRam waveRam_; + u8 waveRamLastWrittenVal_; + u8 sampleIdxCounter_; + + void UpdateFrequency() override; + u8 CalculateOutputVolume() const override; + + u16 GetFrequencyTimerPeriod() const override; + u16 GetMaxLength() const override; +}; + +#endif // SDGBC_APU_CHAN_WAVE_H_ diff --git a/include/hw/cart/cart.h b/include/hw/cart/cart.h new file mode 100644 index 0000000..29fa2ed --- /dev/null +++ b/include/hw/cart/cart.h @@ -0,0 +1,83 @@ +#ifndef SDGBC_CART_H_ +#define SDGBC_CART_H_ + +#include "hw/cart/cart_ext_meta.h" +#include "hw/memory.h" +#include "types.h" +#include +#include +#include + +enum class RomLoadResult { + Ok, + ReadError, + InvalidSize, + InvalidExtension, + Unsupported +}; + +class ICartridgeExtension; + +class Cartridge { +public: + static std::string GetRomLoadResultAsMessage(RomLoadResult result); + + Cartridge(); + explicit Cartridge(const Cartridge& other) = delete; + explicit Cartridge(Cartridge&& other) = default; + ~Cartridge(); + + Cartridge& operator=(const Cartridge& other) = delete; + Cartridge& operator=(Cartridge&& other) = default; + + void Clear(); // clears cartridge ROM and resets + void Reset(); // only clears cartridge RAM/other volatile memory + + RomLoadResult LoadRomFile(const std::string& filePath, + const std::string& fileName = {}); + + // SaveBatteryExtRam() and LoadBatteryExtRam() return true if the operation + // was successful OR if the cartridge doesn't need to save RAM (no battery, no + // RAM etc.) -- false otherwise + bool SaveBatteryExtRam(const std::string& filePath = {}) const; + bool LoadBatteryExtRam(const std::string& filePath = {}); + + void RomBank0Write8(u16 loc, u8 val); + u8 RomBank0Read8(u16 loc) const; + + void RomBankXWrite8(u16 loc, u8 val); + u8 RomBankXRead8(u16 loc) const; + + void RamWrite8(u16 loc, u8 val); + u8 RamRead8(u16 loc) const; + + std::string GetRomFilePath() const; + std::string GetRomFileName() const; + + bool IsRomLoaded() const; + const std::vector& GetRomData() const; + + u16 GetNumRomBanks() const; + u16 GetNum2KBExtRamBanks() const; + + u8 GetExtensionId() const; + const CartridgeExtMeta& GetExtensionMeta() const; + + bool IsInCgbMode() const; + +private: + std::string romFilePath_, romFileName_; + bool isRomLoaded_; + + std::vector romData_; + u16 numRomBanks_, num2KBExtRamBanks_; + bool cgbMode_; + + std::unique_ptr extension_; + u8 extensionId_; + + RomLoadResult ParseRomHeader(Cartridge& newCart); + RomLoadResult ParseExtensions(Cartridge& newCart); +}; + +#endif // SDGBC_CART_H_ diff --git a/include/hw/cart/cart_ext_base.h b/include/hw/cart/cart_ext_base.h new file mode 100644 index 0000000..7e19710 --- /dev/null +++ b/include/hw/cart/cart_ext_base.h @@ -0,0 +1,86 @@ +#ifndef SDGBC_CART_EXT_BASE_H_ +#define SDGBC_CART_EXT_BASE_H_ + +#include "types.h" +#include +#include + +class Cartridge; + +class ICartridgeExtension { +public: + virtual ~ICartridgeExtension() = default; + + virtual void ExtSetCartridge(const Cartridge* cart) = 0; + virtual bool ExtInit() = 0; + + virtual void ExtReset() = 0; + + virtual void ExtRomBank0Write8(u16 loc, u8 val) = 0; + virtual u8 ExtRomBank0Read8(u16 loc) const = 0; + + virtual void ExtRomBankXWrite8(u16 loc, u8 val) = 0; + virtual u8 ExtRomBankXRead8(u16 loc) const = 0; + + virtual void ExtRamWrite8(u16 loc, u8 val) = 0; + virtual u8 ExtRamRead8(u16 loc) const = 0; + + virtual bool ExtSaveRam(std::ostream& os) = 0; + virtual bool ExtLoadRam(std::istream& is) = 0; +}; + +class CartridgeExtensionBase : public ICartridgeExtension { +public: + CartridgeExtensionBase(); + virtual ~CartridgeExtensionBase() = default; + + void ExtSetCartridge(const Cartridge* cart) override; + + virtual void ExtRomBank0Write8(u16 loc, u8 val) override; + virtual u8 ExtRomBank0Read8(u16 loc) const override; + + virtual void ExtRomBankXWrite8(u16 loc, u8 val) override; + virtual u8 ExtRomBankXRead8(u16 loc) const override; + +protected: + const Cartridge* cart_; +}; + +class RamExtensionBase : public CartridgeExtensionBase { +public: + virtual ~RamExtensionBase() = default; + + virtual bool ExtInit() override; + virtual void ExtReset() override; + + virtual void ExtRamWrite8(u16 loc, u8 val) override; + virtual u8 ExtRamRead8(u16 loc) const override; + + bool ExtSaveRam(std::ostream& os) override; + bool ExtLoadRam(std::istream& is) override; + +protected: + std::vector ramData_; + u16 ramBankNum_; + bool ramEnabled_; + + void RamBankXWrite8(u16 loc, u8 val, u16 bankNum); + u8 RamBankXRead8(u16 loc, u16 bankNum) const; +}; + +class MbcBase : public RamExtensionBase { +public: + virtual ~MbcBase() = default; + + virtual void ExtReset() override; + + virtual u8 ExtRomBank0Read8(u16 loc) const override; + virtual u8 ExtRomBankXRead8(u16 loc) const override; + +protected: + u16 romBankNum_; + + u8 RomBankXRead8(u16 loc, u16 bankNum) const; +}; + +#endif // SDGBC_CART_EXT_BASE_H_ diff --git a/include/hw/cart/cart_ext_mbc1.h b/include/hw/cart/cart_ext_mbc1.h new file mode 100644 index 0000000..0ef3c1f --- /dev/null +++ b/include/hw/cart/cart_ext_mbc1.h @@ -0,0 +1,23 @@ +#ifndef SDGBC_CART_EXT_MBC1_H_ +#define SDGBC_CART_EXT_MBC1_H_ + +#include "hw/cart/cart_ext_base.h" + +class Mbc1 : public MbcBase { +public: + bool ExtInit() override; + void ExtReset() override; + + void ExtRomBank0Write8(u16 loc, u8 val) override; + + void ExtRomBankXWrite8(u16 loc, u8 val) override; + u8 ExtRomBankXRead8(u16 loc) const override; + + void ExtRamWrite8(u16 loc, u8 val) override; + u8 ExtRamRead8(u16 loc) const override; + +private: + bool ramBankingMode_; +}; + +#endif // SDGBC_CART_EXT_MBC1_H_ \ No newline at end of file diff --git a/include/hw/cart/cart_ext_mbc2.h b/include/hw/cart/cart_ext_mbc2.h new file mode 100644 index 0000000..0c8647a --- /dev/null +++ b/include/hw/cart/cart_ext_mbc2.h @@ -0,0 +1,13 @@ +#ifndef SDGBC_CART_EXT_MBC2_H_ +#define SDGBC_CART_EXT_MBC2_H_ + +#include "hw/cart/cart_ext_base.h" + +class Mbc2 : public MbcBase { +public: + bool ExtInit() override; + + void ExtRomBank0Write8(u16 loc, u8 val) override; +}; + +#endif // SDGBC_CART_EXT_MBC2_H_ diff --git a/include/hw/cart/cart_ext_mbc3.h b/include/hw/cart/cart_ext_mbc3.h new file mode 100644 index 0000000..c674e29 --- /dev/null +++ b/include/hw/cart/cart_ext_mbc3.h @@ -0,0 +1,55 @@ +#ifndef SDGBC_CART_EXT_MBC3_H_ +#define SDGBC_CART_EXT_MBC3_H_ + +#include "hw/cart/cart_ext_base.h" + +class Mbc3 : public MbcBase { +public: + explicit Mbc3(bool timerEnabled = false); + + bool ExtInit() override; + void ExtReset() override; + + void ExtRomBank0Write8(u16 loc, u8 val) override; + void ExtRomBankXWrite8(u16 loc, u8 val) override; + + void ExtRamWrite8(u16 loc, u8 val) override; + u8 ExtRamRead8(u16 loc) const override; + +private: + enum class RtcRegister : u8 { + S = 0x8, + M = 0x9, + H = 0xa, + Dl = 0xb, + Dh = 0xc, + None + }; + + struct RtcRegisters { + u8 s, m, h; + u16 d; + + RtcRegisters() = default; + RtcRegisters(const RtcRegisters& other) = default; + RtcRegisters(RtcRegisters&& other) = delete; + ~RtcRegisters() = default; + + RtcRegisters& operator=(const RtcRegisters& other) = default; + RtcRegisters& operator=(RtcRegisters&& other) = delete; + }; + + bool timerEnabled_; + + RtcRegister selectedRtc_; + RtcRegisters rtc_, latchedRtc_; + + u8 prevLatchVal_; + bool isRtcLatched_; + + u8 ReadRtcRegister(RtcRegister rtc) const; + void WriteRtcRegister(RtcRegister rtc, u8 val); +}; + + +#endif // SDGBC_CART_EXT_MBC3_H_ diff --git a/include/hw/cart/cart_ext_mbc5.h b/include/hw/cart/cart_ext_mbc5.h new file mode 100644 index 0000000..6a2b407 --- /dev/null +++ b/include/hw/cart/cart_ext_mbc5.h @@ -0,0 +1,14 @@ +#ifndef SDGBC_CART_EXT_MBC5_H_ +#define SDGBC_CART_EXT_MBC5_H_ + +#include "hw/cart/cart_ext_base.h" + +class Mbc5 : public MbcBase { +public: + bool ExtInit() override; + + void ExtRomBank0Write8(u16 loc, u8 val) override; + void ExtRomBankXWrite8(u16 loc, u8 val) override; +}; + +#endif // SDGBC_CART_EXT_MBC5_H_ diff --git a/include/hw/cart/cart_ext_meta.h b/include/hw/cart/cart_ext_meta.h new file mode 100644 index 0000000..f8551d7 --- /dev/null +++ b/include/hw/cart/cart_ext_meta.h @@ -0,0 +1,46 @@ +#ifndef SDGBC_CART_EXT_META_H_ +#define SDGBC_CART_EXT_META_H_ + +#include "types.h" +#include + +enum class CartridgeExtType { + None, + Mbc1, + Mbc2, + Mbc3, + Mbc5 +}; + +struct CartridgeExtMeta { + CartridgeExtType type; + bool hasBattery; + bool supportsExRam; +}; + +const std::unordered_map kCartridgeExtMetas { + { 0x00, { CartridgeExtType::None, false, false } }, + + { 0x01, { CartridgeExtType::Mbc1, false, false } }, + { 0x02, { CartridgeExtType::Mbc1, false, true } }, + { 0x03, { CartridgeExtType::Mbc1, true, true } }, + + // NOTE: MBC2 contains RAM on-chip, but doesn't actually support cart RAM + { 0x05, { CartridgeExtType::Mbc2, false, false } }, + { 0x06, { CartridgeExtType::Mbc2, true, false } }, + + { 0x0f, { CartridgeExtType::Mbc3, true, false } }, + { 0x10, { CartridgeExtType::Mbc3, true, true } }, + { 0x11, { CartridgeExtType::Mbc3, false, false } }, + { 0x12, { CartridgeExtType::Mbc3, false, true } }, + { 0x13, { CartridgeExtType::Mbc3, true, true } }, + + { 0x19, { CartridgeExtType::Mbc5, false, false } }, + { 0x1a, { CartridgeExtType::Mbc5, false, true } }, + { 0x1b, { CartridgeExtType::Mbc5, true, true } }, + { 0x1c, { CartridgeExtType::Mbc5, false, false } }, + { 0x1d, { CartridgeExtType::Mbc5, false, true } }, + { 0x1e, { CartridgeExtType::Mbc5, true, true } }, +}; + +#endif // SDGBC_CART_EXT_META_H_ diff --git a/include/hw/cpu/cpu.h b/include/hw/cpu/cpu.h new file mode 100644 index 0000000..4f2ddad --- /dev/null +++ b/include/hw/cpu/cpu.h @@ -0,0 +1,225 @@ +#ifndef SDGBC_CPU_H_ +#define SDGBC_CPU_H_ + +#include "hw/cpu/cpu_reg.h" + +enum class CpuStatus { + Running, + Halted, + Stopped, + Hung +}; + +enum CpuInterruptMask : u8 { + kCpuInterrupt0x40 = 0x01, + kCpuInterrupt0x48 = 0x02, + kCpuInterrupt0x50 = 0x04, + kCpuInterrupt0x58 = 0x08, + kCpuInterrupt0x60 = 0x10 +}; + +class Mmu; +class Dma; +class Joypad; + +class Cpu { +public: + explicit Cpu(Mmu& mmu, const Dma& dma, const Joypad& joypad); + + void Reset(bool cgbMode); + bool Resume(); // resumes the CPU if it was halted + + unsigned int Update(); // returns the amount of CPU clock cycles spent + + const CpuRegisters& GetRegisters() const; + CpuStatus GetStatus() const; + + bool GetIntme() const; + + void SetInte(u8 val); + u8 GetInte() const; + + void IntfRequest(u8 mask); + void SetIntf(u8 val); + u8 GetIntf() const; + + void SetKey1(u8 val); + u8 GetKey1() const; + + bool IsInCgbMode() const; + + bool IsInDoubleSpeedMode() const; + bool IsSpeedSwitchInProgress() const; + +private: + Mmu& mmu_; + const Dma& dma_; + const Joypad& joypad_; + + CpuStatus status_; + bool cgbMode_; + unsigned int updateCycles_; + + bool speedSwitchRequested_, doubleSpeedMode_; + unsigned int speedSwitchCyclesLeft_; + + // general & interrupt registers + CpuRegisters reg_; + bool intme_; + u8 intf_, inte_; + + void HandleStoppedUpdate(); + + bool HandleInterrupts(); // returns whether or not an int was serviced + template + void ServiceInterrupt() { + intme_ = false; + InternalDelay(2); + ExecPush(reg_.pc.Get()); + reg_.pc.Set(L); + } + + bool ExecuteOp(u8 op); + void ExecuteExOp(u8 exOp); + + void InternalDelay(unsigned int numAccesses = 1); + + u8 IoRead8(u16 loc); + void IoWrite8(u16 loc, u8 val); + u8 IoPcReadNext8(); + u16 IoPcReadNext16(); // NOTE: reads in little-endian + + void ExecOpStop0x10(); + void ExecOpHalt0x76(); + + void ExecOpDi0xf3(); + void ExecOpEi0xfb(); + + void ExecLoad(u16 destLoc, u8 val); + void ExecLoad(u16 destLoc, u16 val); + template + void ExecLoad(CpuRegister& destReg, T val); + template + void ExecLoad(CpuRegister8Pair& destRegPair, u16 val); + void ExecOpLdi0x22(); + void ExecOpLdi0x2a(); + void ExecOpLdd0x32(); + void ExecOpLdd0x3a(); + void ExecOpLdhl0xf8(); + void ExecOpLd0xf9(); + + void ExecPush(u16 val); + u16 ExecPop(); + template + void ExecPop(CpuRegister8Pair& destRegPair); + + void ExecJump(u16 targetLoc, bool shouldJump = true); + void ExecCall(u16 targetLoc, bool shouldCall = true); + template + void ExecRestart(); + void ExecReturn(bool shouldReturn); + void ExecReturn(); // uses one less IO access than ExecReturn(bool) + void ExecOpJp0xe9(); + void ExecOpJr0x18(); + void ExecOpJr0x20(); + void ExecOpJr0x28(); + void ExecOpJr0x30(); + void ExecOpJr0x38(); + void ExecOpReti0xd9(); + + void ExecAdd(u8 val); + void ExecAddWithCarry(u8 val); + void ExecSub(u8 val); + void ExecSubWithCarry(u8 val); + + template + void ExecAdd16(CpuRegister8Pair& destRegPair, u16 val); + void ExecOpAdd0xe8(); + + void ExecAnd(u8 val); + void ExecOr(u8 val); + void ExecXor(u8 val); + void ExecCompare(u8 val); + + u8 ExecInc(u8 val); + u8 ExecDec(u8 val); + template + void ExecInc(CpuRegister& destReg); + template + void ExecDec(CpuRegister& destReg); + void ExecInc(u16 destLoc); + void ExecDec(u16 destLoc); + + template + void ExecInc16(CpuRegister8Pair& destRegPair); + template + void ExecInc16(CpuRegister& destReg); + template + void ExecDec16(CpuRegister8Pair& destRegPair); + template + void ExecDec16(CpuRegister& destReg); + + void ExecOpDaa0x27(); + void ExecOpCpl0x2f(); + void ExecOpScf0x37(); + void ExecOpCcf0x3f(); + void ExecOpCb0xcb(); + + u8 ExecRotLeft(u8 val); + u8 ExecRotLeftThroughCarry(u8 val); + u8 ExecRotRight(u8 val); + u8 ExecRotRightThroughCarry(u8 val); + template + void ExecRotLeft(CpuRegister& destReg); + template + void ExecRotLeftThroughCarry(CpuRegister& destReg); + template + void ExecRotRight(CpuRegister& destReg); + template + void ExecRotRightThroughCarry(CpuRegister& destReg); + void ExecRotLeft(u16 destLoc); + void ExecRotLeftThroughCarry(u16 destLoc); + void ExecRotRight(u16 destLoc); + void ExecRotRightThroughCarry(u16 destLoc); + void ExecOpRlca0x07(); + void ExecOpRla0x17(); + void ExecOpRrca0x0f(); + void ExecOpRra0x1f(); + + u8 ExecShiftLeft(u8 val); + u8 ExecShiftRight(u8 val); + u8 ExecShiftRightSigned(u8 val); + template + void ExecShiftLeft(CpuRegister& destReg); + template + void ExecShiftRight(CpuRegister& destReg); + template + void ExecShiftRightSigned(CpuRegister& destReg); + void ExecShiftLeft(u16 destLoc); + void ExecShiftRight(u16 destLoc); + void ExecShiftRightSigned(u16 destLoc); + + u8 ExecSwap(u8 val); + template + void ExecSwap(CpuRegister& destReg); + void ExecSwap(u16 destLoc); + + template + void ExecTestBit(u8 val); + template + u8 ExecSetBit(u8 val); + template + void ExecSetBit(CpuRegister& destReg); + template + void ExecSetBit(u16 destLoc); + template + u8 ExecResetBit(u8 val); + template + void ExecResetBit(CpuRegister& destReg); + template + void ExecResetBit(u16 destLoc); +}; + +#include "hw/cpu/cpu_ops_inl.h" + +#endif // SDGBC_CPU_H_ diff --git a/include/hw/cpu/cpu_ops_inl.h b/include/hw/cpu/cpu_ops_inl.h new file mode 100644 index 0000000..d28f3c7 --- /dev/null +++ b/include/hw/cpu/cpu_ops_inl.h @@ -0,0 +1,150 @@ +#ifndef SDGBC_CPU_OPS_INL_H_ +#define SDGBC_CPU_OPS_INL_H_ + +template +void Cpu::ExecLoad(CpuRegister& destReg, T val) { + destReg.Set(val); +} + +template +void Cpu::ExecLoad(CpuRegister8Pair& destRegPair, u16 val) { + destRegPair.Set(val); +} + +template +void Cpu::ExecPop(CpuRegister8Pair& destRegPair) { + destRegPair.Set(ExecPop()); +} + +template // accept a u8 as it's technically $0000 + $XX +void Cpu::ExecRestart() { + ExecPush(reg_.pc.Get()); // push current opcode addr + reg_.pc.Set(L); +} + +template +void Cpu::ExecAdd16(CpuRegister8Pair& destRegPair, u16 val) { + const uint_fast32_t result = destRegPair.Get() + val; + + InternalDelay(); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(result > 0xffff); + reg_.f.SetHFlag((destRegPair.Get() & 0xfff) + (val & 0xfff) > 0xfff); + destRegPair.Set(result & 0xffff); +} + +template +void Cpu::ExecInc(CpuRegister& destReg) { + destReg.Set(ExecInc(destReg.Get())); +} + +template +void Cpu::ExecDec(CpuRegister& destReg) { + destReg.Set(ExecDec(destReg.Get())); +} + +template +void Cpu::ExecInc16(CpuRegister8Pair& destRegPair) { + InternalDelay(); + ++destRegPair; +} + +template +void Cpu::ExecInc16(CpuRegister& destReg) { + InternalDelay(); + ++destReg; +} + +template +void Cpu::ExecDec16(CpuRegister8Pair& destRegPair) { + InternalDelay(); + --destRegPair; +} + +template +void Cpu::ExecDec16(CpuRegister& destReg) { + InternalDelay(); + --destReg; +} + +template +void Cpu::ExecRotLeft(CpuRegister& destReg) { + destReg.Set(ExecRotLeft(destReg.Get())); +} + +template +void Cpu::ExecRotLeftThroughCarry(CpuRegister& destReg) { + destReg.Set(ExecRotLeftThroughCarry(destReg.Get())); +} + +template +void Cpu::ExecRotRight(CpuRegister& destReg) { + destReg.Set(ExecRotRight(destReg.Get())); +} + +template +void Cpu::ExecRotRightThroughCarry(CpuRegister& destReg) { + destReg.Set(ExecRotRightThroughCarry(destReg.Get())); +} + +template +void Cpu::ExecShiftLeft(CpuRegister& destReg) { + destReg.Set(ExecShiftLeft(destReg.Get())); +} + +template +void Cpu::ExecShiftRight(CpuRegister& destReg) { + destReg.Set(ExecShiftRight(destReg.Get())); +} + +template +void Cpu::ExecShiftRightSigned(CpuRegister& destReg) { + destReg.Set(ExecShiftRightSigned(destReg.Get())); +} + +template +void Cpu::ExecSwap(CpuRegister& destReg) { + destReg.Set(ExecSwap(destReg.Get())); +} + +template +void Cpu::ExecTestBit(u8 val) { + static_assert(B < 8, "B must have a value from 0-7"); + reg_.f.SetZFlag(!((val >> B) & 1)); + reg_.f.SetNFlag(false); + reg_.f.SetHFlag(true); +} + +template +u8 Cpu::ExecSetBit(u8 val) { + static_assert(B < 8, "B must have a value from 0-7"); + return val | (1 << B); +} + +template +void Cpu::ExecSetBit(CpuRegister& destReg) { + destReg.Set(ExecSetBit(destReg.Get())); +} + +template +void Cpu::ExecSetBit(u16 destLoc) { + IoWrite8(destLoc, ExecSetBit(IoRead8(destLoc))); +} + +template +u8 Cpu::ExecResetBit(u8 val) { + static_assert(B < 8, "B must have a value from 0-7"); + return val & ~(1 << B); +} + +template +void Cpu::ExecResetBit(CpuRegister& destReg) { + destReg.Set(ExecResetBit(destReg.Get())); +} + +template +void Cpu::ExecResetBit(u16 destLoc) { + IoWrite8(destLoc, ExecResetBit(IoRead8(destLoc))); +} + +#endif // SDGBC_CPU_OPS_INL_H_ diff --git a/include/hw/cpu/cpu_reg.h b/include/hw/cpu/cpu_reg.h new file mode 100644 index 0000000..48ad631 --- /dev/null +++ b/include/hw/cpu/cpu_reg.h @@ -0,0 +1,52 @@ +#ifndef SDGBC_CPU_REG_H_ +#define SDGBC_CPU_REG_H_ + +#include "hw/cpu/cpu_reg_base.h" + +// masks of the flag bits within register F +enum CpuFlagRegisterMask : u8 { + kCpuFlagRegisterCMask = 0x10, // carry flag + kCpuFlagRegisterHMask = 0x20, // half carry flag + kCpuFlagRegisterNMask = 0x40, // add/sub flag + kCpuFlagRegisterZMask = 0x80 // zero flag +}; + +class CpuFlagRegister : public CpuRegister { +public: + CpuFlagRegister(); + explicit CpuFlagRegister(u8 val); + + void SetImpl(u8 val); + u8 GetImpl() const; + + void SetZFlag(bool val); + bool GetZFlag() const; + + void SetNFlag(bool val); + bool GetNFlag() const; + + void SetHFlag(bool val); + bool GetHFlag() const; + + void SetCFlag(bool val); + bool GetCFlag() const; + +private: + u8 val_; +}; + +using CpuRegisterF = CpuFlagRegister; +using CpuRegisterAFPair = CpuRegister8Pair; + +struct CpuRegisters { + CpuRegisterBasic16 pc, sp; + CpuRegisterBasic8 a, b, c, d, e, h, l; + CpuRegisterF f; + + CpuRegisterBasic8Pair bc, de, hl; + CpuRegisterAFPair af; + + CpuRegisters(); +}; + +#endif // SDGBC_CPU_REG_H_ diff --git a/include/hw/cpu/cpu_reg_base.h b/include/hw/cpu/cpu_reg_base.h new file mode 100644 index 0000000..1dae697 --- /dev/null +++ b/include/hw/cpu/cpu_reg_base.h @@ -0,0 +1,83 @@ +#ifndef SDGBC_CPU_REG_BASE_H_ +#define SDGBC_CPU_REG_BASE_H_ + +#include "types.h" +#include "util.h" +#include + +template +class CpuRegister { + static_assert(std::is_integral::value, "T must be an integral type"); + +public: + void Set(T val); + T Get() const; + + // prefix inc/dec operators + C& operator++(); + C& operator--(); + + // postfix inc/dec operators + T operator++(int); + T operator--(int); + +private: + C& AsC(); + const C& AsC() const; +}; + +template +class CpuBasicRegister : public CpuRegister, T> { + static_assert(std::is_integral::value, "T must be an integral type"); + +public: + explicit CpuBasicRegister(T val = 0); + + void SetImpl(T val); + T GetImpl() const; + +private: + T val_; +}; + +using CpuRegisterBasic8 = CpuBasicRegister; +using CpuRegisterBasic16 = CpuBasicRegister; + +template +class CpuRegister8Pair { + static_assert(std::is_base_of, H>::value, + "H must be derived from type CpuRegister"); + static_assert(std::is_base_of, L>::value, + "L must be derived from type CpuRegister"); + +public: + explicit CpuRegister8Pair(CpuRegister& hi, CpuRegister& lo); + + void Set(u16 val); + u16 Get() const; + + // prefix inc/dec operators + CpuRegister8Pair& operator++(); + CpuRegister8Pair& operator--(); + + // postfix inc/dec operators + u16 operator++(int); + u16 operator--(int); + + CpuRegister& GetHi(); + const CpuRegister& GetHi() const; + + CpuRegister& GetLo(); + const CpuRegister& GetLo() const; + +private: + CpuRegister& hi_; + CpuRegister& lo_; +}; + +using CpuRegisterBasic8Pair = CpuRegister8Pair; + +#include "hw/cpu/cpu_reg_base_inl.h" + +#endif // SDGBC_CPU_REG_BASE_H_ diff --git a/include/hw/cpu/cpu_reg_base_inl.h b/include/hw/cpu/cpu_reg_base_inl.h new file mode 100644 index 0000000..3ae9a08 --- /dev/null +++ b/include/hw/cpu/cpu_reg_base_inl.h @@ -0,0 +1,133 @@ +#ifndef SDGBC_CPU_REG_BASE_INL_H_ +#define SDGBC_CPU_REG_BASE_INL_H_ + +template +void CpuRegister::Set(T val) { + AsC().SetImpl(val); +} + +template +T CpuRegister::Get() const { + return AsC().GetImpl(); +} + +// prefix inc/dec operators +template +C& CpuRegister::operator++() { + Set(Get() + 1); + return AsC(); +} + +template +C& CpuRegister::operator--() { + Set(Get() - 1); + return AsC(); +} + +// postfix inc/dec operators +template +T CpuRegister::operator++(int) { + const T beforeVal = Get(); + Set(Get() + 1); + + return beforeVal; +} + +template +T CpuRegister::operator--(int) { + const T beforeVal = Get(); + Set(Get() - 1); + + return beforeVal; +} + +template +C& CpuRegister::AsC() { + return *static_cast(this); +} + +template +const C& CpuRegister::AsC() const { + return *static_cast(this); +} + +template +CpuBasicRegister::CpuBasicRegister(T val) : val_(val) {} + +template +void CpuBasicRegister::SetImpl(T val) { + val_ = val; +} + +template +T CpuBasicRegister::GetImpl() const { + return val_; +} + +template +CpuRegister8Pair::CpuRegister8Pair(CpuRegister& hi, + CpuRegister& lo) + : hi_(hi), lo_(lo) {} + +template +void CpuRegister8Pair::Set(u16 val) { + hi_.Set(util::GetHi8(val)); + lo_.Set(util::GetLo8(val)); +} + +template +u16 CpuRegister8Pair::Get() const { + return util::To16(hi_.Get(), lo_.Get()); +} + +// prefix inc/dec operators +template +CpuRegister8Pair& CpuRegister8Pair::operator++() { + Set(Get() + 1); + return *this; +} + +template +CpuRegister8Pair& CpuRegister8Pair::operator--() { + Set(Get() - 1); + return *this; +} + +// postfix inc/dec operators +template +u16 CpuRegister8Pair::operator++(int) { + const u16 beforeVal = Get(); + Set(Get() + 1); + + return beforeVal; +} + +template +u16 CpuRegister8Pair::operator--(int) { + const u16 beforeVal = Get(); + Set(Get() - 1); + + return beforeVal; +} + +template +CpuRegister& CpuRegister8Pair::GetHi() { + return hi_; +} + +template +const CpuRegister& CpuRegister8Pair::GetHi() const { + return hi_; +} + +template +CpuRegister& CpuRegister8Pair::GetLo() { + return lo_; +} + +template +const CpuRegister& CpuRegister8Pair::GetLo() const { + return lo_; +} + +#endif // SDGBC_CPU_REG_BASE_INL_H_ diff --git a/include/hw/dma.h b/include/hw/dma.h new file mode 100644 index 0000000..55172bf --- /dev/null +++ b/include/hw/dma.h @@ -0,0 +1,81 @@ +#ifndef SDGBC_DMA_H_ +#define SDGBC_DMA_H_ + +#include "types.h" + +class Mmu; +class Cpu; +class Ppu; + +enum class NdmaStatus { + Inactive, + HdmaWaitingForHBlank, + HdmaFinishedBlock, + HdmaInProgress, + GdmaInProgress +}; + +enum class OamDmaStatus { + Inactive, + InProgress +}; + +class Dma { +public: + explicit Dma(const Mmu& mmu, const Cpu& cpu, Ppu& ppu); + + void Reset(bool cgbMode); + void Update(unsigned int cycles); + + void StartOamDmaTransfer(u8 sourceLocHi); + u8 GetOamDmaSourceLocHi() const; + + void SetNdma5(u8 val); + u8 GetNdma5() const; + + void SetNdmaSourceLocHi(u8 hi); + void SetNdmaSourceLocLo(u8 lo); + void SetNdmaDestLocHi(u8 hi); + void SetNdmaDestLocLo(u8 lo); + + NdmaStatus GetNdmaStatus() const; + bool IsNdmaInProgress() const; + bool IsHdmaInProgress() const; + bool IsGdmaInProgress() const; + + OamDmaStatus GetOamDmaStatus() const; + bool IsOamDmaInProgress() const; + + bool IsInCgbMode() const; + +private: + const Mmu& mmu_; + const Cpu& cpu_; + Ppu& ppu_; + + bool cgbMode_; + + OamDmaStatus oamDmaStatus_; + u8 oamDmaSourceLocHi_; + unsigned int oamDmaCyclesLeft_; + + NdmaStatus ndmaStatus_; + u16 ndmaReadLoc_, ndmaVramWriteLoc_; + u8 ndmaNumBlocksLeft_; + + unsigned int gdmaCyclesLeft_, hdmaBlockCyclesLeft_; + + void HandleOamDmaUpdate(unsigned int cycles); + void HandleGdmaUpdate(unsigned int cycles); + void HandleHdmaUpdate(unsigned int cycles); + + void UpdateHdmaHBlankState(); + + void DoNdmaTransfer(u8 maxNumBlocks); + void DoOamDmaTransfer(); + + bool IsHdmaEnabled() const; + bool IsNdmaEnabled() const; +}; + +#endif // SDGBC_DMA_H_ diff --git a/include/hw/gbc.h b/include/hw/gbc.h new file mode 100644 index 0000000..20934bb --- /dev/null +++ b/include/hw/gbc.h @@ -0,0 +1,52 @@ +#ifndef SDGBC_GBC_H_ +#define SDGBC_GBC_H_ + +#include "hw/apu/apu.h" +#include "hw/cart/cart.h" +#include "hw/cpu/cpu.h" +#include "hw/dma.h" +#include "hw/joypad.h" +#include "hw/mmu.h" +#include "hw/ppu.h" +#include "hw/serial.h" +#include "hw/timer.h" + +struct GbcHardware { + Cpu cpu; + Dma dma; + Ppu ppu; + Apu apu; + Mmu mmu; + Timer timer; + Serial serial; + Joypad joypad; + + WorkRamBanks wramBanks; + HighRam hram; + + Cartridge cartridge; + + GbcHardware(); +}; + +class Gbc { +public: + Gbc(); + + void Reset(bool forceDmgMode = false); + unsigned int Update(); // returns the amount of CPU cycles spent + + RomLoadResult LoadCartridgeRomFile(const std::string& filePath, + const std::string& fileName = {}); + + GbcHardware& GetHardware(); + const GbcHardware& GetHardware() const; + + bool IsInCgbMode() const; + +private: + GbcHardware hw_; + bool cgbMode_; +}; + +#endif // SDGBC_GBC_H_ diff --git a/include/hw/joypad.h b/include/hw/joypad.h new file mode 100644 index 0000000..3cd5e44 --- /dev/null +++ b/include/hw/joypad.h @@ -0,0 +1,46 @@ +#ifndef SDGBC_JOYPAD_H_ +#define SDGBC_JOYPAD_H_ + +#include "types.h" + +enum class JoypadKey : u8 { + A = 0x01, + B = 0x02, + Select = 0x04, + Start = 0x08, + Right = 0x10, + Left = 0x20, + Up = 0x40, + Down = 0x80 +}; + +class Cpu; + +class Joypad { +public: + explicit Joypad(Cpu& cpu); + + void Reset(); + + void CommitKeyStates(); + void SetKeyState(JoypadKey key, bool pressed); + + void SetJoyp(u8 val); + u8 GetJoyp() const; + + void SetImpossibleInputsAllowed(bool val); + bool AreImpossibleInputsAllowed() const; + + bool WasSelectedKeyPressed() const; + +private: + Cpu& cpu_; + + u8 keyStates_, nextKeyStates_; + bool selectButtonKeys_, selectDirectionKeys_; + bool wasSelectedKeyPressed_; + + bool leftRightOrUpDownAllowed_; +}; + +#endif // SDGBC_JOYPAD_H_ diff --git a/include/hw/memory.h b/include/hw/memory.h new file mode 100644 index 0000000..171725f --- /dev/null +++ b/include/hw/memory.h @@ -0,0 +1,23 @@ +#ifndef SDGBC_MEMORY_H_ +#define SDGBC_MEMORY_H_ + +#include "types.h" +#include + +constexpr u16 kRomBankSize = 0x4000, + kExtRamBankSize = 0x2000; + +using WorkRamBank = std::array; +using WorkRamBanks = std::array; + +using VideoRamBank = std::array; +using VideoRamBanks = std::array; + +using HighRam = std::array; + +using ObjectAttribMemory = std::array; +using CgbPaletteMemory = std::array; + +using WaveRam = std::array; + +#endif // SDGBC_MEMORY_H_ diff --git a/include/hw/mmu.h b/include/hw/mmu.h new file mode 100644 index 0000000..1f56544 --- /dev/null +++ b/include/hw/mmu.h @@ -0,0 +1,98 @@ +#ifndef SDGBC_MMU_H_ +#define SDGBC_MMU_H_ + +#include "hw/memory.h" + +enum IoRegisterId : u8 { + kIoRegisterIdJoyp = 0x00, + + kIoRegisterIdSb = 0x01, + kIoRegisterIdSc = 0x02, + + kIoRegisterIdDiv = 0x04, + kIoRegisterIdTima = 0x05, + kIoRegisterIdTma = 0x06, + kIoRegisterIdTac = 0x07, + + kIoRegisterIdIntf = 0x0f, + + kIoRegisterIdNr10 = 0x10, + kIoRegisterIdNr11 = 0x11, + kIoRegisterIdNr12 = 0x12, + kIoRegisterIdNr13 = 0x13, + kIoRegisterIdNr14 = 0x14, + kIoRegisterIdNr21 = 0x16, + kIoRegisterIdNr22 = 0x17, + kIoRegisterIdNr23 = 0x18, + kIoRegisterIdNr24 = 0x19, + kIoRegisterIdNr30 = 0x1a, + kIoRegisterIdNr31 = 0x1b, + kIoRegisterIdNr32 = 0x1c, + kIoRegisterIdNr33 = 0x1d, + kIoRegisterIdNr34 = 0x1e, + kIoRegisterIdNr41 = 0x20, + kIoRegisterIdNr42 = 0x21, + kIoRegisterIdNr43 = 0x22, + kIoRegisterIdNr44 = 0x23, + kIoRegisterIdNr50 = 0x24, + kIoRegisterIdNr51 = 0x25, + kIoRegisterIdNr52 = 0x26, + + kIoRegisterIdLcdc = 0x40, + kIoRegisterIdStat = 0x41, + kIoRegisterIdScy = 0x42, + kIoRegisterIdScx = 0x43, + kIoRegisterIdLy = 0x44, + kIoRegisterIdLyc = 0x45, + kIoRegisterIdDma = 0x46, + kIoRegisterIdBgp = 0x47, + kIoRegisterIdObp0 = 0x48, + kIoRegisterIdObp1 = 0x49, + kIoRegisterIdWy = 0x4a, + kIoRegisterIdWx = 0x4b, + + kIoRegisterIdKey1 = 0x4d, + + kIoRegisterIdVbk = 0x4f, + kIoRegisterIdHdma1 = 0x51, + kIoRegisterIdHdma2 = 0x52, + kIoRegisterIdHdma3 = 0x53, + kIoRegisterIdHdma4 = 0x54, + kIoRegisterIdHdma5 = 0x55, + kIoRegisterIdBcps = 0x68, + kIoRegisterIdBcpd = 0x69, + kIoRegisterIdOcps = 0x6a, + kIoRegisterIdOcpd = 0x6b, + + kIoRegisterIdSvbk = 0x70, + + kIoRegisterIdInte = 0xff +}; + +struct GbcHardware; + +class Mmu { +public: + explicit Mmu(GbcHardware& hw); + + void Reset(bool cgbMode); + + void Write8(u16 loc, u8 val); + u8 Read8(u16 loc) const; + + void WriteIoRegister(u8 regId, u8 val); + u8 ReadIoRegister(u8 regId) const; + + bool IsInCgbMode() const; + +private: + GbcHardware& hw_; + bool cgbMode_; + + // WRAM bank switch register + u8 svbk_; + + u8 GetWramBankIndex() const; +}; + +#endif // SDGBC_MMU_H_ diff --git a/include/hw/ppu.h b/include/hw/ppu.h new file mode 100644 index 0000000..88d40ff --- /dev/null +++ b/include/hw/ppu.h @@ -0,0 +1,237 @@ +#ifndef SDGBC_PPU_H_ +#define SDGBC_PPU_H_ + +#include "hw/memory.h" +#include "wxui/wx.h" +#include + +constexpr auto kLcdWidthPixels = 160u, + kLcdHeightPixels = 144u; + +struct RgbColor { + static RgbColor FromLcdIntensities(u8 r, u8 g, u8 b); + + u8 r, g, b; + + RgbColor(); + RgbColor(u8 r, u8 g, u8 b); +}; + +const std::array kDmgPaletteColors { + RgbColor(0xff, 0xff, 0xff), + RgbColor(0x9f, 0x9f, 0x9f), + RgbColor(0x5f, 0x5f, 0x5f), + RgbColor(0x00, 0x00, 0x00) +}; + +class ILcd { +public: + virtual ~ILcd() = default; + + virtual void LcdPower(bool powerOn) = 0; + virtual void LcdRefresh() = 0; + virtual void LcdPutPixel(unsigned int x, unsigned int y, + const RgbColor& color) = 0; +}; + +enum class PpuScreenMode : u8 { + HBlank = 0, + VBlank, + SearchingOam, + DataTransfer +}; + +class Cpu; +class Dma; + +class Ppu { +public: + explicit Ppu(Cpu& cpu, const Dma& dma); + + void Reset(bool cgbMode); + void Update(unsigned int cycles); + + void SetLcd(ILcd* lcd); + + void SetScanlineSpritesLimiterEnabled(bool val); + bool IsScanlineSpritesLimiterEnabled() const; + + void SetBgRenderEnabled(bool val); + bool IsBgRenderEnabled() const; + + void SetBgWindowRenderEnabled(bool val); + bool IsBgWindowRenderEnabled() const; + + void SetSpritesRenderEnabled(bool val); + bool IsSpritesRenderEnabled() const; + + void VramWrite8(u16 loc, u8 val); + u8 VramRead8(u16 loc) const; + + void OamWrite8(u16 loc, u8 val, bool oamDmaWrite = false); + u8 OamRead8(u16 loc) const; + + bool IsLcdOn() const; + PpuScreenMode GetScreenMode() const; + + void SetVbk(u8 val); + u8 GetVbk() const; + + void SetLcdc(u8 val); + u8 GetLcdc() const; + + void SetStat(u8 val); + u8 GetStat() const; + + u8 GetLy() const; + + void SetLyc(u8 val); + u8 GetLyc() const; + + void SetScy(u8 val); + u8 GetScy() const; + + void SetScx(u8 val); + u8 GetScx() const; + + void SetWy(u8 val); + u8 GetWy() const; + + void SetWx(u8 val); + u8 GetWx() const; + + void SetBgp(u8 val); + u8 GetBgp() const; + + void SetObp0(u8 val); + u8 GetObp0() const; + + void SetObp1(u8 val); + u8 GetObp1() const; + + void SetBcps(u8 val); + u8 GetBcps() const; + + void SetBcpd(u8 val); + u8 GetBcpd() const; + + void SetOcps(u8 val); + u8 GetOcps() const; + + void SetOcpd(u8 val); + u8 GetOcpd() const; + + bool IsInCgbMode() const; + +private: + struct BgTileInfo { + u8 patternNum; + u8 patternCgbPaletteNum; + u8 patternBankIndex; + bool patternFlipX, patternFlipY; + bool patternPriorityOverSprites; + + BgTileInfo(u8 patternNum, u8 patternCgbPaletteNum, u8 patternBankIndex, + bool patternFlipX, bool patternFlipY, + bool patternPriorityOverSprites); + BgTileInfo(u8 patternNum); + }; + + struct Sprite { + u8 oamLoc; + int x, y; + u8 patternNum; + u8 attribs; + }; + + struct BgPixelInfo { + u8 paletteColorNum; + bool alwaysBehindSprites; + bool ignoreSpritePriority; + + BgPixelInfo(); + }; + + struct SpritePixelInfo { + u8 paletteColorNum; + bool transparent; + + SpritePixelInfo(); + }; + + Cpu& cpu_; + const Dma& dma_; + ILcd* lcd_; + + VideoRamBanks vramBanks_; + ObjectAttribMemory oam_; + CgbPaletteMemory bcpData_, ocpData_; + + std::array scanlineSpritePixelInfos_; + std::array scanlineBgPixelInfos_; + + unsigned int screenModeCycles_; + bool cgbMode_; + + bool limitScanlineSprites_; + bool enableBg_, enableBgWindow_, enableSprites_; + + // LCD control & status registers + u8 lcdc_, stat_; + + // LCD position & scrolling registers + u8 scy_, scx_, ly_, lyc_, wy_, wx_; + + // LCD monochrome palette registers + u8 bgp_, obp0_, obp1_; + + // LCD color palette registers + u8 bcps_, ocps_; + + // LCD VRAM bank register + u8 vbk_; + + void UpdateScreenMode(unsigned int cycles); + + void ChangeScreenMode(PpuScreenMode mode); + unsigned int GetScreenModeMaxCycles() const; + + u8 IncrementLy(); + void SetLy(u8 val); + + void RenderScanline(); + void RenderBufferSpriteScanline(); + void RenderBufferBgScanline(); + void RenderBufferBgWindowScanline(); + + std::vector EnumerateScanlineSprites() const; + + // returns false if there is no need to buffer more pixels in the sprite. + // true otherwise + bool RenderBufferSpritePixel(const Sprite& sprite, u8 obpNum, int pixelX); + void RenderBufferBgPixel(const BgTileInfo& tileInfo, u8 bgpNum, int pixelX); + + RgbColor GetCgbPixelColor(const CgbPaletteMemory& data, + u8 pixelPaletteColorNum) const; + + BgTileInfo GetBgTileInfo(u16 tileMapEntryLocOffset) const; + + std::pair GetPatternLine( + u16 locOffset, u8 bankIndex, bool flipX) const; + std::pair GetBgPatternLine( + u8 patternNum, u8 bankIndex, u8 lineNum, bool flipX, bool flipY) const; + std::pair GetSpritePatternLine( + u8 patternNum, u8 bankIndex, u8 lineNum, bool flipX, bool flipY) const; + + u8 GetPatternNumberFromLine(const std::pair& line, u8 numIndex) const; + + void WriteCgbPaletteData(CgbPaletteMemory& data, u8& selectReg, u8 val); + u8 ReadCgbPaletteData(const CgbPaletteMemory& data, u8 selectReg) const; + + bool IsOamAccessible() const; + bool IsIn8x16SpriteMode() const; + + u8 GetVramBankIndex() const; +}; + +#endif // SDGBC_PPU_H_ diff --git a/include/hw/serial.h b/include/hw/serial.h new file mode 100644 index 0000000..5c808d1 --- /dev/null +++ b/include/hw/serial.h @@ -0,0 +1,46 @@ +#ifndef SDGBC_SERIAL_H_ +#define SDGBC_SERIAL_H_ + +#include "types.h" + +class ISerialOutput { +public: + virtual ~ISerialOutput() = default; + + virtual void SerialReset() = 0; + virtual void SerialWriteBit(bool bitSet) = 0; + virtual void SerialOnByteWritten() = 0; +}; + +class Cpu; + +class Serial { +public: + explicit Serial(Cpu& cpu); + + void Reset(bool cgbMode); + void Update(unsigned int cycles); + + void SetSerialOutput(ISerialOutput* dataOut); + + void SetSb(u8 val); + u8 GetSb() const; + + void SetSc(u8 val); + u8 GetSc() const; + + bool IsInCgbMode() const; + +private: + Cpu& cpu_; + + ISerialOutput* dataOut_; + + u8 sb_, sc_; + unsigned int nextBitTransferCycles_; + u8 transferNextBitIdx_; + + bool cgbMode_; +}; + +#endif // SDGBC_SERIAL_H_ diff --git a/include/hw/timer.h b/include/hw/timer.h new file mode 100644 index 0000000..9b37630 --- /dev/null +++ b/include/hw/timer.h @@ -0,0 +1,37 @@ +#ifndef SDGBC_TIMER_H_ +#define SDGBC_TIMER_H_ + +#include "types.h" + +class Cpu; + +class Timer { +public: + explicit Timer(Cpu& cpu); + + void Reset(); + void Update(unsigned int cycles); + + void ResetDiv(); + u8 GetDiv() const; + + void SetTima(u8 val); + u8 GetTima() const; + + void SetTma(u8 val); + u8 GetTma() const; + + void SetTac(u8 val); + u8 GetTac() const; + +private: + Cpu& cpu_; + + // timer & divider registers & clock counters + unsigned int divCycles_, timaCycles_; + u8 div_, tima_, tma_, tac_; + + unsigned int GetTimaFreqInCycles() const; +}; + +#endif // SDGBC_TIMER_H_ diff --git a/include/types.h b/include/types.h new file mode 100644 index 0000000..7286664 --- /dev/null +++ b/include/types.h @@ -0,0 +1,14 @@ +#ifndef SDGBC_TYPES_H_ +#define SDGBC_TYPES_H_ + +#include + +// unsigned types +using u8 = uint8_t; +using u16 = uint16_t; + +// signed types +using i8 = int8_t; +using i16 = int16_t; + +#endif // SDGBC_TYPES_H_ diff --git a/include/util.h b/include/util.h new file mode 100644 index 0000000..be7ccbf --- /dev/null +++ b/include/util.h @@ -0,0 +1,31 @@ +#ifndef SDGBC_UTIL_H_ +#define SDGBC_UTIL_H_ + +#include "types.h" +#include +#include + +class Cpu; + +namespace util { + u16 To16(u8 hi, u8 lo); + + u16 SetHi8(u16 val, u8 hi); + u16 SetLo8(u16 val, u8 lo); + + u8 GetHi8(u16 val); + u8 GetLo8(u16 val); + + u8 ReverseBits(u8 val); + + // halves the amount of cycles given if the CPU is in double-speed mode. + // used for hardware components that run at a constant speed regardless of + // double-speed mode + unsigned int RescaleCycles(const Cpu& cpu, unsigned int cycles); + + bool ReadBinaryStream(std::istream& is, std::vector& data, + bool resizeToFitData = true); + bool WriteBinaryStream(std::ostream& os, const std::vector& data); +} + +#endif // SDGBC_UTIL_H_ diff --git a/include/wxui/about_dialog.h b/include/wxui/about_dialog.h new file mode 100644 index 0000000..05470b0 --- /dev/null +++ b/include/wxui/about_dialog.h @@ -0,0 +1,11 @@ +#ifndef SDGBC_ABOUT_DIALOG_H_ +#define SDGBC_ABOUT_DIALOG_H_ + +#include "wxui/wx.h" + +class AboutDialog : public wxDialog { +public: + AboutDialog(wxWindow* parent); +}; + +#endif // SDGBC_ABOUT_DIALOG_H_ diff --git a/include/wxui/app.h b/include/wxui/app.h new file mode 100644 index 0000000..b3a0828 --- /dev/null +++ b/include/wxui/app.h @@ -0,0 +1,28 @@ +#ifndef SDGBC_APP_H_ +#define SDGBC_APP_H_ + +#include "wxui/wx.h" +#include + +class App : public wxApp { +public: + bool OnInit() override; + int OnRun() override; + + void OnInitCmdLine(wxCmdLineParser& parser) override; + bool OnCmdLineParsed(wxCmdLineParser& parser) override; + + bool InitCpuCmdMode(); +#ifdef _WIN32 + bool InitWin32Console(); +#endif + + bool IsInCpuCmdMode() const; + std::string GetStartupRomFilePath() const; + +private: + bool cpuCmdMode_; + std::string startupRomFilePath_; +}; + +#endif // SDGBC_APP_H_ diff --git a/include/wxui/joypad_dialog.h b/include/wxui/joypad_dialog.h new file mode 100644 index 0000000..5b7091e --- /dev/null +++ b/include/wxui/joypad_dialog.h @@ -0,0 +1,11 @@ +#ifndef SDGBC_JOYPAD_DIALOG_H_ +#define SDGBC_JOYPAD_DIALOG_H_ + +#include "wxui/wx.h" + +class JoypadDialog : public wxDialog { +public: + JoypadDialog(wxWindow* parent); +}; + +#endif // SDGBC_JOYPAD_DIALOG_H_ diff --git a/include/wxui/lcd_canvas.h b/include/wxui/lcd_canvas.h new file mode 100644 index 0000000..cb3528b --- /dev/null +++ b/include/wxui/lcd_canvas.h @@ -0,0 +1,46 @@ +#ifndef SDGBC_LCD_CANVAS_H_ +#define SDGBC_LCD_CANVAS_H_ + +#include "hw/ppu.h" +#include "wxui/sfml_canvas.h" +#include +#include +#include + +class LcdCanvas : public SfmlCanvas, public ILcd { +public: + LcdCanvas(wxWindow* parent = nullptr, wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, long style = wxBORDER_NONE); + + void LcdPower(bool powerOn) override; + void LcdRefresh() override; + void LcdPutPixel(unsigned int x, unsigned int y, + const RgbColor& color) override; + + void SetMaintainAspectRatio(bool val); + bool IsMaintainingAspectRatio() const; + + void SetSmoothFilterEnabled(bool val); + bool IsSmoothFilterEnabled() const; + +private: + bool maintainAspectRatio_; + bool isLcdOn_; + + std::array lcdFrameBuffers_; + sf::Image* lcdFrontBuffer_; + sf::Image* lcdBackBuffer_; + // NOTE: no need to ever lock for back buffer accesses, as these are only + // done by the emulation thread. only the front buffer is accessed by both + // the emulation and main thread + mutable std::mutex lcdFrontBufferMutex_; + + std::atomic lcdTextureNeedsRefresh_; + sf::Texture lcdTexture_; + + void CanvasRender() override; + void ClearToWhite(); +}; + +#endif // SDGBC_LCD_CANVAS_H_ diff --git a/include/wxui/main_frame.h b/include/wxui/main_frame.h new file mode 100644 index 0000000..dbb86b0 --- /dev/null +++ b/include/wxui/main_frame.h @@ -0,0 +1,80 @@ +#ifndef SDGBC_MAIN_FRAME_H_ +#define SDGBC_MAIN_FRAME_H_ + +#include "audio/sfml_apu_out.h" +#include "wxui/lcd_canvas.h" +#include "wxui/wx.h" +#include "emulator.h" +#include + +class MainFrame : public wxFrame { +public: + MainFrame(); + + bool LoadCartridgeRomFile(std::string filePath = {}); + + bool ExportCartridgeBatteryExtRam(std::string filePath = {}); + bool ImportCartridgeBatteryExtRam(std::string filePath = {}); + +private: + wxDECLARE_EVENT_TABLE(); + + LcdCanvas* lcdCanvas_; + SfmlApuSoundStream audioOut_; + Emulator emulator_; + + void RecursivelyConnectKeyEvents(wxWindow* childComponent); + + void UpdateUIState(); + void UpdateTitle(); + void UpdateMenu(); + + bool HandleJoypadKeyEvent(wxKeyEvent& event); + + void OnClose(wxCloseEvent& event); + void OnSize(wxSizeEvent& event); + void OnExit(wxCommandEvent& event); + + void OnKeyDown(wxKeyEvent& event); + void OnKeyUp(wxKeyEvent& event); + + void OnOpenRomFile(wxCommandEvent& event); + void OnImportBattery(wxCommandEvent& event); + void OnExportBattery(wxCommandEvent& event); + + void OnReset(wxCommandEvent& event); + void OnResetInDmgMode(wxCommandEvent& event); + void OnPause(wxCommandEvent& event); + void OnLimitFramerate(wxCommandEvent& event); + + void OnEnableBg(wxCommandEvent& event); + void OnEnableBgWindow(wxCommandEvent& event); + void OnEnableSprites(wxCommandEvent& event); + void OnLimitScanlineSprites(wxCommandEvent& event); + void OnMaintainAspectRatio(wxCommandEvent& event); + void OnSmoothenVideo(wxCommandEvent& event); + + void OnMuteSound(wxCommandEvent& event); + void OnMuteCh1(wxCommandEvent& event); + void OnMuteCh2(wxCommandEvent& event); + void OnMuteCh3(wxCommandEvent& event); + void OnMuteCh4(wxCommandEvent& event); + + void OnJoypadImpossibleInputs(wxCommandEvent& event); + + void OnJoypadControls(wxCommandEvent& event); + void OnAbout(wxCommandEvent& event); +}; + +class MainFrameFileDropTarget : public wxFileDropTarget { +public: + explicit MainFrameFileDropTarget(MainFrame& mainFrame); + + bool OnDropFiles(wxCoord x, wxCoord y, + const wxArrayString& filenames) override; + +private: + MainFrame& mainFrame_; +}; + +#endif // SDGBC_MAIN_FRAME_H_ diff --git a/include/wxui/sfml_canvas.h b/include/wxui/sfml_canvas.h new file mode 100644 index 0000000..32dde69 --- /dev/null +++ b/include/wxui/sfml_canvas.h @@ -0,0 +1,37 @@ +#ifndef SDGBC_SFML_CANVAS_H_ +#define SDGBC_SFML_CANVAS_H_ + +#include "wxui/wx.h" +#include +#include + +// NOTE: code for this class (header & source file) is mainly derived from: +// https://www.sfml-dev.org/tutorials/1.6/graphics-wxwidgets.php +class SfmlCanvas : public wxControl, public sf::RenderWindow { +public: + SfmlCanvas(wxWindow* parent = nullptr, wxWindowID id = wxID_ANY, + const wxPoint& pos = wxDefaultPosition, + const wxSize& size = wxDefaultSize, long style = wxBORDER_NONE); + virtual ~SfmlCanvas() = default; + + // NOTE: on some platforms (such as Windows) HasFocus() will not return the + // correct value when called from another thread! we provide our own impl + // that fixes this issue here + bool HasFocus() const override; + +private: + wxDECLARE_EVENT_TABLE(); + + std::atomic isInFocus_; + + virtual void CanvasRender(); + + void OnIdle(wxIdleEvent& event); + void OnPaint(wxPaintEvent& event); + void OnSize(wxSizeEvent& event); + void OnSetFocus(wxFocusEvent& event); + void OnKillFocus(wxFocusEvent& event); + void OnEraseBackground(wxEraseEvent& event); +}; + +#endif // SDGBC_SFML_CANVAS_H_ diff --git a/include/wxui/winrc/sdgbc.ico b/include/wxui/winrc/sdgbc.ico new file mode 100644 index 0000000..e926ebb Binary files /dev/null and b/include/wxui/winrc/sdgbc.ico differ diff --git a/include/wxui/winrc/sdgbc.rc b/include/wxui/winrc/sdgbc.rc new file mode 100644 index 0000000..5b103b7 --- /dev/null +++ b/include/wxui/winrc/sdgbc.rc @@ -0,0 +1,2 @@ +sdgbcIcon ICON "sdgbc.ico" +#include diff --git a/include/wxui/wx.h b/include/wxui/wx.h new file mode 100644 index 0000000..7ac6f6f --- /dev/null +++ b/include/wxui/wx.h @@ -0,0 +1,10 @@ +#ifndef SDGBC_WX_H_ +#define SDGBC_WX_H_ + +#include + +#ifndef WX_PRECOMP + #include +#endif + +#endif // SDGBC_WX_H_ diff --git a/include/wxui/xpm/sdgbc16.xpm b/include/wxui/xpm/sdgbc16.xpm new file mode 100644 index 0000000..a9ee9e4 --- /dev/null +++ b/include/wxui/xpm/sdgbc16.xpm @@ -0,0 +1,118 @@ +/* XPM */ +static const char *xpmSdgbc16[] = { +/* columns rows colors chars-per-pixel */ +"16 16 96 2 ", +" c #714D71", +". c #516871", +"X c #567171", +"o c #716871", +"O c #446997", +"+ c #74718A", +"@ c #7B7E8D", +"# c #756799", +"$ c #5C77AA", +"% c #7730F2", +"& c #763CF5", +"* c #4250F4", +"= c #855799", +"- c #935799", +"; c #995799", +": c #995999", +"> c #9C5B9C", +", c #806F8A", +"< c #9C629C", +"1 c #AA68AD", +"2 c #B602ED", +"3 c #A72BF6", +"4 c #A534F8", +"5 c #C033C0", +"6 c #C132C1", +"7 c #E200F2", +"8 c #E500F2", +"9 c #F301F5", +"0 c #F400F4", +"q c #D228FA", +"w c #D22BFB", +"e c #EE25FB", +"r c #9F6FDE", +"t c #9178FF", +"y c #BE66FE", +"u c #BF6DFF", +"i c #C35DC3", +"p c #C67AC6", +"a c #FB4AFB", +"s c #E65BFE", +"d c #FC52FC", +"f c #FB54FD", +"g c #ED64FF", +"h c #FF64FF", +"j c #548099", +"k c #59859C", +"l c #59879C", +"z c #68828C", +"x c #6C8A8D", +"c c #6B8C8C", +"v c #778E9C", +"b c #69909C", +"n c #609C9C", +"m c #65A9AF", +"M c #68AFAF", +"N c #3681D9", +"B c #38A5C6", +"V c #3DBBC6", +"C c #0086F8", +"Z c #0097FB", +"A c #02A7FD", +"S c #04B6FF", +"D c #50BFFF", +"F c #7EBAFF", +"G c #38D4DF", +"H c #3EDFDF", +"J c #06C1FF", +"K c #08C0FF", +"L c #09CEFF", +"P c #0CDCFF", +"I c #23CBFF", +"U c #0FE8FF", +"Y c #10EAFF", +"T c #15F4FF", +"R c #16F8FF", +"E c #15FAFF", +"W c #18FEFF", +"Q c #19FFFF", +"! c #1BFFFF", +"~ c #1DFFFF", +"^ c #43C6C6", +"/ c #44C6C6", +"( c #45C6C6", +") c #4CCAFF", +"_ c #79C0FF", +"` c #85899C", +"' c #90859C", +"] c #ADAFFF", +"[ c #AAB2FF", +"{ c #C68FC6", +"} c #C393C6", +"| c #FF9AFF", +" . c #D8A7FF", +".. c #DCA6FF", +"X. c #F7A8FF", +"o. c None", +/* pixels */ +"o.o.o.o.o. i { p } o o.o.o.o.o.", +"o.o.o.o.o.; a | X...' o.o.o.o.o.", +"o.o.o.o.o.; d h .] ` o.o.o.o.o.", +"o.o.o.o.o.: f g [ F v o.o.o.o.o.", +"o.o.o.o.o.> s u _ D b o.o.o.o.o.", +" ; ; ; < 1 y t ) I m n n n n X ", +"5 0 0 0 e w r @ x G R ! ! ! ! ( ", +"5 0 0 0 q 4 , o.o.c ! ! ! ! ! ( ", +"5 0 9 8 3 & + o.o.c ~ ! ! ! ! ( ", +"5 0 7 2 % * N z c H ! ! ! ! ! ( ", +" ; - = # $ C K T ! M n n n n X ", +"o.o.o.o.o.O Z L R ! n o.o.o.o.o.", +"o.o.o.o.o.j A P ! ! n o.o.o.o.o.", +"o.o.o.o.o.l S U W ! n o.o.o.o.o.", +"o.o.o.o.o.l J U ! ! n o.o.o.o.o.", +"o.o.o.o.o.. B V ^ ^ X o.o.o.o.o." +}; diff --git a/include/wxui/xpm/sdgbc24.xpm b/include/wxui/xpm/sdgbc24.xpm new file mode 100644 index 0000000..b7dfc2e --- /dev/null +++ b/include/wxui/xpm/sdgbc24.xpm @@ -0,0 +1,211 @@ +/* XPM */ +static const char *xpmSdgbc24[] = { +/* columns rows colors chars-per-pixel */ +"24 24 181 2 ", +" c black", +". c #0C0C0C", +"X c #35353A", +"o c #3A353A", +"O c #353A3A", +"+ c #3A3A3A", +"@ c #78747E", +"# c #7A747E", +"$ c #707C7E", +"% c #707E7E", +"& c #727E7E", +"* c #747E7E", +"= c #78787E", +"- c #456898", +"; c #456B98", +": c #547D9B", +"> c #6B6E9B", +", c #796299", +"< c #6876B7", +"1 c #7D1FEF", +"2 c #5A33F0", +"3 c #7D32F3", +"4 c #3156F3", +"5 c #4C57F6", +"6 c #6B52F9", +"7 c #855799", +"8 c #8A5A99", +"9 c #965B96", +"0 c #945E94", +"q c #9B579B", +"w c #985B98", +"e c #98599B", +"r c #9B599B", +"t c #9B5C9B", +"y c #9B5D9B", +"u c #9B5F9B", +"i c #956A95", +"p c #9B699C", +"a c #977997", +"s c #977D97", +"d c #977F97", +"f c #9868B9", +"g c #B66CBB", +"h c #9D1CF1", +"j c #AC00EA", +"k c #BD04F0", +"l c #BC1BF4", +"z c #9C2EF6", +"x c #B823F7", +"c c #CD00EF", +"v c #D905F4", +"b c #DB19F8", +"n c #E900F3", +"m c #EA00F2", +"M c #F300F4", +"N c #F201F5", +"B c #F400F4", +"V c #EB18F9", +"C c #F615F8", +"Z c #D426FB", +"A c #F92BF9", +"S c #F92DF9", +"D c #E73CFF", +"F c #FA36FA", +"G c #FB3FFB", +"H c #8A4DFB", +"J c #A949FD", +"K c #8774FF", +"L c #AE63FC", +"P c #A26EFF", +"I c #B87DFF", +"U c #C841FE", +"Y c #C554FD", +"T c #E14FFE", +"R c #FB44FC", +"E c #F14AFD", +"W c #E15EFF", +"Q c #FF59FF", +"! c #FB5DFF", +"~ c #C265FF", +"^ c #D778FF", +"/ c #F574FF", +"( c #FF71FF", +") c #59849C", +"_ c #59859C", +"` c #5D8899", +"' c #59889C", +"] c #658D95", +"[ c #6B8E9C", +"{ c #748E9C", +"} c #7D8D9C", +"| c #649297", +" . c #659697", +".. c #679797", +"X. c #6A9595", +"o. c #62919C", +"O. c #639999", +"+. c #609C9C", +"@. c #5F81B8", +"#. c #528EB8", +"$. c #59B0BB", +"%. c #5BB7BA", +"&. c #5DB9B9", +"*. c #5EB9B9", +"=. c #67B6BB", +"-. c #6CBBBB", +";. c #0180F6", +":. c #0089F8", +">. c #008FF9", +",. c #1B84F7", +"<. c #0199FB", +"1. c #009BFC", +"2. c #01A5FD", +"3. c #01A4FE", +"4. c #02B0FF", +"5. c #03B7FF", +"6. c #04B9FF", +"7. c #05BCFF", +"8. c #34BEFF", +"9. c #53B8FF", +"0. c #53BEFF", +"q. c #6DB9FF", +"w. c #70B7FF", +"e. c #05C0FF", +"r. c #08CBFF", +"t. c #0DCAFF", +"y. c #0AD1FF", +"u. c #0AD2FF", +"i. c #0BD6FF", +"p. c #0CDBFF", +"a. c #16D2FF", +"s. c #29D8FF", +"d. c #0EE3FF", +"f. c #10ECFF", +"g. c #15ECFF", +"h. c #11F1FF", +"j. c #12F1FF", +"k. c #11F2FF", +"l. c #12F4FF", +"z. c #14F8FF", +"x. c #16FDFF", +"c. c #16FEFF", +"v. c #17FFFF", +"b. c #18FFFF", +"n. c #1AFFFF", +"m. c #1BFFFF", +"M. c #1CFFFF", +"N. c #1DFFFF", +"B. c #20F0FF", +"V. c #20FFFF", +"C. c #29FFFF", +"Z. c #47D7FF", +"A. c #65D3FF", +"S. c #87889C", +"D. c #958595", +"F. c #978597", +"G. c #948599", +"H. c #90859C", +"J. c #8A91BC", +"K. c #958DFF", +"L. c #8DAEFF", +"P. c #8FB4FF", +"I. c #AEA5FF", +"U. c #B1A6FF", +"Y. c #A7BFFF", +"T. c #CD9FFF", +"R. c #FF85FF", +"E. c #FF86FF", +"W. c #FF8CFF", +"Q. c #E89AFF", +"!. c #EE9FFF", +"~. c #FF90FF", +"^. c #FF94FF", +"/. c #F99DFF", +"(. c #D0A1FF", +"). c #C5BBFF", +"_. c #FCA9FF", +"`. c #FFA9FF", +"'. c #E4B7FF", +"]. c #85CCFF", +"[. c None", +/* pixels */ +"[.[.[.[.[.[.[.[.o i F.F.a a D.+ [.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.w A R.`.W._.!.G.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.q S ~.^./.'.(.H.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.q F R.Q Q.).U.S.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.q G ( ! T.Y.P.} [.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.w R / W I.].w.{ [.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.r E ^ ~ L.A.9.[ [.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.y T I P q.Z.8.o.[.[.[.[.[.[.[.[.", +"o w q q r q u p g Y K.K 0.s.a.=.+.+.+.+.+.+.O.O ", +"0 B B B B B C D Z L J.= * $.B.j.N.b.b.N.b.N.N.X.", +"9 B B B B B V U x f [.[. %.b.N.N.N.N.N.N.b...", +"9 B B B B B b J z # [.[.[.[.% N.b.N.b.b.b.N.N...", +"9 B B B N m l H 3 @ [.[.[.[.% V.N.b.N.N.N.b.b...", +"9 B B B v c h 6 2 < [.[.. *.N.b.N.N.b.b.N.N...", +"0 N B m k j 1 5 4 ,.#.% % *.C.N.b.N.N.b.N.N.N.X.", +"o w q e 8 7 , > @.;.<.t.g.b.b.-.+.+.+.+.+.+.O.O ", +"[.[.[.[.[.[.[.[.- :.3.u.h.b.N.+.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.; >.5.p.z.b.b.+.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.: 1.e.d.b.b.N.+.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[._ 3.r.f.b.N.b.O.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[._ 4.u.h.b.b.b.+.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.' 6.i.h.b.N.N.+.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.` 7.i.h.b.N.N.O.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.X ] | ......X.O [.[.[.[.[.[.[.[." +}; diff --git a/include/wxui/xpm/sdgbc256.xpm b/include/wxui/xpm/sdgbc256.xpm new file mode 100644 index 0000000..9b34c7d --- /dev/null +++ b/include/wxui/xpm/sdgbc256.xpm @@ -0,0 +1,501 @@ +/* XPM */ +static const char *xpmSdgbc256[] = { +/* columns rows colors chars-per-pixel */ +"256 256 239 2 ", +" c #000000", +". c #0D0D0D", +"X c #1B1B1B", +"o c #141414", +"O c #232323", +"+ c #2A2A2A", +"@ c #333333", +"# c #454545", +"$ c #555555", +"% c #5B5C5C", +"& c #646464", +"* c #6A6A6B", +"= c #737373", +"- c #5336F0", +"; c #7829F1", +": c #6D31F2", +"> c #790DEB", +", c #105CF1", +"< c #3757F4", +"1 c #2F55F3", +"2 c #0169F1", +"3 c #0077F4", +"4 c #0F66F2", +"5 c #2866F6", +"6 c #4959F6", +"7 c #5757F7", +"8 c #4E47F3", +"9 c #6849F6", +"0 c #6756F9", +"q c #7757FB", +"w c #7847F8", +"e c #7879FF", +"r c #7078FF", +"t c #4462F8", +"y c #3F38EE", +"u c #9801E9", +"i c #8B09EB", +"p c #A701EB", +"a c #B800EC", +"s c #B310F2", +"d c #8828F3", +"f c #9827F4", +"g c #9135F7", +"h c #A728F6", +"j c #B827F8", +"k c #B639FB", +"l c #A938FA", +"z c #C700EE", +"x c #D200EF", +"c c #C803F1", +"v c #DB01F1", +"b c #D503F2", +"n c #CD1BF7", +"m c #E301F2", +"M c #EC01F3", +"N c #E609F6", +"B c #F400F4", +"V c #F90BF9", +"C c #F807F8", +"Z c #F916FB", +"A c #E817FA", +"S c #C826FA", +"D c #D828FB", +"F c #C839FD", +"G c #D739FF", +"H c #FC27FF", +"J c #E737FF", +"K c #FB38FF", +"L c #E627FD", +"P c #8948FA", +"I c #9847FB", +"U c #8E55FD", +"Y c #A745FD", +"T c #B847FE", +"R c #AF55FF", +"E c #9869FF", +"W c #8877FF", +"Q c #9777FF", +"! c #876AFF", +"~ c #A767FF", +"^ c #B668FF", +"/ c #A776FF", +"( c #B77AFF", +") c #C546FF", +"_ c #D747FF", +"` c #C75AFF", +"' c #D757FF", +"] c #E844FF", +"[ c #FF49FF", +"{ c #FB44FF", +"} c #E758FF", +"| c #FF53FF", +" . c #FE5BFF", +".. c #F357FF", +"X. c #C869FF", +"o. c #D867FF", +"O. c #C779FF", +"+. c #D879FF", +"@. c #E766FF", +"#. c #FE64FF", +"$. c #FF6CFF", +"%. c #F367FF", +"&. c #E777FF", +"*. c #FF74FF", +"=. c #FF7CFF", +"-. c #F477FF", +";. c #008CF9", +":. c #0184F7", +">. c #0093FB", +",. c #009CFD", +"<. c #0E94FD", +"1. c #1796FE", +"2. c #2987FB", +"3. c #00A4FE", +"4. c #01ABFF", +"5. c #02B4FF", +"6. c #04BCFF", +"7. c #1BBAFF", +"8. c #38B9FF", +"9. c #29B8FF", +"0. c #7291FF", +"q. c #49B9FF", +"w. c #57B7FF", +"e. c #56AFFF", +"r. c #67B6FF", +"t. c #78B9FF", +"y. c #72ACFF", +"u. c #5294FF", +"i. c #07C4FF", +"p. c #09CCFF", +"a. c #19C8FF", +"s. c #0AD3FF", +"d. c #0CDBFF", +"f. c #13D7FF", +"g. c #27C5FF", +"h. c #37C3FF", +"j. c #35D9FF", +"k. c #0EE4FF", +"l. c #0FE8FF", +"z. c #10ECFF", +"x. c #19E7FF", +"c. c #12F3FF", +"v. c #15FEFF", +"b. c #1BFFFF", +"n. c #27E6FF", +"m. c #38E3FF", +"M. c #2AFDFF", +"N. c #49C6FF", +"B. c #56C6FF", +"V. c #47D8FF", +"C. c #56D8FF", +"Z. c #68C5FF", +"A. c #78CAFF", +"S. c #67D6FF", +"D. c #77D7FF", +"F. c #47E3FF", +"G. c #57E3FF", +"H. c #59FCFF", +"J. c #4CFCFF", +"K. c #65FFFF", +"L. c #6DFDFF", +"P. c #70ECFF", +"I. c #868686", +"U. c #99999B", +"Y. c #A9AAAB", +"T. c #B8BABC", +"R. c #9E9EA1", +"E. c #9787FF", +"W. c #8796FF", +"Q. c #9794FF", +"!. c #8988FF", +"~. c #A887FF", +"^. c #B789FF", +"/. c #B79CFF", +"(. c #A89AFF", +"). c #98ABFF", +"_. c #88B8FF", +"`. c #97B5FF", +"'. c #88AAFF", +"]. c #A8A6FF", +"[. c #B9AAFF", +"{. c #A8B7FF", +"}. c #B7B8FF", +"|. c #C79AFF", +" X c #D798FF", +".X c #CF86FF", +"XX c #FF84FF", +"oX c #FF8BFF", +"OX c #F485FF", +"+X c #E79BFF", +"@X c #FF94FF", +"#X c #FE9CFF", +"$X c #F39AFF", +"%X c #E685FF", +"&X c #C8A8FF", +"*X c #D8A8FF", +"=X c #C8BAFF", +"-X c #D7BAFF", +";X c #E7A8FF", +":X c #FFA3FF", +">X c #FFABFF", +",X c #F4A8FF", +"X1X1X2X1X1X1X2X2X2XsXsXsXsXfXfXfXdXdXdXdXdXsX#XXX=.=.=.XXoXoXoX@X#X>X2XsXfXfXfXsXfXsXfXsXsXsXsXfXzXbXzXY.$ # MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO @ U.fX1X$.K C B B C K .#.$.$.*.*.=.XXXXXXXXXXXXoX@X#X#X#X#X:X:X>X>X1X2X2X1X1X#X*.[ K K K K [ [ | #.*.oX#X:X>X>X:X:X:X>X#X#X#X@X#X:X2XdXzXmXU.@ O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX . * tXfX$.C B B B B C H | #.$.$.$.$.=.XXXXoXXXXXXXoX@X@X#X#X#X:X:X>X1X1X1X2X2X1X@X*.[ K K { { [ [ .*.oX:X:X>X>X>X:X>X:X:X#X#X#X#X$X$X$X,XdXbXtX* MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX + U.zX1XK B B B B B V K .$.*.$.$.*.=.XXXXoXXXXXoXoX@X@X#X#X#X:X>X>X1X1X2X1X2X>XoX#.[ K { [ { [ | $.XX#X>X1X>X>X>X:X>X:X:X:X#X$X#X$X$X$X+X2XzXbXU.+ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX # 4XdX$.C B B B B C V K | $.*.$.*.=.=.XXoXXXXXoXoX@X@X#X#X#X:X:X>X>X1X1X2X2X2X>X@X$.| { K [ [ .$.XX@X>X1X1X1X>X>X>X>X>X:X$X#X$X$X$X$X$X+X;XfXbX4X# MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX+ $ tXfX[ B B B B B B V K | #.*.$.*.*.XXXXoXoXoXoXoX@X@X#X:X#X:X:X>X>X1X1X1X2X2X>X#X*.[ { [ | #.*.oX#X>X1X1X1X1X>X>X>X:X:X:X,X$X$X$X$X+X+X+X+XsXbXtX$ + MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO & rXdX{ B B B B B C Z [ #.$.$.$.*.*.XXXXoXoXoX@X@X@X#X#X#X#X:X:X>X1X1X1X2X2X2X>X@X*.| [ [ #.=.@X>X1X1X1X2X1X1X>X>X>X,X,X,X,X,X,X+X$X+X+X+X+XsXbXrX& O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO & rXdX[ B B B B B B Z [ $.$.*.*.*.*.XXXXoXoXoX@X@X@X#X#X#X#X:X>X>X1X1X2X2X2X1X:X@X*.#. .$.=.@X>X1X1X2X2X1X1X>X>X,X,X,X,X,X,X+X,X+X+X+X+X X XsXbXrX& O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXdX[ B B B B B B C K | $.*.*.*.=.XXXXoXoX@X@X@X@X#X#X#X:X:X:X>X1X1X1X2X2X2X1X#XXX*.*.XX:X1X1X2X2X2X1X2X1X1X,X1X1X,X,X,X,X;X+X+X+X+X+X+X XsXbXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXdX[ B B B B B B C H | $.*.*.*.*.=.XXoXoXoX@X@X@X#X#X:X:X:X>X>X1X2X2X2X2XsX1X#XoXXXXX:X2X2X2X2X2X2X2X2X2X,X3X,X,X,X,X;X+X;X+X+X+X+X X+X XsXbXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXdX[ B B B B B B C K .*.*.*.*.=.XXoXXXoX@X@X@X#X#X:X#X:X:X>X>X1X1X1X2X2X2X1X:X@X#X#X1XsX2XsX2X2X2X2X2X3X3X3X,X,XX>X1X1X2X2XsX2X1X>X:X#X:X1X2XsXsX2X2X2X3X2X3X3X1XX1X1X1X1X2X1X:X@X@X#X>XsXsX2XsX2X2X2X2X3X3X3XX>X>X1X1X2X1X>X:XoXXX@X:X1XsXsXsXsXsX2XsX3X3X3XX>X1X2X1X:X#XoX*.=.@X>X1XsXsXsXsXsX3X3XX>X>X1X1X1X#XXX*.#.#.XX#X1XsXsXsXsXX>X>X>X>X>X:XoX*. .| | $.@X1XsXsXsXsXsX3XsXX>X1X>X:X#X@X=.#.[ [ | *.@X1XsXaXsXaXX>X>X>X:X#XoX*. .[ [ [ | *.#XsXsXsXsXaXaXX>X>X:X#XoX=.#.[ [ { [ | *.:X2XaXaXaXaXaX3XX:X@XXX=.$.#.| [ { { | =.#X,XsXaXaXaXaXaXiXaX-XiXX$.[ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ | | | | [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ | .*.=.XX=.XXXXXX=.*.*.*.=.=.=.*.$.$.$.$.$.*.%...J A n n D G ) R ( / ~.~.E.E.Q.W.W.W.W.W.!.!.W W ! ! ! e e r r r r r r 0.u.q.q.h.j.h.j.n.n.n.n.x.x.x.x.x.a.7.7.a.f.d.k.k.x.z.l.k.k.l.l.n.M.K.H.H.H.H.H.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.wXhXxXlXY.$ # MXMX", +"MXMXO @ U.rX2X$.K C B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C Z H K { { { { { K K K K J K K L L L A L L D L D A n A n n S ) R / / E.E.E.E.W.W.W.W.W.W.!.0.W ! ! !.~./.(.].].{.].`.`.`.6X5X7XwXP.P.j.n.n.n.n.n.x.c.x.x.a.a.7.a.f.l.l.z.z.z.l.l.l.l.l.k.k.l.k.l.z.z.c.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.J.wXhXkXU.@ O MXMX", +"MXMX . * tXdX$.C B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C V H K [ { K { { K K H J K J J L L L L L L L D A n n n n S F T ~ / ~.E.E.E.Q.W.W.W.W.'.W.0.!.!.(.[.=XzXbXbXnXbXnXnXnXcXcXcXnXcXcXbXjXwXL.K.m.x.x.x.z.z.d.a.a.a.f.x.z.z.z.z.z.z.l.l.l.l.k.l.l.k.l.l.z.c.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.wXxXtX* . MXMX", +"MXMX O U.zX1XK B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B Z K K { { { { { { J K J J J J J L L L L L D D D n N A n S k R ~ / E.E.E.E.!.W.W.W.y.0.).{.{.uXzXbXbXnXbXmXrXrXrXrXrXrXrXrXrXrXmXbXbXbXcXkXwXK.H.n.l.l.f.a.a.f.l.c.z.z.z.z.z.z.z.l.z.l.l.k.l.l.l.l.z.c.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.H.hXxXU.+ MXMX", +"MXMX # 4XdX$.C B B B B B B B B B B B B M B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C Z H K { [ { { ] ] K J K J J J L J L L L D D D S n n n n S F T ~ / E.E.E.E.E.W.W.W.0.'.0XnXbXbXbXnXtXY.I.= * & & & & & & & & * = I.Y.tXmXbXcXcXhXL.M.k.f.f.d.l.z.c.c.z.z.z.z.z.z.z.z.l.l.l.l.k.l.z.z.c.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.wXxX4X# MXMX", +"MX+ $ tXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C V Z K { { { ] { ] J J L J J J J L D L D D D D D D n S S S k T ~ / E.Q !.!.!.W.W.'.9X0XnXbXnXtXY.= % @ X X @ $ I.Y.tXnXbXjXL.F.n.d.z.c.c.c.c.c.z.z.z.z.z.z.z.z.l.l.l.l.l.z.c.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.jXtX$ + MX", +"MXO & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B V H K K { ] { ] ] J J J J J G J L D D D D S G S D S S S j k T ~ E Q !.!.!.!.'.{.pXxXbXvXyXU.% + o . @ % U.yXcXxXhXL.J.M.c.c.v.c.z.c.z.c.z.c.z.z.l.l.l.l.l.z.c.c.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& O MX", +"MXO & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B Z K { { { { ] ] ] J J J J G G G D D D D D D D S S S S j S k R ~ Q E.E.!.!.Q.0XnXbXnXrXR.% O O % R.tXcXnXhXL.M.v.c.c.c.z.c.z.c.z.z.z.l.l.l.l.z.z.z.c.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.K.xXrX& O MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B Z K J { ] ] } ] ] J J G J G G L D D D D S S S n S k j j k k Y E Q !.!.W Q.}.nXbXmXY.= @ @ = Y.vXbXkXL.M.c.c.c.z.z.c.z.c.c.z.l.l.l.z.z.z.z.c.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C H K ] { ] ] ] ] J J L G G G G D D D S D S S S S S F j j Y R ! Q !.Q.].uXvXbXtXI.# . . + O X X X X X X O O . . # I.tXcXhXL.J.M.c.c.z.z.c.c.c.z.z.z.z.z.z.c.c.c.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX{ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M H J ] ] ] ] ] ] G J G G G G D G D D S S n n n S j j j k Y E Q W Q.0XnXbXvXT.* O O MXMXMXMXMXMXMXMXMXMXMXMXMXMXO O * T.vXcXkXL.M.z.z.z.c.c.c.c.z.z.z.c.z.c.c.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B V H K ] ] ] _ _ ] G G G G G G G D S n S n n s S j S k j l l I E !.].nXbXmXY.$ o . $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX$ . o % Y.vXbXhXJ.z.z.c.z.c.c.c.c.c.c.c.c.c.v.c.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B Z H J ] ] ] _ ] _ G G G G G F G S D S n n s n j j k j k j l Y W E.uXbXmXR.# $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX$ # U.mXcXL.M.z.z.c.c.c.c.c.c.c.c.c.c.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX{ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M B B B B B B B B B B B B B Z L J ] ] _ _ _ _ G G G G F F F F S S n n n n j j j k h l h I / &XvXvXY.# o MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX # Y.xXhXL.M.z.c.c.v.c.c.c.c.c.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B Z J J ] _ _ _ _ G G G F F G G F S S n n s S S j j l h k h l Y |.zXnXT.% % MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX$ % T.cXkXL.M.c.c.c.c.c.v.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B Z J ] _ _ _ ' _ _ G G F F ) F F S S S n n s s j j h l h l h X.dXbXtX= o O I.MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXI.O o * yXbXhXJ.c.c.c.v.v.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B A J G _ _ ' _ _ ) G F G F F F S S S j n n j s j j l l h h Y XzXmXI.O O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO O I.mXcXwXM.c.c.c.v.v.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M B B B B B B B B B B B B B B B B B B B B B B B B C A J J _ _ _ ) _ F F F F F F F k S S s S s s j j h h h h h ^ fXzXY.# O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX # R.xXhXL.M.c.c.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B A G _ _ _ _ _ ) F F F ) ) k F F S j S s j s j j h l l f l XzXtX= . . I.MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXI.. % yXbXhXJ.c.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B A J _ ) ) ) ) ) ) F F ) F T k k j j j s s j s h h h l Y .XdXzXU.@ $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX% X I.vXbXwXM.c.v.v.v.v.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M B B B B B A J G _ ) ` ` ) ) ) ) T ) T k j S s s j h s s h h f f ^ fXbXyX& o MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX # T.bXhXJ.c.v.v.v.v.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M B B B B B B B B B B B B B A D _ _ ) ) ) T T T T T T T k k j j j h s s h h h f g O.zXbXI.O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX o I.nXxXH.c.v.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M M A D _ ) ) ) ) T T T T T R T T k j j h j s h h h f l Y XnXtX% $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX% $ tXcXwXM.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B N A G ) ) ) ) ) ` T T R R Y Y k l h s s h h h h h f g ^ dXbXY.@ . MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX + R.bXhXJ.v.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B M B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B N D F ) ) ) ) T T R R R R R Y k h h h s h s f h f f g ( zXnX= o $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX o I.mXcXH.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B M B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M D F ) ) T ) T T Y R R Y R Y l l j h h h h s f f f I XnXtX$ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX $ tXcXwXM.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M m D F F ) k ) T T T R R R R Y Y l h h h h f s f f d ^ dXbXY.@ O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO @ T.bXhXJ.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M N D F ) T ) k k T T Y Y U Y Y I l h h f h f f f g g ( zXbXI.o MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX o I.bXcXK.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B A S F k ) k T k k Y R U R U Y Y l l h h f i f f f g ( zXmX= . . MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX. = mXcXH.b.v.v.b.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M b S F ) T k k k Y R U R R U Y g l f f f h f f d d g ( zXrX* O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO * rXxXK.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M m b S k k T T l T Y I R U R R I l g h f f f f f d d g / zXrX& O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO * rXxXH.b.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M B M N b n j k k T l l Y R R U U U I I l f f f f f d d d d ( zXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXxXK.v.b.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M B B M M M m b S S k k T T Y Y Y U R U U U g g g f f f d f d d ; ( zXrX* X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXxXK.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX{ B B B B B B B B B B B B B B M B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M B B M M M m v b S j k k Y l Y Y I U U U I I I g f f i f d d d d : ^.zXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXxXK.v.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C B C B B B B B B B B B B B B B M B M M M m v v n j j k l Y l Y Y R U U U U P g g g d g d d d ; g : / zXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXxXK.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C B B B B B B B B B B B B B M B m m m m v v b c j k Y Y Y Y Y I I U U U U P P g g d d d d ; ; : g ( zXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXxXK.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C C B C B B B B B B B B B B B B B B m m m m m v v x c j l l Y Y l I I I U U U U P P g g d d d d d ; : : / zXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXxXK.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C C B B B B B B B B B B M M B M m m m v v v b c c j k Y l I l I I U U U U U q P d d d d d ; d ; P : / zXrX& O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO & rXxXK.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C B B B B B B B B B B B B M m M m m m v x x x x c j l l Y I l I I U U U U q U w g ; d i d ; ; : : : ~.zXrX* + MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO * rXcXK.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX| B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C B B C C B B B B B m m B M M M M M m m m v v x x c s h l I I I I I I P P q q q q w g d d d ; ; ; : : w / vXmX= . MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX. = mXxXK.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C B C B C C B B B m B B B B B M M M M m m m m x x x z c s l l l I I I U P P U U ! q P g ; d ; ; ; ; ; : : : Q nXbXI.X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX X I.bXcXK.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C C B B B m m m m m m M M m m m m x x x x z a s h I I I I P P P P U q q q q w ; ; d ; ; ; ; : : : E gXbXY.@ O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO @ Y.bXkXH.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C B B B M M M m m m m M m M m m m m m x x x z z a s h l I P I I P P U q q q q q q g ; ; > : : : : : : 9 /.zXyX$ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX $ tXnXwXM.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B C B C C N M M B m m m m m m m m m m m m x z z z z a a h g I I P P P P q U q q q 0 9 : ; ; ; : : : : - - : Q nXnXI.o $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX . I.vXxXK.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX| B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M C N C N N M m M m m m m m m m m m m v v x x z z z a s h f I P P P g P q q q q r q q : : ; : > : > : - : - ! gXbXY.@ . MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX @ Y.bXkXJ.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M N N N m m m m m x m x v m v v x v x x z z a a a s h g I U P P P w q q q q q 0 w : ; ; : > : - - : - - 9 ].nXtX% $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX% $ tXnXwXM.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B N N N v m m m v x x x x m m x m x x x x z z a a a p h g P P P P P P q 0 q r q 0 9 : ; ; - : : : : - - - - W nXbXI.O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX o I.nXcXK.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B M B B B B B B B B B B B B B B B B B B B B M M N N N A v v m v x x m x x m m x x x x z z a a a p p f g P P P w w w q q 0 0 0 0 9 : : : > ; : - - - 8 - - r gXbXtX% X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXo # T.bXhXH.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M M N N N A x v v x x x x x x x x x x x x z z a a p a p p f g P P P w w q q 0 0 0 0 0 0 : : : > - : - - - - 8 8 7 W.gXvXR.@ $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX& O I.mXbXwXM.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M M m N N x A b b x x z z x x x x x x x x z z a a a a p f d g P P w w w q q 0 q 0 0 7 9 - : : - : : : - - - - < - 8 ).zXtX* o I.MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXI.. % yXbXhXH.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M M M N v N n b n b b b c c c c x x x z z z z a a a p p p f f g w w w q w w 0 0 0 r 7 0 9 - : - > : - - - - y y - y < r pXcXY.# O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX @ Y.cXjXL.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B M B B B B B B B B B B B B B B B B M M m v b N v b b b c c c c c c c x x z z z z a a p a p p p d d P w w w q 0 0 0 0 7 7 0 7 : : : - - - - - - < - < < < < `.nXmXI.O O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO O I.mXcXwXM.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M m v v b b b b c b c c c c c z z z z z z z a a p p p p u d P w w w w w 0 0 0 7 0 r 7 9 : - - - y - - - - - 8 8 < 1 5 u.kXbXtX* o O I.MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXI.O o * tXbXhXH.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M M m v v b b b c c c s s c c c z z z z z a a a p p p p u d d w : w w 9 0 9 0 7 0 7 t 0 8 - : - - > - - y - 1 y 1 < 1 5 2.t.kXvXT.$ . % MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX% . % T.cXkXL.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M m v b b b b c c c n c c a z z z z z a a a p p p p u u i d w w w 9 9 9 7 0 7 0 7 t 7 8 - - - - - - - < y < < < 1 5 5 4 2.Z.hXlXY.# o MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXo # Y.xXhXL.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M m v v b b b b c c s c s s a c z a a z z a a a p p p u u i d ; w w 9 9 0 0 7 7 7 7 7 t 8 - - - - - y - - < < < 1 1 5 5 5 1.1.A.cXmXU.# . $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX$ . # U.mXcXwXM.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M m v b b b x c x c c s c a a a a a a z a a a p p p p u u i i d w 9 : 9 9 0 9 0 7 7 t 6 6 7 - - - > y y y 1 < 1 < 1 1 5 5 :.<.:.q.hXbXvXY.% o MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX o % Y.vXbXkXH.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B M B B B B B B B B B M v v b b x x c c c s c s a s a a a z a a a a p p p u u u i d : w w 9 9 9 0 7 7 7 6 6 7 6 7 8 - - y y 1 1 1 1 1 1 1 5 , 5 2.;.<.1.Z.hXxXlXT.* O O MXMXMXMXMXMXMXMXMXMXMXMXMXMXO O * T.jXcXkXL.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX{ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B M v b b b x x c c z s s a s a a a a a a a a a p p u u u u i ; ; 9 9 9 9 9 7 9 7 6 7 t t 7 6 - y - - < < < 1 5 1 1 1 5 5 4 3 <.>.>.1.8.A.hXvXyXI.@ . O O X X X X X X O O @ I.tXcXhXwXH.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXX & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B M m v v b x c c z z z a a s p s p a p a a a p p p p u u u u u i : 9 9 9 9 7 7 7 7 6 6 t t t 6 8 - - < 1 < < 1 1 5 1 1 , 5 5 <.<.>.>.;.;.<.w.hXbXmXY.% X X % Y.mXbXjXL.M.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MX", +"MXO & rXdX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B M v b b c c c c c a a s s s s p a a a a a p p p p u u u u i i > ; : 9 9 8 9 7 7 7 6 t t t t < 6 < < 1 < 1 5 5 5 1 , , , , 4 3 <.>.>.>.;.;.<.w.hXxXvXyXI.# . o # I.yXxXcXhXL.M.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.K.xXrX& O MX", +"MXO & rXdX{ B B B B B B B B B B B B B B B B B B B B B B B B B B B B B v b c c c z z c a a s p p p p p p p p p a p p p u u u i i i > ; 9 9 9 7 7 7 7 6 6 t t t < < 6 < < 5 5 5 1 , 5 , 1 , 1 5 4 3 :.>.>.>.;.;.:.:.8.Z.hXbXmXT.I.$ @ o . @ $ I.T.vXbXkXwXH.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX% O MX", +"MX+ $ tXfX[ B B B B B B B B B B B B B B B B B B B B B B B B B B B B M m b c c c z a z a a s p s p p p p a p p p p u u u u i i i > > : : 9 7 7 7 8 7 6 6 6 6 < < < t 5 5 5 5 5 5 5 5 , 5 , , , , 3 ;.>.>.>.>.;.:.:.:.<.r.hXvXbXnXtXY.I.$ @ X . . o @ $ = Y.tXnXbXbXhXL.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.jXtX% + MX", +"MXMX # 4XfX$.C B B B B B B B B B B B B B B B B B B B B B B B B B B M m v b c c c a z a a a s s p p p p p p p p p u u u u i i i > > > : 8 8 9 8 8 6 6 6 6 < 6 < 6 < 6 5 5 5 5 5 5 , 5 1 , 1 , , 4 :.>.>.,.>.>.;.:.:.:.:.<.8.D.hXjXvXbXvXtXY.I.= & & & & & & & & & * = I.Y.tXnXbXnXxXhXwXJ.M.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.v.M.wXxXT.# MXMX", +"MXMX + U.zX1XK B B B B B B B B B B B B B B B B B B B B B B B B B B M m v c c c a a a a a p p p p p p p p p p p u u u u u u i i > > > : 9 9 8 7 7 6 6 6 < 6 < y 1 < < 5 5 5 5 5 , , 5 , 5 , , 4 2 3 ;.>.,.,.>.;.;.:.:.:.:.;.<.8.C.P.hXcXcXbXbXmXrXrXrXrXtXrXrXrXrXrXmXbXbXcXxXhXwXK.J.M.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.H.hXcXU.+ MXMX", +"MXMX . * tXdX$.C B B B B B B B B B B B B B B B B B B B B B B B B B B M v c c a c a a a a p h p h p u u p p p p u u u u u i u i > > > : - 8 7 6 8 6 6 t < < < y - < < 5 5 5 5 4 5 5 , 5 , , , 4 2 3 :.>.,.>.>.;.;.:.:.:.:.;.>.>.4.a.V.G.P.hXcXxXxXxXkXkXkXxXjXxXxXxXxXcXhXwXK.J.M.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.wXxXtX* . MXMX", +"MXMXO @ U.fX1X#.K B B B B B B B B B B B B B B B B B B B B B B B B M M v x z a a a a p p p s p u s u u p p p u u u u u i > i i > > > - - 8 7 8 6 6 < 6 < < 8 y y < 1 5 5 4 5 5 4 5 5 4 , , , , 2 2 :.>.,.,.>.;.;.:.:.:.;.;.;.,.4.6.i.i.a.m.G.C.C.C.C.C.G.G.G.H.H.H.H.H.J.M.c.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.M.H.wXhXlXU.@ O MXMX", +"MXMX# $ Y.rXdX1X$.[ { [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ { ] ' ' _ _ _ _ ' _ ` ` ` ^ ` ) ) ) F F ) T F k k k l T R Y R E Q E.!.!.W.W.!.!.0.!.e W e e 0.0.0.y.e.0.u.u.u.5 4 , , , 2 3 :.;.,.>.,.,.;.;.:.:.;.;.>.,.4.6.i.i.i.i.i.6.6.5.6.6.i.i.p.d.z.z.z.z.z.z.v.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.H.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.wXhXxXkXY.$ # MXMX", +"MXMXMX . $ R.tXzXdXfXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXdXzXdXdXzXdXdXzXdXdXfXfXdXdXfXdXfXdXmXfXfXfXfXzXmXzXzXvXvXvXvXvXvXvXvXvXgXvXvXvXvXvXvXxXxXnXkX6Xu., , , 2 2 3 :.;.,.>.>.>.;.:.:.:.:.;.,.,.5.6.i.i.i.i.i.6.6.6.6.6.i.p.s.d.k.z.z.z.z.c.c.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.H.hXcXcXcXxXcXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXcXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXtXU.$ . MXMXMX", +"MXMXMX$ @ * U.4XtXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXvXbXbXpXu.4 , , 2 2 3 :.;.>.,.,.,.;.;.;.;.;.;.,.3.5.i.p.i.i.i.i.6.6.6.6.6.i.p.s.d.l.z.z.z.z.c.v.v.v.b.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.cXbXbXnXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXtX4XU.* @ $ MXMXMX", +"MXMXMXMXX + # $ & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & % & & & & & & & & & & & & & & & & & & & & * I.yXnXqXu.2 , 2 2 2 :.;.;.>.>.,.>.;.;.;.;.;.;.>.3.5.6.i.i.p.i.p.i.6.6.i.i.p.s.d.k.l.l.z.z.c.c.v.v.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.cXbXyXI.* & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & % $ # + . O MXMXMXMX", +"MXMXMXMXMXo + I.rX9X5 2 , , 2 2 :.;.>.>.>.,.,.>.;.;.;.;.>.>.3.5.6.i.i.p.p.i.i.i.i.p.i.p.s.d.k.l.z.z.z.z.c.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXnXI.+ o MXMXMXMXMX", +"MXMXMXMXMXMXX * yX9X2.4 , 2 2 3 :.;.,.>.,.,.>.;.;.;.;.;.>.,.,.5.6.i.p.i.p.p.i.i.s.s.s.s.s.s.l.z.z.z.c.c.c.v.v.v.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX* X MXMXMXMXMXMX", +"MXMXMXMXMXMXMX$ & yX9X2.2 , 2 2 2 :.:.;.,.>.,.,.;.;.:.;.;.>.,.3.5.6.i.p.p.p.p.s.s.s.s.s.s.s.d.l.z.z.c.z.c.v.v.v.v.b.b.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& $ MXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMX# O & yX6X2.4 , , 2 2 3 :.>.>.>.,.>.>.;.;.;.>.>.,.4.5.6.i.i.p.i.p.p.s.d.s.d.s.s.d.l.z.z.c.z.c.c.v.b.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& O # MXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMX+ O O X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X O O & yX6X5 2 2 2 2 2 3 :.;.,.3.,.,.>.;.;.;.;.,.,.4.6.i.i.i.i.i.p.s.s.s.d.s.d.d.k.l.z.c.c.z.c.c.v.v.v.b.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& O X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X O O + MXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXY.O & yX8X2.2 2 2 2 3 :.;.,.,.>.,.,.>.;.;.;.>.>.,.3.5.i.i.i.i.i.i.p.s.d.s.d.s.d.d.l.c.z.c.c.c.v.v.v.v.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& O Y.MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X5 2 2 2 3 3 :.;.>.,.>.3.>.>.;.;.;.>.>.,.4.6.6.i.i.i.i.i.p.d.d.d.d.d.d.k.z.c.c.c.c.c.c.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X5 2 2 2 2 2 :.>.,.>.3.3.>.>.;.;.>.>.>.3.5.6.6.i.i.i.p.i.p.d.d.d.s.d.d.k.l.c.c.c.c.c.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 2 2 2 :.>.>.3.,.,.,.>.>.>.>.>.,.3.4.5.6.i.6.i.i.i.p.d.d.d.d.d.d.k.z.c.c.c.c.v.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X5 2 2 2 2 3 :.;.,.,.,.,.,.,.>.>.>.>.,.3.5.6.6.6.i.i.i.p.s.d.d.d.d.s.d.k.z.z.c.c.v.c.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX % yX6X2.2 2 2 2 3 :.>.>.3.3.3.3.>.>.>.>.,.,.4.5.6.i.i.i.i.p.p.s.s.d.d.d.d.s.k.l.c.c.v.c.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 2 3 3 ;.>.>.3.3.3.,.,.>.>.>.>.,.3.4.6.i.i.i.i.i.p.p.s.d.d.d.d.d.k.z.c.c.v.c.v.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X5 2 2 2 2 3 :.>.,.3.3.3.3.>.>.>.,.3.3.4.5.6.i.i.i.i.i.p.p.s.d.d.k.d.k.k.z.c.c.c.c.v.v.v.v.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 2 3 3 :.>.3.3.3.3.,.,.>.,.>.3.4.4.5.i.i.i.i.i.i.p.p.d.d.k.d.k.k.l.c.c.c.v.v.v.v.b.b.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.cXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 2 3 3 ;.>.3.3.3.4.3.,.>.>.3.3.4.4.5.6.i.i.p.i.i.p.s.d.d.d.d.k.k.l.z.c.c.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 3 3 :.;.,.3.3.3.4.3.3.3.3.3.4.4.4.5.6.i.i.p.i.i.p.d.d.d.k.d.d.k.k.c.c.v.c.v.v.v.v.b.b.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX5X2.2 2 3 3 :.>.>.,.3.4.3.3.3.4.3.4.4.5.4.5.6.i.i.p.i.p.p.s.d.k.k.k.k.k.l.c.c.c.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 2 2 :.:.>.,.3.3.3.3.3.4.4.5.4.5.4.6.i.i.p.i.s.p.s.d.d.k.k.k.k.k.l.c.c.v.c.c.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX8X5 2 2 2 3 3 :.,.,.3.3.3.3.4.4.5.5.5.4.5.5.i.p.p.p.p.p.p.s.d.k.k.k.k.k.l.c.c.c.c.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 2 3 3 >.,.3.3.,.,.,.4.5.5.5.4.4.5.6.i.p.p.p.p.p.s.d.k.k.l.k.k.l.l.c.c.c.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 2 3 :.>.3.,.3.,.,.3.4.5.5.5.5.5.5.6.i.p.p.p.p.p.d.d.d.k.l.z.l.z.c.c.c.c.c.c.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 3 3 :.>.,.,.,.,.,.,.4.5.5.5.5.5.5.6.i.p.s.s.p.p.d.d.k.l.l.l.z.z.c.c.c.c.v.c.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 3 3 3 :.>.,.,.,.,.,.3.4.5.5.5.5.5.5.6.6.i.p.p.s.s.s.d.k.l.k.l.z.c.c.c.c.c.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.2 2 3 3 :.>.,.,.3.,.,.3.4.5.5.5.5.5.5.6.i.p.s.s.s.s.d.d.k.l.l.l.z.z.c.c.v.v.c.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & eX6X2.3 3 3 :.:.>.,.,.,.,.3.3.4.5.5.6.5.5.5.6.i.p.p.p.s.s.s.k.k.k.k.l.l.z.z.c.c.v.v.v.v.v.v.b.b.b.v.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.3 3 3 3 :.>.3.,.,.,.3.3.4.5.5.6.5.4.5.6.i.s.s.s.s.s.d.d.k.k.k.l.z.z.c.c.v.v.c.v.v.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX8X2.3 3 3 :.;.>.,.3.,.,.3.4.4.5.6.5.5.5.5.6.i.p.s.s.s.p.d.d.k.k.k.l.l.c.c.c.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX6X2.3 3 :.:.:.>.,.3.,.,.3.4.4.5.5.6.5.5.6.6.i.s.s.s.s.d.d.k.k.k.k.z.z.c.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & yX8X8.3 3 :.;.;.,.,.,.3.,.3.4.4.6.6.6.5.5.6.6.i.p.s.s.s.s.d.d.k.k.l.z.c.c.c.c.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & tXqXw.;.:.:.;.>.,.,.3.3.3.3.3.5.5.5.6.5.6.6.i.i.p.s.s.s.d.k.k.k.l.l.l.c.c.c.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & tXhXB.;.:.;.;.>.,.3.3.3.3.3.3.4.6.5.6.6.5.6.i.i.p.s.s.d.d.d.k.l.z.l.z.c.c.v.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXw.;.;.;.;.>.,.3.3.3.3.3.4.4.5.5.6.6.6.6.i.p.p.p.s.d.s.d.k.l.z.z.z.c.c.c.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.;.;.;.>.>.,.4.4.3.3.3.4.5.6.6.6.6.6.6.i.p.p.p.s.s.d.k.l.l.z.z.z.c.c.v.v.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.;.;.;.;.>.,.3.4.3.3.4.4.5.5.6.6.6.6.i.i.p.s.p.s.d.d.k.k.z.z.z.c.c.c.c.v.v.v.b.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.;.;.;.;.>.,.3.3.3.4.3.4.5.6.i.6.6.i.i.p.p.s.s.s.s.d.k.k.l.z.c.c.c.c.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.;.;.;.;.>.,.3.3.3.3.4.4.5.i.6.i.i.i.i.p.p.s.s.s.d.k.k.l.z.z.c.c.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.>.;.;.>.,.,.3.4.4.3.3.4.5.6.i.i.i.i.p.p.s.s.s.d.d.d.k.l.l.z.c.c.c.c.v.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.;.;.>.>.>.,.3.4.4.4.4.5.5.6.i.6.i.i.p.p.p.s.d.s.d.k.k.l.l.c.z.c.v.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.>.;.;.;.>.3.4.4.4.4.4.5.5.6.6.i.i.i.p.s.s.s.s.d.d.k.k.l.z.z.c.c.c.v.v.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.;.>.;.>.>.3.4.4.4.4.4.5.5.6.i.i.i.i.s.s.s.d.s.d.d.k.k.l.l.c.z.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.>.;.>.>.,.3.3.4.4.4.4.5.6.6.6.i.i.p.p.s.s.s.d.d.d.k.k.l.c.c.c.c.c.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXq.>.;.>.>.>.3.3.4.4.4.5.5.6.6.i.i.i.p.s.s.s.d.d.d.d.k.l.z.l.c.c.c.c.v.v.v.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.>.>.>.>.,.3.4.4.4.4.5.5.6.i.i.i.i.p.p.p.s.d.d.d.d.k.l.z.c.c.c.v.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.>.>.>.>.,.3.4.4.4.4.5.5.i.6.i.p.i.s.p.s.s.d.d.k.k.l.l.l.z.c.c.c.v.v.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXxXq.>.>.>.,.3.4.3.4.4.4.5.6.6.i.i.i.i.p.s.s.s.d.d.d.k.l.z.z.c.c.v.v.c.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.>.>.>.,.3.3.3.4.4.4.5.6.6.i.i.i.p.p.s.s.s.d.d.k.l.l.z.c.c.c.c.c.c.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.,.>.,.,.3.4.4.4.5.5.6.6.i.i.i.p.p.p.s.s.s.d.d.k.k.l.z.z.c.c.v.v.v.v.v.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.>.,.>.,.3.4.4.4.4.5.6.6.i.i.i.p.p.p.s.s.s.d.k.k.l.z.z.c.c.c.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.,.,.,.3.3.4.4.4.5.5.6.6.i.i.i.p.p.p.s.s.d.d.d.k.l.l.z.z.c.c.c.c.c.v.v.b.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.,.,.,.3.3.4.5.4.5.5.6.i.i.i.i.p.p.s.s.d.s.d.k.l.l.z.z.z.c.c.v.v.v.v.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.,.,.,.,.4.4.4.4.5.6.6.i.i.i.p.i.p.p.s.s.d.d.d.l.l.l.z.c.c.c.c.c.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.,.3.3.,.3.4.4.5.6.6.6.6.i.i.p.p.p.s.s.s.d.k.k.l.l.z.z.z.c.c.c.v.v.v.v.v.v.b.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.3.3.3.3.4.4.4.4.5.6.6.i.i.i.i.p.s.s.d.d.d.d.k.k.z.z.z.z.c.v.c.c.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.,.3.3.4.4.5.5.5.5.6.6.6.i.i.p.i.s.s.s.d.k.k.k.l.z.z.z.c.c.c.c.c.v.v.v.v.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.,.3.3.4.4.4.4.5.5.6.6.i.i.p.i.s.s.s.d.d.d.d.d.l.l.z.z.c.c.v.c.v.v.v.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.,.,.4.3.4.5.5.5.6.6.6.i.i.p.p.p.s.d.d.d.d.k.k.l.z.z.z.z.c.c.v.v.v.v.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.,.3.3.4.5.4.5.5.5.6.6.i.i.p.p.p.s.s.d.d.d.d.k.l.z.z.z.c.c.c.c.c.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.3.3.3.4.4.5.5.5.6.6.i.i.p.p.p.s.s.s.s.d.d.k.k.l.l.z.c.c.c.c.v.c.v.v.v.v.b.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.3.3.3.4.4.5.5.5.6.6.i.i.p.p.p.s.s.d.d.d.d.d.k.l.l.z.z.c.c.v.c.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.3.3.4.4.4.5.5.6.6.i.i.i.i.i.s.p.s.d.d.d.d.d.k.k.l.z.z.c.c.c.c.c.v.v.v.v.v.b.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.3.3.4.4.5.5.5.6.6.i.i.p.p.s.p.s.s.d.d.d.d.k.k.k.z.z.z.c.c.c.c.v.c.v.v.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXN.3.3.3.4.5.5.5.6.6.i.i.p.p.p.s.s.s.d.d.d.k.d.k.l.l.z.z.c.c.c.v.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.4.3.4.4.5.5.5.6.6.i.p.i.p.s.p.s.d.s.d.d.d.d.k.l.z.z.z.c.c.c.c.c.c.v.v.v.b.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.3.3.4.4.5.6.6.6.6.i.i.p.p.s.p.s.s.d.d.d.d.k.k.k.l.z.c.z.c.c.v.c.v.v.v.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.3.4.4.4.5.5.5.6.6.i.i.p.p.p.p.p.s.s.d.d.k.d.k.k.l.z.c.c.c.c.v.v.v.v.v.v.v.v.b.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXV.3.4.4.5.5.5.6.i.i.i.i.i.p.p.s.p.s.s.d.d.d.k.k.l.l.z.z.c.c.c.c.v.v.v.v.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.3.4.4.5.5.6.6.6.i.i.i.p.p.p.p.s.s.d.d.d.k.k.k.l.z.z.z.c.c.v.c.c.v.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.4.4.5.5.5.6.6.6.i.i.i.p.p.s.p.s.s.d.d.d.k.d.k.k.l.z.c.c.c.c.c.c.v.v.v.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXV.3.4.5.5.6.6.5.6.i.i.p.p.p.p.s.s.s.s.d.d.d.k.k.l.z.l.c.c.c.c.c.v.v.v.v.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.3.4.5.5.5.6.5.6.i.i.i.i.p.s.s.s.s.s.s.d.k.k.k.k.z.z.c.c.c.v.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXV.4.4.5.5.6.6.6.i.6.i.i.p.s.p.p.p.s.d.d.d.d.d.k.l.z.z.c.c.c.c.c.c.v.v.b.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXC.4.4.5.5.5.5.6.6.6.i.p.p.p.p.s.p.s.d.d.d.d.k.k.k.l.z.c.c.c.c.v.c.v.v.v.v.v.b.v.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXB.4.5.5.5.6.6.6.6.i.i.i.p.p.p.p.s.s.d.d.d.d.k.k.l.l.z.z.c.c.c.c.v.v.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX & rXjXV.4.4.5.5.5.6.6.6.i.i.p.p.p.p.s.s.s.s.d.d.k.k.k.l.l.z.c.c.c.c.v.v.v.v.v.v.b.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO & rXjXC.3.4.5.5.6.5.6.6.6.i.i.p.p.p.p.p.s.d.d.d.d.k.k.k.z.l.c.c.c.c.v.c.v.v.v.v.v.v.b.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.xXrX& O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO & rXjXC.4.5.5.5.5.5.6.6.i.i.i.p.p.p.p.s.s.s.d.d.d.k.k.k.z.z.z.c.c.c.v.c.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.b.b.b.b.b.K.xXrX& O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX+ $ tXkXV.4.4.5.5.6.6.6.6.i.i.i.p.p.p.s.p.s.s.d.d.k.d.k.l.z.z.z.c.c.c.v.v.v.v.v.v.v.b.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.K.jXtX$ + MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX # T.xXS.7.4.5.5.5.6.6.6.i.i.p.p.p.s.p.p.d.s.d.d.k.d.k.k.l.z.z.c.c.v.c.v.v.v.v.v.v.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.wXjX4X# MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX + U.vXhXj.5.5.5.6.6.6.6.i.i.i.p.p.p.p.p.s.d.d.d.d.k.k.l.l.x.c.c.c.c.c.v.v.v.v.v.v.v.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.J.hXcXU.+ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX . * tXjXS.7.4.5.5.6.6.6.i.i.i.p.p.p.s.s.s.d.d.d.d.d.k.l.x.z.c.c.c.v.v.v.v.v.v.v.v.v.b.v.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.wXxXtX* MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO @ U.lXhXP.h.a.5.5.6.6.i.i.i.p.s.s.p.p.d.s.d.d.d.k.k.l.z.z.c.c.c.c.c.v.v.v.v.v.b.v.v.b.b.b.v.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.b.M.H.wXhXlXU.@ O MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX# $ Y.lXxXhXP.C.C.C.G.G.G.G.G.G.H.G.J.G.G.H.H.H.H.H.H.H.H.H.K.H.K.H.K.H.H.K.K.H.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.K.wXhXxXkXY.$ # MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX . $ U.tXcXkXjXjXjXjXjXjXjXxXjXjXxXjXxXjXxXjXjXxXxXjXxXxXjXxXjXjXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXjXxXxXtXU.$ . MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX$ @ * U.yXtXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXrXtXT.U.* @ $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXX O # $ & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & $ # O . X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXo o MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXO X MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX$ $ MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX# O O # MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX+ O O X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X O O + MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX", +"MXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMXMX" +}; diff --git a/include/wxui/xpm/sdgbc32.xpm b/include/wxui/xpm/sdgbc32.xpm new file mode 100644 index 0000000..31d2f53 --- /dev/null +++ b/include/wxui/xpm/sdgbc32.xpm @@ -0,0 +1,204 @@ +/* XPM */ +static const char *xpmSdgbc32[] = { +/* columns rows colors chars-per-pixel */ +"32 32 166 2 ", +" c #010101", +". c #494749", +"X c #474949", +"o c #505050", +"O c #565E5F", +"+ c #5E5E61", +"@ c #586161", +"# c #767478", +"$ c #727878", +"% c #7C5E9A", +"& c #5D7C85", +"* c #466B99", +"= c #537D9C", +"- c #6C719D", +"; c #7B76A4", +": c #483EF1", +"> c #6330F0", +", c #275FF4", +"< c #3F57F5", +"1 c #0579F5", +"2 c #5552F6", +"3 c #594AF5", +"4 c #6C4DF8", +"5 c #7047F7", +"6 c #855A85", +"7 c #8C5A9C", +"8 c #87579A", +"9 c #9D599D", +"0 c #9D6A9E", +"q c #847885", +"w c #9173A4", +"e c #8211EC", +"r c #9910ED", +"t c #AB03EC", +"y c #B20DEF", +"u c #8035F4", +"i c #9732F5", +"p c #B030F9", +"a c #C000EC", +"s c #C603F1", +"d c #CA0EF3", +"f c #DC01F2", +"g c #EC02F4", +"h c #E90BF6", +"j c #F400F4", +"k c #F508F6", +"l c #F816F8", +"z c #C021F7", +"x c #CF34FD", +"c c #D526FB", +"v c #F724F7", +"b c #F72CF7", +"n c #FA2AFA", +"m c #E432FE", +"M c #F737FB", +"N c #8548FA", +"B c #9D44FC", +"V c #B248FD", +"C c #8279FF", +"Z c #9270FF", +"A c #A774FF", +"S c #BC73FF", +"D c #B468F5", +"F c #C773CE", +"G c #C845FE", +"H c #DD43FF", +"J c #DB5BFF", +"K c #E040FD", +"L c #F341FF", +"P c #FB58FF", +"I c #C461FF", +"U c #D370FF", +"Y c #EA6DFF", +"T c #FE6AFF", +"R c #E37EFF", +"E c #FE7CFF", +"W c #5A869D", +"Q c #5A899E", +"! c #618585", +"~ c #7B8F9E", +"^ c #64949E", +"/ c #629E9E", +"( c #6E909E", +") c #74909E", +"_ c #6691A3", +"` c #6DA3A4", +"' c #008DF9", +"] c #0085F7", +"[ c #0096FB", +"{ c #009CFC", +"} c #00A4FE", +"| c #01ACFF", +" . c #03B3FF", +".. c #05BBFF", +"X. c #32A0F3", +"o. c #3BBFFF", +"O. c #658CCB", +"+. c #5185F2", +"@. c #57B8FF", +"#. c #7FADFF", +"$. c #67B9FF", +"%. c #7CBDFF", +"&. c #07C4FF", +"*. c #09CCFF", +"=. c #10C8FF", +"-. c #0AD3FF", +";. c #0CDBFF", +":. c #11DCFF", +">. c #22C7FF", +",. c #35C6FF", +"<. c #2DD7FF", +"1. c #0EE2FF", +"2. c #10EDFF", +"3. c #18E7FF", +"4. c #12F4FF", +"5. c #16FEFF", +"6. c #1BFFFF", +"7. c #2DE3FF", +"8. c #31E7FF", +"9. c #22FFFF", +"0. c #3DFFFF", +"q. c #36F8FF", +"w. c #6EC7CF", +"e. c #73CFCF", +"r. c #4EC4FF", +"t. c #43DFFF", +"y. c #5ADDFF", +"u. c #72D7FF", +"i. c #42ECF8", +"p. c #49F7F7", +"a. c #44FEFE", +"s. c #868C9E", +"d. c #93889F", +"f. c #898CA5", +"g. c #8C80FF", +"h. c #9B8FFF", +"j. c #B487FF", +"k. c #B397FF", +"l. c #99A0FF", +"z. c #80B8FF", +"x. c #96B5FF", +"c. c #A0A3F8", +"v. c #B3A2FF", +"b. c #AAB7FF", +"n. c #CC80FF", +"m. c #CA9AFF", +"M. c #E28BFF", +"N. c #FF83FF", +"B. c #FE8AFF", +"V. c #F289FF", +"C. c #E09DFF", +"Z. c #FF95FF", +"A. c #FF9DFF", +"S. c #C1B3FF", +"D. c #D9B0FF", +"F. c #EFAEFF", +"G. c #FFA2FF", +"H. c #FFAFFF", +"J. c #E8BFFF", +"K. c #FEB9FF", +"L. c #F6B0FE", +"P. c #89D5FF", +"I. c #A2C8FF", +"U. c #BAC5FF", +"Y. c #D1C3FF", +"T. c None", +/* pixels */ +"T.T.T.T.T.T.T.T.T.T.T. . o o o o o o . T.T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 6 M Z.G.K.Z.B.K.L.q T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 9 l E Z.H.G.K.F.C.d. T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 9 l N.A.N.B.J.D.m.d. T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 9 l N.E P V.Y.S.v.s. T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 9 n E T P M.U.b.x.s. T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 9 n E T P m.I.x.%.~ T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 9 b E Y J k.P.%.$.) T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 9 M M.U I l.u.$.@.( T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. 9 M n.S A #.y.r.o.^ T.T.T.T.T.T.T.T.T.T.", +"T. 0 K j.A Z $.t.,.,.^ T.", +" 6 9 9 9 9 9 9 9 0 0 F G h.g.C @.7.=.:.w./ / / ` / / / / ` ! ", +". b j j j j j j k L m c V c.f.# $ ` i.2.2.6.6.6.6.6.6.6.6.6.a.X ", +"o n j j j j j j k H c p D + T.T. @ i.4.6.6.6.6.6.6.6.6.6.0.o ", +"o v j j j j j j h G p p w T.T.T.T. ` 5.6.6.6.6.6.6.6.6.6.0.o ", +"o n j j j j j j h V B i # T.T.T.T.T.T.$ 9.6.6.6.6.6.6.6.6.6.0.o ", +"o v j j j j j g d B N > # T.T.T.T.T.T.$ 9.6.6.6.6.6.6.6.6.6.0.o ", +"o v j j j j f f y N 5 > ; T.T.T.T. ` 6.6.6.6.6.6.6.6.6.6.0.o ", +"o v j j j f s y r 4 2 : +.O T.T. @ p.6.6.6.6.6.6.6.6.6.6.0.o ", +". b j j j s t t e 2 2 , 1 X._ $ $ ` p.6.6.6.6.6.6.6.6.6.6.6.a.. ", +" 6 9 9 9 7 7 8 7 - - O.1 [ | =.3.5.6.6.e./ / / / / / ` / / ! ", +"T. * 1 [ .-.2.5.6.6./ T.", +"T.T.T.T.T.T.T.T.T.T. * ] } ..;.4.5.6.6./ T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. * ] | &.1.4.6.6.6./ T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. = [ .*.1.5.6.6.6./ T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. W { .-.2.5.6.6.6./ T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. W } &.;.4.5.6.6.6./ T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. Q | *.;.4.5.6.6.6./ T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. Q .*.1.4.5.6.6.6./ T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. Q .*.1.4.5.6.6.6.` T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T. ! ,.<.8.q.0.0.0.a.! T.T.T.T.T.T.T.T.T.T.", +"T.T.T.T.T.T.T.T.T.T.T. X o o o o o o . T.T.T.T.T.T.T.T.T.T.T." +}; diff --git a/include/wxui/xpm/sdgbc48.xpm b/include/wxui/xpm/sdgbc48.xpm new file mode 100644 index 0000000..95561ac --- /dev/null +++ b/include/wxui/xpm/sdgbc48.xpm @@ -0,0 +1,260 @@ +/* XPM */ +static const char *xpmSdgbc48[] = { +/* columns rows colors chars-per-pixel */ +"48 48 206 2 ", +" c #000000", +". c #1C1C1C", +"X c #3C3D3F", +"o c #403C40", +"O c #737A7A", +"+ c #7A7A7C", +"@ c #7A777C", +"# c #5536F0", +"$ c #683CF3", +"% c #7230F2", +"& c #3856F4", +"* c #066AF2", +"= c #0077F4", +"- c #1E68F5", +"; c #2662F5", +": c #4C59F7", +"> c #5C55F8", +", c #4740F1", +"< c #6B53F9", +"1 c #7B50FA", +"2 c #466CF6", +"3 c #B076B0", +"4 c #BB77BB", +"5 c #9B01E9", +"6 c #8802E9", +"7 c #A901EB", +"8 c #BA00ED", +"9 c #B100EA", +"0 c #B308F1", +"q c #9B2CF6", +"w c #823BF6", +"e c #9639F7", +"r c #8929F3", +"t c #A825F6", +"y c #B823F7", +"u c #B836FB", +"i c #AF33FA", +"p c #C500EE", +"a c #CC03F2", +"s c #DA02F2", +"d c #D700F0", +"f c #D300EF", +"g c #E402F3", +"h c #EB00F3", +"j c #F400F4", +"k c #F70BF7", +"l c #F912F9", +"z c #F81DFC", +"x c #C724F9", +"c c #D427FB", +"v c #C935FD", +"b c #DB34FE", +"n c #E228FD", +"m c #F529FD", +"M c #E933FE", +"N c #AA72C8", +"B c #B375CA", +"V c #8A4DFB", +"C c #9A4BFC", +"Z c #A949FE", +"A c #B746FD", +"S c #8574FF", +"D c #967AFF", +"F c #9467FF", +"G c #B268FF", +"H c #A67BFF", +"J c #BB7BFF", +"K c #A465FF", +"L c #C077C0", +"P c #CD76CD", +"I c #CF7BCF", +"U c #C273CA", +"Y c #CD4FFF", +"T c #FA47FF", +"R c #FF54FF", +"E c #FF5BFF", +"W c #F457FF", +"Q c #E44EFF", +"! c #D866FF", +"~ c #C87CFF", +"^ c #D879FF", +"/ c #C06AFF", +"( c #ED6AFF", +") c #FE64FF", +"_ c #FD6CFF", +"` c #F76DFF", +"' c #E779FE", +"] c #FF76FF", +"[ c #FE7DFF", +"{ c #F27CFF", +"} c #7E929E", +"| c #7EBBBE", +" . c #7CB4BE", +".. c #7BA9B5", +"X. c #008DF9", +"o. c #0082F6", +"O. c #0094FA", +"+. c #009DFD", +"@. c #1794F9", +"#. c #00A4FE", +"$. c #01ADFF", +"%. c #03B4FF", +"&. c #05BDFF", +"*. c #2DBBFF", +"=. c #5E8ECA", +"-. c #5F92CB", +";. c #638ECC", +":. c #78AFCF", +">. c #7BB8CE", +",. c #79B5D0", +"<. c #6DA3CD", +"1. c #7991FF", +"2. c #44BAFF", +"3. c #53B9FF", +"4. c #41AEFB", +"5. c #68A3FC", +"6. c #72B4FF", +"7. c #06C3FF", +"8. c #08CAFF", +"9. c #0EC7FF", +"0. c #19C8FF", +"q. c #09D2FF", +"w. c #0CDBFF", +"e. c #13D3FF", +"r. c #28C8FF", +"t. c #38C8FF", +"y. c #3DD9FF", +"u. c #0EE5FF", +"i. c #10ECFF", +"p. c #1BE2FF", +"a. c #12F4FF", +"s. c #15FDFF", +"d. c #1BFFFF", +"f. c #19F4FF", +"g. c #27E6FF", +"h. c #26FDFF", +"j. c #31FFFF", +"k. c #33E4FF", +"l. c #7CC6CE", +"z. c #5DC6FF", +"x. c #4CD3FF", +"c. c #6CC4FF", +"v. c #72D6FF", +"b. c #6FCDFF", +"n. c #4DEEFE", +"m. c #60E0FF", +"M. c #77F4FA", +"N. c #938D9A", +"B. c #839C9C", +"V. c #85969B", +"C. c #908F9C", +"Z. c #B19FB1", +"A. c #81AFAF", +"S. c #81BDBD", +"D. c #B8A6BB", +"F. c #8A9ACC", +"G. c #938DCA", +"H. c #88BDCD", +"J. c #96BCCD", +"K. c #BFAFCD", +"L. c #A8B8CD", +"P. c #B7B1CD", +"I. c #9396FF", +"U. c #9091F8", +"Y. c #A58CFF", +"T. c #B584FF", +"R. c #B49DFF", +"E. c #93A7FF", +"W. c #9BB8FF", +"Q. c #89B2FF", +"!. c #A9A6FE", +"~. c #B4B6F8", +"^. c #C196C1", +"/. c #CE8BD1", +"(. c #D086D1", +"). c #CF90D0", +"_. c #C18CC1", +"`. c #C1ABC1", +"'. c #C2A6C5", +"]. c #C796F5", +"[. c #FF8CFF", +"{. c #FF84FF", +"}. c #FF9CFF", +"|. c #FA97FF", +" X c #E19AFF", +".X c #C6BBFF", +"XX c #D6B9FF", +"oX c #D5A8FF", +"OX c #FEA4FF", +"+X c #FBAEFF", +"@X c #E5B8FF", +"#X c #FFB3FF", +"$X c #F7BBFF", +"%X c #EBA5FF", +"&X c #80CDCD", +"*X c #83D0D0", +"=X c #82CFD2", +"-X c #87C6FF", +";X c #92CEFF", +":X c #81D3ED", +">X c #A6C9FF", +",X c #BAC6FF", +"X;XW.Q.c.J. 3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X", +"3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X I k [ { ' ( Y J ;Xv.b.6.z.J. 3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X", +"3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X I l {.' ^ ! Y Y.-Xv.b.5.3.H. 3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X", +"3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X I z ^ ^ ~ G G I.v.m.z.3.2.H. 3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X", +"3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X I z ^ ~ J G H E.z.x.x.*.t.&X 3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X", +"3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X P z ].T.H K D 5.x.x.x.*.r.H. 3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X3X", +"3X (.c T.Y.U.F S 5.x.k.r.r.e.l. 3X", +" o L P P I P P P I P I I ).).)./.{ c H I.U.S S 5.k.g.0.e.u.M.=X*X*X*X*X*X*X*X*X*X*X*X*X*XS.X ", +" 3 j j j j j j j j j j j m M M n c v D E.~.C.+ O B. # # 2 V. 3X3X3X3X B.j.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.S. ", +" 4 j j j j j g a 8 8 9 5 $ > : # ; - *.V.. . B.n.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.d.S. ", +" 3 j j j j j d 8 0 7 5 6 $ : & ; - - o.@.:XV.O O B. c #0075F3", +", c #1061F2", +"< c #2467F5", +"1 c #5958F8", +"2 c #4B51F5", +"3 c #704EF8", +"4 c #4660F8", +"5 c #797AFF", +"6 c #927C92", +"7 c #950BEC", +"8 c #A801EB", +"9 c #B800EC", +"0 c #B311F2", +"q c #9A26F5", +"w c #8D30F5", +"e c #AB27F6", +"r c #BB2AF9", +"t c #AC36FA", +"y c #C800EE", +"u c #C704F1", +"i c #D300F1", +"p c #DC02F2", +"a c #C81AF6", +"s c #D70DF4", +"d c #EC01F4", +"f c #E402F3", +"g c #F400F4", +"h c #F80CF9", +"j c #E818FA", +"k c #F713FA", +"l c #C929FA", +"z c #D626FB", +"x c #D137FE", +"c c #FB27FB", +"v c #FD38FD", +"b c #E531FE", +"n c #874AF9", +"m c #9646FA", +"M c #8C54FC", +"N c #A84AFD", +"B c #B945FE", +"V c #986CFF", +"C c #8776FF", +"Z c #9678FF", +"A c #886CFF", +"S c #A76AFF", +"D c #B867FF", +"F c #B67AFF", +"G c #A871FF", +"H c #C846FF", +"J c #D943FF", +"K c #D257FF", +"L c #E942FF", +"P c #FA48FA", +"I c #F641FF", +"U c #E65BFF", +"Y c #FF53FF", +"T c #FE5CFF", +"R c #F45AFF", +"E c #C26BFF", +"W c #CD69FF", +"Q c #D866FF", +"! c #C87CFB", +"~ c #D679FC", +"^ c #ED62FF", +"/ c #E266FF", +"( c #FC64FD", +") c #FE6CFF", +"_ c #F265FF", +"` c #FE74FF", +"' c #FF7DFF", +"] c #F375FF", +"[ c #E776FF", +"{ c #7E8D92", +"} c #018CF9", +"| c #0489F8", +" . c #0094FA", +".. c #009BFC", +"X. c #1F9EFB", +"o. c #00A3FE", +"O. c #01ABFF", +"+. c #02B3FF", +"@. c #05BCFF", +"#. c #2BBCFF", +"$. c #39B7FF", +"%. c #2F9AFC", +"&. c #5F9EEF", +"*. c #5490F9", +"=. c #7893FF", +"-. c #6C93FA", +";. c #47B9FF", +":. c #58B7FF", +">. c #65A2F4", +",. c #6BB4FB", +"<. c #7AB8FF", +"1. c #77AFFF", +"2. c #07C3FF", +"3. c #08CBFF", +"4. c #17C8FF", +"5. c #0AD2FF", +"6. c #0CDCFF", +"7. c #1BD9FF", +"8. c #23C4FF", +"9. c #35C7FF", +"0. c #2BD9FF", +"q. c #3CD9FF", +"w. c #0FE5FF", +"e. c #0FE8FF", +"r. c #11EDFF", +"t. c #11F2FF", +"y. c #15FDFF", +"u. c #1BFFFF", +"i. c #1AF5FF", +"p. c #2BFFFF", +"a. c #22FAFF", +"s. c #35FBFF", +"d. c #2BE7FF", +"f. c #5ACAFF", +"g. c #4AD7FF", +"h. c #54D9FF", +"j. c #48CDFF", +"k. c #67CAFF", +"l. c #75C9FF", +"z. c #75D9FF", +"x. c #69D8FE", +"c. c #48E4FF", +"v. c #55E6FF", +"b. c #5DFEFF", +"n. c #57F1FF", +"m. c #60FFFF", +"M. c #6EF5FF", +"N. c #7AFCFD", +"B. c #65E0FF", +"V. c #928D93", +"C. c #879393", +"Z. c #919393", +"A. c #A39FA4", +"S. c #9DA4A4", +"D. c #A3A2A4", +"F. c #B6BFC4", +"G. c #968AFE", +"H. c #8996FF", +"J. c #9594FF", +"K. c #A785FF", +"L. c #B98BFF", +"P. c #B697F9", +"I. c #88ADFF", +"U. c #99ADFB", +"Y. c #88B4FF", +"T. c #96B8FC", +"R. c #A6A7F8", +"E. c #B7A9FF", +"W. c #A7B5FF", +"Q. c #B8BDFF", +"!. c #BCB3E0", +"~. c #D584F6", +"^. c #C69AFF", +"/. c #CD8AFA", +"(. c #EE86EE", +"). c #E483FB", +"_. c #F481F4", +"`. c #F589F6", +"'. c #FF83FF", +"]. c #FF8BFF", +"[. c #F783FF", +"{. c #E89BFF", +"}. c #FF93FF", +"|. c #FF9BFF", +" X c #F499FA", +".X c #C9A8FF", +"XX c #D9A5FF", +"oX c #C9B9FF", +"OX c #D7BBFF", +"+X c #D0B5E1", +"@X c #FFA4FF", +"#X c #FFACFF", +"$X c #F5A4FA", +"%X c #E7B6FF", +"&X c #FEB4FF", +"*X c #FABBFF", +"=X c #E8A6FF", +"-X c #B0C5C5", +";X c #85C9FB", +":X c #99C4FF", +">X c #83D3F7", +",X c #8AD3FA", +".j.d.i.7.4.w.w.n.6X6X7X7X7X7X7X8X7X7X7X7X7X7X7X7X7X7X7X7XC. ", +" + ( g g g g g g g g g g g g g g h I I b b b z l G G.H.2XtXA.Z.Z.D.0XM.7.r.r.e.e.e.y.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.N.+ ", +" @ P g g g g g g g g g g g g g g k I L b z z l r V J.tX# . . # 0Xs.t.t.e.r.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g h L J x x l r t K.F.o aXaXaXaX o -Xs.r.t.y.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g k J J x l a e t +Xo aXaXaXaXaXaXaXaX o qXi.y.y.y.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g j K H B r 0 e ~.# aXaXaXaXaXaXaXaXaXaX @ N.y.u.u.u.u.u.u.u.u.a.u.u.u.u.a.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g j H B N e e q +X. aXaXaXaXaXaXaXaXaXaX . qXy.y.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g s H N N t q t A. aXaXaXaXaXaXaXaXaXaXaXaX S.p.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g d a t N M t q n V. aXaXaXaXaXaXaXaXaXaXaXaX C.p.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g d p a N m M w w w V. aXaXaXaXaXaXaXaXaXaXaXaX Z.p.u.a.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g d d f i 0 m M M % % 3 A. aXaXaXaXaXaXaXaXaXaXaXaX S.p.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g h f f f p y e m M 3 3 * & !.. aXaXaXaXaXaXaXaXaXaX . 0Xu.u.u.u.u.u.u.u.u.u.u.u.u.a.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g d f i i i i 9 7 n 3 3 & * $ G.# aXaXaXaXaXaXaXaXaXaX @ N.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g f p y y y 9 9 7 3 3 1 & $ $ - 4Xo aXaXaXaXaXaXaXaX o 0Xp.u.u.u.u.u.u.u.u.a.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g p i a 9 y 9 8 w 3 1 2 $ $ - = %.F.o aXaXaXaX o -Xs.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g d i y 9 8 8 8 7 % 1 2 4 2 ; ; = } X.4X@ . . @ 0Xs.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" O ( g g g g g g d y 9 8 8 8 8 7 % 1 2 4 ; < = , | | | k.4XS.Z.C.S.qXN.p.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.N.+ ", +" V._.] (._._._._.~.~.~.~.~ ! F P.E.R.R.T.T.*.: | .| ..2.4.4.7.a.i.y.u.u.u.u.b.8X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7XC.. ", +"aX X o o o o o o o o o o o o X X o o o X O >.: } ..| o.2.3.3.w.t.y.y.u.u.u.u.8XO o o o o o o o o o o o o o o o o o o X aX", +"aXaX o &.: ...} o.2.3.6.w.t.y.u.u.u.u.u.7Xo aXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o &.: } ..} O.2.3.6.w.y.y.y.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >.> .o...+.2.3.6.e.y.y.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >.> ..o.O.+.3.5.w.e.y.y.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o &.> ..o.+.@.3.6.w.r.y.y.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >.> ..o.+.@.5.6.e.t.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o ,.| o.o.@.2.5.6.e.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX X ;X} o.O.@.2.5.w.t.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >X} o.O.2.3.5.w.t.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o ;X .O.O.2.5.6.e.t.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >X..O.@.2.5.6.r.y.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >Xo.O.@.3.5.w.r.t.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >Xo.+.@.3.6.w.r.y.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >XO.+.2.3.6.w.t.y.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >XO.+.3.5.6.w.r.t.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >XO.@.3.5.6.e.r.y.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >X+.@.3.3.6.w.t.y.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >X+.@.3.5.6.w.r.y.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX X >X+.@.3.5.6.e.t.y.y.u.u.u.u.u.u.u.7XX aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX C.x.g.h.v.v.n.n.b.b.b.b.m.b.m.m.N.C. aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX + @ @ @ @ @ @ @ @ @ @ @ @ @ @ + aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX" +}; diff --git a/include/wxui/xpm/sdgbc96.xpm b/include/wxui/xpm/sdgbc96.xpm new file mode 100644 index 0000000..4c4cc96 --- /dev/null +++ b/include/wxui/xpm/sdgbc96.xpm @@ -0,0 +1,293 @@ +/* XPM */ +static const char *xpmSdgbc96[] = { +/* columns rows colors chars-per-pixel */ +"64 64 223 2 ", +" c #000000", +". c #0E0E0E", +"X c #1B1B1B", +"o c #232323", +"O c #3F3F3F", +"+ c #414141", +"@ c #5A5A5A", +"# c #656668", +"$ c #5039F0", +"% c #7726F0", +"& c #6437F2", +"* c #6E2BF1", +"= c #1D5FF3", +"- c #3858F4", +"; c #2E59F4", +": c #0267F1", +"> c #0075F3", +", c #1061F2", +"< c #2467F5", +"1 c #5958F8", +"2 c #4B51F5", +"3 c #704EF8", +"4 c #4660F8", +"5 c #797AFF", +"6 c #927C92", +"7 c #950BEC", +"8 c #A801EB", +"9 c #B800EC", +"0 c #B311F2", +"q c #9A26F5", +"w c #8D30F5", +"e c #AB27F6", +"r c #BB2AF9", +"t c #AC36FA", +"y c #C800EE", +"u c #C704F1", +"i c #D300F1", +"p c #DC02F2", +"a c #C81AF6", +"s c #D70DF4", +"d c #EC01F4", +"f c #E402F3", +"g c #F400F4", +"h c #F80CF9", +"j c #E818FA", +"k c #F713FA", +"l c #C929FA", +"z c #D626FB", +"x c #D137FE", +"c c #FB27FB", +"v c #FD38FD", +"b c #E531FE", +"n c #874AF9", +"m c #9646FA", +"M c #8C54FC", +"N c #A84AFD", +"B c #B945FE", +"V c #986CFF", +"C c #8776FF", +"Z c #9678FF", +"A c #886CFF", +"S c #A76AFF", +"D c #B867FF", +"F c #B67AFF", +"G c #A871FF", +"H c #C846FF", +"J c #D943FF", +"K c #D257FF", +"L c #E942FF", +"P c #FA48FA", +"I c #F641FF", +"U c #E65BFF", +"Y c #FF53FF", +"T c #FE5CFF", +"R c #F45AFF", +"E c #C26BFF", +"W c #CD69FF", +"Q c #D866FF", +"! c #C87CFB", +"~ c #D679FC", +"^ c #ED62FF", +"/ c #E266FF", +"( c #FC64FD", +") c #FE6CFF", +"_ c #F265FF", +"` c #FE74FF", +"' c #FF7DFF", +"] c #F375FF", +"[ c #E776FF", +"{ c #7E8D92", +"} c #018CF9", +"| c #0489F8", +" . c #0094FA", +".. c #009BFC", +"X. c #1F9EFB", +"o. c #00A3FE", +"O. c #01ABFF", +"+. c #02B3FF", +"@. c #05BCFF", +"#. c #2BBCFF", +"$. c #39B7FF", +"%. c #2F9AFC", +"&. c #5F9EEF", +"*. c #5490F9", +"=. c #7893FF", +"-. c #6C93FA", +";. c #47B9FF", +":. c #58B7FF", +">. c #65A2F4", +",. c #6BB4FB", +"<. c #7AB8FF", +"1. c #77AFFF", +"2. c #07C3FF", +"3. c #08CBFF", +"4. c #17C8FF", +"5. c #0AD2FF", +"6. c #0CDCFF", +"7. c #1BD9FF", +"8. c #23C4FF", +"9. c #35C7FF", +"0. c #2BD9FF", +"q. c #3CD9FF", +"w. c #0FE5FF", +"e. c #0FE8FF", +"r. c #11EDFF", +"t. c #11F2FF", +"y. c #15FDFF", +"u. c #1BFFFF", +"i. c #1AF5FF", +"p. c #2BFFFF", +"a. c #22FAFF", +"s. c #35FBFF", +"d. c #2BE7FF", +"f. c #5ACAFF", +"g. c #4AD7FF", +"h. c #54D9FF", +"j. c #48CDFF", +"k. c #67CAFF", +"l. c #75C9FF", +"z. c #75D9FF", +"x. c #69D8FE", +"c. c #48E4FF", +"v. c #55E6FF", +"b. c #5DFEFF", +"n. c #57F1FF", +"m. c #60FFFF", +"M. c #6EF5FF", +"N. c #7AFCFD", +"B. c #65E0FF", +"V. c #928D93", +"C. c #879393", +"Z. c #919393", +"A. c #A39FA4", +"S. c #9DA4A4", +"D. c #A3A2A4", +"F. c #B6BFC4", +"G. c #968AFE", +"H. c #8996FF", +"J. c #9594FF", +"K. c #A785FF", +"L. c #B98BFF", +"P. c #B697F9", +"I. c #88ADFF", +"U. c #99ADFB", +"Y. c #88B4FF", +"T. c #96B8FC", +"R. c #A6A7F8", +"E. c #B7A9FF", +"W. c #A7B5FF", +"Q. c #B8BDFF", +"!. c #BCB3E0", +"~. c #D584F6", +"^. c #C69AFF", +"/. c #CD8AFA", +"(. c #EE86EE", +"). c #E483FB", +"_. c #F481F4", +"`. c #F589F6", +"'. c #FF83FF", +"]. c #FF8BFF", +"[. c #F783FF", +"{. c #E89BFF", +"}. c #FF93FF", +"|. c #FF9BFF", +" X c #F499FA", +".X c #C9A8FF", +"XX c #D9A5FF", +"oX c #C9B9FF", +"OX c #D7BBFF", +"+X c #D0B5E1", +"@X c #FFA4FF", +"#X c #FFACFF", +"$X c #F5A4FA", +"%X c #E7B6FF", +"&X c #FEB4FF", +"*X c #FABBFF", +"=X c #E8A6FF", +"-X c #B0C5C5", +";X c #85C9FB", +":X c #99C4FF", +">X c #83D3F7", +",X c #8AD3FA", +".j.d.i.7.4.w.w.n.6X6X7X7X7X7X7X8X7X7X7X7X7X7X7X7X7X7X7X7XC. ", +" + ( g g g g g g g g g g g g g g h I I b b b z l G G.H.2XtXA.Z.Z.D.0XM.7.r.r.e.e.e.y.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.N.+ ", +" @ P g g g g g g g g g g g g g g k I L b z z l r V J.tX# . . # 0Xs.t.t.e.r.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g h L J x x l r t K.F.o aXaXaXaX o -Xs.r.t.y.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g k J J x l a e t +Xo aXaXaXaXaXaXaXaX o qXi.y.y.y.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g j K H B r 0 e ~.# aXaXaXaXaXaXaXaXaXaX @ N.y.u.u.u.u.u.u.u.u.a.u.u.u.u.a.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g j H B N e e q +X. aXaXaXaXaXaXaXaXaXaX . qXy.y.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g g s H N N t q t A. aXaXaXaXaXaXaXaXaXaXaXaX S.p.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g g d a t N M t q n V. aXaXaXaXaXaXaXaXaXaXaXaX C.p.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g g g d p a N m M w w w V. aXaXaXaXaXaXaXaXaXaXaXaX Z.p.u.a.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g g g d d f i 0 m M M % % 3 A. aXaXaXaXaXaXaXaXaXaXaXaX S.p.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g g h f f f p y e m M 3 3 * & !.. aXaXaXaXaXaXaXaXaXaX . 0Xu.u.u.u.u.u.u.u.u.u.u.u.u.a.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g d f i i i i 9 7 n 3 3 & * $ G.# aXaXaXaXaXaXaXaXaXaX @ N.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g f p y y y 9 9 7 3 3 1 & $ $ - 4Xo aXaXaXaXaXaXaXaX o 0Xp.u.u.u.u.u.u.u.u.a.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g g p i a 9 y 9 8 w 3 1 2 $ $ - = %.F.o aXaXaXaX o -Xs.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" @ P g g g g g g d i y 9 8 8 8 7 % 1 2 4 2 ; ; = } X.4X@ . . @ 0Xs.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.m.@ ", +" O ( g g g g g g d y 9 8 8 8 8 7 % 1 2 4 ; < = , | | | k.4XS.Z.C.S.qXN.p.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.u.N.+ ", +" V._.] (._._._._.~.~.~.~.~ ! F P.E.R.R.T.T.*.: | .| ..2.4.4.7.a.i.y.u.u.u.u.b.8X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7X7XC.. ", +"aX X o o o o o o o o o o o o X X o o o X O >.: } ..| o.2.3.3.w.t.y.y.u.u.u.u.8XO o o o o o o o o o o o o o o o o o o X aX", +"aXaX o &.: ...} o.2.3.6.w.t.y.u.u.u.u.u.7Xo aXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o &.: } ..} O.2.3.6.w.y.y.y.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >.> .o...+.2.3.6.e.y.y.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >.> ..o.O.+.3.5.w.e.y.y.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o &.> ..o.+.@.3.6.w.r.y.y.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >.> ..o.+.@.5.6.e.t.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o ,.| o.o.@.2.5.6.e.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX X ;X} o.O.@.2.5.w.t.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >X} o.O.2.3.5.w.t.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o ;X .O.O.2.5.6.e.t.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >X..O.@.2.5.6.r.y.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >Xo.O.@.3.5.w.r.t.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >Xo.+.@.3.6.w.r.y.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >XO.+.2.3.6.w.t.y.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >XO.+.3.5.6.w.r.t.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >XO.@.3.5.6.e.r.y.y.y.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >X+.@.3.3.6.w.t.y.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX o >X+.@.3.5.6.w.r.y.y.u.u.u.u.u.u.u.7Xo aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX X >X+.@.3.5.6.e.t.y.y.u.u.u.u.u.u.u.7XX aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX C.x.g.h.v.v.n.n.b.b.b.b.m.b.m.m.N.C. aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX + @ @ @ @ @ @ @ @ @ @ @ @ @ @ + aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX", +"aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX aXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaXaX" +}; diff --git a/include/wxui/xpm/xpm.h b/include/wxui/xpm/xpm.h new file mode 100644 index 0000000..2694a92 --- /dev/null +++ b/include/wxui/xpm/xpm.h @@ -0,0 +1,12 @@ +#ifndef SDGBC_XPM_H_ +#define SDGBC_XPM_H_ + +#include "wxui/xpm/sdgbc16.xpm" +#include "wxui/xpm/sdgbc24.xpm" +#include "wxui/xpm/sdgbc32.xpm" +#include "wxui/xpm/sdgbc48.xpm" +#include "wxui/xpm/sdgbc64.xpm" +#include "wxui/xpm/sdgbc96.xpm" +#include "wxui/xpm/sdgbc256.xpm" + +#endif // SDGBC_XPM_H_ diff --git a/prototype_roms/01-ld-hello-world.gb b/prototype_roms/01-ld-hello-world.gb new file mode 100644 index 0000000..68c3b48 Binary files /dev/null and b/prototype_roms/01-ld-hello-world.gb differ diff --git a/prototype_roms/01-ld-hello-world.s b/prototype_roms/01-ld-hello-world.s new file mode 100644 index 0000000..b36b75b --- /dev/null +++ b/prototype_roms/01-ld-hello-world.s @@ -0,0 +1,57 @@ +; -- Demo 1 -- +; this prototype demo ROM will write "Hello World!" onto the stack in ASCII +; using the push and ld instructions. +; +; ASCII values for "Hello World!" +; 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 + + +; main entry point at $0100 (cartridge ROM bank 0) +; we just jump to our program routine at $4000 +main: ; @ $0100 + nop ; 00 + jp program ; c3 00 40 + + +; program routine at $4000 (cartridge ROM bank 1) +; write "Hello World!" onto the stack backwards so it reads the right way round +program: ; @ $4000 + ; start our SP at $ffff just for completeness + ; (BIOS/boot ROM may have changed our initial stack position) + ld sp,0xffff ; 31 ff ff + + ; start writing "World!" + ; we'll use the 8-bit register loads for demo purposes + + ; "d!" + ld b,0x21 ; 06 21 + ld c,0x64 ; 0e 64 + push bc ; c5 + + ; "rl" + ld d,0x6c ; 16 6c + ld e,0x72 ; 1e 72 + push de ; d5 + + ; "Wo" + ld h,0x6f ; 26 6f + ld l,0x77 ; 2e 77 + push hl ; e5 + + ; start writing "Hello " + ; this time, we'll use some of the 16-bit load instructions + + ; "o " + ld bc,0x206f ; 01 6f 20 + push bc ; c5 + + ; "ll" + ld de,0x6c6c ; 11 6c 6c + push de ; d5 + + ; "He" + ld hl,0x6548 ; 21 48 65 + push hl ; e5 + + ; we're done, just loop forever +- jr - ; 18 fe diff --git a/prototype_roms/02-fib.gb b/prototype_roms/02-fib.gb new file mode 100644 index 0000000..7cd13e7 Binary files /dev/null and b/prototype_roms/02-fib.gb differ diff --git a/prototype_roms/02-fib.s b/prototype_roms/02-fib.s new file mode 100644 index 0000000..86d24dc --- /dev/null +++ b/prototype_roms/02-fib.s @@ -0,0 +1,50 @@ +; -- Demo 2 -- +; this prototype demo ROM will compute 10 numbers of the fibonnaci sequence +; using arithmetic instructions and push them on the stack +; +; (wont be reversed like the ASCII in demo 1, though - sorry!) + + +; main entry point at $0100 (cartridge ROM bank 0) +; we just jump to our program routine at $4000 +main: ; @ $0100 + nop ; 00 + jp program ; c3 00 40 + + +; program routine at $4000 (cartridge ROM bank 1) +program: ; @ $4000 + ld sp,0xffff ; 31 ff ff + + ; load initial register values + ; + ; A : used to store our next fib number - this reg (the accumulator) + ; is written to directly by the arithemtic instructions + ; B : used to store the current fib number + ; C : used to store how many iterations are left + ld a,1 ; 3e 01 + ld b,0 ; 06 00 + ld c,10 ; 0e 0a + + ; begin our fib calc loop & push the current fib number in B as a byte + ; (push instruction only lets us push 16-bits at a time) +-- ldhl sp,-1 ; f8 ff + ld (hl),b ; 70 + dec sp ; 3b + + ; A will become our current fib number afterwards, so store it in D + ; so we don't lose it + ld d,a ; 57 + + ; calculate next fib number and store it in A + add a,b ; 80 + + ; restore the old value of A from D and put it in B + ld b,d ; 42 + + ; decrement C. if C>0, loop again + dec c ; 0d + jr nz,-- ; 20 f6 + + ; we're done, just loop forever +- jr - ; 18 fe diff --git a/prototype_roms/03-call.gb b/prototype_roms/03-call.gb new file mode 100644 index 0000000..4efc684 Binary files /dev/null and b/prototype_roms/03-call.gb differ diff --git a/prototype_roms/03-call.s b/prototype_roms/03-call.s new file mode 100644 index 0000000..2a4d9a1 --- /dev/null +++ b/prototype_roms/03-call.s @@ -0,0 +1,70 @@ +; -- Demo 3 -- +; executes a subroutine using the call instruction and resumes execution using +; ret. +; +; the subroutine is used to print out a null-terminated ASCII string at the +; location pointed to by the HL register to the console in the same manner as +; test ROMs do (by writing the character to $ff01 and then writing $81 to $ff02) + + +; main entry point at $0100 (cartridge ROM bank 0) +; we just jump to our program routine at $4000 +main: ; @ $0100 + nop ; 00 + jp program ; c3 00 40 + + +; the strings that we'll be printing (stored at $3fe0 and $3fea respectively) +bar_str: ; @ 3fe0 + .db "Bar\n\0" ; 42 61 72 0a 00 +foo_str: ; @ 3fea + .db "Foo\n\0" ; 46 6f 6f 0a 00 + + +; define the location of the IO registers we'll be using to print stuff +; locations of the IO registers are $ff00 + n +.define IO_SB 0x01 +.define IO_SC 0x02 + + +; prints the null-terminated ASCII string at HL using serial IO test ROM debug +; functionality; subroutine at $3ff0 (cartridge ROM bank 0) +print_str_at_hl: ; @ $3ff0 + ; load the ASCII character at HL into register B + ld b,(hl) ; 46 + + ; set A=0 (A xor A always = 0). check for null-terminator by comparing B with + ; A=0. if B=0, then the Z flag in register F will be set - this means we've + ; reached the end of our string, so we return + xor a ; af + cp b ; b8 + ret z ; c8 + + ; write to the console via serial IO. + ; to do this, we write our character to IO_SB ($ff01) and then set + ; IO_SB ($ff02) to $81 to print our character to the console using the test + ; ROM debugging functionality of sdgbc + ld a,b ; 78 + ldh (IO_SB),a ; e0 01 + ld a,0x81 ; 3e 81 + ldh (IO_SC),a ; e0 02 + + ; increment HL to point to the next character in the string and then run again + inc hl ; 23 + jr print_str_at_hl ; 18 f2 + + +; program routine at $4000 (cartridge ROM bank 1) +program: ; @ $4000 + ld sp,0xffff ; 31 ff ff + + ; print "Foo\n\0" + ld hl,foo_str ; 21 ea 3f + call print_str_at_hl ; cd f0 3f + + ; print "Bar\n\0" + ld hl,bar_str ; 21 e0 3f + call print_str_at_hl ; cd f0 3f + + ; we're done, just loop forever +- jr - ; 18 fe diff --git a/prototype_roms/04-int.gb b/prototype_roms/04-int.gb new file mode 100644 index 0000000..ab80f39 Binary files /dev/null and b/prototype_roms/04-int.gb differ diff --git a/prototype_roms/04-int.s b/prototype_roms/04-int.s new file mode 100644 index 0000000..2a00cf8 --- /dev/null +++ b/prototype_roms/04-int.s @@ -0,0 +1,100 @@ +; -- Demo 4 -- +; demonstrates interrupt functionality of the CPU by triggering some ISRs + + +; main entry point at $0100 (cartridge ROM bank 0) +; we just jump to our program routine at $4000 +main: ; @ $0100 + nop ; 00 + jp program ; c3 00 40 + + +; contents of the interrupt vectors at $0040, $0048, $0050, $0058 and $0060 +; for the sake of demonstration, we'll JP to a simple ISR for vector $0060 +ivec_40h: ; @ $0040 + reti ; d9 +ivec_48h: ; @ $0048 + reti ; d9 +ivec_50h: ; @ $0050 + reti ; d9 +ivec_58h: ; @ $0058 + reti ; d9 +ivec_60h: ; @ $0060 + jp fancy_isr ; c3 f0 3f + + +; our super fancy custom ISR at $3ff0 (cartridge ROM bank 0) +; just pointlessly sets A to 1 and multiplies it by 4 via 2 bitwise left shifts +; +; in a real application, we'd likely use an ISR to start writing to VRAM after +; the V-Blank interrupt has been triggered (vector $40 in that case), for +; example +fancy_isr: ; @ $3ff0 + ld a,1 ; 3e 01 + + ; bitwise shift A to the left twice to mul by 4 - because why not? + ; NOTE: SLA is a 2 byte instr because it is part of the extended instruction + ; set (prefixed by opcode $cb) + sla a ; cb 27 + sla a ; cb 27 + + ; returns from our ISR and re-enables interrupts (interrupts are automatically + ; disabled at the CPU-level when an interrupt is triggered - this stops other + ; ints from triggering while execution is inside of an ISR) + reti ; d9 + + +; the different IO registers for controlling interrupts at $ff0f and $ffff +; +; IF - allows us to request an interrupt to be triggered (only happens if +; the corrisponding bit in IE is set) +; IE - allows us to enable/disable specific interrupts from triggering +; (even if the corrisponding bit in IF is set to request an interrupt, it +; will not be triggered until the bit in IE is enabled) +; +; NOTE: interrupts themselves need to be enabled on the CPU via the EI instr +; for anything to happen, regardless of the values of IF and IE +.define IO_IF 0x0f +.define IO_IE 0xff + + +; program routine at $4000 (cartridge ROM bank 1) +program: ; @ $4000 + ld sp,0xffff ; 31 ff ff + + ; ensure that interrupts are enabled at the CPU-level + ei ; fb + + ; firstly, as a demonstration, we'll enable and then request all 5 different + ; interrupt types + ; + ; to do this, we enable all the 5 int types via setting the lower 5 bits of + ; IE to 1. then, we request the ints by setting the lower 5 bits of IF to 1 + ; ($1f = 0b00011111) + ld a,0x1f ; 3e 1f + ldh (IO_IE),a ; e0 ff + ldh (IO_IF),a ; e0 0f + + ; secondly, as a demonstration, we'll request all 5 different int types, while + ; only enabling the interrupt that vectors at $0060 + ; in this case, only that interrupt will fire, even though all int types + ; were requested + ; + ; to do this, we enable the int that vectors to $60 by setting bit 4 of IE to + ; 1. then, as before, we set the lower 5 bits of IF to 1. + ; ($10 = 0b00010000) + ld a,0x10 ; 3e 10 + ldh (IO_IE),a ; e0 ff + ld a,0x1f ; 3e 1f + ldh (IO_IF),a ; e0 0f + + ; thirdly, we'll enable and request all 5 int types, this time with interrupts + ; disabled on the CPU via the DI instruction (no interrupts should be + ; triggered) + di ; f3 + ld a,0x1f ; 3e 1f + ldh (IO_IE),a ; e0 ff + ldh (IO_IF),a ; e0 0f + + ; we're done, just loop forever +- jr - ; 18 fe diff --git a/prototype_roms/05-bk-switch.gbc b/prototype_roms/05-bk-switch.gbc new file mode 100644 index 0000000..c050b3e Binary files /dev/null and b/prototype_roms/05-bk-switch.gbc differ diff --git a/prototype_roms/05-bk-switch.s b/prototype_roms/05-bk-switch.s new file mode 100644 index 0000000..470921b --- /dev/null +++ b/prototype_roms/05-bk-switch.s @@ -0,0 +1,61 @@ +; -- Demo 5 -- +; demonstration of the functionality of switching VRAM banks +; +; NOTE: there's also support for switching WRAM banks 1-7 at $d000-dfff, but +; its the exact same mechanism as switching VRAM banks, which is simpler +; to demo imo + + +; enable CGB features - VRAM banks can't be switched in normal DMG mode +cgb_mode: ; @ $0143 + .db 0xc0 ; c0 + + +; main entry point at $0100 (cartridge ROM bank 0) +; we just jump to our program routine at $4000 +main: ; @ $0100 + nop ; 00 + jp program ; c3 00 40 + + +; the different IO registers for bank switching VRAM +; VBK - switches between VRAM banks 0 and 1 (addressed at $8000-$9fff) +.define IO_VBK 0x4f + + +; program routine at $4000 (cartridge ROM bank 1) +program: ; @ $4000 + ld sp,0xffff ; 31 ff ff + + ; first, we'll ensure that we're currently using VRAM bank #0 + xor a ; af + ldh (IO_VBK),a ; e0 4f + + ; now, to demonstrate, let's write $aa to $8000 (VRAM bank 0) + ld hl,0x8000 ; 21 00 80 + ld (hl),0xaa ; 36 aa + ; and then switch to VRAM bank 1 + inc a ; 3c + ldh (IO_VBK),a ; e0 4f + + ; now, if you inspect $8000 the $aa we wrote is now gone! + ; + ; actually, it's still in memory within VRAM bank #0 but we've switched banks + ; so that $8000-$9fff now maps to VRAM bank #1! + ; + ; we'll write $bb to $8000 (VRAM bank 1 this time) (HL is already $8000) + ld (hl),0xbb ; 36 bb + + ; and we'll switch back to VRAM bank 0 + dec a ; 3d + ldh (IO_VBK),a ; e0 4f + + ; now, if you inspect $8000, $aa is now shown again + ; + ; let's again switch banks to VRAM bank 1 + inc a ; 3c + ldh (IO_VBK),a ; e0 4f + ; and again, we now see $bb at $8000 + + ; we're done, just loop forever +- jr - ; 18 fe diff --git a/src/audio/sfml_apu_out.cpp b/src/audio/sfml_apu_out.cpp new file mode 100644 index 0000000..a9c1223 --- /dev/null +++ b/src/audio/sfml_apu_out.cpp @@ -0,0 +1,83 @@ +#include "audio/sfml_apu_out.h" + +SfmlApuSoundStream::SfmlApuSoundStream() + : isStreaming_(false), + sampleFrontBuffer_(&sampleBuffers_[0]), + sampleBackBuffer_(&sampleBuffers_[1]) { + // 2 channels for left and right speakers @ our APU's downsampled sample rate + initialize(2, kApuOutputSampleRateHz); +} + +SfmlApuSoundStream::~SfmlApuSoundStream() { + StopStreaming(); +} + +void SfmlApuSoundStream::StartStreaming() { + { + std::unique_lock lock(sampleBackBufferMutex_); + samplesNextIdx_ = 0; + } + + isStreaming_ = true; + play(); +} + +void SfmlApuSoundStream::StopStreaming() { + isStreaming_ = false; + streamWaitCondition_.notify_one(); +} + +bool SfmlApuSoundStream::IsStreaming() const { + return isStreaming_; +} + +bool SfmlApuSoundStream::AudioIsMuted() const { + return !isStreaming_; +} + +void SfmlApuSoundStream::AudioBufferSamples(i16 leftSample, i16 rightSample) { + if (samplesNextIdx_ < sampleBackBuffer_->size()) { + // locking here has the potential that samplesNextIdx_ is set to 0 by the + // onGetData() thread, but this shouldn't cause any issues (it'll mean that + // these samples will get queued for the next audio driver copy instead) + std::unique_lock lock(sampleBackBufferMutex_); + + (*sampleBackBuffer_)[samplesNextIdx_++] = leftSample; + (*sampleBackBuffer_)[samplesNextIdx_++] = rightSample; + } + + // wake up onGetData() if it is waiting for more samples if we've now + // collected enough + if (samplesNextIdx_ >= kMinStreamedSamples) { + streamWaitCondition_.notify_one(); + } +} + +bool SfmlApuSoundStream::onGetData(Chunk& data) { + // wait until we have enough samples first (at least kMinStreamedSamples) + { + std::unique_lock waitLock(streamWaitMutex_); + + streamWaitCondition_.wait(waitLock, [&] () { + return samplesNextIdx_ >= kMinStreamedSamples || !isStreaming_; + }); + } + + if (!isStreaming_) { + return false; // returning false stops the stream + } + + { + std::unique_lock lock(sampleBackBufferMutex_); + + std::swap(sampleFrontBuffer_, sampleBackBuffer_); + data.sampleCount = samplesNextIdx_; + samplesNextIdx_ = 0; + } + + // the buffer will be copied to the audio driver at the end of this function + data.samples = &(*sampleFrontBuffer_)[0]; + return true; +} + +void SfmlApuSoundStream::onSeek(sf::Time) {} // don't support seeks diff --git a/src/debug/cpu_cmd_mode.cpp b/src/debug/cpu_cmd_mode.cpp new file mode 100644 index 0000000..09d587c --- /dev/null +++ b/src/debug/cpu_cmd_mode.cpp @@ -0,0 +1,289 @@ +#include "debug/cpu_cmd_mode.h" +#include "util.h" +#include +#include +#include + +CpuCmdMode::CpuCmdMode() + : autoResume_(false), updateGbcOnStep_(true), totalCpuSteps_(0), + totalCpuCycles_(0) { + gbc_.GetHardware().serial.SetSerialOutput(&serialStdout_); +} + +int CpuCmdMode::Run(const std::string& romFilePath) { + std::cout << " *-*-*-* sdgbc cpu-cmd mode *-*-*-*\n"; + std::cout << "basic tool for debugging sdgbc's LR35902 emulation\n"; + std::cout << std::endl; + + // try to load the ROM we've been given from command-line args first + // (will query for ROM if none given) + if (ExecuteLoadRomCommand(romFilePath) != RomLoadResult::Ok) { + do { + std::cout << std::endl; + } while (ExecuteLoadRomCommand() != RomLoadResult::Ok); + } + + std::cout << "\ntype \"help\" for a list of commands\n"; + std::cout << "NOTE: commands are case sensitive!\n\n"; + + std::cout << "stepgbc is on by default\n\n"; + + while (true) { + if(!ExecuteUserCommand(QueryUserCommand())) { + break; // exit requested + } + } + + return EXIT_SUCCESS; +} + +RomLoadResult CpuCmdMode::ExecuteLoadRomCommand(std::string romFilePath) { + if (romFilePath.empty()) { + std::cout << "input ROM file path > "; + std::getline(std::cin, romFilePath); + } + + const auto result = gbc_.LoadCartridgeRomFile(romFilePath); + switch (result) { + case RomLoadResult::Ok: + std::cout << "ROM file loaded!\n"; + break; + + default: + std::cout << "failed to load ROM - \"" + << Cartridge::GetRomLoadResultAsMessage(result) << "\"\n"; + } + + return result; +} + +void CpuCmdMode::ExecuteStepCpuCommand() { + std::cout << "input number of steps > "; + unsigned int steps; + + if (!(std::cin >> steps)) { + std::cout << "steps must be an integral number!\n"; + } else if (steps < 0) { + std::cout << "invalid number of steps - must be a positive integer!\n"; + } else { + StepCpu(steps); + } + + ResetInputStream(); +} + +void CpuCmdMode::StepCpu(unsigned int steps) { + auto& cpu = gbc_.GetHardware().cpu; + + auto stepped = 0u; + for (; stepped < steps; ++stepped) { + // check if the CPU can be auto-resumed from here + if (autoResume_ && (cpu.GetStatus() == CpuStatus::Halted || + cpu.GetStatus() == CpuStatus::Stopped)) { + cpu.Resume(); + } + + if (cpu.GetStatus() == CpuStatus::Running || updateGbcOnStep_) { + totalCpuCycles_ += updateGbcOnStep_ ? gbc_.Update() : cpu.Update(); + } else { + std::cout << "failed to step the CPU after " << stepped << " step(s)\n"; + std::cout << "NOTE: the CPU might have halted or is hung...\n\n"; + break; + } + } + + totalCpuSteps_ += stepped; + printf("stepped the CPU %d time(s) -- new PC=$%04x\n", + stepped, cpu.GetRegisters().pc.Get()); +} + +void CpuCmdMode::PrintCpuStatus() const { + // NOTE: we use printf for hexadecimal formatting because C++'s stream + // formatters are awful. This is really only code for debug purposes, anyway + const auto& cpu = gbc_.GetHardware().cpu; + + // print cpu run status + std::cout << "stat: " << GetCpuStatusString() << "\n"; + // print cpu total cycles & steps ran + std::cout << "cycl: " << totalCpuCycles_ << "\n"; + std::cout << "step: " << totalCpuSteps_ << "\n\n"; + + // print register pair values + printf("regs: PC=$%04x SP=$%04x\n", cpu.GetRegisters().pc.Get(), + cpu.GetRegisters().sp.Get()); + printf(" AF=$%04x BC=$%04x DE=$%04x HL=$%04x\n", + cpu.GetRegisters().af.Get(), + cpu.GetRegisters().bc.Get(), + cpu.GetRegisters().de.Get(), + cpu.GetRegisters().hl.Get()); + + // state of status flags in register F + std::cout << "flag: " + << (cpu.GetRegisters().f.GetZFlag() ? "Z " : "- ") + << (cpu.GetRegisters().f.GetNFlag() ? "N " : "- ") + << (cpu.GetRegisters().f.GetHFlag() ? "H " : "- ") + << (cpu.GetRegisters().f.GetCFlag() ? "C" : "-") + << "\n"; + + // state of interrupt master enable register + std::cout << "inme: " << (cpu.GetIntme() ? "on" : "off") << "\n\n"; + + + std::cout << "memory around PC:\n"; + PrintMemoryArea(cpu.GetRegisters().pc.Get(), 2); + std::cout << "\nmemory around SP:\n"; + PrintMemoryArea(cpu.GetRegisters().sp.Get(), 2); +} + +void CpuCmdMode::ExecuteViewMemoryAreaCommand() { + std::cout << "input memory address > $"; + u16 loc; + + if (!(std::cin >> std::hex >> loc)) { + std::cout << "address must be a hexadecimal value! ($0000 to $ffff)\n"; + } else { + printf("memory around $%04x:\n", loc); + PrintMemoryArea(loc, 2); + } + + std::cin.unsetf(std::ios::hex); + ResetInputStream(); +} + +std::string CpuCmdMode::QueryUserCommand() const { + const auto& hw = gbc_.GetHardware(); + + std::cout << std::endl; + + std::ostringstream oss; + oss << GetCpuStatusString() + << (hw.dma.IsOamDmaInProgress() ? " oamdma" : "") + << (hw.dma.IsHdmaInProgress() ? " hdma" : "") + << (hw.dma.IsGdmaInProgress() ? " gdma" : ""); + + printf("(%s, PC=$%04x, (PC)=[$%02x $%02x $%02x]) > ", + oss.str().c_str(), + hw.cpu.GetRegisters().pc.Get(), + hw.mmu.Read8(hw.cpu.GetRegisters().pc.Get()), + hw.mmu.Read8(hw.cpu.GetRegisters().pc.Get() + 1), + hw.mmu.Read8(hw.cpu.GetRegisters().pc.Get() + 2)); + + std::string cmd; + std::getline(std::cin, cmd); + return cmd; +} + +bool CpuCmdMode::ExecuteUserCommand(const std::string& cmd) { + if (cmd == "quit" || cmd == "exit" || cmd == "q") { + return false; // indicate that we want to exit now + } else if (cmd == "print" || cmd == "p") { + PrintCpuStatus(); + } else if (cmd == "memory" || cmd == "mem" || cmd == "m") { + ExecuteViewMemoryAreaCommand(); + } else if (cmd == "" || cmd == "next" || cmd == "n") { + StepCpu(); + } else if (cmd == "step" || cmd == "s") { + ExecuteStepCpuCommand(); + } else if (cmd == "unhalt" || cmd == "u") { + if (gbc_.GetHardware().cpu.Resume()) { + std::cout << "OK - CPU resumed!\n"; + } else { + std::cout << "could not resume the CPU! (is it hung?)\n"; + } + } else if (cmd == "autoresume" || cmd == "a") { + autoResume_ = !autoResume_; + std::cout << "autoresume is now " << (autoResume_ ? "on" : "off") << "\n"; + } else if (cmd == "stepgbc" || cmd == "g") { + updateGbcOnStep_ = !updateGbcOnStep_; + std::cout << "stepgbc is now " << (updateGbcOnStep_ ? "on" : "off") << "\n"; + if (updateGbcOnStep_) { + std::cout << "NOTE: the GBC can be stepped while stepgbc is on even if\n"; + std::cout << " the CPU is halted or hung\n"; + } + } else if (cmd == "reset" || cmd == "r") { + gbc_.Reset(); + totalCpuSteps_ = totalCpuCycles_ = 0; + std::cout << "OK - system reset!\n"; + } else if (cmd == "load" || cmd == "l") { + ExecuteLoadRomCommand(); + } else if (cmd == "help" || cmd == "h" || cmd == "?") { + std::cout << "help | h | ? - print a list of commands\n"; + std::cout << "load | l - load a new program ROM file and reset\n"; + std::cout << "print | p - print CPU status information\n"; + std::cout << "memory | mem | m - print contents of a memory location\n"; + std::cout << "reset | r - reset the system\n"; + std::cout << "unhalt | u - resume the CPU if suspended\n"; + std::cout << "autoresume | a - toggle automatic CPU unhalting\n"; + std::cout << "stepgbc | g - toggle steps updating all hardware\n"; + std::cout << " | next | n - step to the next CPU instruction\n"; + std::cout << "step | s - step a number of CPU instructions\n"; + std::cout << "quit | q | exit - quit the application\n"; + } else { + std::cout << "bad command - type \"help\" for a list of commands\n"; + std::cout << "NOTE: commands are case sensitive!\n"; + } + + return true; +} + +void CpuCmdMode::PrintMemoryArea(u16 loc, unsigned int linesAboveBelow) const { + // print column hex offsets + std::cout << " | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f | "; + std::cout << "0123456789abcdef\n"; + std::cout << "======|=================================================|="; + std::cout << "================\n"; + + for (auto i = 1u; i <= linesAboveBelow; ++i) { + PrintMemoryRow((loc & 0xfff0) - (linesAboveBelow - i + 1) * 0x10); + std::cout << "\n"; + } + + PrintMemoryRow(loc, true); + std::cout << "\n"; + + for (auto i = 1u; i <= linesAboveBelow; ++i) { + PrintMemoryRow((loc & 0xfff0) + i * 0x10); + std::cout << "\n"; + } +} + +void CpuCmdMode::PrintMemoryRow(u16 loc, bool markLoc) const { + const u16 startLoc = loc & 0xfff0; + printf("$%04x |", startLoc); + + // print hex values + for (auto i = 0u; i < 0x10; ++i) { + const auto formatCStr = markLoc && (loc & 0xfu) == i ? ">%02x" : " %02x"; + printf(formatCStr, gbc_.GetHardware().mmu.Read8(startLoc + i)); + } + + std::cout << " | "; + + // print ascii values + for (auto i = 0u; i < 0x10; ++i) { + // make sure we don't print escape/control characters + const u8 val = gbc_.GetHardware().mmu.Read8(startLoc + i); + printf("%c", val < 32 || val > 126 ? '.' : val); + } +} + +std::string CpuCmdMode::GetCpuStatusString() const { + switch (gbc_.GetHardware().cpu.GetStatus()) { + case CpuStatus::Running: + return "running"; + case CpuStatus::Halted: + return "halt"; + case CpuStatus::Stopped: + return "stop"; + case CpuStatus::Hung: + return "hung"; + + default: assert(!"unhandled cpu status string!"); + return "unknown"; + } +} + +void CpuCmdMode::ResetInputStream() const { + std::cin.clear(); + std::cin.ignore(std::numeric_limits::max(), '\n'); +} diff --git a/src/debug/serial_stdout.cpp b/src/debug/serial_stdout.cpp new file mode 100644 index 0000000..e9256d0 --- /dev/null +++ b/src/debug/serial_stdout.cpp @@ -0,0 +1,14 @@ +#include "debug/serial_stdout.h" +#include + +void SerialStdout::SerialReset() { + outByte_ = 0; +} + +void SerialStdout::SerialWriteBit(bool bitSet) { + outByte_ = ((outByte_ << 1) | (bitSet ? 1 : 0)) & 0xff; +} + +void SerialStdout::SerialOnByteWritten() { + std::cout << outByte_; +} diff --git a/src/emulator.cpp b/src/emulator.cpp new file mode 100644 index 0000000..e20b303 --- /dev/null +++ b/src/emulator.cpp @@ -0,0 +1,314 @@ +#include "emulator.h" + +// the minimum amount of time that the emulation thread sleeps for. +// allows the main thread to do work while waiting on the emu thread +constexpr std::chrono::microseconds kMinFrameSleepTime(50); + +// the max amount of frames that can be processed at one time without +// sleeping the emulation thread to catch-up with the target framerate +constexpr auto kMaxFrameSkip = 3u; +static_assert(kMaxFrameSkip > 0, "kMaxFrameSkip must be at least 1"); + +// if the target frame time is kMaxFrameTimeLateness or more behind the +// current time after handling frame skipping, the next frame will be +// rescheduled for the current time +constexpr std::chrono::seconds kMaxFrameTimeLateness(1); + +Emulator::Emulator() + : isPaused_(false), isStarted_(false), limitFramerate_(true) {} + +Emulator::~Emulator() { + StopEmulation(); +} + +void Emulator::EmulationLoop() { + using namespace std::chrono; + + // schedule the next frame to happen immediately + auto nextFrameTime_ = steady_clock::now(); + + while (isStarted_) { + if (isPaused_) { + // pause and adjust the next frame time to account for the pause + const auto pauseStartTime = steady_clock::now(); + PauseUntilNotify(); + + nextFrameTime_ += steady_clock::now() - pauseStartTime; + } else { + // update joypad key states for the frame(s) that we are about to process + gbc_.GetHardware().joypad.CommitKeyStates(); + + if (limitFramerate_) { + // frame skip until we process enough frames to catch up to our expected + // frame rate (or until we hit kMaxFrameSkip) + { + std::unique_lock lock(emulationMutex_); + + for (auto i = 0u; + i < kMaxFrameSkip && steady_clock::now() >= nextFrameTime_; + ++i) { + EmulateFrame(); + nextFrameTime_ += duration_cast(kFrameTime); + } + } + + const auto nowTime = steady_clock::now(); + + // if the next frame's target time is too far in the past, reschedule it + if (nowTime - nextFrameTime_ > kMaxFrameTimeLateness) { + nextFrameTime_ = nowTime; + } + + // sleep for the remaining frame time or kMinFrameSleepTime if too small + const auto frameTimeLeft = duration_cast( + nextFrameTime_ - nowTime); + + const auto frameSleepDur = std::max(frameTimeLeft, + duration_cast(kMinFrameSleepTime)); + + std::this_thread::sleep_for(frameSleepDur); + } else { + // emulation frame rate unlimited - just render the next frame here and + // ignore the time that was scheduled for processing the next frame. + // we wont bother handling frame skip here + { + std::unique_lock lock(emulationMutex_); + EmulateFrame(); + } + + // sleep for the minimum required time + std::this_thread::sleep_for(kMinFrameSleepTime); + + // seeing that we ignored the next frame time, we'll reschedule it here + nextFrameTime_ = steady_clock::now() + + duration_cast(kFrameTime); + } + } + } +} + +void Emulator::EmulateFrame() { + while (normalSpeedFrameCycles_ < kNormalSpeedCyclesPerFrame) { + // don't scale the amount of cycles left with CPU double speed mode + const auto cycles = util::RescaleCycles(gbc_.GetHardware().cpu, + gbc_.Update()); + normalSpeedFrameCycles_ += cycles; + } + + normalSpeedFrameCycles_ -= kNormalSpeedCyclesPerFrame; +} + +void Emulator::PauseUntilNotify() { + std::unique_lock waitLock(emulationPauseMutex_); + + emulationPauseCondition_.wait(waitLock, [&] () { + return !isPaused_ || !isStarted_; + }); +} + +bool Emulator::StartEmulation() { + if (!gbc_.GetHardware().cartridge.IsRomLoaded()) { + return false; + } + + isStarted_ = true; + emulationThread_ = std::thread(&Emulator::EmulationLoop, this); + return true; +} + +void Emulator::StopEmulation() { + // mark emulation as not started and notify the paused condition variable. + // this is required, as if emulation is paused, the thread will need to be + // woken up so it knows it has to exit! + isStarted_ = false; + emulationPauseCondition_.notify_one(); + + // wait for the emulation thread to exit + if (emulationThread_.joinable()) { + emulationThread_.join(); + } + + emulationThread_ = std::thread(); +} + +RomLoadResult Emulator::LoadCartridgeRomFile(const std::string& filePath, + const std::string& fileName) { + StopEmulation(); + + const auto result = gbc_.LoadCartridgeRomFile(filePath, fileName); + if (result == RomLoadResult::Ok) { + Reset(); + } + + StartEmulation(); + return result; +} + +bool Emulator::ExportCartridgeBatteryExtRam(const std::string& filePath) const { + std::unique_lock lock(emulationMutex_); + return gbc_.GetHardware().cartridge.SaveBatteryExtRam(filePath); +} + +bool Emulator::ImportCartridgeBatteryExtRam(const std::string& filePath) { + StopEmulation(); + + const bool success = gbc_.GetHardware().cartridge.LoadBatteryExtRam(filePath); + if (success) { + Reset(); + } + + StartEmulation(); + return success; +} + +bool Emulator::IsCartridgeRomLoaded() const { + return gbc_.GetHardware().cartridge.IsRomLoaded(); +} + +const CartridgeExtMeta& Emulator::GetCartridgeExtMeta() const { + return gbc_.GetHardware().cartridge.GetExtensionMeta(); +} + +void Emulator::Reset(bool forceDmgMode) { + if (gbc_.GetHardware().cartridge.IsRomLoaded()) { + std::unique_lock lock(emulationMutex_); + + normalSpeedFrameCycles_ = 0; + gbc_.Reset(forceDmgMode); + } +} + +std::string Emulator::GetCartridgeRomFileName() const { + return gbc_.GetHardware().cartridge.GetRomFileName(); +} + +std::string Emulator::GetCartridgeRomFilePath() const { + return gbc_.GetHardware().cartridge.GetRomFilePath(); +} + +void Emulator::SetVideoScanlineSpritesLimiterEnabled(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().ppu.SetScanlineSpritesLimiterEnabled(val); +} + +bool Emulator::IsVideoScanlineSpritesLimiterEnabled() const { + return gbc_.GetHardware().ppu.IsScanlineSpritesLimiterEnabled(); +} + +void Emulator::SetVideoBgRenderEnabled(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().ppu.SetBgRenderEnabled(val); +} + +bool Emulator::IsVideoBgRenderEnabled() const { + return gbc_.GetHardware().ppu.IsBgRenderEnabled(); +} + +void Emulator::SetVideoBgWindowRenderEnabled(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().ppu.SetBgWindowRenderEnabled(val); +} + +bool Emulator::IsVideoBgWindowRenderEnabled() const { + return gbc_.GetHardware().ppu.IsBgWindowRenderEnabled(); +} + +void Emulator::SetVideoSpritesRenderEnabled(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().ppu.SetSpritesRenderEnabled(val); +} + +bool Emulator::IsVideoSpritesRenderEnabled() const { + return gbc_.GetHardware().ppu.IsSpritesRenderEnabled(); +} + +void Emulator::SetJoypadKeyState(JoypadKey key, bool pressed) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().joypad.SetKeyState(key, pressed); +} + +void Emulator::SetJoypadImpossibleInputsAllowed(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().joypad.SetImpossibleInputsAllowed(val); +} + +bool Emulator::AreJoypadImpossibleInputsAllowed() const { + return gbc_.GetHardware().joypad.AreImpossibleInputsAllowed(); +} + +void Emulator::SetVideoLcd(ILcd* lcd) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().ppu.SetLcd(lcd); +} + +void Emulator::SetApuOutput(IApuOutput* audioOut) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().apu.SetApuOutput(audioOut); +} + +void Emulator::SetPaused(bool val) { + isPaused_ = val; + + // if we unpaused, notify the paused condition variable so that the emulation + // thread can wake back up + if (!isPaused_) { + emulationPauseCondition_.notify_one(); + } +} + +bool Emulator::IsPaused() const { + return isPaused_; +} + +void Emulator::SetLimitFramerate(bool val) { + limitFramerate_ = val; +} + +bool Emulator::IsLimitingFramerate() const { + return limitFramerate_; +} + +void Emulator::SetApuMuteCh1(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().apu.SetMuteCh1(val); +} + +bool Emulator::IsApuCh1Muted() const { + return gbc_.GetHardware().apu.IsCh1Muted(); +} + +void Emulator::SetApuMuteCh2(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().apu.SetMuteCh2(val); +} + +bool Emulator::IsApuCh2Muted() const { + return gbc_.GetHardware().apu.IsCh2Muted(); +} + +void Emulator::SetApuMuteCh3(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().apu.SetMuteCh3(val); +} + +bool Emulator::IsApuCh3Muted() const { + return gbc_.GetHardware().apu.IsCh3Muted(); +} + +void Emulator::SetApuMuteCh4(bool val) { + std::unique_lock lock(emulationMutex_); + gbc_.GetHardware().apu.SetMuteCh4(val); +} + +bool Emulator::IsApuCh4Muted() const { + return gbc_.GetHardware().apu.IsCh4Muted(); +} + +bool Emulator::IsStarted() const { + return isStarted_; +} + +bool Emulator::IsInCgbMode() const { + std::unique_lock lock(emulationMutex_); + return gbc_.IsInCgbMode(); +} diff --git a/src/hw/apu/apu.cpp b/src/hw/apu/apu.cpp new file mode 100644 index 0000000..87b6d41 --- /dev/null +++ b/src/hw/apu/apu.cpp @@ -0,0 +1,423 @@ +#include "hw/apu/apu.h" +#include "hw/cpu/cpu.h" +#include "emulator.h" +#include "util.h" +#include +#include + +// total amount of clock cycles needed for a frame sequencer update. +// frame seq updates at 512 Hz +constexpr auto kFrameSeqUpdateTotalCycles = kNormalSpeedClockRateHz / 512u; + +// we need to downsample to a sample rate supported by modern systems. +// to do so, we only take samples every kCyclesPerBufferedSamples clock cycles +// TODO remove the minus constant when emulation speed is more accurate +constexpr auto kCyclesPerBufferedSamples = kNormalSpeedClockRateHz + / kApuOutputSampleRateHz; + +Apu::Apu(const Cpu& cpu) + : cpu_(cpu), audioOut_(nullptr), + muteCh1_(false), muteCh2_(false), muteCh3_(false), muteCh4_(false) {} + +void Apu::Reset() { + outSampleCycles_ = 0; + frameSeqCycles_ = frameSeqStep_ = 0; + + // initial register values + channelCtrl_ = 0x77; + outCtrl_ = 0xf3; + soundOn_ = true; + + ch1_.Reset(); + ch2_.Reset(); + ch3_.Reset(); + ch4_.Reset(); +} + +void Apu::Update(unsigned int cycles) { + if (!soundOn_) { + return; + } + + // APU speed does not scale with double speed mode + cycles = util::RescaleCycles(cpu_, cycles); + + for (auto i = 0u; i < cycles; ++i) { + UpdateFrameSequencerCycle(); + + ch1_.UpdateFrequencyTimer(); + ch2_.UpdateFrequencyTimer(); + ch3_.UpdateFrequencyTimer(); + ch4_.UpdateFrequencyTimer(); + + // check if it's time to buffer more samples before continuing the update + if (++outSampleCycles_ >= kCyclesPerBufferedSamples) { + outSampleCycles_ -= kCyclesPerBufferedSamples; + + if (audioOut_ && !audioOut_->AudioIsMuted()) { + const auto samples = MixChannels(); + audioOut_->AudioBufferSamples(samples.first, samples.second); + } + } + } +} + +void Apu::UpdateFrameSequencerCycle() { + if (++frameSeqCycles_ < kFrameSeqUpdateTotalCycles) { + return; // not time to update the frame seq yet + } + + frameSeqCycles_ -= kFrameSeqUpdateTotalCycles; + + if (frameSeqStep_ % 2 == 0) { + // clock 256 Hz length control (ch1-4) + ch1_.UpdateLengthCounter(); + ch2_.UpdateLengthCounter(); + ch3_.UpdateLengthCounter(); + ch4_.UpdateLengthCounter(); + } + + if (frameSeqStep_ == 2 || frameSeqStep_ == 6) { + // clock 128 Hz sweep (ch1) + ch1_.UpdateSweep(); + } else if (frameSeqStep_ == 7) { + // clock 64 Hz volume envelope (ch1,2,4) + ch1_.UpdateEnvelope(); + ch2_.UpdateEnvelope(); + ch4_.UpdateEnvelope(); + } + + // we have to keep track of at most 8 steps + if (++frameSeqStep_ >= 8) { + frameSeqStep_ = 0; + } +} + +std::pair Apu::MixChannels() const { + i16 left = 0, right = 0; + + // mix the output volumes of the APU sound channels + { + u8 vol; + + // mix ch1 + vol = muteCh1_ ? 0 : ch1_.CalculateDacOutputVolume(); + left += outCtrl_ & 0x10 ? vol : 0; + right += outCtrl_ & 0x01 ? vol : 0; + + // mix ch2 + vol = muteCh2_ ? 0 : ch2_.CalculateDacOutputVolume(); + left += outCtrl_ & 0x20 ? vol : 0; + right += outCtrl_ & 0x02 ? vol : 0; + + // mix ch3 + vol = muteCh3_ ? 0 : ch3_.CalculateDacOutputVolume(); + left += outCtrl_ & 0x40 ? vol : 0; + right += outCtrl_ & 0x04 ? vol : 0; + + // mix ch4 + vol = muteCh4_ ? 0 : ch4_.CalculateDacOutputVolume(); + left += outCtrl_ & 0x80 ? vol : 0; + right += outCtrl_ & 0x08 ? vol : 0; + } + + // scale output mixed volumes by output terminal volumes set in channel ctrl. + // NOTE: due to the way the APU calculates this, a terminal volume of 0 still + // produces sound (due to the +1 of the terminal volume value) + const u8 leftVol = (channelCtrl_ >> 4) & 7; + left *= leftVol + 1; + + const u8 rightVol = channelCtrl_ & 7; + right *= rightVol + 1; + + // scale mixed output volumes to the 16-bit signed sample range. + // as there are 4 channels, which can at most have their mixed output volumes + // multiplied by 8 (terminal volume of 7), we divide INT16_MAX by these values + const auto sampleScale = std::numeric_limits::max() + / (kApuChannelMaxOutputVolume * 4 * 8); + left *= sampleScale; + right *= sampleScale; + + return std::make_pair(left, right); +} + +void Apu::WriteWaveRam8(u8 loc, u8 val) { + ch3_.WriteWaveRam8(loc, val); +} + +u8 Apu::ReadWaveRam8(u8 loc) const { + return ch3_.ReadWaveRam8(loc); +} + +u8 Apu::GetWaveRamLastWritten8() const { + return ch3_.GetWaveRamLastWritten8(); +} + +void Apu::WriteCh1Register(ApuCh1Register reg, u8 val) { + if (!soundOn_) { + return; + } + + switch (reg) { + case ApuCh1Register::SweepCtrl: + ch1_.SetSweepCtrl(val); + break; + case ApuCh1Register::LengthLoadDutyCtrl: + ch1_.SetLengthLoadDutyCtrl(val); + break; + case ApuCh1Register::EnvelopeCtrl: + ch1_.SetEnvelopeCtrl(val); + break; + case ApuCh1Register::FreqLoadLo: + ch1_.SetFreqLoadLo(val); + break; + case ApuCh1Register::LengthCtrlFreqLoadHi: + ch1_.SetLengthCtrlFreqLoadHi(val); + break; + default: assert(!"unknown apu ch1 register write!"); + } +} + +u8 Apu::ReadCh1Register(ApuCh1Register reg) const { + switch (reg) { + case ApuCh1Register::SweepCtrl: + return ch1_.GetSweepCtrl(); + case ApuCh1Register::LengthLoadDutyCtrl: + return ch1_.GetLengthLoadDutyCtrl(); + case ApuCh1Register::EnvelopeCtrl: + return ch1_.GetEnvelopeCtrl(); + case ApuCh1Register::LengthCtrlFreqLoadHi: + return ch1_.GetLengthCtrl(); + + default: assert(!"unknown apu ch1 register read!"); + case ApuCh1Register::FreqLoadLo: + return 0xff; + } +} + +void Apu::WriteCh2Register(ApuCh2Register reg, u8 val) { + if (!soundOn_) { + return; + } + + switch (reg) { + case ApuCh2Register::LengthLoadDutyCtrl: + ch2_.SetLengthLoadDutyCtrl(val); + break; + case ApuCh2Register::EnvelopeCtrl: + ch2_.SetEnvelopeCtrl(val); + break; + case ApuCh2Register::FreqLoadLo: + ch2_.SetFreqLoadLo(val); + break; + case ApuCh2Register::LengthCtrlFreqLoadHi: + ch2_.SetLengthCtrlFreqLoadHi(val); + break; + default: assert(!"unknown apu ch2 register write!"); + } +} + +u8 Apu::ReadCh2Register(ApuCh2Register reg) const { + switch (reg) { + case ApuCh2Register::LengthLoadDutyCtrl: + return ch2_.GetLengthLoadDutyCtrl(); + case ApuCh2Register::EnvelopeCtrl: + return ch2_.GetEnvelopeCtrl(); + case ApuCh2Register::LengthCtrlFreqLoadHi: + return ch2_.GetLengthCtrl(); + + default: assert(!"unknown apu ch2 register read!"); + case ApuCh2Register::FreqLoadLo: + return 0xff; + } +} + +void Apu::WriteCh3Register(ApuCh3Register reg, u8 val) { + if (!soundOn_) { + return; + } + + switch (reg) { + case ApuCh3Register::DacOnCtrl: + ch3_.SetDacOnCtrl(val); + break; + case ApuCh3Register::LengthLoad: + ch3_.SetLengthLoad(val); + break; + case ApuCh3Register::VolumeCtrl: + ch3_.SetVolumeCtrl(val); + break; + case ApuCh3Register::FreqLoadLo: + ch3_.SetFreqLoadLo(val); + break; + case ApuCh3Register::LengthCtrlFreqLoadHi: + ch3_.SetLengthCtrlFreqLoadHi(val); + break; + default: assert(!"unknown apu ch3 register write!"); + } +} + +u8 Apu::ReadCh3Register(ApuCh3Register reg) const { + switch (reg) { + case ApuCh3Register::DacOnCtrl: + return ch3_.GetDacOnCtrl(); + case ApuCh3Register::VolumeCtrl: + return ch3_.GetVolumeCtrl(); + case ApuCh3Register::LengthCtrlFreqLoadHi: + return ch3_.GetLengthCtrl(); + + default: assert(!"unknown apu ch3 register read!"); + case ApuCh3Register::FreqLoadLo: + case ApuCh3Register::LengthLoad: + return 0xff; + } +} + +void Apu::WriteCh4Register(ApuCh4Register reg, u8 val) { + if (!soundOn_) { + return; + } + + switch (reg) { + case ApuCh4Register::LengthLoad: + ch4_.SetLengthLoad(val); + break; + case ApuCh4Register::EnvelopeCtrl: + ch4_.SetEnvelopeCtrl(val); + break; + case ApuCh4Register::PolynomialCtrl: + ch4_.SetPolynomialCtrl(val); + break; + case ApuCh4Register::LengthCtrl: + ch4_.SetLengthCtrl(val); + break; + default: assert(!"unknown apu ch4 register write!"); + } +} + +u8 Apu::ReadCh4Register(ApuCh4Register reg) const { + switch (reg) { + case ApuCh4Register::EnvelopeCtrl: + return ch4_.GetEnvelopeCtrl(); + case ApuCh4Register::PolynomialCtrl: + return ch4_.GetPolynomialCtrl(); + case ApuCh4Register::LengthCtrl: + return ch4_.GetLengthCtrl(); + + default: assert(!"unknown apu ch4 register read!"); + case ApuCh4Register::LengthLoad: + return 0xff; + } +} + +void Apu::ZeroWriteAllRegisters() { + channelCtrl_ = outCtrl_ = 0; + + WriteCh1Register(ApuCh1Register::SweepCtrl, 0); + WriteCh1Register(ApuCh1Register::LengthLoadDutyCtrl, 0); + WriteCh1Register(ApuCh1Register::EnvelopeCtrl, 0); + WriteCh1Register(ApuCh1Register::FreqLoadLo, 0); + WriteCh1Register(ApuCh1Register::LengthCtrlFreqLoadHi, 0); + + WriteCh2Register(ApuCh2Register::LengthLoadDutyCtrl, 0); + WriteCh2Register(ApuCh2Register::EnvelopeCtrl, 0); + WriteCh2Register(ApuCh2Register::FreqLoadLo, 0); + WriteCh2Register(ApuCh2Register::LengthCtrlFreqLoadHi, 0); + + WriteCh3Register(ApuCh3Register::DacOnCtrl, 0); + WriteCh3Register(ApuCh3Register::LengthLoad, 0); + WriteCh3Register(ApuCh3Register::VolumeCtrl, 0); + WriteCh3Register(ApuCh3Register::FreqLoadLo, 0); + WriteCh3Register(ApuCh3Register::LengthCtrlFreqLoadHi, 0); + + WriteCh4Register(ApuCh4Register::LengthLoad, 0); + WriteCh4Register(ApuCh4Register::EnvelopeCtrl, 0); + WriteCh4Register(ApuCh4Register::PolynomialCtrl, 0); + WriteCh4Register(ApuCh4Register::LengthCtrl, 0); +} + +void Apu::SetChannelCtrl(u8 val) { + if (soundOn_) { + channelCtrl_ = val; + } +} + +u8 Apu::GetChannelCtrl() const { + return channelCtrl_; +} + +void Apu::SetOutputCtrl(u8 val) { + if (soundOn_) { + outCtrl_ = val; + } +} + +u8 Apu::GetOutputCtrl() const { + return outCtrl_; +} + +void Apu::SetOnCtrl(u8 val) { + const bool newOn = (val & 0x80) != 0; + + if (newOn != soundOn_) { + if (!newOn) { + ZeroWriteAllRegisters(); + } + else { + // reset frame seq step, square duty and clear wave RAM + frameSeqStep_ = 0; + + ch1_.ResetDutyCounter(); + ch2_.ResetDutyCounter(); + ch3_.ClearWaveRam(); + } + } + + soundOn_ = newOn; +} + +u8 Apu::GetOnCtrl() const { + u8 onCtrl = soundOn_ ? 0xf0 : 0x70; + onCtrl |= (ch1_.IsEnabled() ? 1 : 0); + onCtrl |= (ch2_.IsEnabled() ? 2 : 0); + onCtrl |= (ch3_.IsEnabled() ? 4 : 0); + onCtrl |= (ch4_.IsEnabled() ? 8 : 0); + + return onCtrl; +} + +void Apu::SetMuteCh1(bool val) { + muteCh1_ = val; +} + +bool Apu::IsCh1Muted() const { + return muteCh1_; +} + +void Apu::SetMuteCh2(bool val) { + muteCh2_ = val; +} + +bool Apu::IsCh2Muted() const { + return muteCh2_; +} + +void Apu::SetMuteCh3(bool val) { + muteCh3_ = val; +} + +bool Apu::IsCh3Muted() const { + return muteCh3_; +} + +void Apu::SetMuteCh4(bool val) { + muteCh4_ = val; +} + +bool Apu::IsCh4Muted() const { + return muteCh4_; +} + +void Apu::SetApuOutput(IApuOutput* audioOut) { + audioOut_ = audioOut; +} diff --git a/src/hw/apu/apu_chan_base.cpp b/src/hw/apu/apu_chan_base.cpp new file mode 100644 index 0000000..125ba6e --- /dev/null +++ b/src/hw/apu/apu_chan_base.cpp @@ -0,0 +1,136 @@ +#include "hw/apu/apu_chan_base.h" +#include + +// if (envelopeCtrl & mask) is not 0, DAC is enabled. +// NOTE: the wave channel does not have an envelope ctrl register. DAC is +// instead turned on/off via the channel's on ctrl register (NR30) +constexpr u8 kEnvelopeCtrlDacEnabledMask = 0xf8; + +void ApuSoundChannelBase::Reset() { + enabled_ = dacEnabled_ = false; + + lengthEnabled_ = false; + freqTimer_ = lengthCounter_ = 0; +} + +void ApuSoundChannelBase::Restart() { + enabled_ = dacEnabled_; + + if (lengthCounter_ <= 0) { + ResetLengthCounter(); + } + + freqTimer_ = GetFrequencyTimerPeriod(); +} + +void ApuSoundChannelBase::UpdateFrequencyTimer() { + // NOTE: if the timer is already 0, it will overflow and not trigger this + // condition - audio seems to sound better (especially with the noise channel) + // than when handling that case! + if (--freqTimer_ <= 0) { + freqTimer_ = GetFrequencyTimerPeriod(); + UpdateFrequency(); + } +} + +void ApuSoundChannelBase::UpdateLengthCounter() { + if (lengthEnabled_ && lengthCounter_ > 0 && --lengthCounter_ <= 0) { + enabled_ = false; + } +} + +u8 ApuSoundChannelBase::CalculateDacOutputVolume() const { + const u8 vol = (enabled_ && IsDacEnabled() ? CalculateOutputVolume() : 0); + assert(vol <= kApuChannelMaxOutputVolume); + + return vol; +} + +void ApuSoundChannelBase::SetLengthCtrl(u8 val) { + lengthEnabled_ = (val & 0x40) != 0; + + // restart if bit 7 is set + if (val & 0x80) { + Restart(); + } +} + +u8 ApuSoundChannelBase::GetLengthCtrl() const { + return lengthEnabled_ ? 0xff : 0xbf; // only bit 6 readable +} + +bool ApuSoundChannelBase::IsEnabled() const { + return enabled_; +} + +void ApuSoundChannelBase::ResetLengthCounter(u8 lengthSubtract) { + lengthCounter_ = GetMaxLength() - lengthSubtract; +} + +void ApuSoundChannelBase::SetDacEnabled(bool val) { + dacEnabled_ = val; + + // NOTE: enabling DAC again won't re-enable the channel + if (!dacEnabled_) { + enabled_ = false; + } +} + +bool ApuSoundChannelBase::IsDacEnabled() const { + return dacEnabled_; +} + +void ApuEnvelopeChannelBase::Reset() { + ApuSoundChannelBase::Reset(); + + envTimer_ = envVolume_ = 0; + envCtrl_ = 0x00; +} + +void ApuEnvelopeChannelBase::Restart() { + ApuSoundChannelBase::Restart(); + + ResetEnvelopeTimer(); + ResetEnvelopeVolume(); +} + +void ApuEnvelopeChannelBase::UpdateEnvelope() { + if (--envTimer_ <= 0 && ResetEnvelopeTimer() > 0) { + // update current envelope volume depending on the add mode (bit 3). + // the new volume cannot be greater than 15 or below 0 + if (envCtrl_ & 8) { + if (envVolume_ < 15) { + ++envVolume_; + } + } else { + if (envVolume_ > 0) { + --envVolume_; + } + } + } +} + +u8 ApuEnvelopeChannelBase::ResetEnvelopeTimer() { + // reload the period from bits 7-4 of the ctrl reg. + // due to weird APU behaviour, a new period of 0 is treated as 8 instead + const u8 newPeriod = envCtrl_ & 7; + envTimer_ = newPeriod == 0 ? 8 : newPeriod; + return newPeriod; +} + +void ApuEnvelopeChannelBase::SetEnvelopeCtrl(u8 val) { + envCtrl_ = val; + SetDacEnabled((envCtrl_ & kEnvelopeCtrlDacEnabledMask) != 0); +} + +u8 ApuEnvelopeChannelBase::GetEnvelopeCtrl() const { + return envCtrl_; +} + +void ApuEnvelopeChannelBase::ResetEnvelopeVolume() { + envVolume_ = (envCtrl_ & 0xf0) >> 4; +} + +u16 ApuEnvelopeChannelBase::GetMaxLength() const { + return 64; +} diff --git a/src/hw/apu/apu_chan_noise.cpp b/src/hw/apu/apu_chan_noise.cpp new file mode 100644 index 0000000..eb92bcc --- /dev/null +++ b/src/hw/apu/apu_chan_noise.cpp @@ -0,0 +1,58 @@ +#include "hw/apu/apu_chan_noise.h" + +constexpr std::array kNoiseFreqDivisors { + 8, 16, 32, 48, 64, 80, 96, 112 +}; + +void ApuNoiseChannel::Reset() { + ApuEnvelopeChannelBase::Reset(); + + polyCtrl_ = 0x00; + linearShift_ = 0; +} + +void ApuNoiseChannel::Restart() { + ApuEnvelopeChannelBase::Restart(); + linearShift_ = 0x7fff; +} + +void ApuNoiseChannel::UpdateFrequency() { + // generate a 15-bit pseudo-random bit sequence using the linear feedback + // shift register (LSFR): + // + // XOR bits 0 & 1 of the LFSR, then right shift. + // replace the now unset bit 7 with the XOR result bit + const u8 xorBit = (linearShift_ & 1) ^ ((linearShift_ >> 1) & 1); + linearShift_ = (linearShift_ >> 1) | (xorBit << 14); + + // if width mode (poly ctrl bit 3) set, also replace bit 6 with the XOR bit + if (polyCtrl_ & 8) { + linearShift_ = (linearShift_ & 0xbf) | (xorBit << 6); + } +} + +u8 ApuNoiseChannel::CalculateOutputVolume() const { + // output the envelope volume if bit 0 of the LFSR is unset + return linearShift_ & 1 ? 0 : envVolume_; +} + +u16 ApuNoiseChannel::GetFrequencyTimerPeriod() const { + // extract the noise frequency divisor number and freq clock shift amount + // from the polynomial ctrl + const u8 divisorNum = polyCtrl_ & 0x7; // bits 0-2 + const u8 freqShift = (polyCtrl_ >> 4) & 0xf; // bits 4-7 + + return kNoiseFreqDivisors[divisorNum] << freqShift; +} + +void ApuNoiseChannel::SetLengthLoad(u8 val) { + ResetLengthCounter(val & 0x3f); // bits 0-5 writable +} + +void ApuNoiseChannel::SetPolynomialCtrl(u8 val) { + polyCtrl_ = val; +} + +u8 ApuNoiseChannel::GetPolynomialCtrl() const { + return polyCtrl_; +} diff --git a/src/hw/apu/apu_chan_square.cpp b/src/hw/apu/apu_chan_square.cpp new file mode 100644 index 0000000..13bbb00 --- /dev/null +++ b/src/hw/apu/apu_chan_square.cpp @@ -0,0 +1,129 @@ +#include "hw/apu/apu_chan_square.h" +#include "util.h" + +constexpr std::array kSquareDutyWaveforms { + 0b00000001, + 0b10000001, + 0b10000111, + 0b01111110 +}; + +void ApuSquareChannel::Reset() { + ApuEnvelopeChannelBase::Reset(); + + freqLoad_ = 0x0000; + dutyNum_ = dutyBitIdxCounter_ = 0; +} + +void ApuSquareChannel::ResetDutyCounter() { + dutyBitIdxCounter_ = 0; +} + +void ApuSquareChannel::UpdateFrequency() { + // increment the duty bit pos + dutyBitIdxCounter_ = (dutyBitIdxCounter_ + 1) % 8; +} + +u8 ApuSquareChannel::CalculateOutputVolume() const { + // output envelope volume if the selected bit of the duty waveform is set + const u8 duty = kSquareDutyWaveforms[dutyNum_]; + return (1 << dutyBitIdxCounter_) & duty ? envVolume_ : 0; +} + +u16 ApuSquareChannel::GetFrequencyTimerPeriod() const { + return ((kApuChannelMaxFreqLoad + 1) - freqLoad_) * 4; +} + +void ApuSquareChannel::SetLengthLoadDutyCtrl(u8 val) { + dutyNum_ = (val >> 6) & 3; // duty number is bits 6-7 + ResetLengthCounter(val & 0x3f); // load value is bits 0-5 +} + +u8 ApuSquareChannel::GetLengthLoadDutyCtrl() const { + return (dutyNum_ << 6) | 0x3f; // only duty number readable (bits 6-7) +} + +void ApuSquareChannel::SetFreqLoadLo(u8 val) { + freqLoad_ = util::SetLo8(freqLoad_, val); +} + +void ApuSquareChannel::SetLengthCtrlFreqLoadHi(u8 val) { + freqLoad_ = util::SetHi8(freqLoad_, val & 7); // bits 0-2 freq MSB + SetLengthCtrl(val); +} + +void ApuSquareSweepChannel::Reset() { + ApuSquareChannel::Reset(); + + // sweep channel starts enabled (with DAC on), with duty number 2 + enabled_ = true; + envCtrl_ = 0xf3; + dutyNum_ = 2; + + sweepCtrl_ = 0x80; + sweepEnabled_ = false; + ResetSweepShadowFrequency(); +} + +void ApuSquareSweepChannel::Restart() { + ApuSquareChannel::Restart(); + + ResetSweepShadowFrequency(); + sweepEnabled_ = ResetSweepTimer() > 0 || GetSweepShift() > 0; + + if (GetSweepShift() > 0) { + // perform a new frequency calc without the write to check for overflow + CalculateNewSweepFreq(); + } +} + +void ApuSquareSweepChannel::UpdateSweep() { + if (--sweepTimer_ <= 0 && ResetSweepTimer() > 0 && sweepEnabled_) { + const u16 newFreq = CalculateNewSweepFreq(); + + if (newFreq <= kApuChannelMaxFreqLoad && GetSweepShift() > 0) { + // the freq ctrl initial freq value is changed to the new freq, and the + // new freq replaces shadow freq, and a new freq calc is performed (its + // value is NOT written and is ignored, but it modifies internal state) + freqLoad_ = sweepShadow_ = newFreq; + CalculateNewSweepFreq(); + } + } +} + +u16 ApuSquareSweepChannel::CalculateNewSweepFreq() { + // shift shadow freq right using the sweep shift value. this value is either + // subtracted or added to the shadow freq depending on ctrl bit 3 + const u16 newFreq = sweepShadow_ + ((sweepCtrl_ & 8 ? -1 : 1) + * (sweepShadow_ >> GetSweepShift())); + if (newFreq > kApuChannelMaxFreqLoad) { + // internal overflow check - disables channel + enabled_ = false; + } + + return newFreq; +} + +u8 ApuSquareSweepChannel::ResetSweepTimer() { + // reload the period from bits 7-4 of the ctrl reg. + // due to weird APU behaviour, a new period of 0 is treated as 8 instead + const u8 newPeriod = (sweepCtrl_ >> 4) & 7; + sweepTimer_ = newPeriod == 0 ? 8 : newPeriod; + return newPeriod; +} + +void ApuSquareSweepChannel::ResetSweepShadowFrequency() { + sweepShadow_ = freqLoad_; +} + +u8 ApuSquareSweepChannel::GetSweepShift() const { + return sweepCtrl_ & 7; +} + +void ApuSquareSweepChannel::SetSweepCtrl(u8 val) { + sweepCtrl_ = val; +} + +u8 ApuSquareSweepChannel::GetSweepCtrl() const { + return sweepCtrl_; +} diff --git a/src/hw/apu/apu_chan_wave.cpp b/src/hw/apu/apu_chan_wave.cpp new file mode 100644 index 0000000..3cb9633 --- /dev/null +++ b/src/hw/apu/apu_chan_wave.cpp @@ -0,0 +1,95 @@ +#include "hw/apu/apu_chan_wave.h" +#include "util.h" +#include + +void ApuWaveChannel::Reset() { + ApuSoundChannelBase::Reset(); + + freqLoad_ = 0x0000; + volumeCode_ = 0; + sampleIdxCounter_ = 0; + + // on the CGB, wave RAM has consistent values, repeating $00 and $FF + for (std::size_t i = 0; i < waveRam_.size(); ++i) { + waveRam_[i] = (i % 2 == 0 ? 0x00 : 0xff); + } + waveRamLastWrittenVal_ = 0xff; +} + +void ApuWaveChannel::Restart() { + ApuSoundChannelBase::Restart(); + sampleIdxCounter_ = 0; +} + +void ApuWaveChannel::UpdateFrequency() { + // increment the sample index counter. + // sample is 4 bits, so wave RAM has (2 * (size of wave RAM bytes)) samples + sampleIdxCounter_ = (sampleIdxCounter_ + 1) % (waveRam_.size() * 2); +} + +u8 ApuWaveChannel::CalculateOutputVolume() const { + // first 4-bit sample is stored in the high nibble + const u8 twoSamples = waveRam_[sampleIdxCounter_ / 2]; + const u8 sample = sampleIdxCounter_ % 2 == 0 ? (twoSamples >> 4) & 0xf + : twoSamples & 0xf; + + // calculate the output vol by shifting right using the volume code. + // a code of 0 produces silence, 1-3 indicates a right shift of (code - 1) + assert(volumeCode_ <= 3); + return volumeCode_ > 0 ? sample >> (volumeCode_ - 1) : 0; +} + +void ApuWaveChannel::ClearWaveRam() { + waveRam_.fill(waveRamLastWrittenVal_ = 0x00); +} + +void ApuWaveChannel::WriteWaveRam8(u8 loc, u8 val) { + assert(loc < waveRam_.size()); + waveRam_[loc] = waveRamLastWrittenVal_ = val; +} + +u8 ApuWaveChannel::ReadWaveRam8(u8 loc) const { + assert(loc < waveRam_.size()); + return waveRam_[loc]; +} + +u8 ApuWaveChannel::GetWaveRamLastWritten8() const { + return waveRamLastWrittenVal_; +} + +void ApuWaveChannel::SetDacOnCtrl(u8 val) { + SetDacEnabled((val & 0x80) != 0); +} + +u8 ApuWaveChannel::GetDacOnCtrl() const { + return IsDacEnabled() ? 0xff : 0x7f; +} + +void ApuWaveChannel::SetLengthLoad(u8 val) { + ResetLengthCounter(val); +} + +void ApuWaveChannel::SetVolumeCtrl(u8 val) { + volumeCode_ = (val >> 5) & 3; // code number in bits 5-6 +} + +u8 ApuWaveChannel::GetVolumeCtrl() const { + return (volumeCode_ << 5) | 0x9f; +} + +void ApuWaveChannel::SetFreqLoadLo(u8 val) { + freqLoad_ = util::SetLo8(freqLoad_, val); +} + +void ApuWaveChannel::SetLengthCtrlFreqLoadHi(u8 val) { + freqLoad_ = util::SetHi8(freqLoad_, val & 7); // bits 0-2 freq MSB + SetLengthCtrl(val); +} + +u16 ApuWaveChannel::GetFrequencyTimerPeriod() const { + return ((kApuChannelMaxFreqLoad + 1) - freqLoad_) * 2; +} + +u16 ApuWaveChannel::GetMaxLength() const { + return 256; +} diff --git a/src/hw/cart/cart.cpp b/src/hw/cart/cart.cpp new file mode 100644 index 0000000..06c310a --- /dev/null +++ b/src/hw/cart/cart.cpp @@ -0,0 +1,298 @@ +#include "hw/cart/cart.h" +#include "hw/cart/cart_ext_base.h" +#include "hw/cart/cart_ext_mbc1.h" +#include "hw/cart/cart_ext_mbc2.h" +#include "hw/cart/cart_ext_mbc3.h" +#include "hw/cart/cart_ext_mbc5.h" +#include "util.h" +#include +#include + +std::string Cartridge::GetRomLoadResultAsMessage(RomLoadResult result) { + switch (result) { + case RomLoadResult::Ok: + return "ROM image loaded successfully!"; + + case RomLoadResult::ReadError: + return "The ROM image could not be properly read."; + + case RomLoadResult::InvalidSize: + return "The ROM image has an invalid or unexpected size. The ROM image " + "might be corrupted!"; + + case RomLoadResult::InvalidExtension: + return "The ROM requires emulation of extended cartridge features that " + "it cannot support."; + + case RomLoadResult::Unsupported: + return "The ROM requires emulation of features that are not yet " + "supported by sdgbc."; + + default: assert(!"unimplemented RomLoadResult message!"); + return "Unknown error!"; + } +} + +Cartridge::Cartridge() { + Clear(); +} + +Cartridge::~Cartridge() { + // cartridge object is destroyed when new ROM loaded or program quits + SaveBatteryExtRam(); +} + +bool Cartridge::SaveBatteryExtRam(const std::string& filePath) const { + if (!isRomLoaded_ || !extension_ || !GetExtensionMeta().hasBattery) { + return true; + } + + std::ofstream file(filePath.empty() ? romFilePath_ + ".sav" : filePath, + std::ios::binary); + return extension_->ExtSaveRam(file); +} + +bool Cartridge::LoadBatteryExtRam(const std::string& filePath) { + if (!isRomLoaded_ || !extension_ || !GetExtensionMeta().hasBattery) { + return true; + } + + std::ifstream file(filePath.empty() ? romFilePath_ + ".sav" : filePath, + std::ios::binary); + return extension_->ExtLoadRam(file); +} + +void Cartridge::Clear() { + Reset(); + + romData_.clear(); + romFilePath_.clear(); + romFileName_.clear(); + + num2KBExtRamBanks_ = numRomBanks_ = 0; + + extension_.reset(); + extensionId_ = 0x00; + + cgbMode_ = false; + isRomLoaded_ = false; +} + +void Cartridge::Reset() { + if (extension_) { + extension_->ExtReset(); + } +} + +RomLoadResult Cartridge::LoadRomFile(const std::string& filePath, + const std::string& fileName) { + { + // start creating the new cartridge from the ROM data + Cartridge newCart; + + std::ifstream file(filePath, std::ios::binary); + if (!util::ReadBinaryStream(file, newCart.romData_)) { + return RomLoadResult::ReadError; + } + + const auto parseResult = ParseRomHeader(newCart); + if (parseResult != RomLoadResult::Ok) { + return parseResult; + } + + newCart.romFilePath_ = filePath; + newCart.romFileName_ = fileName.empty() ? filePath : fileName; + newCart.isRomLoaded_ = true; + + // loading successful, move to this new cartridge and re-assign cart ptr + *this = std::move(newCart); + if (extension_) { + extension_->ExtSetCartridge(this); + } + } + + // old cartridge is destroyed at this point + Reset(); + LoadBatteryExtRam(); + return RomLoadResult::Ok; +} + +RomLoadResult Cartridge::ParseRomHeader(Cartridge& newCart) { + // make sure that the ROM is big enough to have a header + if (newCart.romData_.size() < 0x150) { + return RomLoadResult::InvalidSize; + } + + // parse ROM size attribute and determine how many ROM banks we should have + const u8 romSize = newCart.romData_[0x148]; + if (romSize <= 0x07) { + newCart.numRomBanks_ = 2 << romSize; + } else if (romSize == 0x52) { + newCart.numRomBanks_ = 72; + } else if (romSize == 0x53) { + newCart.numRomBanks_ = 80; + } else if (romSize == 0x54) { + newCart.numRomBanks_ = 96; + } else { + return RomLoadResult::InvalidSize; + } + + // verify that the size of the ROM is what it claims to be + if (newCart.romData_.size() != + static_cast(newCart.numRomBanks_ * kRomBankSize)) { + return RomLoadResult::InvalidSize; + } + + // parse external RAM size attribute + const u8 exRamSize = newCart.romData_[0x149]; + if (exRamSize == 0x00) { + newCart.num2KBExtRamBanks_ = 0; + } else if (exRamSize <= 0x04) { + newCart.num2KBExtRamBanks_ = 1 << ((exRamSize - 1) * 2); + } else if (exRamSize == 0x05) { + newCart.num2KBExtRamBanks_ = 32; + } else { + return RomLoadResult::InvalidSize; + } + + // parse the Game Boy/Game Boy Color compatibility flag + const u8 cgbFlag = newCart.romData_[0x143]; + newCart.cgbMode_ = cgbFlag == 0x80 || cgbFlag == 0xc0; + + // parse cartridge extensions, if any + const auto extParseResult = ParseExtensions(newCart); + if (extParseResult != RomLoadResult::Ok) { + return extParseResult; + } + + return RomLoadResult::Ok; +} + +RomLoadResult Cartridge::ParseExtensions(Cartridge& newCart) { + // determine which mapper/hardware extension the cartridge uses, and whether + // or not we support it + newCart.extensionId_ = newCart.romData_[0x147]; + + const auto metaIt = kCartridgeExtMetas.find(newCart.extensionId_); + if (metaIt == kCartridgeExtMetas.end()) { + return RomLoadResult::Unsupported; + } + + if (!metaIt->second.supportsExRam && newCart.num2KBExtRamBanks_ != 0) { + // the ROM claims to have external RAM, but the mapper doesn't support it + return RomLoadResult::InvalidExtension; + } + + switch (metaIt->second.type) { + case CartridgeExtType::None: + newCart.extension_.reset(); + break; + case CartridgeExtType::Mbc1: + newCart.extension_ = std::make_unique(); + break; + case CartridgeExtType::Mbc2: + newCart.extension_ = std::make_unique(); + break; + case CartridgeExtType::Mbc3: + // TODO check for RTC if actually going to support it + newCart.extension_ = std::make_unique(false); + break; + case CartridgeExtType::Mbc5: + // TODO check for rumble if actually going to support it + newCart.extension_ = std::make_unique(); + break; + + default: + return RomLoadResult::Unsupported; + } + + // check if this ROM can actually support this extension + if (newCart.extension_) { + newCart.extension_->ExtSetCartridge(&newCart); + + if (!newCart.extension_->ExtInit()) { + return RomLoadResult::InvalidExtension; + } + } + + return RomLoadResult::Ok; +} + +void Cartridge::RomBank0Write8(u16 loc, u8 val) { + assert(isRomLoaded_ && loc < kRomBankSize); + + if (extension_) { + extension_->ExtRomBank0Write8(loc, val); + } +} + +u8 Cartridge::RomBank0Read8(u16 loc) const { + assert(isRomLoaded_ && loc < kRomBankSize); + return extension_ ? extension_->ExtRomBank0Read8(loc) : romData_[loc]; +} + +void Cartridge::RomBankXWrite8(u16 loc, u8 val) { + assert(isRomLoaded_ && loc < kRomBankSize); + + if (extension_) { + extension_->ExtRomBankXWrite8(loc, val); + } +} + +u8 Cartridge::RomBankXRead8(u16 loc) const { + assert(isRomLoaded_ && loc < kRomBankSize); + return extension_ ? extension_->ExtRomBankXRead8(loc) + : romData_[kRomBankSize + loc]; +} + +void Cartridge::RamWrite8(u16 loc, u8 val) { + assert(isRomLoaded_ && loc < kExtRamBankSize); + + if (extension_) { + extension_->ExtRamWrite8(loc, val); + } +} + +u8 Cartridge::RamRead8(u16 loc) const { + assert(isRomLoaded_ && loc < kExtRamBankSize); + return extension_ ? extension_->ExtRamRead8(loc) : 0xff; +} + +bool Cartridge::IsRomLoaded() const { + return isRomLoaded_; +} + +std::string Cartridge::GetRomFilePath() const { + return romFilePath_; +} + +std::string Cartridge::GetRomFileName() const { + return romFileName_; +} + +const std::vector& Cartridge::GetRomData() const { + return romData_; +} + +u16 Cartridge::GetNumRomBanks() const { + return numRomBanks_; +} + +u16 Cartridge::GetNum2KBExtRamBanks() const { + return num2KBExtRamBanks_; +} + +u8 Cartridge::GetExtensionId() const { + return extensionId_; +} + +const CartridgeExtMeta& Cartridge::GetExtensionMeta() const { + const auto metaIt = kCartridgeExtMetas.find(extensionId_); + assert(metaIt != kCartridgeExtMetas.end()); + + return metaIt->second; +} + +bool Cartridge::IsInCgbMode() const { + return cgbMode_; +} diff --git a/src/hw/cart/cart_ext_base.cpp b/src/hw/cart/cart_ext_base.cpp new file mode 100644 index 0000000..929e2db --- /dev/null +++ b/src/hw/cart/cart_ext_base.cpp @@ -0,0 +1,92 @@ +#include "hw/cart/cart.h" +#include "hw/cart/cart_ext_base.h" +#include "util.h" + +CartridgeExtensionBase::CartridgeExtensionBase() : cart_(nullptr) {} + +void CartridgeExtensionBase::ExtSetCartridge(const Cartridge* cart) { + cart_ = cart; +} + +// ROM read-only by default +void CartridgeExtensionBase::ExtRomBank0Write8(u16, u8) {} + +void CartridgeExtensionBase::ExtRomBankXWrite8(u16, u8) {} + +u8 CartridgeExtensionBase::ExtRomBank0Read8(u16 loc) const { + return cart_->GetRomData()[loc]; +} + +u8 CartridgeExtensionBase::ExtRomBankXRead8(u16 loc) const { + return cart_->GetRomData()[kRomBankSize + loc]; +} + +bool RamExtensionBase::ExtInit() { + // clear and zero-out RAM to new size + ramData_.clear(); + ramData_.resize(cart_->GetNum2KBExtRamBanks() * 0x800, 0x00); + return true; +} + +void RamExtensionBase::ExtReset() { + ramEnabled_ = false; + ramBankNum_ = 0; + + // zero-out RAM that isn't battery-packed + if (!cart_->GetExtensionMeta().hasBattery) { + std::fill(ramData_.begin(), ramData_.end(), 0x00); + } +} + +void RamExtensionBase::RamBankXWrite8(u16 loc, u8 val, u16 bankNum) { + if (ramEnabled_ && ramData_.size() > 0) { + const std::size_t dataIndex = (kExtRamBankSize * bankNum + loc) + % ramData_.size(); + ramData_[dataIndex] = val; + } +} + +u8 RamExtensionBase::RamBankXRead8(u16 loc, u16 bankNum) const { + if (ramEnabled_ && ramData_.size() > 0) { + const std::size_t dataIndex = (kExtRamBankSize * bankNum + loc) + % ramData_.size(); + return ramData_[dataIndex]; + } else { + return 0xff; + } +} + +void RamExtensionBase::ExtRamWrite8(u16 loc, u8 val) { + RamBankXWrite8(loc, val, ramBankNum_); +} + +u8 RamExtensionBase::ExtRamRead8(u16 loc) const { + return RamBankXRead8(loc, ramBankNum_); +} + +bool RamExtensionBase::ExtSaveRam(std::ostream& os) { + return util::WriteBinaryStream(os, ramData_); +} + +bool RamExtensionBase::ExtLoadRam(std::istream& is) { + return util::ReadBinaryStream(is, ramData_, false); +} + +void MbcBase::ExtReset() { + romBankNum_ = cart_->GetNumRomBanks() > 1 ? 1 : 0; + RamExtensionBase::ExtReset(); +} + +u8 MbcBase::RomBankXRead8(u16 loc, u16 bankNum) const { + const std::size_t dataIndex = (kRomBankSize * bankNum + loc) + % cart_->GetRomData().size(); + return cart_->GetRomData()[dataIndex]; +} + +u8 MbcBase::ExtRomBank0Read8(u16 loc) const { + return RomBankXRead8(loc, 0); +} + +u8 MbcBase::ExtRomBankXRead8(u16 loc) const { + return RomBankXRead8(loc, romBankNum_); +} diff --git a/src/hw/cart/cart_ext_mbc1.cpp b/src/hw/cart/cart_ext_mbc1.cpp new file mode 100644 index 0000000..0b472d7 --- /dev/null +++ b/src/hw/cart/cart_ext_mbc1.cpp @@ -0,0 +1,79 @@ +#include "hw/cart/cart_ext_mbc1.h" +#include "hw/cart/cart.h" + +bool Mbc1::ExtInit() { + if (cart_->GetNumRomBanks() <= 1 || cart_->GetNumRomBanks() > 0x80 || + cart_->GetNum2KBExtRamBanks() > 16) { + return false; + } + + return MbcBase::ExtInit(); +} + +void Mbc1::ExtReset() { + ramBankingMode_ = false; + MbcBase::ExtReset(); +} + +void Mbc1::ExtRomBank0Write8(u16 loc, u8 val) { + if (loc < 0x2000) { + // ext RAM read/write enable switch + if ((val & 0xf) == 0xa) { + ramEnabled_ = true; + } else if (val == 0) { + ramEnabled_ = false; + } + } else { + // ROM bank bits 0-4 select + romBankNum_ = (romBankNum_ & 0x60) | (val & 0x1f); + + // if bits 0-4 are now 0, make it 1 instead. although this stops bank 0 + // from being selected, it also stops banks $20, $40 and $60 from being + // selected (MBC1 design flaw!) + if ((romBankNum_ & 0x1f) == 0) { + ++romBankNum_; + } + } +} + +void Mbc1::ExtRomBankXWrite8(u16 loc, u8 val) { + if (loc < 0x2000) { + // ROM bank number bits 5-6 select OR RAM bank number select. the number + // that is affected depends on the current ROM/RAM banking mode + if (ramBankingMode_) { + ramBankNum_ = val & 3; + } else { + romBankNum_ = ((val & 3) << 5) | (romBankNum_ & 0x1f); + } + } else { + // ROM/RAM banking mode select + ramBankingMode_ = (val & 1) != 0; + } +} + +u8 Mbc1::ExtRomBankXRead8(u16 loc) const { + if (ramBankingMode_) { + // can only access ROM banks 00-1F in RAM banking mode + return RomBankXRead8(loc, romBankNum_ % 0x20); + } else { + return MbcBase::ExtRomBankXRead8(loc); + } +} + +void Mbc1::ExtRamWrite8(u16 loc, u8 val) { + if (ramBankingMode_) { + RamExtensionBase::ExtRamWrite8(loc, val); + } else { + // can only access RAM bank 0 in ROM banking mode + RamBankXWrite8(loc, val, 0); + } +} + +u8 Mbc1::ExtRamRead8(u16 loc) const { + if (ramBankingMode_) { + return RamExtensionBase::ExtRamRead8(loc); + } else { + // can only access RAM bank 0 in ROM banking mode + return RamBankXRead8(loc, 0); + } +} diff --git a/src/hw/cart/cart_ext_mbc2.cpp b/src/hw/cart/cart_ext_mbc2.cpp new file mode 100644 index 0000000..53a62ac --- /dev/null +++ b/src/hw/cart/cart_ext_mbc2.cpp @@ -0,0 +1,27 @@ +#include "hw/cart/cart_ext_mbc2.h" +#include "hw/cart/cart.h" +#include "util.h" + +bool Mbc2::ExtInit() { + if (cart_->GetNumRomBanks() < 1 || cart_->GetNumRomBanks() > 16 || + cart_->GetNum2KBExtRamBanks() != 0) { + return false; + } + + // MBC2 contains 512x4 bits of RAM. + // to simplify the implementation, we'll represent this as 512 bytes of RAM, + // while only writing to the lower 4 bits when ExtRamWrite() is called + ramData_.resize(0x200); + ramData_.clear(); + return true; +} + +void Mbc2::ExtRomBank0Write8(u16 loc, u8 val) { + if (loc < 0x2000) { + if (!(util::GetHi8(loc) & 1)) { + ramEnabled_ = !ramEnabled_; + } + } else { + romBankNum_ = std::max(val & 0xf, 1); + } +} diff --git a/src/hw/cart/cart_ext_mbc3.cpp b/src/hw/cart/cart_ext_mbc3.cpp new file mode 100644 index 0000000..3c007ee --- /dev/null +++ b/src/hw/cart/cart_ext_mbc3.cpp @@ -0,0 +1,119 @@ +#include "hw/cart/cart_ext_mbc3.h" +#include "hw/cart/cart.h" +#include "util.h" +#include + +Mbc3::Mbc3(bool timerEnabled) : timerEnabled_(timerEnabled) {} + +bool Mbc3::ExtInit() { + if (cart_->GetNumRomBanks() < 1 || cart_->GetNumRomBanks() > 128 || + cart_->GetNum2KBExtRamBanks() > 16) { + return false; + } + + // TODO remember to edit code in Cartridge::ParseExtensions() if timer impl + return MbcBase::ExtInit(); +} + +void Mbc3::ExtReset() { + selectedRtc_ = RtcRegister::None; + rtc_.d = rtc_.s = rtc_.m = rtc_.h = 0; // TODO + + prevLatchVal_ = 0xff; + isRtcLatched_ = false; + + MbcBase::ExtReset(); +} + +void Mbc3::ExtRomBank0Write8(u16 loc, u8 val) { + if (loc < 0x2000) { + // RAM & RTC enable + if ((val & 0xf) == 0xa) { + ramEnabled_ = true; + } else if (val == 0) { + ramEnabled_ = false; + } + } else { + // ROM 7-bit bank number select + romBankNum_ = std::max(val & 0x7f, 1); + } +} + +void Mbc3::ExtRomBankXWrite8(u16 loc, u8 val) { + if (loc < 0x2000) { + if (ramEnabled_) { + if (val >= 0x8 && val <= 0xc) { + // RTC select + selectedRtc_ = static_cast(val); + } else { + // RAM bank number select + selectedRtc_ = RtcRegister::None; + ramBankNum_ = val & 3; + } + } + } else { + if (prevLatchVal_ == 0 && val == 1) { + // toggle RTC latch + latchedRtc_ = rtc_; + isRtcLatched_ = !isRtcLatched_; + } + prevLatchVal_ = val; + } +} + +u8 Mbc3::ReadRtcRegister(RtcRegister rtc) const { + const auto& activeRtc = isRtcLatched_ ? latchedRtc_ : rtc_; + switch (rtc) { + case RtcRegister::S: + return activeRtc.s; + case RtcRegister::M: + return activeRtc.m; + case RtcRegister::H: + return activeRtc.h; + case RtcRegister::Dl: + return util::GetLo8(activeRtc.d); + case RtcRegister::Dh: + return util::GetHi8(activeRtc.d); + + default: assert(!"attempt to read from unmapped MBC3 RTC register!"); + return 0xff; + } +} + +void Mbc3::WriteRtcRegister(RtcRegister rtc, u8 val) { + auto& activeRtc = isRtcLatched_ ? latchedRtc_ : rtc_; + switch (rtc) { + case RtcRegister::S: + activeRtc.s = val % 60; + break; + case RtcRegister::M: + activeRtc.m = val % 60; + break; + case RtcRegister::H: + activeRtc.h = val % 24; + break; + case RtcRegister::Dl: + activeRtc.d = util::SetLo8(activeRtc.d, val); + break; + case RtcRegister::Dh: + activeRtc.d = util::SetHi8(activeRtc.d, (val & 0xc1) | 0x3e); + break; + default: assert(!"attempt to write to unmapped MBC3 RTC register!"); + } +} + +u8 Mbc3::ExtRamRead8(u16 loc) const { + if (selectedRtc_ == RtcRegister::None) { + return RamExtensionBase::ExtRamRead8(loc); + } else { + return ReadRtcRegister(selectedRtc_); // RTC read + } +} + +void Mbc3::ExtRamWrite8(u16 loc, u8 val) { + if (selectedRtc_ == RtcRegister::None) { + RamExtensionBase::ExtRamWrite8(loc, val); + } else { + WriteRtcRegister(selectedRtc_, val); // RTC write + } +} diff --git a/src/hw/cart/cart_ext_mbc5.cpp b/src/hw/cart/cart_ext_mbc5.cpp new file mode 100644 index 0000000..4da780e --- /dev/null +++ b/src/hw/cart/cart_ext_mbc5.cpp @@ -0,0 +1,36 @@ +#include "hw/cart/cart_ext_mbc5.h" +#include "hw/cart/cart.h" +#include "util.h" + +bool Mbc5::ExtInit() { + if (cart_->GetNumRomBanks() < 1 || cart_->GetNumRomBanks() > 0x200 || + cart_->GetNum2KBExtRamBanks() > 64) { + return false; + } + + return MbcBase::ExtInit(); +} + +void Mbc5::ExtRomBank0Write8(u16 loc, u8 val) { + if (loc < 0x2000) { + // ext RAM read/write enable switch + if ((val & 0xf) == 0xa) { + ramEnabled_ = true; + } else if (val == 0) { + ramEnabled_ = false; + } + } else if (loc < 0x3000) { + // lower 8 bits of the 9-bit ROM bank number + romBankNum_ = util::SetLo8(romBankNum_, val); + } else { + // high bit of the 9-bit ROM bank number + romBankNum_ = util::SetHi8(romBankNum_, val & 1); + } +} + +void Mbc5::ExtRomBankXWrite8(u16 loc, u8 val) { + if (loc < 0x2000) { + // 4-bit RAM bank number + ramBankNum_ = val & 0xf; + } +} diff --git a/src/hw/cpu/cpu.cpp b/src/hw/cpu/cpu.cpp new file mode 100644 index 0000000..306836e --- /dev/null +++ b/src/hw/cpu/cpu.cpp @@ -0,0 +1,195 @@ +#include "hw/cpu/cpu.h" +#include "hw/dma.h" +#include "hw/joypad.h" +#include "hw/mmu.h" +#include + +Cpu::Cpu(Mmu& mmu, const Dma& dma, const Joypad& joypad) + : status_(CpuStatus::Hung), mmu_(mmu), dma_(dma), joypad_(joypad) {} + +void Cpu::Reset(bool cgbMode) { + cgbMode_ = cgbMode; + + // set initial register values; this is usually done by the boot ROM. + // in DMG mode, set the accumulator to the value set by the DMG boot ROM. + // this allows games to auto-detect DMG mode when we force it on + reg_.pc.Set(0x0100); + reg_.sp.Set(0xfffe); + reg_.af.Set(cgbMode_ ? 0x1180 : 0x0180); + reg_.bc.Set(0x0000); + reg_.de.Set(0x0008); + reg_.hl.Set(0x007c); + + // disable interrupts and reset int flags to reset vals + intme_ = false; + intf_ = 0xe1; + inte_ = 0x00; + + // reset cpu state and ready it for running + status_ = CpuStatus::Running; + speedSwitchRequested_ = doubleSpeedMode_ = false; + speedSwitchCyclesLeft_ = 0; +} + +bool Cpu::Resume() { + if (status_ != CpuStatus::Hung && speedSwitchCyclesLeft_ <= 0) { + status_ = CpuStatus::Running; + return true; + } + + return false; +} + +unsigned int Cpu::Update() { + updateCycles_ = 0; + InternalDelay(); // spin for 4 clock cycles by default + + if (status_ != CpuStatus::Hung && !dma_.IsNdmaInProgress()) { + if (status_ == CpuStatus::Stopped) { + HandleStoppedUpdate(); + } else if (!HandleInterrupts() && + status_ == CpuStatus::Running && + !ExecuteOp(mmu_.Read8(reg_.pc++))) { + // unknown opcode executed - hang + status_ = CpuStatus::Hung; + } + } + + return updateCycles_; +} + +void Cpu::HandleStoppedUpdate() { + if (joypad_.WasSelectedKeyPressed()) { + // a selected key press during the speed switch hangs the CPU, + // otherwise, wake up + status_ = IsSpeedSwitchInProgress() ? CpuStatus::Hung + : CpuStatus::Running; + } else if (IsSpeedSwitchInProgress()) { + if (updateCycles_ >= speedSwitchCyclesLeft_) { + // double speed switch finished, wake up + speedSwitchRequested_ = false; + speedSwitchCyclesLeft_ = 0; + + doubleSpeedMode_ = !doubleSpeedMode_; + status_ = CpuStatus::Running; + } else { + speedSwitchCyclesLeft_ -= updateCycles_; + } + } +} + +bool Cpu::IsSpeedSwitchInProgress() const { + return speedSwitchCyclesLeft_ > 0; +} + +bool Cpu::HandleInterrupts() { + // have an int to service if both the enable and flag bits for an int are set + // and the master interrupt enable is on + if (intf_ & inte_) { + if (intme_) { + // service one int prioritised from vec $40 to $60, then clear the flag bit + if (inte_ & intf_ & kCpuInterrupt0x40) { + intf_ &= ~kCpuInterrupt0x40; + ServiceInterrupt<0x40>(); + } else if (inte_ & intf_ & kCpuInterrupt0x48) { + intf_ &= ~kCpuInterrupt0x48; + ServiceInterrupt<0x48>(); + } else if (inte_ & intf_ & kCpuInterrupt0x50) { + intf_ &= ~kCpuInterrupt0x50; + ServiceInterrupt<0x50>(); + } else if (inte_ & intf_ & kCpuInterrupt0x58) { + intf_ &= ~kCpuInterrupt0x58; + ServiceInterrupt<0x58>(); + } else if (inte_ & intf_ & kCpuInterrupt0x60) { + intf_ &= ~kCpuInterrupt0x60; + ServiceInterrupt<0x60>(); + } + } + + // resume from suspension, even if a requested interrupt couldn't be + // serviced due to intme being off + status_ = CpuStatus::Running; + return intme_; + } + + return false; +} + +void Cpu::InternalDelay(unsigned int numAccesses) { + updateCycles_ += 4 * numAccesses; // each access takes 4 clock cycles +} + +u8 Cpu::IoRead8(u16 loc) { + InternalDelay(); + return mmu_.Read8(loc); +} + +void Cpu::IoWrite8(u16 loc, u8 val) { + InternalDelay(); + mmu_.Write8(loc, val); +} + +u8 Cpu::IoPcReadNext8() { + return IoRead8(reg_.pc++); +} + +u16 Cpu::IoPcReadNext16() { + const u8 lo = IoPcReadNext8(); + const u8 hi = IoPcReadNext8(); + return util::To16(hi, lo); +} + +const CpuRegisters& Cpu::GetRegisters() const { + return reg_; +} + +CpuStatus Cpu::GetStatus() const { + return status_; +} + +bool Cpu::GetIntme() const { + return intme_; +} + +void Cpu::SetInte(u8 val) { + inte_ = val & 0x1f; +} + +u8 Cpu::GetInte() const { + return inte_; +} + +void Cpu::SetIntf(u8 val) { + intf_ = val & 0x1f; +} + +void Cpu::IntfRequest(u8 mask) { + intf_ |= mask & 0x1f; +} + +u8 Cpu::GetIntf() const { + return intf_; +} + +void Cpu::SetKey1(u8 val) { + if (cgbMode_) { + speedSwitchRequested_ = (val & 1) != 0; + } +} + +u8 Cpu::GetKey1() const { + if (cgbMode_) { + return 0x7e | (doubleSpeedMode_ ? 0x80 : 0) + | (speedSwitchRequested_ ? 1 : 0); + } else { + return 0xff; + } +} + +bool Cpu::IsInCgbMode() const { + return cgbMode_; +} + +bool Cpu::IsInDoubleSpeedMode() const { + return doubleSpeedMode_; +} diff --git a/src/hw/cpu/cpu_ops.cpp b/src/hw/cpu/cpu_ops.cpp new file mode 100644 index 0000000..ee1392b --- /dev/null +++ b/src/hw/cpu/cpu_ops.cpp @@ -0,0 +1,441 @@ +#include "hw/cpu/cpu.h" +#include "hw/memory.h" +#include "util.h" + +constexpr auto kSpeedSwitchTotalCycles = 130992u; + +void Cpu::ExecOpStop0x10() { + if (speedSwitchRequested_) { + speedSwitchCyclesLeft_ = kSpeedSwitchTotalCycles; + } + + status_ = CpuStatus::Stopped; +} + +void Cpu::ExecOpHalt0x76() { + status_ = CpuStatus::Halted; +} + +void Cpu::ExecOpDi0xf3() { + intme_ = false; +} + +void Cpu::ExecOpEi0xfb() { + intme_ = true; +} + +void Cpu::ExecLoad(u16 destLoc, u8 val) { + IoWrite8(destLoc, val); +} + +void Cpu::ExecLoad(u16 destLoc, u16 val) { + IoWrite8(destLoc, util::GetLo8(val)); + IoWrite8(destLoc + 1, util::GetHi8(val)); +} + +void Cpu::ExecOpLdd0x3a() { + ExecLoad(reg_.a, IoRead8(reg_.hl--)); +} + +void Cpu::ExecOpLdd0x32() { + ExecLoad(reg_.hl--, reg_.a.Get()); +} + +void Cpu::ExecOpLdi0x2a() { + ExecLoad(reg_.a, IoRead8(reg_.hl++)); +} + +void Cpu::ExecOpLdi0x22() { + ExecLoad(reg_.hl++, reg_.a.Get()); +} + +void Cpu::ExecOpLdhl0xf8() { + const u8 val = IoPcReadNext8(); + + InternalDelay(); + reg_.f.SetZFlag(false); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag((reg_.sp.Get() & 0xff) + val > 0xff); + reg_.f.SetHFlag((reg_.sp.Get() & 0xf) + (val & 0xf) > 0xf); + reg_.hl.Set(reg_.sp.Get() + static_cast(val)); +} + +void Cpu::ExecOpLd0xf9() { + InternalDelay(); + ExecLoad(reg_.sp, reg_.hl.Get()); +} + +void Cpu::ExecPush(u16 val) { + InternalDelay(); + IoWrite8((--reg_.sp).Get(), util::GetHi8(val)); + IoWrite8((--reg_.sp).Get(), util::GetLo8(val)); +} + +u16 Cpu::ExecPop() { + const u8 lo = IoRead8(reg_.sp++); + const u8 hi = IoRead8(reg_.sp++); + return util::To16(hi, lo); +} + +void Cpu::ExecJump(u16 targetLoc, bool shouldJump) { + if (shouldJump) { + InternalDelay(); + reg_.pc.Set(targetLoc); + } +} + +void Cpu::ExecCall(u16 targetLoc, bool shouldCall) { + if (shouldCall) { + ExecPush(reg_.pc.Get()); // push pc (points to next op) + reg_.pc.Set(targetLoc); + } +} + +void Cpu::ExecReturn() { + InternalDelay(); + reg_.pc.Set(ExecPop()); +} + +void Cpu::ExecReturn(bool shouldReturn) { + InternalDelay(); + if (shouldReturn) { + ExecReturn(); + } +} + +void Cpu::ExecOpJp0xe9() { + reg_.pc.Set(reg_.hl.Get()); +} + +void Cpu::ExecOpJr0x18() { + const i8 val = static_cast(IoPcReadNext8()); + ExecJump(reg_.pc.Get() + val); +} + +void Cpu::ExecOpJr0x20() { + const i8 val = static_cast(IoPcReadNext8()); + ExecJump(reg_.pc.Get() + val, !reg_.f.GetZFlag()); +} + +void Cpu::ExecOpJr0x28() { + const i8 val = static_cast(IoPcReadNext8()); + ExecJump(reg_.pc.Get() + val, reg_.f.GetZFlag()); +} + +void Cpu::ExecOpJr0x30() { + const i8 val = static_cast(IoPcReadNext8()); + ExecJump(reg_.pc.Get() + val, !reg_.f.GetCFlag()); +} + +void Cpu::ExecOpJr0x38() { + const i8 val = static_cast(IoPcReadNext8()); + ExecJump(reg_.pc.Get() + val, reg_.f.GetCFlag()); +} + +void Cpu::ExecOpReti0xd9() { + ExecReturn(); + intme_ = true; +} + +void Cpu::ExecAdd(u8 val) { + const uint_fast16_t result = reg_.a.Get() + val; + + reg_.f.SetZFlag((result & 0xff) == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(result > 0xff); + reg_.f.SetHFlag((reg_.a.Get() & 0xf) + (val & 0xf) > 0xf); + reg_.a.Set(result & 0xff); +} + +void Cpu::ExecAddWithCarry(u8 val) { + const uint_fast8_t carryAdd = reg_.f.GetCFlag() ? 1 : 0; + const uint_fast16_t result = reg_.a.Get() + val + carryAdd; + + reg_.f.SetZFlag((result & 0xff) == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(result > 0xff); + reg_.f.SetHFlag((reg_.a.Get() & 0xf) + (val & 0xf) + carryAdd > 0xf); + reg_.a.Set(result & 0xff); +} + +void Cpu::ExecSub(u8 val) { + const uint_fast16_t result = reg_.a.Get() - val; + + reg_.f.SetZFlag((result & 0xff) == 0); + reg_.f.SetNFlag(true); + reg_.f.SetCFlag(result > 0xff); + reg_.f.SetHFlag((reg_.a.Get() & 0xf) < (val & 0xf)); + reg_.a.Set(result & 0xff); +} + +void Cpu::ExecSubWithCarry(u8 val) { + const uint_fast8_t carrySub = reg_.f.GetCFlag() ? 1 : 0; + const uint_fast16_t result = reg_.a.Get() - val - carrySub; + + reg_.f.SetZFlag((result & 0xff) == 0); + reg_.f.SetNFlag(true); + reg_.f.SetCFlag(result > 0xff); + reg_.f.SetHFlag((reg_.a.Get() & 0xf) < (val & 0xf) + carrySub); + reg_.a.Set(result & 0xff); +} + +void Cpu::ExecCompare(u8 val) { + const uint_fast16_t result = reg_.a.Get() - val; + + reg_.f.SetZFlag((result & 0xff) == 0); + reg_.f.SetNFlag(true); + reg_.f.SetCFlag(result > 0xff); + reg_.f.SetHFlag((reg_.a.Get() & 0xf) < (val & 0xf)); +} + +void Cpu::ExecAnd(u8 val) { + const u8 result = reg_.a.Get() & val; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(false); + reg_.f.SetHFlag(true); + reg_.a.Set(result); +} + +void Cpu::ExecOr(u8 val) { + const u8 result = reg_.a.Get() | val; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(false); + reg_.f.SetHFlag(false); + reg_.a.Set(result); +} + +void Cpu::ExecXor(u8 val) { + const u8 result = reg_.a.Get() ^ val; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(false); + reg_.f.SetHFlag(false); + reg_.a.Set(result); +} + +u8 Cpu::ExecInc(u8 val) { + const u8 result = val + 1; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetHFlag((val & 0xf) + 1 > 0xf); + return result; +} + +void Cpu::ExecInc(u16 destLoc) { + IoWrite8(destLoc, ExecInc(IoRead8(destLoc))); +} + +u8 Cpu::ExecDec(u8 val) { + const u8 result = val - 1; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(true); + reg_.f.SetHFlag((val & 0xf) == 0); + return result; +} + +void Cpu::ExecDec(u16 destLoc) { + IoWrite8(destLoc, ExecDec(IoRead8(destLoc))); +} + +void Cpu::ExecOpAdd0xe8() { + const u8 val = IoPcReadNext8(); + + InternalDelay(2); + reg_.f.SetZFlag(false); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag((reg_.sp.Get() & 0xff) + val > 0xff); + reg_.f.SetHFlag((reg_.sp.Get() & 0xf) + (val & 0xf) > 0xf); + reg_.sp.Set(reg_.sp.Get() + static_cast(val)); +} + +void Cpu::ExecOpDaa0x27() { + // adjust A (and flag C) to allow for BCD addition and subtraction + // NOTE: based on AWJ's very nice implementation of DAA @ + // https://forums.nesdev.com/viewtopic.php?t=15944#p196282 + u8 result = reg_.a.Get(); + + if (reg_.f.GetNFlag()) { + if (reg_.f.GetCFlag()) { + result -= 0x60; + } + + if (reg_.f.GetHFlag()) { + result -= 6; + } + } else { + if (reg_.f.GetCFlag() || result > 0x99) { + result += 0x60; + reg_.f.SetCFlag(true); + } + + if (reg_.f.GetHFlag() || (result & 0xf) > 9) { + result += 6; + } + } + + reg_.f.SetZFlag(result == 0); + reg_.f.SetHFlag(false); + reg_.a.Set(result); +} + +void Cpu::ExecOpCpl0x2f() { + reg_.f.SetNFlag(true); + reg_.f.SetHFlag(true); + reg_.a.Set(~reg_.a.Get()); +} + +void Cpu::ExecOpCcf0x3f() { + reg_.f.SetNFlag(false); + reg_.f.SetHFlag(false); + reg_.f.SetCFlag(!reg_.f.GetCFlag()); +} + +void Cpu::ExecOpScf0x37() { + reg_.f.SetNFlag(false); + reg_.f.SetHFlag(false); + reg_.f.SetCFlag(true); +} + +void Cpu::ExecOpCb0xcb() { + ExecuteExOp(IoPcReadNext8()); +} + +u8 Cpu::ExecRotLeft(u8 val) { + const u8 result = val << 1 | (val & 0x80) >> 7; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag((val & 0x80) != 0); + reg_.f.SetHFlag(false); + return result; +} + +void Cpu::ExecRotLeft(u16 destLoc) { + IoWrite8(destLoc, ExecRotLeft(IoRead8(destLoc))); +} + +u8 Cpu::ExecRotLeftThroughCarry(u8 val) { + const u8 result = val << 1 | (reg_.f.GetCFlag() ? 1 : 0); + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag((val & 0x80) != 0); + reg_.f.SetHFlag(false); + return result; +} + +void Cpu::ExecRotLeftThroughCarry(u16 destLoc) { + IoWrite8(destLoc, ExecRotLeftThroughCarry(IoRead8(destLoc))); +} + +u8 Cpu::ExecRotRight(u8 val) { + const u8 result = val >> 1 | (val & 1) << 7; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(val & 1); + reg_.f.SetHFlag(false); + return result; +} + +void Cpu::ExecRotRight(u16 destLoc) { + IoWrite8(destLoc, ExecRotRight(IoRead8(destLoc))); +} + +u8 Cpu::ExecRotRightThroughCarry(u8 val) { + const u8 result = val >> 1 | (reg_.f.GetCFlag() ? 0x80 : 0); + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(val & 1); + reg_.f.SetHFlag(false); + return result; +} + +void Cpu::ExecRotRightThroughCarry(u16 destLoc) { + IoWrite8(destLoc, ExecRotRightThroughCarry(IoRead8(destLoc))); +} + +void Cpu::ExecOpRlca0x07() { + ExecRotLeft(reg_.a); + reg_.f.SetZFlag(false); +} + +void Cpu::ExecOpRla0x17() { + ExecRotLeftThroughCarry(reg_.a); + reg_.f.SetZFlag(false); +} + +void Cpu::ExecOpRrca0x0f() { + ExecRotRight(reg_.a); + reg_.f.SetZFlag(false); +} + +void Cpu::ExecOpRra0x1f() { + ExecRotRightThroughCarry(reg_.a); + reg_.f.SetZFlag(false); +} + +u8 Cpu::ExecShiftLeft(u8 val) { + const u8 result = val << 1; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag((val & 0x80) != 0); + reg_.f.SetHFlag(false); + return result; +} + +void Cpu::ExecShiftLeft(u16 destLoc) { + IoWrite8(destLoc, ExecShiftLeft(IoRead8(destLoc))); +} + +u8 Cpu::ExecShiftRight(u8 val) { + const u8 result = val >> 1; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(val & 1); + reg_.f.SetHFlag(false); + return result; +} + +void Cpu::ExecShiftRight(u16 destLoc) { + IoWrite8(destLoc, ExecShiftRight(IoRead8(destLoc))); +} + +u8 Cpu::ExecShiftRightSigned(u8 val) { + // we preserve the msb; essentially does a sign extend + const u8 result = val >> 1 | (val & 0x80); + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(val & 1); + reg_.f.SetHFlag(false); + return result; +} + +void Cpu::ExecShiftRightSigned(u16 destLoc) { + IoWrite8(destLoc, ExecShiftRightSigned(IoRead8(destLoc))); +} + +u8 Cpu::ExecSwap(u8 val) { + const u8 result = (val & 0x0f) << 4 | (val & 0xf0) >> 4; + + reg_.f.SetZFlag(result == 0); + reg_.f.SetNFlag(false); + reg_.f.SetCFlag(false); + reg_.f.SetHFlag(false); + return result; +} + +void Cpu::ExecSwap(u16 destLoc) { + IoWrite8(destLoc, ExecSwap(IoRead8(destLoc))); +} diff --git a/src/hw/cpu/cpu_ops_decode.cpp b/src/hw/cpu/cpu_ops_decode.cpp new file mode 100644 index 0000000..a3ea2ac --- /dev/null +++ b/src/hw/cpu/cpu_ops_decode.cpp @@ -0,0 +1,284 @@ +#include "hw/cpu/cpu.h" +#include "util.h" +#include + +#define NOP(opcode) case opcode: break +#define OP(opcode, expr) case opcode: expr; break + +// defines a standard group of 8 opcodes that read from register arguments +#define OP_REG_ARG_READ_GROUP(startOpcode, funcName) \ + OP(startOpcode , funcName(reg_.b.Get())); \ + OP(startOpcode + 1, funcName(reg_.c.Get())); \ + OP(startOpcode + 2, funcName(reg_.d.Get())); \ + OP(startOpcode + 3, funcName(reg_.e.Get())); \ + OP(startOpcode + 4, funcName(reg_.h.Get())); \ + OP(startOpcode + 5, funcName(reg_.l.Get())); \ + OP(startOpcode + 6, funcName(IoRead8(reg_.hl.Get()))); \ + OP(startOpcode + 7, funcName(reg_.a.Get())) + +// variadic version of OP_REG_ARG_READ_GROUP() +#define OP_REG_ARG_READ_GROUP_VAR(startOpcode, funcName, ...) \ + OP(startOpcode , funcName(__VA_ARGS__, reg_.b.Get())); \ + OP(startOpcode + 1, funcName(__VA_ARGS__, reg_.c.Get())); \ + OP(startOpcode + 2, funcName(__VA_ARGS__, reg_.d.Get())); \ + OP(startOpcode + 3, funcName(__VA_ARGS__, reg_.e.Get())); \ + OP(startOpcode + 4, funcName(__VA_ARGS__, reg_.h.Get())); \ + OP(startOpcode + 5, funcName(__VA_ARGS__, reg_.l.Get())); \ + OP(startOpcode + 6, funcName(__VA_ARGS__, IoRead8(reg_.hl.Get()))); \ + OP(startOpcode + 7, funcName(__VA_ARGS__, reg_.a.Get())) + +// defines a standard group of 8 opcodes that write to register arguments +#define OP_REG_ARG_WRITE_GROUP(startOpcode, funcName) \ + OP(startOpcode , funcName(reg_.b)); \ + OP(startOpcode + 1, funcName(reg_.c)); \ + OP(startOpcode + 2, funcName(reg_.d)); \ + OP(startOpcode + 3, funcName(reg_.e)); \ + OP(startOpcode + 4, funcName(reg_.h)); \ + OP(startOpcode + 5, funcName(reg_.l)); \ + OP(startOpcode + 6, funcName(reg_.hl.Get())); \ + OP(startOpcode + 7, funcName(reg_.a)) + +bool Cpu::ExecuteOp(u8 op) { + // standard instructions table + switch (op) { + NOP(0x00); // NOP + OP(0x01, ExecLoad(reg_.bc, IoPcReadNext16())); // LD BC,nn + OP(0x02, ExecLoad(reg_.bc.Get(), reg_.a.Get())); // LD (BC),A + OP(0x03, ExecInc16(reg_.bc)); // INC BC + OP(0x04, ExecInc(reg_.b)); // INC B + OP(0x05, ExecDec(reg_.b)); // DEC B + OP(0x06, ExecLoad(reg_.b, IoPcReadNext8())); // LD B,n + OP(0x07, ExecOpRlca0x07()); // RLCA + OP(0x08, ExecLoad(IoPcReadNext16(), reg_.sp.Get())); // LD (nn),SP + OP(0x09, ExecAdd16(reg_.hl, reg_.bc.Get())); // ADD HL,BC + OP(0x0a, ExecLoad(reg_.a, IoRead8(reg_.bc.Get()))); // LD A,(BC) + OP(0x0b, ExecDec16(reg_.bc)); // DEC BC + OP(0x0c, ExecInc(reg_.c)); // INC C + OP(0x0d, ExecDec(reg_.c)); // DEC C + OP(0x0e, ExecLoad(reg_.c, IoPcReadNext8())); // LD C,n + OP(0x0f, ExecOpRrca0x0f()); // RRCA + OP(0x10, ExecOpStop0x10()); // STOP 0 + OP(0x11, ExecLoad(reg_.de, IoPcReadNext16())); // LD DE,nn + OP(0x12, ExecLoad(reg_.de.Get(), reg_.a.Get())); // LD (DE),A + OP(0x13, ExecInc16(reg_.de)); // INC DE + OP(0x14, ExecInc(reg_.d)); // INC D + OP(0x15, ExecDec(reg_.d)); // DEC D + OP(0x16, ExecLoad(reg_.d, IoPcReadNext8())); // LD D,n + OP(0x17, ExecOpRla0x17()); // RLA + OP(0x18, ExecOpJr0x18()); // JR n + OP(0x19, ExecAdd16(reg_.hl, reg_.de.Get())); // ADD HL,DE + OP(0x1a, ExecLoad(reg_.a, IoRead8(reg_.de.Get()))); // LD A,(DE) + OP(0x1b, ExecDec16(reg_.de)); // DEC DE + OP(0x1c, ExecInc(reg_.e)); // INC E + OP(0x1d, ExecDec(reg_.e)); // DEC E + OP(0x1e, ExecLoad(reg_.e, IoPcReadNext8())); // LD E,n + OP(0x1f, ExecOpRra0x1f()); // RRA + OP(0x20, ExecOpJr0x20()); // JR NZ,n + OP(0x21, ExecLoad(reg_.hl, IoPcReadNext16())); // LD HL,nn + OP(0x22, ExecOpLdi0x22()); // LDI (HL),A + OP(0x23, ExecInc16(reg_.hl)); // INC HL + OP(0x24, ExecInc(reg_.h)); // INC H + OP(0x25, ExecDec(reg_.h)); // DEC H + OP(0x26, ExecLoad(reg_.h, IoPcReadNext8())); // LD H,n + OP(0x27, ExecOpDaa0x27()); // DAA + OP(0x28, ExecOpJr0x28()); // JR Z,n + OP(0x29, ExecAdd16(reg_.hl, reg_.hl.Get())); // ADD HL,HL + OP(0x2a, ExecOpLdi0x2a()); // LDI A,(HL) + OP(0x2b, ExecDec16(reg_.hl)); // DEC HL + OP(0x2c, ExecInc(reg_.l)); // INC L + OP(0x2d, ExecDec(reg_.l)); // DEC L + OP(0x2e, ExecLoad(reg_.l, IoPcReadNext8())); // LD L,n + OP(0x2f, ExecOpCpl0x2f()); // CPL + OP(0x30, ExecOpJr0x30()); // JR NC,n + OP(0x31, ExecLoad(reg_.sp, IoPcReadNext16())); // LD SP,nn + OP(0x32, ExecOpLdd0x32()); // LDD (HL),A + OP(0x33, ExecInc16(reg_.sp)); // INC SP + OP(0x34, ExecInc(reg_.hl.Get())); // INC (HL) + OP(0x35, ExecDec(reg_.hl.Get())); // DEC (HL) + OP(0x36, ExecLoad(reg_.hl.Get(), IoPcReadNext8())); // LD (HL),n + OP(0x37, ExecOpScf0x37()); // SCF + OP(0x38, ExecOpJr0x38()); // JR C,n + OP(0x39, ExecAdd16(reg_.hl, reg_.sp.Get())); // ADD HL,SP + OP(0x3a, ExecOpLdd0x3a()); // LDD A,(HL) + OP(0x3b, ExecDec16(reg_.sp)); // DEC SP + OP(0x3c, ExecInc(reg_.a)); // INC A + OP(0x3d, ExecDec(reg_.a)); // DEC A + OP(0x3e, ExecLoad(reg_.a, IoPcReadNext8())); // LD A,n + OP(0x3f, ExecOpCcf0x3f()); // CCF + + // LD B,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP_VAR(0x40, ExecLoad, reg_.b); + // LD C,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP_VAR(0x48, ExecLoad, reg_.c); + // LD D,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP_VAR(0x50, ExecLoad, reg_.d); + // LD E,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP_VAR(0x58, ExecLoad, reg_.e); + // LD H,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP_VAR(0x60, ExecLoad, reg_.h); + // LD L,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP_VAR(0x68, ExecLoad, reg_.l); + // LD (HL),v where v = B,C,D,E,H,L + OP(0x70, ExecLoad(reg_.hl.Get(), reg_.b.Get())); + OP(0x71, ExecLoad(reg_.hl.Get(), reg_.c.Get())); + OP(0x72, ExecLoad(reg_.hl.Get(), reg_.d.Get())); + OP(0x73, ExecLoad(reg_.hl.Get(), reg_.e.Get())); + OP(0x74, ExecLoad(reg_.hl.Get(), reg_.h.Get())); + OP(0x75, ExecLoad(reg_.hl.Get(), reg_.l.Get())); + + OP(0x76, ExecOpHalt0x76()); // HALT + OP(0x77, ExecLoad(reg_.hl.Get(), reg_.a.Get())); // LD (HL),A + + // LD A,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP_VAR(0x78, ExecLoad, reg_.a); + // ADD A,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x80, ExecAdd); + // ADC A,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x88, ExecAddWithCarry); + // SUB A,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x90, ExecSub); + // SBC A,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x98, ExecSubWithCarry); + // AND v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0xa0, ExecAnd); + // XOR v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0xa8, ExecXor); + // OR v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0xb0, ExecOr); + // CP v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0xb8, ExecCompare); + + OP(0xc0, ExecReturn(!reg_.f.GetZFlag())); // RET NZ + OP(0xc1, ExecPop(reg_.bc)); // POP BC + OP(0xc2, ExecJump(IoPcReadNext16(), !reg_.f.GetZFlag())); // JP NZ,nn + OP(0xc3, ExecJump(IoPcReadNext16())); // JP nn + OP(0xc4, ExecCall(IoPcReadNext16(), !reg_.f.GetZFlag())); // CALL NZ,nn + OP(0xc5, ExecPush(reg_.bc.Get())); // PUSH BC + OP(0xc6, ExecAdd(IoPcReadNext8())); // ADD A,n + OP(0xc7, ExecRestart<0x00>()); // RST $00 + OP(0xc8, ExecReturn(reg_.f.GetZFlag())); // RET Z + OP(0xc9, ExecReturn()); // RET + OP(0xca, ExecJump(IoPcReadNext16(), reg_.f.GetZFlag())); // JP Z,nn + OP(0xcb, ExecOpCb0xcb()); // $cb prefix for extended instruction set + OP(0xcc, ExecCall(IoPcReadNext16(), reg_.f.GetZFlag())); // CALL Z,nn + OP(0xcd, ExecCall(IoPcReadNext16())); // CALL nn + OP(0xce, ExecAddWithCarry(IoPcReadNext8())); // ADC A,n + OP(0xcf, ExecRestart<0x08>()); // RST $08 + OP(0xd0, ExecReturn(!reg_.f.GetCFlag())); // RET NC + OP(0xd1, ExecPop(reg_.de)); // POP DE + OP(0xd2, ExecJump(IoPcReadNext16(), !reg_.f.GetCFlag())); // JP NC,nn + OP(0xd4, ExecCall(IoPcReadNext16(), !reg_.f.GetCFlag())); // CALL NC,nn + OP(0xd5, ExecPush(reg_.de.Get())); // PUSH DE + OP(0xd6, ExecSub(IoPcReadNext8())); // SUB A,n + OP(0xd7, ExecRestart<0x10>()); // RST $10 + OP(0xd8, ExecReturn(reg_.f.GetCFlag())); // RET C + OP(0xd9, ExecOpReti0xd9()); // RETI + OP(0xda, ExecJump(IoPcReadNext16(), reg_.f.GetCFlag())); // JP C,nn + OP(0xdc, ExecCall(IoPcReadNext16(), reg_.f.GetCFlag())); // CALL C,nn + OP(0xde, ExecSubWithCarry(IoPcReadNext8())); // SBC A,n + OP(0xdf, ExecRestart<0x18>()); // RST $18 + OP(0xe0, ExecLoad(0xff00 + IoPcReadNext8(), reg_.a.Get())); // LD (n),A + OP(0xe1, ExecPop(reg_.hl)); // POP HL + OP(0xe2, ExecLoad(0xff00 + reg_.c.Get(), reg_.a.Get())); // LD (C),A + OP(0xe5, ExecPush(reg_.hl.Get())); // PUSH HL + OP(0xe6, ExecAnd(IoPcReadNext8())); // AND n + OP(0xe7, ExecRestart<0x20>()); // RST $20 + OP(0xe8, ExecOpAdd0xe8()); // ADD SP,n + OP(0xe9, ExecOpJp0xe9()); // JP (HL) + OP(0xea, ExecLoad(IoPcReadNext16(), reg_.a.Get())); // LD (nn),A + OP(0xee, ExecXor(IoPcReadNext8())); // XOR n + OP(0xef, ExecRestart<0x28>()); // RST $28 + OP(0xf0, ExecLoad(reg_.a, IoRead8(0xff00 + IoPcReadNext8()))); // LD A,(n) + OP(0xf1, ExecPop(reg_.af)); // POP AF + OP(0xf2, ExecLoad(reg_.a, IoRead8(0xff00 + reg_.c.Get()))); // LD A,(C) + OP(0xf3, ExecOpDi0xf3()); // DI + OP(0xf5, ExecPush(reg_.af.Get())); // PUSH AF + OP(0xf6, ExecOr(IoPcReadNext8())); // OR n + OP(0xf7, ExecRestart<0x30>()); // RST $30 + OP(0xf8, ExecOpLdhl0xf8()); // LDHL SP,n + OP(0xf9, ExecOpLd0xf9()); // LD SP,HL + OP(0xfa, ExecLoad(reg_.a, IoRead8(IoPcReadNext16()))); // LD A,(nn) + OP(0xfb, ExecOpEi0xfb()); // EI + OP(0xfe, ExecCompare(IoPcReadNext8())); // CP n + OP(0xff, ExecRestart<0x38>()); // RST $38 + + // unknown op + default: + return false; + } + + return true; +} + +void Cpu::ExecuteExOp(u8 exOp) { + // extended instructions ($cb prefix) table + switch (exOp) { + // RLC v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x00, ExecRotLeft); + // RRC v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x08, ExecRotRight); + // RL v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x10, ExecRotLeftThroughCarry); + // RR v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x18, ExecRotRightThroughCarry); + // SLA v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x20, ExecShiftLeft); + // SRA v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x28, ExecShiftRightSigned); + // SWAP v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x30, ExecSwap); + // SRL v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x38, ExecShiftRight); + // BIT 0,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x40, ExecTestBit<0>); + // BIT 1,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x48, ExecTestBit<1>); + // BIT 2,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x50, ExecTestBit<2>); + // BIT 3,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x58, ExecTestBit<3>); + // BIT 4,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x60, ExecTestBit<4>); + // BIT 5,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x68, ExecTestBit<5>); + // BIT 6,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x70, ExecTestBit<6>); + // BIT 7,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_READ_GROUP(0x78, ExecTestBit<7>); + // RES 0,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x80, ExecResetBit<0>); + // RES 1,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x88, ExecResetBit<1>); + // RES 2,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x90, ExecResetBit<2>); + // RES 3,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0x98, ExecResetBit<3>); + // RES 4,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xa0, ExecResetBit<4>); + // RES 5,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xa8, ExecResetBit<5>); + // RES 6,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xb0, ExecResetBit<6>); + // RES 7,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xb8, ExecResetBit<7>); + // SET 0,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xc0, ExecSetBit<0>); + // SET 1,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xc8, ExecSetBit<1>); + // SET 2,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xd0, ExecSetBit<2>); + // SET 3,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xd8, ExecSetBit<3>); + // SET 4,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xe0, ExecSetBit<4>); + // SET 5,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xe8, ExecSetBit<5>); + // SET 6,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xf0, ExecSetBit<6>); + // SET 7,v where v = B,C,D,E,H,L,(HL),A + OP_REG_ARG_WRITE_GROUP(0xf8, ExecSetBit<7>); + + // unknown ex op - this shouldn't happen as all ex ops should be mapped + default: assert(!"unknown ex opcode - all ex ops should be handled!"); + } +} diff --git a/src/hw/cpu/cpu_reg.cpp b/src/hw/cpu/cpu_reg.cpp new file mode 100644 index 0000000..d960ffb --- /dev/null +++ b/src/hw/cpu/cpu_reg.cpp @@ -0,0 +1,50 @@ +#include "hw/cpu/cpu_reg.h" + +CpuFlagRegister::CpuFlagRegister() : val_(0) {} + +CpuFlagRegister::CpuFlagRegister(u8 val) { + SetImpl(val); +} + +void CpuFlagRegister::SetImpl(u8 val) { + val_ = val & 0xf0; // ensure bits 0-3 are always 0 +} + +u8 CpuFlagRegister::GetImpl() const { + return val_; +} + +void CpuFlagRegister::SetZFlag(bool val) { + Set(val ? val_ | kCpuFlagRegisterZMask : val_ & ~kCpuFlagRegisterZMask); +} + +void CpuFlagRegister::SetNFlag(bool val) { + Set(val ? val_ | kCpuFlagRegisterNMask : val_ & ~kCpuFlagRegisterNMask); +} + +void CpuFlagRegister::SetHFlag(bool val) { + Set(val ? val_ | kCpuFlagRegisterHMask : val_ & ~kCpuFlagRegisterHMask); +} + +void CpuFlagRegister::SetCFlag(bool val) { + Set(val ? val_ | kCpuFlagRegisterCMask : val_ & ~kCpuFlagRegisterCMask); +} + +bool CpuFlagRegister::GetZFlag() const { + return (val_ & kCpuFlagRegisterZMask) != 0; +} + +bool CpuFlagRegister::GetNFlag() const { + return (val_ & kCpuFlagRegisterNMask) != 0; +} + +bool CpuFlagRegister::GetHFlag() const { + return (val_ & kCpuFlagRegisterHMask) != 0; +} + +bool CpuFlagRegister::GetCFlag() const { + return (val_ & kCpuFlagRegisterCMask) != 0; +} + +CpuRegisters::CpuRegisters() : af(a, f), bc(b, c), de(d, e), hl(h, l) {} + diff --git a/src/hw/dma.cpp b/src/hw/dma.cpp new file mode 100644 index 0000000..a76d5c2 --- /dev/null +++ b/src/hw/dma.cpp @@ -0,0 +1,231 @@ +#include "hw/cpu/cpu.h" +#include "hw/dma.h" +#include "hw/mmu.h" +#include "hw/ppu.h" +#include "util.h" +#include + +constexpr u8 kNdmaBlockSize = 16; + +constexpr auto kNdmaCyclesPerBlock = 32u, + kOamDmaTotalCycles = 648u; + +Dma::Dma(const Mmu& mmu, const Cpu& cpu, Ppu& ppu) + : mmu_(mmu), cpu_(cpu), ppu_(ppu) {} + +void Dma::Reset(bool cgbMode) { + cgbMode_ = cgbMode; + + oamDmaStatus_ = OamDmaStatus::Inactive; + oamDmaSourceLocHi_ = 0x00; + + ndmaStatus_ = NdmaStatus::Inactive; + ndmaReadLoc_ = ndmaVramWriteLoc_ = 0x0000; + ndmaNumBlocksLeft_ = 0; +} + +void Dma::Update(unsigned int cycles) { + // DMAs only happen while the CPU isn't suspended + if (cpu_.GetStatus() != CpuStatus::Running) { + return; + } + + HandleGdmaUpdate(cycles); + HandleHdmaUpdate(cycles); + HandleOamDmaUpdate(cycles); +} + +void Dma::DoNdmaTransfer(u8 maxNumBlocks) { + const u8 numBlocks = std::min(maxNumBlocks, ndmaNumBlocksLeft_); + + for (u8 i = 0; i < numBlocks; ++i) { + for (u8 j = 0; j < kNdmaBlockSize; ++j) { + // don't copy past the bounds of the VRAM bank + if (ndmaVramWriteLoc_ + j >= std::tuple_size::value) { + break; + } + + ppu_.VramWrite8(ndmaVramWriteLoc_ + j, mmu_.Read8(ndmaReadLoc_ + j)); + } + + ndmaVramWriteLoc_ += kNdmaBlockSize; + ndmaReadLoc_ += kNdmaBlockSize; + --ndmaNumBlocksLeft_; + } +} + +void Dma::HandleGdmaUpdate(unsigned int cycles) { + if (!IsGdmaInProgress()) { + return; + } + + // GDMA speed does not scale with double speed mode + cycles = util::RescaleCycles(cpu_, cycles); + + if (cycles >= gdmaCyclesLeft_) { + // GDMA time finished + DoNdmaTransfer(ndmaNumBlocksLeft_); + ndmaStatus_ = NdmaStatus::Inactive; + + gdmaCyclesLeft_ = 0; + } else { + gdmaCyclesLeft_ -= cycles; + } +} + +void Dma::HandleHdmaUpdate(unsigned int cycles) { + if (!IsHdmaEnabled()) { + return; + } + + UpdateHdmaHBlankState(); + if (!IsHdmaInProgress()) { + return; + } + + // HDMA speed does not scale with double speed mode + cycles = util::RescaleCycles(cpu_, cycles); + + if (cycles >= hdmaBlockCyclesLeft_) { + // HDMA block transfer time finished + DoNdmaTransfer(1); + ndmaStatus_ = (ndmaNumBlocksLeft_ <= 0 ? NdmaStatus::Inactive + : NdmaStatus::HdmaFinishedBlock); + hdmaBlockCyclesLeft_ = 0; + } else { + hdmaBlockCyclesLeft_ -= cycles; + } +} + +void Dma::UpdateHdmaHBlankState() { + if (ndmaStatus_ == NdmaStatus::HdmaWaitingForHBlank && + ppu_.GetScreenMode() == PpuScreenMode::HBlank && + ppu_.IsLcdOn()) { + // we can now start the HBlank block transfer + hdmaBlockCyclesLeft_ = kNdmaCyclesPerBlock + util::RescaleCycles(cpu_, 4); + ndmaStatus_ = NdmaStatus::HdmaInProgress; + } else if (ndmaStatus_ == NdmaStatus::HdmaFinishedBlock && + ppu_.GetScreenMode() != PpuScreenMode::HBlank) { + // HBlank finished, wait for the next one + ndmaStatus_ = NdmaStatus::HdmaWaitingForHBlank; + } +} + +void Dma::DoOamDmaTransfer() { + for (u8 i = 0; i < std::tuple_size::value; ++i) { + ppu_.OamWrite8(i, mmu_.Read8(util::To16(oamDmaSourceLocHi_, i)), true); + } +} + +void Dma::HandleOamDmaUpdate(unsigned int cycles) { + if (!IsOamDmaInProgress()) { + return; + } + + if (cycles >= oamDmaCyclesLeft_) { + // OAM DMA transfer time finished + DoOamDmaTransfer(); + oamDmaStatus_ = OamDmaStatus::Inactive; + + oamDmaCyclesLeft_ = 0; + } else { + oamDmaCyclesLeft_ -= cycles; + } +} + +void Dma::StartOamDmaTransfer(u8 sourceLocHi) { + if (sourceLocHi <= 0xdf) { + oamDmaSourceLocHi_ = sourceLocHi; + oamDmaCyclesLeft_ = kOamDmaTotalCycles; + oamDmaStatus_ = OamDmaStatus::InProgress; + } +} + +u8 Dma::GetOamDmaSourceLocHi() const { + return oamDmaSourceLocHi_; +} + +void Dma::SetNdma5(u8 val) { + if (!cgbMode_) { + return; + } + + if (IsHdmaEnabled() && !(val & 0x80)) { + // HDMA disable requested + ndmaStatus_ = NdmaStatus::Inactive; + } else { + ndmaNumBlocksLeft_ = (val & 0x7f) + 1; + + if (val & 0x80) { + // HDMA start/restart requested + ndmaStatus_ = NdmaStatus::HdmaWaitingForHBlank; + } else { + // GDMA requested + gdmaCyclesLeft_ = (kNdmaCyclesPerBlock * ndmaNumBlocksLeft_) + + util::RescaleCycles(cpu_, 4); + ndmaStatus_ = NdmaStatus::GdmaInProgress; + } + } +} + +u8 Dma::GetNdma5() const { + // will be $FF when NDMA transfer finished + return (!IsNdmaEnabled() ? 0x80 : 0) | ((ndmaNumBlocksLeft_ - 1) & 0x7f); +} + +void Dma::SetNdmaSourceLocHi(u8 hi) { + ndmaReadLoc_ = util::SetHi8(ndmaReadLoc_, hi); +} + +void Dma::SetNdmaSourceLocLo(u8 lo) { + // lowest 4 bits ignored + ndmaReadLoc_ = util::SetLo8(ndmaReadLoc_, lo & 0xf0); +} + +void Dma::SetNdmaDestLocHi(u8 hi) { + // highest 3 bits ignored + ndmaVramWriteLoc_ = util::SetHi8(ndmaVramWriteLoc_, hi & 0x1f); +} + +void Dma::SetNdmaDestLocLo(u8 lo) { + // lowest 4 bits ignored + ndmaVramWriteLoc_ = util::SetLo8(ndmaVramWriteLoc_, lo & 0xf0); +} + +bool Dma::IsNdmaInProgress() const { + return IsHdmaInProgress() || IsGdmaInProgress(); +} + +bool Dma::IsHdmaInProgress() const { + return ndmaStatus_ == NdmaStatus::HdmaInProgress; +} + +bool Dma::IsGdmaInProgress() const { + return ndmaStatus_ == NdmaStatus::GdmaInProgress; +} + +bool Dma::IsOamDmaInProgress() const { + return oamDmaStatus_ == OamDmaStatus::InProgress; +} + +bool Dma::IsHdmaEnabled() const { + return ndmaStatus_ == NdmaStatus::HdmaWaitingForHBlank || + ndmaStatus_ == NdmaStatus::HdmaFinishedBlock || + IsHdmaInProgress(); +} + +bool Dma::IsNdmaEnabled() const { + return IsHdmaEnabled() || IsGdmaInProgress(); +} + +OamDmaStatus Dma::GetOamDmaStatus() const { + return oamDmaStatus_; +} + +NdmaStatus Dma::GetNdmaStatus() const { + return ndmaStatus_; +} + +bool Dma::IsInCgbMode() const { + return cgbMode_; +} diff --git a/src/hw/gbc.cpp b/src/hw/gbc.cpp new file mode 100644 index 0000000..206bbae --- /dev/null +++ b/src/hw/gbc.cpp @@ -0,0 +1,62 @@ +#include "hw/gbc.h" + +GbcHardware::GbcHardware() + : cpu(mmu, dma, joypad), timer(cpu), apu(cpu), ppu(cpu, dma), joypad(cpu), + serial(cpu), dma(mmu, cpu, ppu), mmu(*this) {} + +Gbc::Gbc() : cgbMode_(false) {} + +void Gbc::Reset(bool forceDmgMode) { + cgbMode_ = !forceDmgMode && hw_.cartridge.IsInCgbMode(); + + hw_.cpu.Reset(cgbMode_); + hw_.ppu.Reset(cgbMode_); + hw_.mmu.Reset(cgbMode_); + hw_.dma.Reset(cgbMode_); + hw_.serial.Reset(cgbMode_); + hw_.apu.Reset(); + hw_.timer.Reset(); + hw_.joypad.Reset(); + hw_.cartridge.Reset(); + + // zero-out contents of WRAM and HRAM + hw_.hram.fill(0x00); + + for (auto& b : hw_.wramBanks) { + b.fill(0x00); + } +} + +unsigned int Gbc::Update() { + const auto cycles = hw_.cpu.Update(); + + hw_.dma.Update(cycles); + hw_.apu.Update(cycles); + hw_.ppu.Update(cycles); + hw_.timer.Update(cycles); + hw_.serial.Update(cycles); + + return cycles; +} + +RomLoadResult Gbc::LoadCartridgeRomFile(const std::string& filePath, + const std::string& fileName) { + const auto result = hw_.cartridge.LoadRomFile(filePath, fileName); + if (result == RomLoadResult::Ok) { + Reset(); + } + + return result; +} + +GbcHardware& Gbc::GetHardware() { + return hw_; +} + +const GbcHardware& Gbc::GetHardware() const { + return hw_; +} + +bool Gbc::IsInCgbMode() const { + return cgbMode_; +} diff --git a/src/hw/joypad.cpp b/src/hw/joypad.cpp new file mode 100644 index 0000000..0837bc9 --- /dev/null +++ b/src/hw/joypad.cpp @@ -0,0 +1,88 @@ +#include "hw/cpu/cpu.h" +#include "hw/joypad.h" + +Joypad::Joypad(Cpu& cpu) : cpu_(cpu), leftRightOrUpDownAllowed_(false) {} + +void Joypad::Reset() { + keyStates_ = nextKeyStates_ = 0; + selectButtonKeys_ = selectDirectionKeys_ = true; + + wasSelectedKeyPressed_ = false; +} + +void Joypad::CommitKeyStates() { + const u8 nowPressedKeys = nextKeyStates_ & ~keyStates_; + + wasSelectedKeyPressed_ = ((nowPressedKeys & 0x0f) && selectButtonKeys_) || + ((nowPressedKeys & 0xf0) && selectDirectionKeys_); + if (wasSelectedKeyPressed_) { + // a selected key that was prev unpressed was since pressed. trigger int $60 + cpu_.IntfRequest(kCpuInterrupt0x60); + } + + keyStates_ = nextKeyStates_; +} + +void Joypad::SetImpossibleInputsAllowed(bool val) { + leftRightOrUpDownAllowed_ = val; +} + +bool Joypad::AreImpossibleInputsAllowed() const { + return leftRightOrUpDownAllowed_; +} + +bool Joypad::WasSelectedKeyPressed() const { + return wasSelectedKeyPressed_; +} + +void Joypad::SetKeyState(JoypadKey key, bool pressed) { + if (pressed) { + if (!leftRightOrUpDownAllowed_) { + // if we don't allow simultaneous presses of up+down or left+right (which + // the real hardware doesn't also doesn't allow), unpress the opposite + // direction key + switch (key) { + case JoypadKey::Up: + SetKeyState(JoypadKey::Down, false); + break; + case JoypadKey::Down: + SetKeyState(JoypadKey::Up, false); + break; + case JoypadKey::Left: + SetKeyState(JoypadKey::Right, false); + break; + case JoypadKey::Right: + SetKeyState(JoypadKey::Left, false); + break; + } + } + + nextKeyStates_ |= static_cast(key); + } else { + nextKeyStates_ &= ~static_cast(key); + } +} + +void Joypad::SetJoyp(u8 val) { + selectButtonKeys_ = (val & 0x20) == 0; + selectDirectionKeys_ = (val & 0x10) == 0; +} + +u8 Joypad::GetJoyp() const { + // set all button input lines to high initially, and set bit 5 and 4 to + // indicate whether the button/direction keys are selected. + // bits 7 & 6 are always set (and are unused) + u8 joyp = 0xcf | (selectButtonKeys_ ? 0x20 : 0) + | (selectDirectionKeys_ ? 0x10 : 0); + + // joypad register works on inverses as it doesn't use an inverter + // (1 = unpressed, 0 = pressed), so complement the state values we have first + if (selectButtonKeys_) { + joyp = (joyp & 0xf0) | (~keyStates_ & 0xf); + } + if (selectDirectionKeys_) { + joyp = (joyp & 0xf0) | ((~keyStates_ >> 4) & 0xf); + } + + return joyp; +} diff --git a/src/hw/mmu.cpp b/src/hw/mmu.cpp new file mode 100644 index 0000000..a3b6d81 --- /dev/null +++ b/src/hw/mmu.cpp @@ -0,0 +1,432 @@ +#include "hw/gbc.h" +#include + +Mmu::Mmu(GbcHardware& hw) : hw_(hw) {} + +void Mmu::Reset(bool cgbMode) { + cgbMode_ = cgbMode; + svbk_ = cgbMode_ ? 0xf8 : 0xff; +} + +u8 Mmu::GetWramBankIndex() const { + return cgbMode_ ? std::max(svbk_ & 7, 1) : 1; +} + +bool Mmu::IsInCgbMode() const { + return cgbMode_; +} + +void Mmu::Write8(u16 loc, u8 val) { + if (loc < 0x4000) { + // cartridge ROM bank 0 + hw_.cartridge.RomBank0Write8(loc, val); + } else if (loc < 0x8000) { + // cartridge switchable ROM bank 0-N + hw_.cartridge.RomBankXWrite8(loc - 0x4000, val); + } else if (loc < 0xa000) { + // VRAM switchable bank 0-1 + hw_.ppu.VramWrite8(loc - 0x8000, val); + } else if (loc < 0xc000) { + // external cartridge RAM + hw_.cartridge.RamWrite8(loc - 0xa000, val); + } else if (loc < 0xd000) { + // WRAM fixed bank 0 + hw_.wramBanks[0][loc & 0xfff] = val; + } else if (loc < 0xe000) { + // WRAM switchable bank 1-7 + hw_.wramBanks[GetWramBankIndex()][loc & 0xfff] = val; + } else if (loc < 0xfe00) { + // echo RAM (same as $C000 to $DDFF) + Write8(loc - 0x2000, val); + } else if (loc < 0xfea0) { + // OAM + hw_.ppu.OamWrite8(loc - 0xfe00, val); + } else if (loc < 0xff00) { + // unusable area. writes have no effect + } else if (loc >= 0xff30 && loc < 0xff40) { + // wave RAM + hw_.apu.WriteWaveRam8(loc & 0xf, val); + } else if (loc < 0xff80) { + // IO registers + WriteIoRegister(loc & 0x7f, val); + } else if (loc < 0xffff) { + // HRAM + hw_.hram[loc & 0x7f] = val; + } else { + // interrupt enable IO register + WriteIoRegister(kIoRegisterIdInte, val); + } +} + +u8 Mmu::Read8(u16 loc) const { + if (loc < 0x4000) { + // cartridge ROM bank 0 + return hw_.cartridge.RomBank0Read8(loc); + } else if (loc < 0x8000) { + // cartridge switchable ROM bank 0-N + return hw_.cartridge.RomBankXRead8(loc - 0x4000); + } else if (loc < 0xa000) { + // VRAM switchable bank 0-1 + return hw_.ppu.VramRead8(loc - 0x8000); + } else if (loc < 0xc000) { + // external cartridge RAM + return hw_.cartridge.RamRead8(loc - 0xa000); + } else if (loc < 0xd000) { + // WRAM fixed bank 0 + return hw_.wramBanks[0][loc & 0xfff]; + } else if (loc < 0xe000) { + // WRAM switchable bank 1-7 + return hw_.wramBanks[GetWramBankIndex()][loc & 0xfff]; + } else if (loc < 0xfe00) { + // echo RAM (same as $C000 to $DDFF) + return Read8(loc - 0x2000); + } else if (loc < 0xfea0) { + // OAM + return hw_.ppu.OamRead8(loc - 0xfe00); + } else if (loc < 0xff00) { + // unusable area + return 0xff; + } else if (loc >= 0xff30 && loc < 0xff40) { + // wave RAM - reads return the last value written to wave RAM + return hw_.apu.GetWaveRamLastWritten8(); + } else if (loc < 0xff80) { + // IO registers + return ReadIoRegister(loc & 0x7f); + } else if (loc < 0xffff) { + // HRAM + return hw_.hram[loc & 0x7f]; + } else { + // interrupt enable IO register + return ReadIoRegister(kIoRegisterIdInte); + } +} + +void Mmu::WriteIoRegister(u8 regId, u8 val) { + switch (regId) { + // serial data transfer registers + case kIoRegisterIdSb: + hw_.serial.SetSb(val); + break; + case kIoRegisterIdSc: + hw_.serial.SetSc(val); + break; + + // joypad register + case kIoRegisterIdJoyp: + hw_.joypad.SetJoyp(val); + break; + + // CPU interrupt registers + case kIoRegisterIdIntf: + hw_.cpu.SetIntf(val); + break; + case kIoRegisterIdInte: + hw_.cpu.SetInte(val); + break; + + // CPU double-speed prepare switch + case kIoRegisterIdKey1: + hw_.cpu.SetKey1(val); + break; + + // APU channel 1 registers + case kIoRegisterIdNr10: + hw_.apu.WriteCh1Register(ApuCh1Register::SweepCtrl, val); + break; + case kIoRegisterIdNr11: + hw_.apu.WriteCh1Register(ApuCh1Register::LengthLoadDutyCtrl, val); + break; + case kIoRegisterIdNr12: + hw_.apu.WriteCh1Register(ApuCh1Register::EnvelopeCtrl, val); + break; + case kIoRegisterIdNr13: + hw_.apu.WriteCh1Register(ApuCh1Register::FreqLoadLo, val); + break; + case kIoRegisterIdNr14: + hw_.apu.WriteCh1Register(ApuCh1Register::LengthCtrlFreqLoadHi, val); + break; + + // APU channel 2 registers + case kIoRegisterIdNr21: + hw_.apu.WriteCh2Register(ApuCh2Register::LengthLoadDutyCtrl, val); + break; + case kIoRegisterIdNr22: + hw_.apu.WriteCh2Register(ApuCh2Register::EnvelopeCtrl, val); + break; + case kIoRegisterIdNr23: + hw_.apu.WriteCh2Register(ApuCh2Register::FreqLoadLo, val); + break; + case kIoRegisterIdNr24: + hw_.apu.WriteCh2Register(ApuCh2Register::LengthCtrlFreqLoadHi, val); + break; + + // APU channel 3 registers + case kIoRegisterIdNr30: + hw_.apu.WriteCh3Register(ApuCh3Register::DacOnCtrl, val); + break; + case kIoRegisterIdNr31: + hw_.apu.WriteCh3Register(ApuCh3Register::LengthLoad, val); + break; + case kIoRegisterIdNr32: + hw_.apu.WriteCh3Register(ApuCh3Register::VolumeCtrl, val); + break; + case kIoRegisterIdNr33: + hw_.apu.WriteCh3Register(ApuCh3Register::FreqLoadLo, val); + break; + case kIoRegisterIdNr34: + hw_.apu.WriteCh3Register(ApuCh3Register::LengthCtrlFreqLoadHi, val); + break; + + // APU channel 4 registers + case kIoRegisterIdNr41: + hw_.apu.WriteCh4Register(ApuCh4Register::LengthLoad, val); + break; + case kIoRegisterIdNr42: + hw_.apu.WriteCh4Register(ApuCh4Register::EnvelopeCtrl, val); + break; + case kIoRegisterIdNr43: + hw_.apu.WriteCh4Register(ApuCh4Register::PolynomialCtrl, val); + break; + case kIoRegisterIdNr44: + hw_.apu.WriteCh4Register(ApuCh4Register::LengthCtrl, val); + break; + + // APU control registers + case kIoRegisterIdNr50: + hw_.apu.SetChannelCtrl(val); + break; + case kIoRegisterIdNr51: + hw_.apu.SetOutputCtrl(val); + break; + case kIoRegisterIdNr52: + hw_.apu.SetOnCtrl(val); + break; + + // timer & divider registers + case kIoRegisterIdDiv: + hw_.timer.ResetDiv(); + break; + case kIoRegisterIdTima: + hw_.timer.SetTima(val); + break; + case kIoRegisterIdTma: + hw_.timer.SetTma(val); + break; + case kIoRegisterIdTac: + hw_.timer.SetTac(val); + break; + + // LCD control & status registers + case kIoRegisterIdLcdc: + hw_.ppu.SetLcdc(val); + break; + case kIoRegisterIdStat: + hw_.ppu.SetStat(val); + break; + + // LCD position & scrolling registers + case kIoRegisterIdScy: + hw_.ppu.SetScy(val); + break; + case kIoRegisterIdScx: + hw_.ppu.SetScx(val); + break; + case kIoRegisterIdLyc: + hw_.ppu.SetLyc(val); + break; + case kIoRegisterIdWy: + hw_.ppu.SetWy(val); + break; + case kIoRegisterIdWx: + hw_.ppu.SetWx(val); + break; + + // LCD monochrome palette registers + case kIoRegisterIdBgp: + hw_.ppu.SetBgp(val); + break; + case kIoRegisterIdObp0: + hw_.ppu.SetObp0(val); + break; + case kIoRegisterIdObp1: + hw_.ppu.SetObp1(val); + break; + + // LCD color palette registers + case kIoRegisterIdBcps: + hw_.ppu.SetBcps(val); + break; + case kIoRegisterIdBcpd: + hw_.ppu.SetBcpd(val); + break; + case kIoRegisterIdOcps: + hw_.ppu.SetOcps(val); + break; + case kIoRegisterIdOcpd: + hw_.ppu.SetOcpd(val); + break; + + // LCD VRAM bank register + case kIoRegisterIdVbk: + hw_.ppu.SetVbk(val); + break; + + // new DMA (GDMA/HDMA) registers + case kIoRegisterIdDma: + hw_.dma.StartOamDmaTransfer(val); + break; + case kIoRegisterIdHdma1: + hw_.dma.SetNdmaSourceLocHi(val); + break; + case kIoRegisterIdHdma2: + hw_.dma.SetNdmaSourceLocLo(val); + break; + case kIoRegisterIdHdma3: + hw_.dma.SetNdmaDestLocHi(val); + break; + case kIoRegisterIdHdma4: + hw_.dma.SetNdmaDestLocLo(val); + break; + case kIoRegisterIdHdma5: + hw_.dma.SetNdma5(val); + break; + + // WRAM bank switch registers + case kIoRegisterIdSvbk: + svbk_ = cgbMode_ ? val | 0xf8 : 0xff; + break; + } +} + +u8 Mmu::ReadIoRegister(u8 regId) const { + switch (regId) { + // serial data transfer registers + case kIoRegisterIdSb: + return hw_.serial.GetSb(); + case kIoRegisterIdSc: + return hw_.serial.GetSc(); + + // joypad register + case kIoRegisterIdJoyp: + return hw_.joypad.GetJoyp(); + + // CPU interrupt registers + case kIoRegisterIdIntf: + return hw_.cpu.GetIntf(); + case kIoRegisterIdInte: + return hw_.cpu.GetInte(); + + // CPU double-speed prepare switch + case kIoRegisterIdKey1: + return hw_.cpu.GetKey1(); + + // APU channel 1 registers + case kIoRegisterIdNr10: + return hw_.apu.ReadCh1Register(ApuCh1Register::SweepCtrl); + case kIoRegisterIdNr11: + return hw_.apu.ReadCh1Register(ApuCh1Register::LengthLoadDutyCtrl); + case kIoRegisterIdNr12: + return hw_.apu.ReadCh1Register(ApuCh1Register::EnvelopeCtrl); + case kIoRegisterIdNr14: + return hw_.apu.ReadCh1Register(ApuCh1Register::LengthCtrlFreqLoadHi); + + // APU channel 2 registers + case kIoRegisterIdNr21: + return hw_.apu.ReadCh2Register(ApuCh2Register::LengthLoadDutyCtrl); + case kIoRegisterIdNr22: + return hw_.apu.ReadCh2Register(ApuCh2Register::EnvelopeCtrl); + case kIoRegisterIdNr24: + return hw_.apu.ReadCh2Register(ApuCh2Register::LengthCtrlFreqLoadHi); + + // APU channel 3 registers + case kIoRegisterIdNr30: + return hw_.apu.ReadCh3Register(ApuCh3Register::DacOnCtrl); + case kIoRegisterIdNr32: + return hw_.apu.ReadCh3Register(ApuCh3Register::VolumeCtrl); + case kIoRegisterIdNr34: + return hw_.apu.ReadCh3Register(ApuCh3Register::LengthCtrlFreqLoadHi); + + // APU channel 4 registers + case kIoRegisterIdNr42: + return hw_.apu.ReadCh4Register(ApuCh4Register::EnvelopeCtrl); + case kIoRegisterIdNr43: + return hw_.apu.ReadCh4Register(ApuCh4Register::PolynomialCtrl); + case kIoRegisterIdNr44: + return hw_.apu.ReadCh4Register(ApuCh4Register::LengthCtrl); + + // APU control registers + case kIoRegisterIdNr50: + return hw_.apu.GetChannelCtrl(); + case kIoRegisterIdNr51: + return hw_.apu.GetOutputCtrl(); + case kIoRegisterIdNr52: + return hw_.apu.GetOnCtrl(); + + // timer & divider registers + case kIoRegisterIdDiv: + return hw_.timer.GetDiv(); + case kIoRegisterIdTima: + return hw_.timer.GetTima(); + case kIoRegisterIdTma: + return hw_.timer.GetTma(); + case kIoRegisterIdTac: + return hw_.timer.GetTac(); + + // LCD control & status registers + case kIoRegisterIdLcdc: + return hw_.ppu.GetLcdc(); + case kIoRegisterIdStat: + return hw_.ppu.GetStat(); + + // LCD position & scrolling registers + case kIoRegisterIdScy: + return hw_.ppu.GetScy(); + case kIoRegisterIdScx: + return hw_.ppu.GetScx(); + case kIoRegisterIdLy: + return hw_.ppu.GetLy(); + case kIoRegisterIdLyc: + return hw_.ppu.GetLyc(); + case kIoRegisterIdWy: + return hw_.ppu.GetWy(); + case kIoRegisterIdWx: + return hw_.ppu.GetWx(); + + // LCD monochrome palette registers + case kIoRegisterIdBgp: + return hw_.ppu.GetBgp(); + case kIoRegisterIdObp0: + return hw_.ppu.GetObp0(); + case kIoRegisterIdObp1: + return hw_.ppu.GetObp1(); + + // LCD color palette registers + case kIoRegisterIdBcps: + return hw_.ppu.GetBcps(); + case kIoRegisterIdBcpd: + return hw_.ppu.GetBcpd(); + case kIoRegisterIdOcps: + return hw_.ppu.GetOcps(); + case kIoRegisterIdOcpd: + return hw_.ppu.GetOcpd(); + + // LCD VRAM bank register + case kIoRegisterIdVbk: + return hw_.ppu.GetVbk(); + + // OAM DMA address high byte + case kIoRegisterIdDma: + return hw_.dma.GetOamDmaSourceLocHi(); + + // new DMA (GDMA/HDMA) start/status register + case kIoRegisterIdHdma5: + return hw_.dma.GetNdma5(); + + // WRAM bank switch register + case kIoRegisterIdSvbk: + return svbk_; + + default: + return 0xff; + } +} diff --git a/src/hw/ppu.cpp b/src/hw/ppu.cpp new file mode 100644 index 0000000..04bc50f --- /dev/null +++ b/src/hw/ppu.cpp @@ -0,0 +1,755 @@ +#include "hw/cpu/cpu.h" +#include "hw/dma.h" +#include "hw/ppu.h" +#include +#include + +RgbColor RgbColor::FromLcdIntensities(u8 r, u8 g, u8 b) { + // a good fast naive intensity to RGB approximation. + // RGBs can range from 7 to 255, allowing for a near-enough pure black + assert(r < 0x20 && g < 0x20 && b < 0x20); + return RgbColor((r * 8) + 7, (g * 8) + 7, (b * 8) + 7); +} + +RgbColor::RgbColor() : r(0), g(0), b(0) {} + +RgbColor::RgbColor(u8 r, u8 g, u8 b) : r(r), g(g), b(b) {} + +constexpr auto kOamMaxSprites = 40u, + kScanlineMaxTiles = 21u; + +constexpr auto kTileMapWidth = 32u, + kTileMapHeight = 32u, + kTileMapSize = 256u; + +Ppu::Ppu(Cpu& cpu, const Dma& dma) + : cpu_(cpu), dma_(dma), lcd_(nullptr), limitScanlineSprites_(true), + enableBg_(true), enableBgWindow_(true), enableSprites_(true) {} + +void Ppu::Reset(bool cgbMode) { + cgbMode_ = cgbMode; + screenModeCycles_ = 0; + + if (lcd_) { + lcd_->LcdPower(true); + } + + // initial register values + lcdc_ = 0x91; + stat_ = 0x80; + + scy_ = scx_ = ly_ = lyc_ = wy_ = wx_ = 0x00; + + bgp_ = 0xfc; + obp0_ = obp1_ = 0x00; + + bcps_ = 0xc8; + ocps_ = 0xd0; + + vbk_ = 0xfe; + + // zero-out contents of VRAM and OAM + for (auto& b : vramBanks_) { + b.fill(0x00); + } + oam_.fill(0x00); + + // init contents of BCPD to white (all $FF) + bcpData_.fill(0xff); + ocpData_.fill(0xff); +} + +void Ppu::Update(unsigned int cycles) { + if (!IsLcdOn()) { + return; + } + + // speed of LCD video hw doesn't scale with double speed mode + cycles = util::RescaleCycles(cpu_, cycles); + UpdateScreenMode(cycles); + + // check if we have an LY coincidence at this moment. if we do, set bit 2 in + // STAT; otherwise, clear it + stat_ = (ly_ == lyc_ ? stat_ | 4 : stat_ & 0xfb); +} + +void Ppu::UpdateScreenMode(unsigned int cycles) { + screenModeCycles_ += cycles; + + while (screenModeCycles_ >= GetScreenModeMaxCycles()) { + // account for the extra spent cycles after the mode transition + screenModeCycles_ -= GetScreenModeMaxCycles(); + + switch (GetScreenMode()) { + case PpuScreenMode::HBlank: + if (IncrementLy() < kLcdHeightPixels) { + ChangeScreenMode(PpuScreenMode::SearchingOam); + } else { + // finished rendering the last scanline for this frame. now refresh + // the LCD with our finished frame + if (lcd_) { + lcd_->LcdRefresh(); + } + + cpu_.IntfRequest(kCpuInterrupt0x40); + ChangeScreenMode(PpuScreenMode::VBlank); + } + break; + + case PpuScreenMode::VBlank: + if (IncrementLy() == 0) { + // finished last VBlank scanline. now start work on the next frame + ChangeScreenMode(PpuScreenMode::SearchingOam); + } + break; + + case PpuScreenMode::SearchingOam: + ChangeScreenMode(PpuScreenMode::DataTransfer); + break; + + case PpuScreenMode::DataTransfer: + RenderScanline(); + ChangeScreenMode(PpuScreenMode::HBlank); + break; + } + } +} + +u8 Ppu::IncrementLy() { + // LY can assume values 0 to 153. >153 wraps around to 0 + SetLy(ly_ < 153 ? ly_ + 1 : 0); + return ly_; +} + +void Ppu::SetLy(u8 val) { + assert(val <= 153); + ly_ = val; + + // request interrupt if there is an LY coincidence and STAT bit 6 is set + if (ly_ == lyc_ && stat_ & 0x40) { + cpu_.IntfRequest(kCpuInterrupt0x48); + } +} + +void Ppu::ChangeScreenMode(PpuScreenMode mode) { + stat_ = (stat_ & 0xfc) | (static_cast(mode) & 3); + + // depending on what mode we've transitioned to, we may need to request an + // interrupt (if enabled in the STAT register) + if ((mode == PpuScreenMode::HBlank && stat_ & 0x08) || + (mode == PpuScreenMode::VBlank && stat_ & 0x10) || + (mode == PpuScreenMode::SearchingOam && stat_ & 0x20)) { + cpu_.IntfRequest(kCpuInterrupt0x48); + } +} + +PpuScreenMode Ppu::GetScreenMode() const { + return static_cast(stat_ & 3); +} + +void Ppu::RenderScanline() { + if (!lcd_) { + return; + } + + RenderBufferBgScanline(); + RenderBufferBgWindowScanline(); + RenderBufferSpriteScanline(); + + // update LCD scanline + for (auto x = 0u; x < kLcdWidthPixels; ++x) { + const auto& bgPixInfo = scanlineBgPixelInfos_[x]; + const auto& spritePixInfo = scanlineSpritePixelInfos_[x]; + + lcd_->LcdPutPixel(x, ly_, + cgbMode_ ? GetCgbPixelColor(bcpData_, bgPixInfo.paletteColorNum) + : kDmgPaletteColors[bgPixInfo.paletteColorNum]); + + if (!spritePixInfo.transparent) { + lcd_->LcdPutPixel(x, ly_, + cgbMode_ ? GetCgbPixelColor(ocpData_, spritePixInfo.paletteColorNum) + : kDmgPaletteColors[spritePixInfo.paletteColorNum]); + } + } +} + +RgbColor Ppu::GetCgbPixelColor(const CgbPaletteMemory& data, + u8 pixelPaletteColorNum) const { + const u16 cgbColor = util::To16(data[(pixelPaletteColorNum * 2) + 1], + data[pixelPaletteColorNum * 2]); + + return RgbColor::FromLcdIntensities(cgbColor & 0x1f, + (cgbColor >> 5) & 0x1f, + (cgbColor >> 10) & 0x1f); +} + +Ppu::BgPixelInfo::BgPixelInfo() + : paletteColorNum(0), alwaysBehindSprites(true), + ignoreSpritePriority(false) {} + +void Ppu::RenderBufferBgScanline() { + scanlineBgPixelInfos_.fill(BgPixelInfo()); + + // no BG rendered if LCDC bit 0 unset in DMG mode + if (!enableBg_ || (!cgbMode_ && !(lcdc_ & 1))) { + return; + } + + // LCDC bit 3 determines where the BG's tile map is + const u16 tileMapStartLocOffset = lcdc_ & 0x08 ? 0x1c00 : 0x1800; + + for (auto i = 0u; i < kScanlineMaxTiles; ++i) { + const auto tileX = i + (scx_ / 8); + + // get the offset location of the tile entry within the tile map + const u16 tileMapEntryLocOffset = tileMapStartLocOffset + + (tileX % kTileMapWidth) + + (kTileMapWidth * (((ly_ + scy_) / 8) + % kTileMapHeight)); + + // fetch the attribs and pattern line for this tile from VRAM + const auto tileInfo = GetBgTileInfo(tileMapEntryLocOffset); + const auto patternLine = GetBgPatternLine(tileInfo.patternNum, + tileInfo.patternBankIndex, + (ly_ + scy_) % 8, + tileInfo.patternFlipX, + tileInfo.patternFlipY); + + // buffer the pixel palette values + for (auto x = 0u; x < 8; ++x) { + // the BG map wraps around the screen, and is 256x256 pixels + RenderBufferBgPixel(tileInfo, GetPatternNumberFromLine(patternLine, x), + ((tileX * 8) + x - scx_) % kTileMapSize); + } + } +} + +void Ppu::RenderBufferBgWindowScanline() { + // no window rendered if LCDC bit 0 unset in DMG mode or LCDC bit 5 unset + if (!enableBgWindow_ || (!cgbMode_ && !(lcdc_ & 1)) || !(lcdc_ & 0x20)) { + return; + } + + // top-left window screen pixel co-ords. don't bother rendering the window if + // it's off-screen or not on this scanline + const int wxActual = wx_ - 7; + if (wxActual >= static_cast(kLcdWidthPixels) || + wy_ >= kLcdHeightPixels || ly_ < wy_) { + return; + } + + // LCDC bit 3 determines where the window's tile map is + const u16 tileMapStartLocOffset = lcdc_ & 0x40 ? 0x1c00 : 0x1800; + + // determine the number of tiles on screen from our window X coordinate and + // iterate through them for rendering + const auto numTiles = kScanlineMaxTiles - (wxActual / 8); + + for (auto tileX = 0u; tileX < numTiles; ++tileX) { + // get the offset location of the tile entry within the 32x32 tile map + const u16 tileMapEntryLocOffset = tileMapStartLocOffset + + tileX + + (kTileMapWidth * ((ly_ - wy_) / 8)); + + // fetch the attribs and pattern line for this tile from VRAM + const auto tileInfo = GetBgTileInfo(tileMapEntryLocOffset); + const auto patternLine = GetBgPatternLine(tileInfo.patternNum, + tileInfo.patternBankIndex, + (ly_ - wy_) % 8, + tileInfo.patternFlipX, + tileInfo.patternFlipY); + + // buffer the pixel palette values + for (auto x = 0u; x < 8; ++x) { + RenderBufferBgPixel(tileInfo, GetPatternNumberFromLine(patternLine, x), + (tileX * 8) + x + wxActual); + } + } +} + +void Ppu::RenderBufferBgPixel(const BgTileInfo& tileInfo, u8 bgpNum, + int pixelX) { + if (pixelX < 0 || pixelX >= static_cast(kLcdWidthPixels)) { + return; + } + + auto& bgPixelInfo = scanlineBgPixelInfos_[pixelX]; + bgPixelInfo.alwaysBehindSprites = bgpNum == 0; + bgPixelInfo.ignoreSpritePriority = tileInfo.patternPriorityOverSprites; + + if (cgbMode_) { + // draw using the color palette attribute + bgPixelInfo.paletteColorNum = (tileInfo.patternCgbPaletteNum * 4) + + bgpNum; + } else { + // draw using the monochrome palette register + bgPixelInfo.paletteColorNum = (bgp_ >> (bgpNum * 2)) & 3; + } +} + +Ppu::SpritePixelInfo::SpritePixelInfo() + : paletteColorNum(0), transparent(true) {} + +void Ppu::RenderBufferSpriteScanline() { + scanlineSpritePixelInfos_.fill(SpritePixelInfo()); + + // no sprites are rendered during OAM DMA or if LCDC bit 1 set + if (!enableSprites_ || dma_.IsOamDmaInProgress() || !(lcdc_ & 2)) { + return; + } + + // enumerate the sprites that are on this scanline. + // iterate in reverse order so we buffer over sprites with lower priority + const auto sprites = EnumerateScanlineSprites(); + + for (auto rit = sprites.rbegin(); rit != sprites.rend(); ++rit) { + const auto& sprite = *rit; + + // don't draw hidden sprites + if (sprite.x == -8 || sprite.x >= static_cast(kLcdWidthPixels)) { + continue; + } + + // fetch the pattern line for this sprite from VRAM + const auto patternLine = GetSpritePatternLine( + sprite.patternNum, + cgbMode_ && sprite.attribs & 0x08 ? 1 : 0, + ly_ - sprite.y, + (sprite.attribs & 0x20) != 0, + (sprite.attribs & 0x40) != 0); + + // buffer the pixel palette values + for (auto x = 0u; x < 8; ++x) { + if (!RenderBufferSpritePixel(sprite, + GetPatternNumberFromLine(patternLine, x), + sprite.x + x)) { + break; // no need to render any more pixels in this sprite + } + } + } +} + +std::vector Ppu::EnumerateScanlineSprites() const { + // sprites with lower OAM index values will have higher rendering priority + // (unless we're in DMG mode, where we need to account for X values) + std::vector sprites; + sprites.reserve(limitScanlineSprites_ ? 10 : kOamMaxSprites); + + for (auto i = 0u; i < kOamMaxSprites; ++i) { + // only consider 10 sprites at most (hardware limitation) + if (limitScanlineSprites_ && sprites.size() >= 10) { + break; + } + + const u8 oamLoc = i * 4; + + // minus 16 from attrib 0 and 8 from attrib 1 to get Y & X values of the + // top-left corner of the sprite + const int spriteY = oam_[oamLoc] - 16, + spriteX = oam_[oamLoc + 1] - 8; + + // only consider sprites that are visible on the current scanline + if (spriteY <= static_cast(ly_) && + spriteY + (IsIn8x16SpriteMode() ? 16 : 8) > static_cast(ly_)) { + sprites.push_back({oamLoc, + spriteX, spriteY, + oam_[oamLoc + 2], oam_[oamLoc + 3]}); + } + } + + // DMG mode gives priority to the sprite with the lowest X values + if (!cgbMode_) { + std::sort(sprites.begin(), sprites.end(), + [] (const Sprite& a, const Sprite& b) { + return (a.x == b.x && a.oamLoc < b.oamLoc) || a.x < b.x; + }); + } + + return sprites; +} + +bool Ppu::RenderBufferSpritePixel(const Sprite& sprite, u8 obpNum, int pixelX) { + if (pixelX < 0 || obpNum == 0) { + return true; // pixel off-screen or transparent (pallete color 0) + } else if (pixelX >= static_cast(kLcdWidthPixels)) { + return false; // no point buffering more pixels as they'll be off-screen + } + + // buffer this sprite's pixel if: + // + // [BG palette color at this pixel position is 0 (always behind sprites)] + // OR + // [LCDC bit 0 unset in CGB mode (acts as a BG master priority switch)] + // OR + // [sprite has a higher priority than BG (bit 7 in attribs unset) AND] + // [BG palette color at this pixel is NOT ignoring sprite priorities ] + const auto& bgPixelInfo = scanlineBgPixelInfos_[pixelX]; + auto& spritePixelInfo = scanlineSpritePixelInfos_[pixelX]; + + if (bgPixelInfo.alwaysBehindSprites || (cgbMode_ && !(lcdc_ & 1)) || + (!(sprite.attribs & 0x80) && !bgPixelInfo.ignoreSpritePriority)) { + spritePixelInfo.transparent = false; + + if (cgbMode_) { + // draw using the color palette attribute + spritePixelInfo.paletteColorNum = ((sprite.attribs & 7) * 4) + obpNum; + } else { + // draw using the selected monochrome palette register + const u8 obp = sprite.attribs & 0x10 ? obp1_ : obp0_; + spritePixelInfo.paletteColorNum = (obp >> (obpNum * 2)) & 3; + } + } + + return true; +} + +Ppu::BgTileInfo::BgTileInfo(u8 patternNum, u8 patternCgbPaletteNum, + u8 patternBankIndex, bool patternFlipX, bool patternFlipY, + bool patternPriorityOverSprites) + : patternNum(patternNum), patternCgbPaletteNum(patternCgbPaletteNum), + patternBankIndex(patternBankIndex), patternFlipX(patternFlipX), + patternFlipY(patternFlipY), + patternPriorityOverSprites(patternPriorityOverSprites) {} + +Ppu::BgTileInfo::BgTileInfo(u8 patternNum) + : BgTileInfo(patternNum, 0, 0, false, false, false) {} + +Ppu::BgTileInfo Ppu::GetBgTileInfo(u16 tileMapEntryLocOffset) const { + const u8 tilePatternNum = vramBanks_[0][tileMapEntryLocOffset]; + + if (cgbMode_) { + const u8 tileAttribs = vramBanks_[1][tileMapEntryLocOffset]; + + return BgTileInfo(tilePatternNum, + tileAttribs & 0x07, + tileAttribs & 0x08 ? 1 : 0, + (tileAttribs & 0x20) != 0, (tileAttribs & 0x40) != 0, + (tileAttribs & 0x80) != 0); + } else { + // DMG mode doesn't support custom BG tile attribs + return BgTileInfo(tilePatternNum); + } +} + +std::pair Ppu::GetPatternLine( + u16 locOffset, u8 bankIndex, bool flipX) const { + auto patternLine = std::make_pair(vramBanks_[bankIndex][locOffset], + vramBanks_[bankIndex][locOffset + 1]); + + if (flipX) { + patternLine.first = util::ReverseBits(patternLine.first); + patternLine.second = util::ReverseBits(patternLine.second); + } + + return patternLine; +} + +std::pair Ppu::GetBgPatternLine( + u8 patternNum, u8 bankIndex, u8 lineNum, bool flipX, bool flipY) const { + assert(lineNum < 8); + + if (flipY) { + lineNum = 7 - lineNum; + } + + // treat pattern number as signed if LCDC bit 4 set, where pattern number 0 + // refers to offset $1000 in the selected VRAM bank + if (lcdc_ & 0x10) { + return GetPatternLine(patternNum * 16 + lineNum * 2, bankIndex, flipX); + } else { + return GetPatternLine(0x1000 + static_cast(patternNum) * 16 + + lineNum * 2, + bankIndex, flipX); + } +} + +std::pair Ppu::GetSpritePatternLine( + u8 patternNum, u8 bankIndex, u8 lineNum, bool flipX, bool flipY) const { + assert(bankIndex < 2 && lineNum < (IsIn8x16SpriteMode() ? 16 : 8)); + + if (flipY) { + lineNum = (IsIn8x16SpriteMode() ? 15 : 7) - lineNum; + } + + // bit 0 of pattern number ignored in 8x16 mode + if (IsIn8x16SpriteMode()) { + patternNum &= 0xfe; + } + + return GetPatternLine(patternNum * 16 + lineNum * 2, bankIndex, flipX); +} + +u8 Ppu::GetPatternNumberFromLine(const std::pair& line, + u8 numIndex) const { + assert(numIndex < 8); + return (((line.second >> (7 - numIndex)) & 1) << 1) | + ((line.first >> (7 - numIndex)) & 1); +} + +bool Ppu::IsIn8x16SpriteMode() const { + return (lcdc_ & 4) != 0; +} + +u8 Ppu::GetVramBankIndex() const { + return cgbMode_ ? vbk_ & 1 : 0; +} + +void Ppu::SetVbk(u8 val) { + vbk_ = cgbMode_ ? val | 0xfe : 0xfe; +} + +u8 Ppu::GetVbk() const { + return vbk_; +} + +void Ppu::VramWrite8(u16 loc, u8 val) { + assert(loc < std::tuple_size::value); + + // VRAM inaccessible during use + if (GetScreenMode() != PpuScreenMode::DataTransfer) { + vramBanks_[GetVramBankIndex()][loc] = val; + } +} + +u8 Ppu::VramRead8(u16 loc) const { + assert(loc < std::tuple_size::value); + + // VRAM inaccessible during use + if (GetScreenMode() != PpuScreenMode::DataTransfer) { + return vramBanks_[GetVramBankIndex()][loc]; + } else { + return 0xff; + } +} + +bool Ppu::IsOamAccessible() const { + return GetScreenMode() != PpuScreenMode::DataTransfer && + GetScreenMode() != PpuScreenMode::SearchingOam && + !dma_.IsOamDmaInProgress(); +} + +void Ppu::OamWrite8(u16 loc, u8 val, bool oamDmaWrite) { + assert(loc < oam_.size()); + + // OAM inaccessible during use unless this is a DMA write + if (oamDmaWrite || IsOamAccessible()) { + oam_[loc] = val; + } +} + +u8 Ppu::OamRead8(u16 loc) const { + assert(loc < oam_.size()); + return IsOamAccessible() ? oam_[loc] : 0xff; +} + +unsigned int Ppu::GetScreenModeMaxCycles() const { + switch (GetScreenMode()) { + case PpuScreenMode::HBlank: + return 204; + default: assert(!"unknown screen mode!"); + case PpuScreenMode::VBlank: + return 456; + case PpuScreenMode::SearchingOam: + return 80; + case PpuScreenMode::DataTransfer: + return 172; + } +} + +bool Ppu::IsLcdOn() const { + return (lcdc_ & 0x80) != 0; +} + +void Ppu::SetLcdc(u8 val) { + const bool prevLcdOn = IsLcdOn(); + lcdc_ = val; + + // check that the power state actually changed + const bool newLcdOn = IsLcdOn(); + if (prevLcdOn == newLcdOn) { + return; + } + + if (!newLcdOn) { + // reset screen mode to HBlank at line 0 + ChangeScreenMode(PpuScreenMode::HBlank); + screenModeCycles_ = 0; + SetLy(0); + } + + if (lcd_) { + lcd_->LcdPower(newLcdOn); + } +} + +u8 Ppu::GetLcdc() const { + return lcdc_; +} + +void Ppu::SetStat(u8 val) { + stat_ = (val & 0x78) | (stat_ & 0x87); // bits 7,0-2 read-only +} + +u8 Ppu::GetStat() const { + return stat_ & (IsLcdOn() ? 0xff : 0xf8); // bits 0-2 are 0 when LCD off +} + +u8 Ppu::GetLy() const { + return IsLcdOn() ? ly_ : 0; +} + +void Ppu::SetLyc(u8 val) { + lyc_ = val; +} + +u8 Ppu::GetLyc() const { + return lyc_; +} + +void Ppu::SetScy(u8 val) { + scy_ = val; +} + +u8 Ppu::GetScy() const { + return scy_; +} + +void Ppu::SetScx(u8 val) { + scx_ = val; +} + +u8 Ppu::GetScx() const { + return scx_; +} + +void Ppu::SetWy(u8 val) { + wy_ = val; +} + +u8 Ppu::GetWy() const { + return wy_; +} + +void Ppu::SetWx(u8 val) { + wx_ = val; +} + +u8 Ppu::GetWx() const { + return wx_; +} + +void Ppu::SetLcd(ILcd* lcd) { + lcd_ = lcd; +} + +void Ppu::SetBgp(u8 val) { + bgp_ = val; +} + +u8 Ppu::GetBgp() const { + return bgp_; +} + +void Ppu::SetObp0(u8 val) { + obp0_ = val; +} + +u8 Ppu::GetObp0() const { + return obp0_; +} + +void Ppu::SetObp1(u8 val) { + obp1_ = val; +} + +u8 Ppu::GetObp1() const { + return obp1_; +} + +void Ppu::WriteCgbPaletteData(CgbPaletteMemory& data, u8& selectReg, u8 val) { + if (cgbMode_ && GetScreenMode() != PpuScreenMode::DataTransfer) { + data[selectReg & 0x3f] = val; + + // increment the index value in XCPS (bits 0-5) if bit 7 is set in it + if (selectReg & 0x80) { + selectReg = (selectReg & 0xc0) | ((selectReg + 1) & 0x3f); + } + } +} + +u8 Ppu::ReadCgbPaletteData(const CgbPaletteMemory& data, u8 selectReg) const { + return GetScreenMode() != PpuScreenMode::DataTransfer ? data[selectReg & 0x3f] + : 0xff; +} + +void Ppu::SetBcps(u8 val) { + if (cgbMode_) { + bcps_ = val | 0x40; // bit 6 always set + } +} + +u8 Ppu::GetBcps() const { + return bcps_; +} + +void Ppu::SetBcpd(u8 val) { + WriteCgbPaletteData(bcpData_, bcps_, val); +} + +u8 Ppu::GetBcpd() const { + return ReadCgbPaletteData(bcpData_, bcps_); +} + +void Ppu::SetOcps(u8 val) { + if (cgbMode_) { + ocps_ = val | 0x40; // bit 6 always set + } +} + +u8 Ppu::GetOcps() const { + return ocps_; +} + +void Ppu::SetOcpd(u8 val) { + WriteCgbPaletteData(ocpData_, ocps_, val); +} + +u8 Ppu::GetOcpd() const { + return ReadCgbPaletteData(ocpData_, ocps_); +} + +void Ppu::SetScanlineSpritesLimiterEnabled(bool val) { + limitScanlineSprites_ = val; +} + +bool Ppu::IsScanlineSpritesLimiterEnabled() const { + return limitScanlineSprites_; +} + +void Ppu::SetBgRenderEnabled(bool val) { + enableBg_ = val; +} + +bool Ppu::IsBgRenderEnabled() const { + return enableBg_; +} + +void Ppu::SetBgWindowRenderEnabled(bool val) { + enableBgWindow_ = val; +} + +bool Ppu::IsBgWindowRenderEnabled() const { + return enableBgWindow_; +} + +void Ppu::SetSpritesRenderEnabled(bool val) { + enableSprites_ = val; +} + +bool Ppu::IsSpritesRenderEnabled() const { + return enableSprites_; +} + +bool Ppu::IsInCgbMode() const { + return cgbMode_; +} diff --git a/src/hw/serial.cpp b/src/hw/serial.cpp new file mode 100644 index 0000000..781944c --- /dev/null +++ b/src/hw/serial.cpp @@ -0,0 +1,87 @@ +#include "hw/cpu/cpu.h" +#include "hw/serial.h" + +// the amount of clock cycles needed for a single bit transfer. +// normal/fast transfer speed modes are controlled by SC bit 1 (CGB mode only) +// (not to be confused with CPU normal and double-speed modes, but these modes +// do also affect the speed of the transfer) +constexpr auto kBitTransferNormalCycles = 512u, + kBitTransferFastCycles = 16u; + +Serial::Serial(Cpu& cpu) : cpu_(cpu), dataOut_(nullptr) {} + +void Serial::Reset(bool cgbMode) { + cgbMode_ = cgbMode; + + nextBitTransferCycles_ = transferNextBitIdx_ = 0; + + sb_ = 0x00; + sc_ = 0x7e; + + if (dataOut_) { + dataOut_->SerialReset(); + } +} + +void Serial::Update(unsigned int cycles) { + // SC bit 7 is unset if there is no transfer in progress. + // NOTE: because we're not bothering to implement serial properly, we'll do + // nothing if we're set to listen for incoming data (SC bit 0 unset) + if (!(sc_ & 0x80) || !(sc_ & 1)) { + return; + } + + nextBitTransferCycles_ += cycles; + + // fast transfer mode is CGB mode and bit 1 of SC set + const auto transferCycles = cgbMode_ && sc_ & 2 ? kBitTransferFastCycles + : kBitTransferNormalCycles; + + while (nextBitTransferCycles_ > transferCycles) { + nextBitTransferCycles_ -= transferCycles; + + // send SB bit 7 and shift it out of the register by shifting left. + // replace the new bit 0 with the received bit. + // NOTE: because we're ignoring serial for the project, we'll receive a 1. + // this is usually what's received when there is no connection + if (dataOut_) { + dataOut_->SerialWriteBit(((sb_ >> 7) & 1) != 0); + } + sb_ = ((sb_ << 1) | 1) & 0xff; + + transferNextBitIdx_ = (transferNextBitIdx_ + 1) % 8; + if (transferNextBitIdx_ == 0) { + // finished sending a full byte. request int $58 and clear SC bit 7 + cpu_.IntfRequest(kCpuInterrupt0x58); + sc_ &= 0x7f; + + if (dataOut_) { + dataOut_->SerialOnByteWritten(); + } + } + } +} + +void Serial::SetSerialOutput(ISerialOutput* dataOut) { + dataOut_ = dataOut; +} + +void Serial::SetSb(u8 val) { + sb_ = val; +} + +u8 Serial::GetSb() const { + return sb_; +} + +void Serial::SetSc(u8 val) { + sc_ = val | 0x7c; // bits 2-6 unused +} + +u8 Serial::GetSc() const { + return sc_; +} + +bool Serial::IsInCgbMode() const { + return cgbMode_; +} \ No newline at end of file diff --git a/src/hw/timer.cpp b/src/hw/timer.cpp new file mode 100644 index 0000000..a0182c1 --- /dev/null +++ b/src/hw/timer.cpp @@ -0,0 +1,98 @@ +#include "hw/cpu/cpu.h" +#include "hw/timer.h" +#include + +// calculated as (non-double speed frequency in Hz) / (timer frequency in Hz) +enum TimerFreqInCycles : unsigned int { + kTimerFreq262144HzCycles = 16, + kTimerFreq65536HzCycles = 64, + kTimerFreq16384HzCycles = 256, + kTimerFreq4096HzCycles = 1024 +}; + +Timer::Timer(Cpu& cpu) : cpu_(cpu) {} + +void Timer::Reset() { + // initial register & clock counter values + divCycles_ = timaCycles_ = 0xff; + + div_ = 0xd3; + tima_ = tma_ = 0x00; + tac_ = 0xf8; +} + +void Timer::Update(unsigned int cycles) { + divCycles_ += cycles; + + // DIV increments at 16384 Hz, which takes 256 clock cycles + div_ += (divCycles_ / kTimerFreq16384HzCycles) & 0xff; + divCycles_ %= kTimerFreq16384HzCycles; + + // increment the timer register (TIMA) if it is running (bit 2 set of TAC) + if (tac_ & 4) { + timaCycles_ += cycles; + + // TIMA increments using the frequency value set in TAC + const auto timaFreq = GetTimaFreqInCycles(); + const auto newTima = tima_ + (timaCycles_ / timaFreq); + + if (newTima > 0xff) { + // TIMA is going to overflow. set it to TMA while accounting for the + // additional cycles after the change, and request int $50 + cpu_.IntfRequest(kCpuInterrupt0x50); + tima_ = (newTima % (0x100 - tma_)) & 0xff; + } else { + tima_ = newTima & 0xff; + } + + timaCycles_ %= timaFreq; + } +} + +unsigned int Timer::GetTimaFreqInCycles() const { + // determined by the first 2 bits of TAC + switch (tac_ & 3) { + case 0: + return kTimerFreq4096HzCycles; + case 1: + return kTimerFreq262144HzCycles; + case 2: + return kTimerFreq65536HzCycles; + default: assert(!"unknown TIMA freq value! this should be unreachable"); + case 3: + return kTimerFreq16384HzCycles; + } +} + +void Timer::ResetDiv() { + divCycles_ = div_ = 0; +} + +u8 Timer::GetDiv() const { + return div_; +} + +void Timer::SetTima(u8 val) { + tima_ = val; +} + +u8 Timer::GetTima() const { + return tima_; +} + +void Timer::SetTma(u8 val) { + tma_ = val; +} + +u8 Timer::GetTma() const { + return tma_; +} + +void Timer::SetTac(u8 val) { + tac_ = val | 0xf8; + timaCycles_ = 0; +} + +u8 Timer::GetTac() const { + return tac_; +} diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..8827614 --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,83 @@ +#include "hw/cpu/cpu.h" +#include "util.h" +#include + +u16 util::To16(u8 hi, u8 lo) { + return (hi << 8) | lo; +} + +u16 util::SetHi8(u16 val, u8 hi) { + return (hi << 8) | (val & 0xff); +} + +u16 util::SetLo8(u16 val, u8 lo) { + return (val & 0xff00) | lo; +} + +u8 util::GetHi8(u16 val) { + return (val >> 8) & 0xff; +} + +u8 util::GetLo8(u16 val) { + return val & 0xff; +} + +u8 util::ReverseBits(u8 val) { + // divide-and-conquer impl derived from https://stackoverflow.com/a/2602885 + val = (val & 0xf0) >> 4 | (val & 0x0f) << 4; + val = (val & 0xcc) >> 2 | (val & 0x33) << 2; + val = (val & 0xaa) >> 1 | (val & 0x55) << 1; + return val; +} + +unsigned int util::RescaleCycles(const Cpu& cpu, unsigned int cycles) { + // clock cycles should always be a multiple of 2 to allow for rescales. + // as far as I'm aware, no hardware components work at odd clock cycle + // intervals in normal-speed mode (min of 4) + assert(cycles % 2 == 0); + return cpu.IsInDoubleSpeedMode() ? cycles / 2 : cycles; +} + +bool util::ReadBinaryStream(std::istream& is, std::vector& data, + bool resizeToFitData) { + if ((data.size() <= 0 && !resizeToFitData) || !is) { + return false; + } + + // determine the size of the stream and resize the data vector to fit if + // we're allowed to. resize also avoids a load of reallocs during the read + is.seekg(0, std::ios::end); + const auto size = static_cast(is.tellg()); + if (data.size() < size) { + if (resizeToFitData) { + data.resize(size); + } else { + return false; + } + } + + // read the stream in one (or a few underlying) big chunks. we'll test if + // the stream's failbit or badbit is set afterwards just in case some IO + // issue occurred during the read itself + is.seekg(0, std::ios::beg); + is.read(reinterpret_cast(&data[0]), size); + if (is.fail()) { + return false; + } + + // zero fill the remaining contents of the data vector if the file was smaller + if (size < data.size()) { + std::fill(data.begin() + size, data.end(), 0); + } + + return true; +} + +bool util::WriteBinaryStream(std::ostream& os, const std::vector& data) { + if (data.size() <= 0 || !os) { + return false; + } + + os.write(reinterpret_cast(&data[0]), data.size()); + return !os.fail(); +} diff --git a/src/wxui/about_dialog.cpp b/src/wxui/about_dialog.cpp new file mode 100644 index 0000000..5604b9a --- /dev/null +++ b/src/wxui/about_dialog.cpp @@ -0,0 +1,39 @@ +#include "wxui/xpm/xpm.h" +#include "wxui/about_dialog.h" +#include + +AboutDialog::AboutDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, "About") { + auto infoSizer = new wxBoxSizer(wxVERTICAL); + + infoSizer->Add(new wxStaticBitmap(this, wxID_ANY, wxBitmap(xpmSdgbc64)), + 0, wxCENTER); + infoSizer->AddSpacer(15); + + infoSizer->Add(new wxStaticText(this, wxID_ANY, + "sdgbc - the Nintendo Game Boy Color emulator" + "."), + 0, wxCENTER); + infoSizer->AddSpacer(15); + + infoSizer->Add(new wxStaticText(this, wxID_ANY, + "Final year B.Sc. Computer Science project at" + " the University of Leicester."), + 0, wxCENTER); + infoSizer->Add(new wxStaticText(this, wxID_ANY, + "Developed by Sean Dewar, 2017-18."), + 0, wxCENTER); + infoSizer->AddSpacer(10); + + wxString versionText; + versionText << "Compiled using wxWidgets " << wxMAJOR_VERSION << "." + << wxMINOR_VERSION << " and SFML " << SFML_VERSION_MAJOR << "." + << SFML_VERSION_MINOR << "."; + infoSizer->Add(new wxStaticText(this, wxID_ANY, versionText), 0, wxCENTER); + + auto mainSizer = new wxBoxSizer(wxVERTICAL); + mainSizer->Add(infoSizer, 0, wxALL, 16); + mainSizer->Add(CreateButtonSizer(wxOK), 0, wxBOTTOM | wxCENTER, 16); + + SetSizerAndFit(mainSizer); +} diff --git a/src/wxui/app.cpp b/src/wxui/app.cpp new file mode 100644 index 0000000..88aadfb --- /dev/null +++ b/src/wxui/app.cpp @@ -0,0 +1,111 @@ +#include "debug/cpu_cmd_mode.h" +#include "wxui/app.h" +#include "wxui/main_frame.h" +#include + +#ifdef _WIN32 + #include +#endif + +wxIMPLEMENT_APP(App); + +bool App::OnInit() { + // allow wxWidgets to initialize the wxApp first + if (!wxApp::OnInit()) { + return false; + } + + if (cpuCmdMode_) { + // CPU command-line debugger mode + if (!InitCpuCmdMode()) { + return false; + } + } else { + // normal GUI mode + auto mainFrame = new MainFrame; + mainFrame->Show(true); + SetTopWindow(mainFrame); + + // load our startup ROM file if specified + if (!startupRomFilePath_.empty()) { + mainFrame->LoadCartridgeRomFile(startupRomFilePath_); + } + } + + return true; +} + +int App::OnRun() { + if (cpuCmdMode_) { + return CpuCmdMode().Run(startupRomFilePath_); + } else { + return wxApp::OnRun(); + } +} + +#ifdef _WIN32 +bool App::InitWin32Console() { + // implementation derived from Roger Sanders' @ + // https://stackoverflow.com/a/25927081 + + // NOTE: attaching to an existing console causes issues with programs like + // cmd.exe that continue to capture from stdin, so we'll always just alloc + // our own console instead :) + if (AllocConsole()) { + // attach console output to stdout/err and console input to stdin streams + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + freopen("CONIN$", "r", stdin); + + // clear stream error states + std::cout.clear(); + std::cerr.clear(); + std::cin.clear(); + std::wcout.clear(); + std::wcerr.clear(); + std::wcin.clear(); + return true; + } + + return false; +} +#endif + +bool App::InitCpuCmdMode() { + // redirect wx logs to stderr + delete wxLog::SetActiveTarget(new wxLogStream(&std::cerr)); + +#ifdef _WIN32 + // win32 won't handle console IO for us - set it up manually + if (!InitWin32Console()) { + return false; + } +#endif + + return true; +} + +bool App::IsInCpuCmdMode() const { + return cpuCmdMode_; +} + +void App::OnInitCmdLine(wxCmdLineParser& parser) { + parser.SetSwitchChars("-"); + parser.AddParam("path of the program ROM file to be loaded at startup", + wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL); + parser.AddLongSwitch("cpu-cmd", "start using the command-line CPU debugger"); +} + +bool App::OnCmdLineParsed(wxCmdLineParser& parser) { + // read startup ROM file paths, if any + if (parser.GetParamCount() > 0) { + startupRomFilePath_ = parser.GetParam(0); + } + + cpuCmdMode_ = parser.Found("cpu-cmd"); + return true; +} + +std::string App::GetStartupRomFilePath() const { + return startupRomFilePath_; +} diff --git a/src/wxui/joypad_dialog.cpp b/src/wxui/joypad_dialog.cpp new file mode 100644 index 0000000..0ac47c5 --- /dev/null +++ b/src/wxui/joypad_dialog.cpp @@ -0,0 +1,30 @@ +#include "wxui/joypad_dialog.h" +#include "wxui/xpm/xpm.h" + +JoypadDialog::JoypadDialog(wxWindow* parent) + : wxDialog(parent, wxID_ANY, "Joypad Controls") { + auto infoSizer = new wxBoxSizer(wxVERTICAL); + + infoSizer->Add(new wxStaticText(this, wxID_ANY, "Return = Start Button."), + 0, wxCENTER); + infoSizer->Add(new wxStaticText(this, wxID_ANY, "Shift = Select Button."), + 0, wxCENTER); + infoSizer->AddSpacer(15); + + infoSizer->Add(new wxStaticText(this, wxID_ANY, "Z = B Button."), + 0, wxCENTER); + infoSizer->Add(new wxStaticText(this, wxID_ANY, "X = A Button."), + 0, wxCENTER); + infoSizer->AddSpacer(15); + + infoSizer->Add(new wxStaticText(this, wxID_ANY, + "Arrow Keys = Direction Buttons."), + 0, wxCENTER); + infoSizer->AddSpacer(10); + + auto mainSizer = new wxBoxSizer(wxVERTICAL); + mainSizer->Add(infoSizer, 0, wxALL, 16); + mainSizer->Add(CreateButtonSizer(wxOK), 0, wxBOTTOM | wxCENTER, 16); + + SetSizerAndFit(mainSizer); +} diff --git a/src/wxui/lcd_canvas.cpp b/src/wxui/lcd_canvas.cpp new file mode 100644 index 0000000..d532f38 --- /dev/null +++ b/src/wxui/lcd_canvas.cpp @@ -0,0 +1,103 @@ +#include "wxui/lcd_canvas.h" + +LcdCanvas::LcdCanvas(wxWindow* parent, wxWindowID id, const wxPoint& pos, + const wxSize& size, long style) + : SfmlCanvas(parent, id, pos, size, style), + lcdFrontBuffer_(&lcdFrameBuffers_[0]), + lcdBackBuffer_(&lcdFrameBuffers_[1]), + lcdTextureNeedsRefresh_(false), maintainAspectRatio_(true), + isLcdOn_(false) { + setFramerateLimit(60); + lcdTexture_.setSmooth(true); + + for (auto& b : lcdFrameBuffers_) { + b.create(kLcdWidthPixels, kLcdHeightPixels); + } +} + +void LcdCanvas::CanvasRender() { + clear(); + + // save current view & create a view scaling the LCD to fit the canvas + const auto prevView = getView(); + sf::View newView(sf::FloatRect(0.0f, 0.0f, + kLcdWidthPixels, kLcdHeightPixels)); + + if (maintainAspectRatio_) { + const auto prevRatio = prevView.getSize().x / prevView.getSize().y, + targetRatio = newView.getSize().x / newView.getSize().y; + + if (prevRatio > targetRatio) { + // view is too wide + newView.setSize(newView.getSize().x * (prevRatio / targetRatio), + newView.getSize().y); + } else { + // view is too tall + newView.setSize(newView.getSize().x, + newView.getSize().y * (targetRatio / prevRatio)); + } + } + + setView(newView); + + // refresh the texture if a new frame was just finished + if (lcdTextureNeedsRefresh_) { + std::unique_lock lock(lcdFrontBufferMutex_); + lcdTexture_.loadFromImage(*lcdFrontBuffer_); + lcdTextureNeedsRefresh_ = false; + } + + draw(sf::Sprite(lcdTexture_)); + + // restore the previous view + setView(prevView); +} + +void LcdCanvas::LcdRefresh() { + std::unique_lock lockFront(lcdFrontBufferMutex_); + std::swap(lcdFrontBuffer_, lcdBackBuffer_); + lcdTextureNeedsRefresh_ = true; +} + +void LcdCanvas::ClearToWhite() { + lcdBackBuffer_->create(kLcdWidthPixels, kLcdHeightPixels, + sf::Color(0xff, 0xff, 0xff)); + { + std::unique_lock lockFront(lcdFrontBufferMutex_); + lcdFrontBuffer_->create(kLcdWidthPixels, kLcdHeightPixels, + sf::Color(0xff, 0xff, 0xff)); + } + + LcdRefresh(); +} + +void LcdCanvas::LcdPutPixel(unsigned int x, unsigned int y, + const RgbColor& color) { + if (x < kLcdWidthPixels && y < kLcdHeightPixels) { + lcdBackBuffer_->setPixel(x, y, sf::Color(color.r, color.g, color.b)); + } +} + +void LcdCanvas::LcdPower(bool powerOn) { + isLcdOn_ = powerOn; + + if (!isLcdOn_) { + ClearToWhite(); + } +} + +void LcdCanvas::SetMaintainAspectRatio(bool val) { + maintainAspectRatio_ = val; +} + +bool LcdCanvas::IsMaintainingAspectRatio() const { + return maintainAspectRatio_; +} + +void LcdCanvas::SetSmoothFilterEnabled(bool val) { + lcdTexture_.setSmooth(val); +} + +bool LcdCanvas::IsSmoothFilterEnabled() const { + return lcdTexture_.isSmooth(); +} diff --git a/src/wxui/main_frame.cpp b/src/wxui/main_frame.cpp new file mode 100644 index 0000000..24d8ebf --- /dev/null +++ b/src/wxui/main_frame.cpp @@ -0,0 +1,558 @@ +#include "wxui/about_dialog.h" +#include "wxui/joypad_dialog.h" +#include "wxui/main_frame.h" +#include "wxui/xpm/xpm.h" +#include + +enum MenuItemId { + kMenuIdOpenRomFile = wxID_HIGHEST + 1, + kMenuIdImportBattery, + kMenuIdExportBattery, + + kMenuIdReset, + kMenuIdResetInDmgMode, + kMenuIdPause, + kMenuIdLimitFramerate, + + kMenuIdEnableBg, + kMenuIdEnableBgWindow, + kMenuIdEnableSprites, + kMenuIdLimitScanlineSprites, + kMenuIdMaintainAspectRatio, + kMenuIdSmoothenVideo, + + kMenuIdMuteSound, + kMenuIdMuteCh1, + kMenuIdMuteCh2, + kMenuIdMuteCh3, + kMenuIdMuteCh4, + + kMenuIdJoypadImpossibleInputs, + kMenuIdJoypadControls +}; + +wxBEGIN_EVENT_TABLE(MainFrame, wxFrame) + EVT_CLOSE(MainFrame::OnClose) + EVT_SIZE(MainFrame::OnSize) + EVT_MENU(wxID_EXIT, MainFrame::OnExit) + + EVT_KEY_DOWN(MainFrame::OnKeyDown) + EVT_KEY_UP(MainFrame::OnKeyUp) + + EVT_MENU(kMenuIdOpenRomFile, MainFrame::OnOpenRomFile) + EVT_MENU(kMenuIdImportBattery, MainFrame::OnImportBattery) + EVT_MENU(kMenuIdExportBattery, MainFrame::OnExportBattery) + + EVT_MENU(kMenuIdReset, MainFrame::OnReset) + EVT_MENU(kMenuIdResetInDmgMode, MainFrame::OnResetInDmgMode) + EVT_MENU(kMenuIdPause, MainFrame::OnPause) + EVT_MENU(kMenuIdLimitFramerate, MainFrame::OnLimitFramerate) + + EVT_MENU(kMenuIdEnableBg, MainFrame::OnEnableBg) + EVT_MENU(kMenuIdEnableBgWindow, MainFrame::OnEnableBgWindow) + EVT_MENU(kMenuIdEnableSprites, MainFrame::OnEnableSprites) + EVT_MENU(kMenuIdLimitScanlineSprites, MainFrame::OnLimitScanlineSprites) + EVT_MENU(kMenuIdMaintainAspectRatio, MainFrame::OnMaintainAspectRatio) + EVT_MENU(kMenuIdSmoothenVideo, MainFrame::OnSmoothenVideo) + + EVT_MENU(kMenuIdMuteSound, MainFrame::OnMuteSound) + EVT_MENU(kMenuIdMuteCh1, MainFrame::OnMuteCh1) + EVT_MENU(kMenuIdMuteCh2, MainFrame::OnMuteCh2) + EVT_MENU(kMenuIdMuteCh3, MainFrame::OnMuteCh3) + EVT_MENU(kMenuIdMuteCh4, MainFrame::OnMuteCh4) + + EVT_MENU(kMenuIdJoypadImpossibleInputs, MainFrame::OnJoypadImpossibleInputs) + + EVT_MENU(kMenuIdJoypadControls, MainFrame::OnJoypadControls) + EVT_MENU(wxID_ABOUT, MainFrame::OnAbout) +wxEND_EVENT_TABLE() + +MainFrame::MainFrame() + : wxFrame(nullptr, wxID_ANY, "sdgbc"), lcdCanvas_(nullptr) { + // create a bundle of all our different sized icons to use as the window icon + wxIconBundle iconBundle; + iconBundle.AddIcon(wxIcon(xpmSdgbc16)); + iconBundle.AddIcon(wxIcon(xpmSdgbc24)); + iconBundle.AddIcon(wxIcon(xpmSdgbc32)); + iconBundle.AddIcon(wxIcon(xpmSdgbc48)); + iconBundle.AddIcon(wxIcon(xpmSdgbc64)); + iconBundle.AddIcon(wxIcon(xpmSdgbc96)); + iconBundle.AddIcon(wxIcon(xpmSdgbc256)); + SetIcons(iconBundle); + + // create menu items + auto menuFile = new wxMenu; + menuFile->Append(kMenuIdOpenRomFile, "&Open ROM File...\tCtrl+O"); + menuFile->AppendSeparator(); + menuFile->Append(kMenuIdImportBattery, + "&Import Battery-Packed RAM Snapshot..."); + menuFile->Append(kMenuIdExportBattery, + "&Export Battery-Packed RAM Snapshot..."); + menuFile->AppendSeparator(); + menuFile->Append(wxID_EXIT); + + auto menuEmulator = new wxMenu; + menuEmulator->Append(kMenuIdReset, "&Reset\tCtrl+R"); + + menuEmulator->AppendCheckItem(kMenuIdPause, "&Pause\tCtrl+P"); + menuEmulator->Check(kMenuIdPause, emulator_.IsPaused()); + + menuEmulator->AppendSeparator(); + menuEmulator->Append(kMenuIdResetInDmgMode, + "Reset in Game Boy Compatibility Mode"); + + menuEmulator->AppendSeparator(); + menuEmulator->AppendCheckItem(kMenuIdLimitFramerate, + "&Limit Emulation Speed\tCtrl+L"); + + auto menuVideo = new wxMenu; + menuVideo->AppendCheckItem(kMenuIdEnableBg, "Render Background Layer"); + menuVideo->AppendCheckItem(kMenuIdEnableBgWindow, "Render Window Layer"); + menuVideo->AppendCheckItem(kMenuIdEnableSprites, + "Render Sprite (Object) Layer"); + + menuVideo->AppendSeparator(); + menuVideo->AppendCheckItem(kMenuIdLimitScanlineSprites, + "Emulate Scanline Sprite Limit"); + + menuVideo->AppendSeparator(); + menuVideo->AppendCheckItem(kMenuIdMaintainAspectRatio, + "&Maintain Aspect Ratio"); + menuVideo->AppendCheckItem(kMenuIdSmoothenVideo, "&Smoothen Output"); + + auto menuAudio = new wxMenu; + menuAudio->AppendCheckItem(kMenuIdMuteSound, "&Mute Audio\tCtrl+M"); + + menuAudio->AppendSeparator(); + menuAudio->AppendCheckItem(kMenuIdMuteCh1, + "Mute Sound Channel 1 (Square with Sweep)"); + menuAudio->AppendCheckItem(kMenuIdMuteCh2, + "Mute Sound Channel 2 (Square)"); + menuAudio->AppendCheckItem(kMenuIdMuteCh3, + "Mute Sound Channel 3 (Wave)"); + menuAudio->AppendCheckItem(kMenuIdMuteCh4, + "Mute Sound Channel 4 (Noise)"); + + auto menuJoypad = new wxMenu; + menuJoypad->AppendCheckItem(kMenuIdJoypadImpossibleInputs, + "&Allow Impossible Inputs\tCtrl+I"); + + auto menuHelp = new wxMenu; + menuHelp->Append(kMenuIdJoypadControls, "&Joypad Controls"); + menuHelp->Append(wxID_ABOUT); + + // add menu items to menu bar + auto menuBar = new wxMenuBar; + menuBar->Append(menuFile, "&File"); + menuBar->Append(menuEmulator, "&Emulation"); + menuBar->Append(menuVideo, "&Video"); + menuBar->Append(menuAudio, "&Audio"); + menuBar->Append(menuJoypad, "&Joypad"); + menuBar->Append(menuHelp, "&Help"); + + SetMenuBar(menuBar); + + // add main screen elements + auto mainSizer = new wxBoxSizer(wxVERTICAL); + + // add the lcd video canvas + lcdCanvas_ = new LcdCanvas(this, wxID_ANY, wxPoint(0, 0), + wxSize(kLcdWidthPixels, kLcdHeightPixels)); + emulator_.SetVideoLcd(lcdCanvas_); + mainSizer->Add(lcdCanvas_, 1, wxEXPAND | wxALL, 0); + + // manually connect key events so we can capture them + RecursivelyConnectKeyEvents(this); + + // allow ROM loads via file drops onto the window + SetDropTarget(new MainFrameFileDropTarget(*this)); + + // position controls inside sizer correctly and fit the window to accommodate + mainSizer->Layout(); + SetSizerAndFit(mainSizer); + SetSize(wxSize(600, 570)); // initial size of the window + + emulator_.SetApuOutput(&audioOut_); + audioOut_.StartStreaming(); + + UpdateUIState(); +} + +void MainFrame::UpdateUIState() { + UpdateTitle(); + UpdateMenu(); +} + +void MainFrame::UpdateTitle() { + wxString title; + if (emulator_.IsCartridgeRomLoaded()) { + title << emulator_.GetCartridgeRomFileName() + << (emulator_.IsPaused() ? " (Paused)" : "") + << (emulator_.GetCartridgeExtMeta().hasBattery ? " [Battery]" : "") + << (emulator_.IsInCgbMode() ? "" : " [Game Boy compatibility mode]") + << " - "; + } + + title << "sdgbc"; + SetTitle(title); +} + +void MainFrame::UpdateMenu() { + const bool romLoaded = emulator_.IsCartridgeRomLoaded(), + romBattery = emulator_.GetCartridgeExtMeta().hasBattery && + romLoaded; + + auto menu = GetMenuBar(); + + // file menu + menu->Enable(kMenuIdImportBattery, romBattery); + menu->Enable(kMenuIdExportBattery, romBattery); + + // emulation menu + menu->Enable(kMenuIdReset, romLoaded); + menu->Enable(kMenuIdResetInDmgMode, romLoaded); + menu->Check(kMenuIdLimitFramerate, emulator_.IsLimitingFramerate()); + + // video menu + menu->Check(kMenuIdEnableBg, emulator_.IsVideoBgRenderEnabled()); + menu->Check(kMenuIdEnableBgWindow, emulator_.IsVideoBgWindowRenderEnabled()); + menu->Check(kMenuIdEnableSprites, emulator_.IsVideoSpritesRenderEnabled()); + menu->Check(kMenuIdLimitScanlineSprites, + emulator_.IsVideoScanlineSpritesLimiterEnabled()); + menu->Check(kMenuIdMaintainAspectRatio, + lcdCanvas_->IsMaintainingAspectRatio()); + menu->Check(kMenuIdSmoothenVideo, lcdCanvas_->IsSmoothFilterEnabled()); + + // audio menu + menu->Check(kMenuIdMuteSound, audioOut_.AudioIsMuted()); + menu->Check(kMenuIdMuteCh1, emulator_.IsApuCh1Muted()); + menu->Check(kMenuIdMuteCh2, emulator_.IsApuCh2Muted()); + menu->Check(kMenuIdMuteCh3, emulator_.IsApuCh3Muted()); + menu->Check(kMenuIdMuteCh4, emulator_.IsApuCh4Muted()); + + // joypad menu + menu->Check(kMenuIdJoypadImpossibleInputs, + emulator_.AreJoypadImpossibleInputsAllowed()); +} + +bool MainFrame::LoadCartridgeRomFile(std::string filePath) { + // prompt the user for a file if empty filePath given + if (filePath.empty()) { + wxFileDialog openFileDialog(this, "Open ROM File", wxEmptyString, + wxEmptyString, + "Game Boy and Game Boy Color ROMs (*.gb;*.gbc)|" + "*.gb;*.gbc|" + "All Files (*.*)|*.*", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (openFileDialog.ShowModal() == wxID_CANCEL) { + return false; + } + + filePath = openFileDialog.GetPath().ToStdString(); + } + + const auto fileName = wxFileName(filePath).GetFullName().ToStdString(); + const auto result = emulator_.LoadCartridgeRomFile(filePath, fileName); + if (result != RomLoadResult::Ok) { + wxMessageDialog(this, Cartridge::GetRomLoadResultAsMessage(result), + "Failed to load ROM file", wxICON_ERROR | wxOK | wxCENTER) + .ShowModal(); + } + + UpdateUIState(); + return result == RomLoadResult::Ok; +} + +void MainFrame::OnOpenRomFile(wxCommandEvent&) { + LoadCartridgeRomFile(); +} + +bool MainFrame::ExportCartridgeBatteryExtRam(std::string filePath) { + if (!emulator_.IsCartridgeRomLoaded() || + !emulator_.GetCartridgeExtMeta().hasBattery) { + return false; + } + + // prompt the user for a file if empty filePath given + if (filePath.empty()) { + wxFileDialog saveFileDialog(this, "Export Battery-Packed RAM Snapshot", + wxEmptyString, wxEmptyString, + "Battery-Packed RAM Snapshot (*.sav)|*.sav|" + "All Files (*.*)|*.*", + wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + saveFileDialog.SetPath(emulator_.GetCartridgeRomFilePath() + + "EXPORTED.sav"); + + if (saveFileDialog.ShowModal() == wxID_CANCEL) { + return false; + } + + filePath = saveFileDialog.GetPath().ToStdString(); + } + + const bool success = emulator_.ExportCartridgeBatteryExtRam(filePath); + if (!success) { + wxMessageDialog(this, + "Failed to export snapshot file! Ensure that the file is " + "writable and try again.", + "Battery-packed RAM export failed", + wxICON_ERROR | wxOK | wxCENTER) + .ShowModal(); + } + + return success; +} + +bool MainFrame::ImportCartridgeBatteryExtRam(std::string filePath) { + if (!emulator_.IsCartridgeRomLoaded() || + !emulator_.GetCartridgeExtMeta().hasBattery) { + return false; + } + + wxMessageDialog confirmDialog(this, + "Importing a new snapshot will reset the " + "running program and overwrite the currently " + "used snapshot!\n\n" + "Saved data within the program (leaderboard " + "scores, character saves, etc.) will be " + "overwritten!\n\n" + "Consider exporting the currently used " + "snapshot first to avoid losing important " + "saved data.\n\n" + "Really continue?", + "Import Battery-Packed RAM Snapshot", + wxICON_EXCLAMATION | wxYES_NO | wxCENTER); + + if (confirmDialog.ShowModal() != wxID_YES) { + return false; + } + + // prompt the user for a file if empty filePath given + if (filePath.empty()) { + wxFileDialog openFileDialog(this, "Import Battery-Packed RAM Snapshot", + wxEmptyString, wxEmptyString, + "Battery-Packed RAM Snapshot (*.sav)|*.sav|" + "All Files (*.*)|*.*", + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (openFileDialog.ShowModal() == wxID_CANCEL) { + return false; + } + + filePath = openFileDialog.GetPath().ToStdString(); + } + + const bool success = emulator_.ImportCartridgeBatteryExtRam(filePath); + if (!success) { + wxMessageDialog(this, + "Failed to import snapshot file! Ensure that the file is " + "readable and compatible with the loaded program and try " + "again.", + "Battery-packed RAM import failed", + wxICON_ERROR | wxOK | wxCENTER) + .ShowModal(); + } + + UpdateUIState(); + return success; +} + +void MainFrame::OnImportBattery(wxCommandEvent&) { + ImportCartridgeBatteryExtRam(); +} + +void MainFrame::OnExportBattery(wxCommandEvent&) { + ExportCartridgeBatteryExtRam(); +} + +void MainFrame::OnPause(wxCommandEvent& event) { + emulator_.SetPaused(event.IsChecked()); + UpdateUIState(); +} + +void MainFrame::OnReset(wxCommandEvent&) { + emulator_.Reset(); + UpdateUIState(); +} + +void MainFrame::OnResetInDmgMode(wxCommandEvent&) { + emulator_.Reset(true); + UpdateUIState(); +} + +void MainFrame::OnLimitFramerate(wxCommandEvent& event) { + emulator_.SetLimitFramerate(event.IsChecked()); +} + +void MainFrame::OnEnableBg(wxCommandEvent& event) { + emulator_.SetVideoBgRenderEnabled(event.IsChecked()); +} + +void MainFrame::OnEnableBgWindow(wxCommandEvent& event) { + emulator_.SetVideoBgWindowRenderEnabled(event.IsChecked()); +} + +void MainFrame::OnEnableSprites(wxCommandEvent& event) { + emulator_.SetVideoSpritesRenderEnabled(event.IsChecked()); +} + +void MainFrame::OnLimitScanlineSprites(wxCommandEvent& event) { + emulator_.SetVideoScanlineSpritesLimiterEnabled(event.IsChecked()); +} + +void MainFrame::OnMaintainAspectRatio(wxCommandEvent& event) { + lcdCanvas_->SetMaintainAspectRatio(event.IsChecked()); +} + +void MainFrame::OnSmoothenVideo(wxCommandEvent& event) { + lcdCanvas_->SetSmoothFilterEnabled(event.IsChecked()); +} + +void MainFrame::OnMuteSound(wxCommandEvent& event) { + if (event.IsChecked()) { + audioOut_.StopStreaming(); + } else { + audioOut_.StartStreaming(); + } +} + +void MainFrame::OnMuteCh1(wxCommandEvent& event) { + emulator_.SetApuMuteCh1(event.IsChecked()); +} + +void MainFrame::OnMuteCh2(wxCommandEvent& event) { + emulator_.SetApuMuteCh2(event.IsChecked()); +} + +void MainFrame::OnMuteCh3(wxCommandEvent& event) { + emulator_.SetApuMuteCh3(event.IsChecked()); +} + +void MainFrame::OnMuteCh4(wxCommandEvent& event) { + emulator_.SetApuMuteCh4(event.IsChecked()); +} + +void MainFrame::OnJoypadImpossibleInputs(wxCommandEvent& event) { + emulator_.SetJoypadImpossibleInputsAllowed(event.IsChecked()); +} + +void MainFrame::OnJoypadControls(wxCommandEvent&) { + JoypadDialog controlsDialog(this); + controlsDialog.CentreOnParent(); + controlsDialog.ShowModal(); +} + +void MainFrame::OnAbout(wxCommandEvent&) { + AboutDialog aboutDialog(this); + aboutDialog.CentreOnParent(); + aboutDialog.ShowModal(); +} + +void MainFrame::OnExit(wxCommandEvent&) { + Close(true); +} + +void MainFrame::OnClose(wxCloseEvent&) { + Destroy(); +} + +void MainFrame::OnSize(wxSizeEvent&) { + Layout(); +} + +bool MainFrame::HandleJoypadKeyEvent(wxKeyEvent& event) { + const bool keyDown = event.GetEventType() == wxEVT_KEY_DOWN; + + switch (event.GetKeyCode()) { + case 'Z': + emulator_.SetJoypadKeyState(JoypadKey::B, keyDown); + return true; + case 'X': + emulator_.SetJoypadKeyState(JoypadKey::A, keyDown); + return true; + case WXK_LEFT: + emulator_.SetJoypadKeyState(JoypadKey::Left, keyDown); + return true; + case WXK_RIGHT: + emulator_.SetJoypadKeyState(JoypadKey::Right, keyDown); + return true; + case WXK_UP: + emulator_.SetJoypadKeyState(JoypadKey::Up, keyDown); + return true; + case WXK_DOWN: + emulator_.SetJoypadKeyState(JoypadKey::Down, keyDown); + return true; + case WXK_SHIFT: + emulator_.SetJoypadKeyState(JoypadKey::Select, keyDown); + return true; + case WXK_RETURN: + emulator_.SetJoypadKeyState(JoypadKey::Start, keyDown); + return true; + + default: + event.Skip(); + return false; + } +} + +void MainFrame::OnKeyDown(wxKeyEvent& event) { + HandleJoypadKeyEvent(event); +} + +void MainFrame::OnKeyUp(wxKeyEvent& event) { + HandleJoypadKeyEvent(event); +} + +void MainFrame::RecursivelyConnectKeyEvents(wxWindow* childComponent) { + // frames do not capture key events from their children, so to do so, we need + // to recursively go through our children and connect events so that they + // propogate to our frame + // + // implementation from https://wiki.wxwidgets.org/Catching_key_events_globally + if (!childComponent) { + return; + } + + childComponent->Connect(wxID_ANY, + wxEVT_KEY_DOWN, + wxKeyEventHandler(MainFrame::OnKeyDown), + nullptr, + this); + + childComponent->Connect(wxID_ANY, + wxEVT_KEY_UP, + wxKeyEventHandler(MainFrame::OnKeyUp), + nullptr, + this); + + auto childNode = childComponent->GetChildren().GetFirst(); + + while (childNode) { + auto child = childNode->GetData(); + RecursivelyConnectKeyEvents(child); + + childNode = childNode->GetNext(); + } +} + +MainFrameFileDropTarget::MainFrameFileDropTarget(MainFrame& mainFrame) + : mainFrame_(mainFrame) {} + +bool MainFrameFileDropTarget::OnDropFiles(wxCoord, wxCoord, + const wxArrayString& filenames) { + // only accept one dropped file + if (filenames.GetCount() != 1) { + return false; + } + + if (wxFileName(filenames[0]).GetExt() == "sav") { + // battery-packed ext RAM snapshot + mainFrame_.ImportCartridgeBatteryExtRam(filenames[0].ToStdString()); + } else { + // probably a ROM file + mainFrame_.LoadCartridgeRomFile(filenames[0].ToStdString()); + } + + return true; +} diff --git a/src/wxui/sfml_canvas.cpp b/src/wxui/sfml_canvas.cpp new file mode 100644 index 0000000..31d98f8 --- /dev/null +++ b/src/wxui/sfml_canvas.cpp @@ -0,0 +1,78 @@ +#include "wxui/sfml_canvas.h" + +#ifdef __WXGTK__ + #include + #include +#endif + +wxBEGIN_EVENT_TABLE(SfmlCanvas, wxControl) + EVT_IDLE(SfmlCanvas::OnIdle) + EVT_PAINT(SfmlCanvas::OnPaint) + EVT_SIZE(SfmlCanvas::OnSize) + EVT_ERASE_BACKGROUND(SfmlCanvas::OnEraseBackground) + EVT_SET_FOCUS(SfmlCanvas::OnSetFocus) + EVT_KILL_FOCUS(SfmlCanvas::OnKillFocus) +wxEND_EVENT_TABLE() + +SfmlCanvas::SfmlCanvas(wxWindow* parent, wxWindowID id, const wxPoint& pos, + const wxSize& size, long style) + : wxControl(parent, id, pos, size, style), isInFocus_(false) { +#ifdef __WXGTK__ + + // GTK requires the X11 identifier of the widget + // NOTE: using SFML 2.3.2 or higher causes the program to crash with an X11 + // BadAccess! not exactly sure why but its a weird incompatability + gtk_widget_realize(m_wxwindow); + gtk_widget_set_double_buffered(m_wxwindow, false); + const auto window = gtk_widget_get_window(m_wxwindow); + XFlush(GDK_WINDOW_XDISPLAY(window)); + sf::RenderWindow::create(GDK_WINDOW_XID(window)); + +#else + + // NOTE: not sure if this works on mac + sf::RenderWindow::create(GetHandle()); + +#endif +} + +void SfmlCanvas::CanvasRender() {} + +bool SfmlCanvas::HasFocus() const { + return isInFocus_; +} + +void SfmlCanvas::OnIdle(wxIdleEvent&) { + Refresh(); // send a paint msg when control is idle to ensure max frame rate +} + +void SfmlCanvas::OnPaint(wxPaintEvent&) { + wxPaintDC dc(this); // prepare control to be re-painted + + // adjust size of the sfml view to work with wxWidgets properly + const auto canvasWxSize = GetSize(); + const auto canvasSize = sf::Vector2f(canvasWxSize.x, canvasWxSize.y); + + setView(sf::View(sf::FloatRect(0.0f, 0.0f, canvasSize.x, canvasSize.y))); + + CanvasRender(); + display(); +} + +void SfmlCanvas::OnSize(wxSizeEvent& event) { + // adjust size of the sfml canvas render window to work with wxWidgets + setSize(sf::Vector2u(event.GetSize().x, event.GetSize().y)); +} + +void SfmlCanvas::OnSetFocus(wxFocusEvent& event) { + isInFocus_ = true; + event.Skip(); +} + +void SfmlCanvas::OnKillFocus(wxFocusEvent& event) { + isInFocus_ = false; + event.Skip(); +} + +// do not erase what we've drawn - avoids flickering +void SfmlCanvas::OnEraseBackground(wxEraseEvent&) {}