├── .gitignore ├── 32blit ├── icon.png ├── splash.png ├── assets │ ├── bg.png │ ├── 12x8font.png │ └── bg_square.png ├── gameblit.hpp ├── assets.yml ├── metadata.yml ├── menu.hpp ├── CMakeLists.txt ├── menu.cpp └── gameblit.cpp ├── .gitmodules ├── tests ├── CMakeLists.txt └── runner.cpp ├── core ├── GCCBuiltin.h ├── CMakeLists.txt ├── AGBDisplay.h ├── DMGDisplay.h ├── DMGAPU.h ├── AGBAPU.h ├── DMGSaveState.h ├── DMGMemory.h ├── DMGRegs.h ├── DMGCPU.h ├── AGBRegs.h ├── AGBMemory.h ├── AGBCPU.h ├── AGBMemory.cpp ├── DMGMemory.cpp ├── DMGDisplay.cpp └── AGBAPU.cpp ├── minsdl ├── CMakeLists.txt ├── emscripten-shell.html └── Main.cpp ├── LICENSE ├── README.md ├── CMakeLists.txt └── .github └── workflows └── build.yml /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build.*/ 3 | .vscode 4 | roms 5 | -------------------------------------------------------------------------------- /32blit/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daft-Freak/DaftBoy32/HEAD/32blit/icon.png -------------------------------------------------------------------------------- /32blit/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daft-Freak/DaftBoy32/HEAD/32blit/splash.png -------------------------------------------------------------------------------- /32blit/assets/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daft-Freak/DaftBoy32/HEAD/32blit/assets/bg.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "32blit/DUH"] 2 | path = 32blit/DUH 3 | url = https://github.com/Daft-Freak/DUH 4 | -------------------------------------------------------------------------------- /32blit/assets/12x8font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daft-Freak/DaftBoy32/HEAD/32blit/assets/12x8font.png -------------------------------------------------------------------------------- /32blit/assets/bg_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daft-Freak/DaftBoy32/HEAD/32blit/assets/bg_square.png -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(test-runner 2 | runner.cpp 3 | ) 4 | find_package(PNG REQUIRED) 5 | target_link_libraries(test-runner PNG::PNG DaftBoyCore) -------------------------------------------------------------------------------- /32blit/gameblit.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "32blit.hpp" 6 | 7 | void init(); 8 | void update(uint32_t time); 9 | void render(uint32_t time); 10 | -------------------------------------------------------------------------------- /32blit/assets.yml: -------------------------------------------------------------------------------- 1 | assets.cpp: 2 | assets/12x8font.png: 3 | name: tall_font 4 | type: font/image 5 | vertical_spacing: 0 6 | space_width: 4 7 | 8 | assets/bg.png: asset_background 9 | assets/bg_square.png: asset_background_square 10 | -------------------------------------------------------------------------------- /32blit/metadata.yml: -------------------------------------------------------------------------------- 1 | title: DaftBoy32 2 | author: Daft_Freak 3 | description: Yet another slightly unfinished Game Boy (Color) Emulator! 4 | version: v1.7.0 5 | icon: 6 | file: icon.png 7 | splash: 8 | file: splash.png 9 | category: emulator 10 | url: https://github.com/Daft-Freak/DaftBoy32 11 | filetypes: ["gb", "gbc"] 12 | -------------------------------------------------------------------------------- /core/GCCBuiltin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef _MSC_VER 4 | #include 5 | 6 | #define __builtin_bswap32(x) _byteswap_ulong(x) 7 | #define __builtin_bswap64(x) _byteswap_uint64(x) 8 | 9 | inline int __builtin_clz(unsigned int x) 10 | { 11 | unsigned long index; 12 | _BitScanReverse(&index, x); 13 | return 31 ^ index; 14 | } 15 | 16 | #define __builtin_unreachable() __assume(0) 17 | 18 | // else generic fallbask? 19 | #endif -------------------------------------------------------------------------------- /core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(DaftBoyCore INTERFACE) 2 | 3 | target_sources(DaftBoyCore INTERFACE 4 | DMGAPU.cpp 5 | DMGCPU.cpp 6 | DMGDisplay.cpp 7 | DMGMemory.cpp 8 | ) 9 | 10 | target_include_directories(DaftBoyCore INTERFACE ${CMAKE_CURRENT_LIST_DIR}) 11 | 12 | # DBA 13 | add_library(DaftBoyAdvanceCore INTERFACE) 14 | 15 | target_sources(DaftBoyAdvanceCore INTERFACE 16 | AGBAPU.cpp 17 | AGBCPU.cpp 18 | AGBDisplay.cpp 19 | AGBMemory.cpp 20 | ) 21 | 22 | target_include_directories(DaftBoyAdvanceCore INTERFACE ${CMAKE_CURRENT_LIST_DIR}) -------------------------------------------------------------------------------- /32blit/menu.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "engine/menu.hpp" 7 | 8 | class Menu final : public blit::Menu { 9 | public: 10 | Menu(std::string_view title, std::vector items, const blit::Font &font = blit::minimal_font); 11 | 12 | void set_on_item_activated(void (*func)(const Item &)); 13 | 14 | private: 15 | void render_item(const Item &item, int y, int index) const override; 16 | void item_activated(const Item &item) override; 17 | 18 | std::vector items_vec; 19 | 20 | void (*on_item_pressed)(const Item &) = nullptr; 21 | }; -------------------------------------------------------------------------------- /minsdl/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # minimal SDL shell 2 | 3 | add_executable(DaftBoySDL Main.cpp) 4 | 5 | find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) 6 | 7 | target_link_libraries(DaftBoySDL DaftBoyCore DaftBoyAdvanceCore SDL3::SDL3) 8 | 9 | if(EMSCRIPTEN) 10 | # Emscripten-specific magic 11 | 12 | set(EMSCRIPTEN_SHELL ${CMAKE_CURRENT_LIST_DIR}/emscripten-shell.html) 13 | 14 | set_target_properties(DaftBoySDL PROPERTIES 15 | SUFFIX ".html" 16 | LINK_FLAGS "-s ENVIRONMENT=web -s ASYNCIFY=1 --shell-file ${EMSCRIPTEN_SHELL}" 17 | LINK_DEPENDS ${EMSCRIPTEN_SHELL} 18 | ) 19 | endif() 20 | -------------------------------------------------------------------------------- /32blit/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | blit_executable(DaftBoy32 2 | menu.cpp 3 | 4 | gameblit.cpp 5 | ) 6 | blit_assets_yaml(DaftBoy32 assets.yml) 7 | blit_metadata(DaftBoy32 metadata.yml) 8 | 9 | add_subdirectory(DUH) 10 | 11 | target_link_libraries(DaftBoy32 DaftBoyCore DUH) 12 | 13 | if(32BLIT_PICO) 14 | # we do very little in render and need the extra RAM 15 | target_compile_definitions(DaftBoy32 PRIVATE -DDOUBLE_BUFFERED_HIRES=0) 16 | 17 | if(${PICO_BOARD} STREQUAL "pimoroni_picosystem") 18 | target_compile_definitions(DaftBoy32 PRIVATE -DDISPLAY_RGB565) 19 | elseif(${PICO_ADDON} STREQUAL "pimoroni_picovision") 20 | target_compile_definitions(DaftBoy32 PRIVATE -DDISPLAY_RB_SWAP) 21 | endif() 22 | else() 23 | set_target_properties(DaftBoy32 PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) 24 | endif() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Charlie Birks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /core/AGBDisplay.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | class AGBCPU; 5 | class AGBMemory; 6 | 7 | class AGBDisplay 8 | { 9 | public: 10 | AGBDisplay(AGBCPU &cpu); 11 | 12 | void reset(); 13 | 14 | void update(); 15 | int getCyclesToNextUpdate(uint32_t cycleCount) const; 16 | 17 | void setFramebuffer(uint16_t *data); 18 | 19 | uint16_t readReg(uint32_t addr, uint16_t val); 20 | bool writeReg(uint32_t addr, uint16_t data); 21 | 22 | private: 23 | void drawScanLine(int y); 24 | 25 | AGBCPU &cpu; 26 | AGBMemory &mem; 27 | 28 | uint32_t lastUpdateCycle = 0; 29 | 30 | uint8_t y = 0; 31 | bool yInWin0 = false, yInWin1 = false; 32 | 33 | // internal reference points for affine bg 34 | int32_t refPointX[2]{0}, refPointY[2]{0}; 35 | 36 | static const int scanlineDots = 308; // * 4 cpu cycles 37 | static const int screenWidth = 240, screenHeight = 160; 38 | 39 | unsigned int remainingScanlineDots = scanlineDots; 40 | unsigned int remainingModeDots = screenWidth; 41 | uint16_t *screenData; // rgb555 42 | uint16_t lastBGData[4][screenWidth]; // used for mosaic 43 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DaftBoy32 2 | 3 | ## Things this is: 4 | - A Game Boy (+Color) emulator for 32blit, PicoSystem and other devices supported by the 32blit SDK 5 | - Also includes a WIP GBA core, this does not run on the 32blit. 6 | - Written by me 7 | 8 | ## Things this is NOT: 9 | - Complete (missing the link port, a few cartrige types and some GBC bits) 10 | - The fastest 11 | - [The most accurate](https://cupboard.daftgames.net/GBEmulatorShootout/) 12 | - A good example (?) 13 | 14 | 15 | Originally built in about a week with the goal of "run Tetris". Also, first time writing an emulator. May explode on anything I haven't tested. 16 | 17 | ## Building 18 | 19 | ### 32blit 20 | 21 | Uses the 32blit SDK, for a 32blit device build: 22 | ``` 23 | mkdir build 24 | cd build 25 | cmake -DCMAKE_TOOLCHAIN_FILE=path/to/32blit-sdk/32blit.toolchain .. 26 | make 27 | ``` 28 | See [the docs](https://github.com/32blit/32blit-sdk/tree/master/docs#readme) for more info (and building for other platforms). 29 | 30 | ### SDL 31 | 32 | More minimal SDL-based shell, useful for testing: 33 | 34 | ``` 35 | mkdir build 36 | cd build 37 | cmake -DBUILD_32BLIT=OFF -DBUILD_SDL=1 .. 38 | make 39 | ``` 40 | 41 | (Currently the only way to use the GBA core) 42 | 43 | There's also a really minimal test runner (`-DBUILD_TESTS=1`), but you _probably_ don't want that. 44 | -------------------------------------------------------------------------------- /32blit/menu.cpp: -------------------------------------------------------------------------------- 1 | #include "menu.hpp" 2 | 3 | #include "control-icons.hpp" 4 | 5 | #include "engine/engine.hpp" 6 | 7 | Menu::Menu(std::string_view title, std::vector items, const blit::Font &font) : blit::Menu(title, nullptr, 0, font), items_vec(std::move(items)) { 8 | this->items = items_vec.data(); 9 | num_items = items_vec.size(); 10 | 11 | item_h = font.char_h + 2; 12 | item_adjust_y = 0; 13 | 14 | header_h = item_h; 15 | footer_h = 0; 16 | margin_y = 0; 17 | 18 | background_colour = blit::Pen(0x11, 0x11, 0x11); 19 | foreground_colour = blit::Pen(0xF7, 0xF7, 0xF7); 20 | selected_item_background = blit::Pen(0x22, 0x22, 0x22); 21 | } 22 | 23 | void Menu::set_on_item_activated(void (*func)(const Item &)) { 24 | on_item_pressed = func; 25 | } 26 | 27 | void Menu::render_item(const Item &item, int y, int index) const { 28 | blit::Menu::render_item(item, y, index); 29 | 30 | if(index == current_item) { 31 | const int iconSize = font.char_h > 8 ? 12 : 8; 32 | blit::Point iconPos = blit::Point(display_rect.x + display_rect.w - item_padding_x -iconSize, y + 1); // from the top-right 33 | blit::Pen icon_bg((header_foreground.r + header_background.r) / 2, (header_foreground.g + header_background.g) / 2, (header_foreground.b + header_background.b) / 2); 34 | duh::draw_control_icon(&blit::screen, duh::Icon::A, iconPos, iconSize, foreground_colour, icon_bg); 35 | } 36 | } 37 | 38 | void Menu::item_activated(const Item &item) { 39 | if(on_item_pressed) 40 | on_item_pressed(item); 41 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13.0) 2 | 3 | set(CMAKE_CXX_STANDARD 17) 4 | set(CMAKE_CXX_EXTENSIONS OFF) 5 | 6 | project(gameblit) 7 | 8 | #set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address") 9 | 10 | if(MSVC) 11 | add_compile_options("/W4" "/wd4244" "/wd4324" "/wd4458" "/wd4100") 12 | else() 13 | add_compile_options("-Wall" "-Wextra" "-Wdouble-promotion" "-Wno-unused-parameter") 14 | endif() 15 | 16 | option(BUILD_32BLIT "Build 32blit UI" ON) 17 | option(BUILD_SDL "Build minimal SDL UI" OFF) 18 | option(BUILD_TESTS "Build test runner" OFF) 19 | 20 | #add_definitions("-DPROFILER") 21 | 22 | add_subdirectory(core) 23 | 24 | if(BUILD_32BLIT) 25 | find_package (32BLIT CONFIG REQUIRED PATHS ../32blit-sdk) 26 | 27 | if(32BLIT_HW OR 32BLIT_PICO) 28 | if(BUILD_SDL) 29 | message(WARNING "Disabling SDL UI for 32blit hardware build") 30 | set(BUILD_SDL OFF) 31 | endif() 32 | if(BUILD_TESTS) 33 | message(WARNING "Disabling test runner for 32blit hardware build") 34 | set(BUILD_TESTS OFF) 35 | endif() 36 | endif() 37 | 38 | add_subdirectory(32blit DaftBoy32) 39 | endif() 40 | 41 | if(BUILD_SDL) 42 | add_subdirectory(minsdl) 43 | endif() 44 | 45 | if(BUILD_TESTS) 46 | add_subdirectory(tests) 47 | endif() 48 | 49 | # setup release packages 50 | set(PROJECT_DISTRIBS LICENSE README.md) 51 | install (FILES ${PROJECT_DISTRIBS} DESTINATION .) 52 | set (CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) 53 | set (CPACK_GENERATOR "ZIP" "TGZ") 54 | include (CPack) 55 | -------------------------------------------------------------------------------- /core/DMGDisplay.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | struct BESSCore; 6 | struct DaftState; 7 | class DMGCPU; 8 | class DMGMemory; 9 | 10 | class DMGDisplay 11 | { 12 | public: 13 | using VBlankCallback = void(*)(); 14 | 15 | DMGDisplay(DMGCPU &cpu); 16 | 17 | void reset(); 18 | 19 | void loadSaveState(BESSCore &bess, DaftState &state, std::function readFunc); 20 | void saveState(DaftState &state); 21 | void savePaletteState(BESSCore &bess, std::function writeFunc, uint32_t &offset); 22 | 23 | void update(); 24 | void updateForInterrupts(); 25 | int getCyclesToNextUpdate() const; 26 | 27 | void setFramebuffer(uint16_t *data); 28 | 29 | uint8_t readReg(uint16_t addr, uint8_t val); 30 | bool writeReg(uint16_t addr, uint8_t data); 31 | 32 | void setVBlankCallback(VBlankCallback callback){vBlankCallback = callback;} 33 | 34 | private: 35 | void drawScanLine(int y); 36 | void drawBackground(uint16_t *scanLine, uint8_t *bgRaw); 37 | void drawSprites(uint16_t *scanLine, uint8_t *bgRaw); 38 | 39 | void updateCompare(bool newVal); 40 | 41 | DMGCPU &cpu; 42 | DMGMemory &mem; 43 | 44 | uint32_t lastUpdateCycle = 0; 45 | 46 | bool enabled = true; 47 | uint8_t y = 0; 48 | uint8_t statMode = 0; 49 | bool compareMatch = false; 50 | int windowY = 0; 51 | bool firstFrame = false; // first frame after enabling display 52 | 53 | bool interruptsEnabled = false; 54 | uint8_t statInterruptActive = 0; 55 | 56 | static const int scanlineCycles = 456; 57 | static const int screenWidth = 160, screenHeight = 144; 58 | 59 | int remainingScanlineCycles = scanlineCycles; 60 | uint32_t remainingModeCycles = 0; 61 | uint16_t *screenData = nullptr; // rgb555 62 | 63 | // GBC 64 | uint16_t bgPalette[8 * 4], objPalette[8 * 4]; 65 | 66 | #if defined(DISPLAY_RGB565) || defined(DISPLAY_RB_SWAP) 67 | uint16_t bgPaletteRaw[8 * 4], objPaletteRaw[8 * 4]; 68 | bool bgPaletteDirty = false, objPaletteDirty = false; 69 | #endif 70 | 71 | VBlankCallback vBlankCallback = nullptr; 72 | }; 73 | -------------------------------------------------------------------------------- /core/DMGAPU.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | struct DaftState; 5 | class DMGCPU; 6 | 7 | class DMGAPU 8 | { 9 | public: 10 | DMGAPU(DMGCPU &cpu); 11 | 12 | void reset(); 13 | 14 | void loadSaveState(DaftState &state); 15 | void saveState(DaftState &state); 16 | 17 | void update(); 18 | 19 | int16_t getSample(); 20 | int getNumSamples() const; 21 | 22 | uint8_t readReg(uint16_t addr, uint8_t val); 23 | bool writeReg(uint16_t addr, uint8_t data); 24 | 25 | private: 26 | void updateFrameSequencer(); 27 | void updateFreq(int cyclesPassed); 28 | 29 | void sampleOutput(); 30 | 31 | DMGCPU &cpu; 32 | 33 | bool enabled = true; 34 | uint8_t channelEnabled = 0; 35 | 36 | uint8_t frameSeqClock = 0; 37 | bool skipNextFrameSeqUpdate = false; 38 | 39 | // deferred updates 40 | uint32_t lastUpdateCycle = 0; 41 | uint16_t lastDivValue = 0; 42 | 43 | uint32_t enableCycle = 0; // used to align timers (only really need & 4) 44 | 45 | // channel 1 46 | bool ch1SweepEnable = false; 47 | bool ch1SweepCalcWithNeg = false; // for some wierdness 48 | uint8_t ch1SweepTimer = 0; 49 | uint16_t ch1SweepFreq = 0; 50 | uint8_t ch1Len = 0; 51 | uint8_t ch1EnvVolume, ch1EnvTimer; 52 | bool ch1Val = false; 53 | int ch1FreqTimer = 0; 54 | uint16_t ch1FreqTimerPeriod = 1; 55 | uint8_t ch1DutyStep = 0; 56 | uint8_t ch1DutyPattern = 0; 57 | 58 | // channel 2 59 | uint8_t ch2Len = 0; 60 | uint8_t ch2EnvVolume, ch2EnvTimer; 61 | bool ch2Val = false; 62 | int ch2FreqTimer = 0; 63 | uint16_t ch2FreqTimerPeriod = 1; 64 | uint8_t ch2DutyStep = 0; 65 | uint8_t ch2DutyPattern = 0; 66 | 67 | // channel 3 68 | uint16_t ch3Len = 0; // this one can be 256 69 | int ch3FreqTimer = 0; 70 | uint16_t ch3FreqTimerPeriod = 1; 71 | uint8_t ch3Sample = 0; 72 | uint8_t ch3SampleIndex = 0; 73 | uint32_t ch3LastAccessCycle = 0; 74 | 75 | // channel 4 76 | uint8_t ch4Len = 0; 77 | uint8_t ch4EnvVolume, ch4EnvTimer; 78 | int ch4FreqTimer = 0; 79 | int ch4FreqTimerPeriod = 8; 80 | uint16_t ch4LFSRBits = 0; // really 15 bit 81 | bool ch4Narrow = false; 82 | bool ch4Val = false; 83 | 84 | // output 85 | int sampleClock = 0; 86 | static const int bufferSize = 1024; 87 | volatile uint16_t readOff = 0, writeOff = 64; 88 | int16_t sampleData[bufferSize] = {0}; 89 | int32_t filterVal[2]{}; 90 | }; -------------------------------------------------------------------------------- /core/AGBAPU.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | class AGBCPU; 5 | 6 | class AGBAPU 7 | { 8 | public: 9 | AGBAPU(AGBCPU &cpu); 10 | 11 | void reset(); 12 | 13 | void update(); 14 | 15 | void timerOverflow(int timer, uint32_t cycle); 16 | 17 | int16_t getSample(); 18 | int getNumSamples() const; 19 | bool hasSample() const {return readOff != writeOff;} 20 | 21 | uint16_t readReg(uint32_t addr, uint16_t val); 22 | bool writeReg(uint32_t addr, uint16_t data, uint16_t mask); 23 | 24 | private: 25 | void updateFrameSequencer(); 26 | void updateFreq(int cyclesPassed); 27 | 28 | void sampleOutput(); 29 | 30 | AGBCPU &cpu; 31 | 32 | bool enabled = true; 33 | uint8_t channelEnabled = 0; 34 | 35 | uint8_t frameSeqClock = 0; 36 | // deferred updates 37 | uint32_t lastUpdateCycle = 0; 38 | 39 | // channel 1 40 | bool ch1SweepEnable = false; 41 | bool ch1SweepCalcWithNeg = false; // for some wierdness 42 | uint8_t ch1SweepTimer = 0; 43 | uint16_t ch1SweepFreq = 0; 44 | uint8_t ch1Len = 0; 45 | uint8_t ch1EnvVolume, ch1EnvTimer; 46 | bool ch1Val = false; 47 | int ch1FreqTimer = 0; 48 | uint16_t ch1FreqTimerPeriod = 1; 49 | uint8_t ch1DutyStep = 0; 50 | uint8_t ch1DutyPattern = 0; 51 | 52 | // channel 2 53 | uint8_t ch2Len = 0; 54 | uint8_t ch2EnvVolume, ch2EnvTimer; 55 | bool ch2Val = false; 56 | int ch2FreqTimer = 0; 57 | uint16_t ch2FreqTimerPeriod = 1; 58 | uint8_t ch2DutyStep = 0; 59 | uint8_t ch2DutyPattern = 0; 60 | 61 | // channel 3 62 | uint16_t ch3Len = 0; // this one can be 256 63 | int ch3FreqTimer = 0; 64 | uint16_t ch3FreqTimerPeriod = 1; 65 | uint8_t ch3Sample = 0; 66 | uint8_t ch3SampleIndex = 0; 67 | uint8_t ch3BankIndex = 0; 68 | uint64_t ch3WaveBuf[4]{0}; // 2x 128-bit buffers 69 | 70 | // channel 4 71 | uint8_t ch4Len = 0; 72 | uint8_t ch4EnvVolume, ch4EnvTimer; 73 | int ch4FreqTimer = 0; 74 | int ch4FreqTimerPeriod = 8; 75 | uint16_t ch4LFSRBits = 0; // really 15 bit 76 | bool ch4Narrow = false; 77 | bool ch4Val = false; 78 | 79 | // "DMA" channels 80 | uint8_t dmaAFIFO[32], dmaBFIFO[32]; 81 | uint8_t fifoARead = 0, fifoAWrite = 0, fifoBRead = 0, fifoBWrite = 0; 82 | uint8_t fifoAFilled = 0, fifoBFilled = 0; 83 | 84 | int8_t dmaAVal = 0, dmaBVal = 0; 85 | 86 | // output 87 | int sampleClock = 0; 88 | static const int bufferSize = 2048; 89 | volatile uint16_t readOff = 0, writeOff = 64; 90 | int16_t sampleData[bufferSize] = {0}; 91 | }; -------------------------------------------------------------------------------- /core/DMGSaveState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | struct BESSHeader 5 | { 6 | char id[4]; 7 | uint32_t len; 8 | }; 9 | 10 | struct BESSCore 11 | { 12 | uint16_t versionMajor; 13 | uint16_t versionMinor; 14 | 15 | char model[4]; 16 | 17 | uint16_t pc; 18 | uint16_t af; 19 | uint16_t bc; 20 | uint16_t de; 21 | uint16_t hl; 22 | uint16_t sp; 23 | 24 | uint8_t ime; 25 | uint8_t ie; 26 | uint8_t execState; 27 | uint8_t reserved; 28 | uint8_t ioRegs[128]; 29 | 30 | uint32_t ramSize; 31 | uint32_t ramOff; 32 | uint32_t vramSize; 33 | uint32_t vramOff; 34 | uint32_t cartRAMSize; 35 | uint32_t cartRAMOff; 36 | uint32_t oamSize; 37 | uint32_t oamOff; 38 | uint32_t hramSize; 39 | uint32_t hramOff; 40 | uint32_t bgPalSize; 41 | uint32_t bgPalOff; 42 | uint32_t objPalSize; 43 | uint32_t objPalOff; 44 | }; 45 | static_assert(sizeof(BESSCore) == 208); 46 | 47 | inline const int daftBoyStateVersion = 1; 48 | 49 | enum StateFlags 50 | { 51 | State_EnableInterruptsNextCycle = 1 << 0, // for EI 52 | State_HaltBug = 1 << 1, 53 | 54 | State_TimerReload = 1 << 2, 55 | State_TimerReloaded = 1 << 3, 56 | State_TimerOldVal = 1 << 4, 57 | 58 | State_GDMATriggered = 1 << 5, 59 | 60 | State_SerialStart = 1 << 6, 61 | 62 | // display 63 | State_DispFirstFrame = 1 << 7, 64 | 65 | // APU 66 | State_APUSkipNextFrameSeqUpdate = 1 << 8, 67 | State_APUCh1SweepCalcWithNeg = 1 << 9 68 | }; 69 | 70 | struct DaftState 71 | { 72 | uint8_t version; 73 | 74 | uint8_t reserved[2]; 75 | 76 | uint8_t divLow; 77 | 78 | uint32_t flags; 79 | 80 | uint32_t cycleCount; 81 | 82 | uint8_t oamDMACount; 83 | uint8_t oamDMADelay; 84 | 85 | uint8_t serialBits; 86 | 87 | // display 88 | uint8_t windowY; 89 | uint8_t statInterruptActive; 90 | uint8_t padding; 91 | uint16_t remainingScalineCycles; 92 | uint16_t remainingModeCycles; 93 | 94 | // APU 95 | uint8_t frameSeqClock; 96 | uint8_t padding2; 97 | uint32_t enableCycle; 98 | 99 | uint8_t envVolume[3]; 100 | uint8_t envTimer[3]; 101 | uint8_t ch3Sample; 102 | uint8_t ch3SampleIndex; 103 | uint32_t freqTimer[4]; // could be smaller 104 | 105 | uint8_t ch1SweepTimer; 106 | uint8_t ch1DutyStep; 107 | 108 | uint8_t ch2DutyStep; 109 | 110 | uint8_t padding3; 111 | uint32_t ch3LastAccessCycle; 112 | 113 | uint16_t ch4LFSRBits; 114 | 115 | uint8_t padding4[2]; 116 | }; 117 | 118 | static_assert(sizeof(DaftState) == 64); 119 | -------------------------------------------------------------------------------- /core/DMGMemory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | class DMGCPU; 7 | class DMGMemory 8 | { 9 | public: 10 | DMGMemory(DMGCPU &cpu); 11 | 12 | using ROMBankCallback = void(*)(uint8_t, uint8_t *); 13 | 14 | using CartRamUpdateCallback = void(*)(uint8_t *, unsigned int); 15 | 16 | void setROMBankCallback(ROMBankCallback callback); 17 | void setCartROM(const uint8_t *rom); 18 | void loadCartridgeRAM(const uint8_t *ram, uint32_t len); 19 | 20 | void addROMCache(uint8_t *ptr, uint32_t size); 21 | 22 | void reset(); 23 | 24 | void saveMBCState(std::function writeFunc, uint32_t &offset); 25 | 26 | void setGBC(bool gbc) {isGBC = gbc;} 27 | 28 | uint8_t read(uint16_t addr) const; 29 | void write(uint16_t addr, uint8_t data); 30 | 31 | const uint8_t *mapAddress(uint16_t addr) const; 32 | 33 | // fast access to IO regs 34 | uint8_t readIOReg(uint8_t addr) const {return iohram[addr];} 35 | uint8_t &getIOReg(uint8_t addr) {return iohram[addr];} 36 | void writeIOReg(uint8_t addr, uint8_t val) {iohram[addr] = val;} 37 | 38 | uint8_t *getWRAM() {return wram;} 39 | uint8_t *getVRAM() {return vram;} 40 | uint8_t *getOAM() {return oam;} 41 | 42 | uint8_t *getCartridgeRAM() {return cartRam;} 43 | int getCartridgeRAMSize() {return cartRamSize;} 44 | void setCartRamUpdateCallback(CartRamUpdateCallback callback); 45 | 46 | bool hasRTC() const; 47 | void getRTCData(uint32_t buf[12]); 48 | void setRTCData(uint32_t buf[12]); 49 | 50 | private: 51 | void writeMBC(uint16_t addr, uint8_t data); 52 | void updateCurrentROMBank(unsigned int bank, int region); 53 | 54 | void updateRTC(); 55 | 56 | enum class MBCType : uint8_t 57 | { 58 | None = 0, 59 | MBC1, 60 | MBC1M, // multicart - wired slightly differently 61 | MBC2, 62 | MBC3, 63 | MBC5 64 | }; 65 | 66 | struct ROMCacheEntry 67 | { 68 | uint8_t *ptr; 69 | uint8_t bank; 70 | }; 71 | 72 | // memory map with pointers offset so that regions[addr >> 12][addr] works 73 | const uint8_t *regions[16]; 74 | 75 | uint8_t iohram[0x100]; // io @ 0xFF00, hram @ 0xFF80, ie & 0xFFFF 76 | 77 | uint8_t vram[0x2000 * 2]; // 8k @ 0x8000, two banks on GBC 78 | uint8_t wram[0x1000 * 8]; // 8k @ 0xC000, second half switchable on GBC 79 | uint8_t oam[0xA0]; // @ 0xFE00 80 | 81 | bool isGBC = false; 82 | uint8_t vramBank = 0; 83 | uint8_t wramBank = 1; 84 | 85 | // cartridge 86 | MBCType mbcType = MBCType::None; 87 | bool mbcRAMEnabled = false; 88 | bool mbcRAMBankMode = false; 89 | bool cartRamWritten = false; 90 | 91 | int mbcROMBank = 1, mbcRAMBank = 0; 92 | uint8_t cartRam[0x8000]; 93 | 94 | unsigned int cartRamSize = 0; 95 | 96 | // RTC 97 | uint8_t rtcRegs[10]; // internal / latched 98 | uint16_t rtcMilliseconds = 0; 99 | uint32_t rtcUpdateTime = 0; 100 | 101 | uint8_t cartROMBank0[0x4000]; 102 | const uint8_t *cartROM = nullptr; // used if entire rom is loaded somewhere 103 | unsigned int cartROMBanks = 0; // read from the header 104 | 105 | // cache ROM banks in RAM 106 | std::list cachedROMBanks; 107 | 108 | DMGCPU &cpu; 109 | 110 | ROMBankCallback romBankCallback; 111 | 112 | CartRamUpdateCallback cartRamUpdateCallback; 113 | }; -------------------------------------------------------------------------------- /core/DMGRegs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | enum IOReg 5 | { 6 | IO_JOYP = 0x00, 7 | 8 | IO_SB, 9 | IO_SC, 10 | 11 | IO_DIV = 0x04, 12 | IO_TIMA, 13 | IO_TMA, 14 | IO_TAC, 15 | 16 | IO_NR10 = 0x10, // ch1 sweep 17 | IO_NR11, // ch1 len/duty 18 | IO_NR12, // ch1 envelope/volume 19 | IO_NR13, // ch1 freq lo 20 | IO_NR14, // ch1 freq hi 21 | 22 | IO_NR21 = 0x16, // ch2 len/duty 23 | IO_NR22, // ch2 envelope/volume 24 | IO_NR23, // ch2 freq lo 25 | IO_NR24, // ch2 freq hi 26 | 27 | IO_NR30 = 0x1A, // ch3 on/off 28 | IO_NR31, // ch3 len 29 | IO_NR32, // ch3 out level 30 | IO_NR33, // ch3 freq lo 31 | IO_NR34, // ch3 freq hi 32 | 33 | IO_NR41 = 0x20, // ch4 len 34 | IO_NR42, // ch4 envelope/volume 35 | IO_NR43, // ch4 36 | IO_NR44, // ch4 37 | 38 | IO_NR50 = 0x24, // volume 39 | IO_NR51, // channel selection 40 | IO_NR52, // sound on/off 41 | 42 | IO_LCDC = 0x40, 43 | IO_STAT, 44 | IO_SCY, 45 | IO_SCX, 46 | IO_LY, 47 | IO_LYC, 48 | IO_DMA, 49 | IO_BGP = 0x47, 50 | IO_OBP0, 51 | IO_OBP1, 52 | IO_WY, 53 | IO_WX, 54 | 55 | // GBC 56 | IO_KEY1 = 0x4D, 57 | 58 | IO_VBK = 0x4F, 59 | 60 | IO_HDMA1 = 0x51, // src high 61 | IO_HDMA2, // src low 62 | IO_HDMA3, // dest hi 63 | IO_HDMA4, // dest low 64 | IO_HDMA5, // len/mode/start 65 | 66 | IO_RP = 0x56, 67 | 68 | IO_BCPS = 0x68, 69 | IO_BCPD, 70 | IO_OCPS, 71 | IO_OCPD, 72 | IO_OPRI, 73 | 74 | IO_SVBK = 0x70, 75 | 76 | IO_PCM12 = 0x76, 77 | IO_PCM34, 78 | 79 | IO_IF = 0x0F, // interrupt flag 80 | IO_IE = 0xFF // interrupt enable 81 | }; 82 | 83 | enum JOYPBits 84 | { 85 | JOYP_Right = 1 << 0, 86 | JOYP_Left = 1 << 1, 87 | JOYP_Up = 1 << 2, 88 | JOYP_Down = 1 << 3, 89 | 90 | JOYP_A = 1 << 0, 91 | JOYP_B = 1 << 1, 92 | JOYP_Select = 1 << 2, 93 | JOYP_Start = 1 << 3, 94 | 95 | JOYP_SelectDir = 1 << 4, 96 | JOYP_SelectButtons = 1 << 5 97 | }; 98 | 99 | enum SCBits 100 | { 101 | SC_IntClock = 1 << 0, 102 | SC_Fast = 1 << 1, 103 | SC_StartTransfer = 1 << 7 104 | }; 105 | 106 | enum TACBits 107 | { 108 | TAC_Start = 1 << 2, 109 | TAC_Clock = 0x3, 110 | }; 111 | 112 | enum NR10Bits 113 | { 114 | NR10_Shift = 0x7, 115 | NR10_Negate = 1 << 3, 116 | NR10_Period = 0x70 117 | }; 118 | 119 | // common bits for all channels 120 | enum NRx4Bits 121 | { 122 | NRx4_Counter = 1 << 6, 123 | NRx4_Trigger = 1 << 7 124 | }; 125 | 126 | enum NR52Bits 127 | { 128 | NR52_Ch1On = 1 << 0, 129 | NR52_Ch2On = 1 << 1, 130 | NR52_Ch3On = 1 << 2, 131 | NR52_Ch4On = 1 << 3, 132 | 133 | NR52_Enable = 1 << 7 134 | }; 135 | 136 | enum LCDCBits 137 | { 138 | LCDC_BGDisp = 1 << 0, 139 | LCDC_OBJDisp = 1 << 1, 140 | LCDC_Sprite8x16 = 1 << 2, 141 | LCDC_BGTileMap9C00 = 1 << 3, 142 | LCDC_TileData8000 = 1 << 4, 143 | LCDC_WindowEnable = 1 << 5, 144 | LCDC_WindowTileMap9C00 = 1 << 6, 145 | LCDC_DisplayEnable = 1 << 7 146 | }; 147 | 148 | enum STATBits 149 | { 150 | STAT_Mode = 0x3, 151 | STAT_Coincidence = 1 << 2, 152 | STAT_HBlankInt = 1 << 3, 153 | STAT_VBlankInt = 1 << 4, 154 | STAT_OAMInt = 1 << 5, 155 | STAT_CoincidenceInt = 1 << 6 156 | }; -------------------------------------------------------------------------------- /core/DMGCPU.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "DMGAPU.h" 6 | #include "DMGDisplay.h" 7 | #include "DMGMemory.h" 8 | 9 | 10 | enum Interrupts 11 | { 12 | Int_VBlank = 1 << 0, 13 | Int_LCDStat = 1 << 1, 14 | Int_Timer = 1 << 2, 15 | Int_Serial = 1 << 3, 16 | Int_Joypad = 1 << 4, 17 | }; 18 | 19 | class DMGCPU final 20 | { 21 | public: 22 | enum class Console 23 | { 24 | Auto, 25 | DMG, 26 | CGB 27 | }; 28 | 29 | DMGCPU(); 30 | 31 | void reset(); 32 | 33 | void loadSaveState(uint32_t fileLen, std::function readFunc); 34 | void saveSaveState(std::function writeFunc); 35 | 36 | void run(int ms); 37 | 38 | Console getConsole() {return console;} 39 | void setConsole(Console c) {console = c;} 40 | 41 | void flagInterrupt(int interrupt); 42 | 43 | uint8_t readReg(uint16_t addr, uint8_t val); 44 | bool writeReg(uint16_t addr, uint8_t data); 45 | 46 | DMGMemory &getMem() {return mem;} 47 | DMGAPU &getAPU(){return apu;} 48 | DMGDisplay &getDisplay(){return display;} 49 | 50 | bool getStopped() const {return stopped;} 51 | bool getBreakpointTriggered() {return breakpoint;} 52 | 53 | uint32_t getCycleCount() const {return cycleCount;} 54 | uint16_t getInternalTimer() {updateTimer(); return divCounter;} 55 | 56 | bool getColourMode() const {return isGBC;} // CGB in CGB mode 57 | bool getDoubleSpeedMode() const {return doubleSpeed;} 58 | 59 | void setInputs(uint8_t newInputs); 60 | 61 | private: 62 | enum class Reg 63 | { 64 | A = 0, 65 | F, 66 | B, 67 | C, 68 | D, 69 | E, 70 | H, 71 | L 72 | }; 73 | 74 | enum class WReg 75 | { 76 | AF = 0, 77 | BC, 78 | DE, 79 | HL 80 | }; 81 | 82 | enum Flags 83 | { 84 | Flag_C = (1 << 4), 85 | Flag_H = (1 << 5), 86 | Flag_N = (1 << 6), 87 | Flag_Z = (1 << 7) 88 | }; 89 | 90 | // this only works on little-endian... 91 | uint8_t reg(Reg r) const {return reinterpret_cast(regs)[static_cast(r) ^ 1];} 92 | uint8_t ®(Reg r) {return reinterpret_cast(regs)[static_cast(r) ^ 1];} 93 | uint16_t reg(WReg r) const {return regs[static_cast(r)];} 94 | uint16_t ®(WReg r) {return regs[static_cast(r)];} 95 | 96 | uint8_t readMem(uint16_t addr) const; 97 | void writeMem(uint16_t addr, uint8_t data); 98 | 99 | void executeInstruction(); 100 | void executeExInstruction(); 101 | 102 | void cycleExecuted(); 103 | 104 | void updateTimer(); 105 | void incrementTimer(); 106 | void caclulateNextTimerInterrupt(uint32_t cycleCount, uint16_t div); 107 | bool serviceInterrupts(); 108 | 109 | void updateOAMDMA(); 110 | void doGDMA(); 111 | 112 | void updateSerial(); 113 | void calculateNextSerialUpdate(); 114 | 115 | static const uint32_t clockSpeed = 4194304; 116 | 117 | // internal state 118 | bool stopped, halted, breakpoint; 119 | bool masterInterruptEnable, enableInterruptsNextCycle; 120 | uint8_t serviceableInterrupts; 121 | bool haltBug = false; 122 | 123 | int cyclesToRun = 0; 124 | uint32_t cycleCount = 0; 125 | 126 | uint16_t divCounter = 0; 127 | bool timerEnabled = false; 128 | bool timerReload = false, timerReloaded = false; 129 | unsigned int timerBit = 1 << 9; 130 | bool timerOldVal = false; 131 | uint32_t lastTimerUpdate = 0; 132 | uint32_t nextTimerInterrupt = 0; 133 | 134 | bool isGBC = false; 135 | Console console = Console::Auto; 136 | bool doubleSpeed = false, speedSwitch = false; 137 | 138 | int oamDMACount, oamDMADelay; 139 | const uint8_t *oamDMASrc = nullptr; 140 | uint8_t *oamDMADest = nullptr; 141 | bool gdmaTriggered; 142 | 143 | bool serialStart = false, serialMaster = false; 144 | uint8_t serialBits = 0; 145 | uint32_t nextSerialBitCycle = 0; 146 | uint32_t lastSerialUpdate = 0; 147 | 148 | // registers 149 | uint16_t regs[4]; 150 | uint16_t pc, sp; 151 | 152 | // RAM 153 | DMGMemory mem; 154 | 155 | DMGAPU apu; 156 | DMGDisplay display; 157 | uint8_t inputs = 0; 158 | }; 159 | -------------------------------------------------------------------------------- /minsdl/emscripten-shell.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DaftBoyWeb 7 | 36 | 37 | 38 |
39 |
Downloading...
40 |
41 | 42 |
43 | 44 | 45 | 46 | 119 | {{{ SCRIPT }}} 120 | 121 | 122 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Build Github Action, to run a test build on all targets 2 | # (Linux, Blit, MacOS, Visual Studio) when the project is checked in. 3 | # 4 | # Thanks in large part to the phenomenal examples of DaftFreak. 5 | 6 | name: Build 7 | 8 | on: 9 | push: 10 | pull_request: 11 | release: 12 | types: [created] 13 | 14 | env: 15 | BUILD_TYPE: Release 16 | 17 | jobs: 18 | 19 | build: 20 | 21 | name: ${{matrix.name}} 22 | strategy: 23 | matrix: 24 | include: 25 | - os: ubuntu-22.04 26 | name: Linux 27 | release-suffix: LIN64 28 | cmake-args: -D32BLIT_DIR=$GITHUB_WORKSPACE/32blit-sdk 29 | apt-packages: libsdl2-dev libsdl2-image-dev libsdl2-net-dev python3-setuptools 30 | 31 | - os: ubuntu-22.04 32 | name: STM32 33 | release-suffix: STM32 34 | cmake-args: -DCMAKE_TOOLCHAIN_FILE=$GITHUB_WORKSPACE/32blit-sdk/32blit.toolchain 35 | apt-packages: gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib python3-setuptools 36 | 37 | - os: ubuntu-22.04 38 | pico-sdk: true 39 | name: PicoSystem 40 | cache-key: picosystem 41 | release-suffix: PicoSystem 42 | cmake-args: -DCMAKE_TOOLCHAIN_FILE=$GITHUB_WORKSPACE/32blit-sdk/pico.toolchain -DPICO_BOARD=pimoroni_picosystem 43 | apt-packages: gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib python3-setuptools 44 | 45 | - os: macos-latest 46 | name: macOS 47 | release-suffix: MACOS 48 | cmake-args: -D32BLIT_DIR=$GITHUB_WORKSPACE/32blit-sdk 49 | brew-packages: sdl2 sdl2_image sdl2_net pipx 50 | 51 | - os: windows-latest 52 | name: Visual Studio 53 | release-suffix: WIN64 54 | cmake-args: -D32BLIT_DIR=$GITHUB_WORKSPACE/32blit-sdk 55 | 56 | runs-on: ${{matrix.os}} 57 | 58 | env: 59 | RELEASE_FILE: ${{github.event.repository.name}}-${{github.event.release.tag_name}}-${{matrix.release-suffix}} 60 | 61 | steps: 62 | # Check out the main repo 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | with: 66 | path: main 67 | submodules: true 68 | 69 | # Check out the 32Blit API we build against 70 | - name: Checkout 32Blit API 71 | uses: actions/checkout@v4 72 | with: 73 | repository: 32blit/32blit-sdk 74 | path: 32blit-sdk 75 | 76 | # pico sdk/extras for some builds 77 | - name: Checkout Pico SDK 78 | if: matrix.pico-sdk 79 | uses: actions/checkout@v4 80 | with: 81 | repository: raspberrypi/pico-sdk 82 | path: pico-sdk 83 | submodules: true 84 | 85 | - name: Checkout Pico Extras 86 | if: matrix.pico-sdk 87 | uses: actions/checkout@v4 88 | with: 89 | repository: raspberrypi/pico-extras 90 | path: pico-extras 91 | 92 | # Linux dependencies 93 | - name: Install Linux deps 94 | if: runner.os == 'Linux' 95 | run: | 96 | sudo apt update && sudo apt install ${{matrix.apt-packages}} 97 | pip3 install 32blit 98 | 99 | # MacOS dependencies 100 | - name: Install macOS deps 101 | if: runner.os == 'macOS' 102 | run: | 103 | brew install ${{matrix.brew-packages}} 104 | pipx install 32blit 105 | 106 | # Windows dependencies 107 | - name: Install Windows deps 108 | if: runner.os == 'Windows' 109 | shell: bash 110 | run: | 111 | python -m pip install 32blit 112 | 113 | # Set up the cmake build environment 114 | - name: Create Build Environment 115 | run: cmake -E make_directory ${{runner.workspace}}/main/build 116 | 117 | # Ask cmake to build the makefiles 118 | - name: Configure CMake 119 | shell: bash 120 | working-directory: ${{runner.workspace}}/main/build 121 | run: cmake $GITHUB_WORKSPACE/main -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCPACK_PACKAGE_FILE_NAME=${{env.RELEASE_FILE}} ${{matrix.cmake-args}} 122 | 123 | # And then run the build itself 124 | - name: Build 125 | working-directory: ${{runner.workspace}}/main/build 126 | shell: bash 127 | run: | 128 | cmake --build . --config $BUILD_TYPE -j 2 129 | 130 | # When it's a release, generate tar/zip files of the build 131 | - name: Package Release 132 | if: matrix.release-suffix != '' 133 | shell: bash 134 | working-directory: ${{runner.workspace}}/main/build 135 | run: | 136 | cmake --build . --target package 137 | 138 | # Push the tar file to the release 139 | - name: Upload tar 140 | if: github.event_name == 'release' && matrix.release-suffix != '' 141 | uses: softprops/action-gh-release@v1 142 | with: 143 | files: ${{runner.workspace}}/main/build/${{env.RELEASE_FILE}}.tar.gz 144 | 145 | # Push the zip file to the release 146 | - name: Upload zip 147 | if: github.event_name == 'release' && matrix.release-suffix != '' 148 | uses: softprops/action-gh-release@v1 149 | with: 150 | files: ${{runner.workspace}}/main/build/${{env.RELEASE_FILE}}.zip 151 | 152 | - name: Upload artifact 153 | if: github.event_name != 'release' && matrix.release-suffix != '' 154 | uses: actions/upload-artifact@v4 155 | with: 156 | name: ${{env.RELEASE_FILE}} 157 | path: ${{runner.workspace}}/main/build/${{env.RELEASE_FILE}}.zip 158 | -------------------------------------------------------------------------------- /core/AGBRegs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | enum AGBIOReg 4 | { 5 | // LCD 6 | IO_DISPCNT = 0x0, 7 | IO_DISPSTAT = 0x4, 8 | IO_VCOUNT = 0x6, 9 | IO_BG0CNT = 0x8, 10 | IO_BG1CNT = 0xA, 11 | IO_BG2CNT = 0xC, 12 | IO_BG3CNT = 0xE, 13 | IO_BG0HOFS = 0x10, 14 | IO_BG0VOFS = 0x12, 15 | IO_BG1HOFS = 0x14, 16 | IO_BG1VOFS = 0x16, 17 | IO_BG2HOFS = 0x18, 18 | IO_BG2VOFS = 0x1A, 19 | IO_BG3HOFS = 0x1C, 20 | IO_BG3VOFS = 0x1E, 21 | 22 | IO_BG2PA = 0x20, 23 | IO_BG2PB = 0x22, 24 | IO_BG2PC = 0x24, 25 | IO_BG2PD = 0x26, 26 | IO_BG2X_L = 0x28, 27 | IO_BG2X_H = 0x2A, 28 | IO_BG2Y_L = 0x2C, 29 | IO_BG2Y_H = 0x2E, 30 | IO_BG3PA = 0x30, 31 | IO_BG3PB = 0x32, 32 | IO_BG3PC = 0x34, 33 | IO_BG3PD = 0x36, 34 | IO_BG3X_L = 0x38, 35 | IO_BG3X_H = 0x3A, 36 | IO_BG3Y_L = 0x3C, 37 | IO_BG3Y_H = 0x3E, 38 | 39 | IO_WIN0H = 0x40, 40 | IO_WIN1H = 0x42, 41 | IO_WIN0V = 0x44, 42 | IO_WIN1V = 0x46, 43 | IO_WININ = 0x48, 44 | IO_WINOUT = 0x4A, 45 | 46 | IO_MOSAIC = 0x4C, 47 | 48 | IO_BLDCNT = 0x50, 49 | IO_BLDALPHA = 0x52, 50 | IO_BLDY = 0x54, 51 | 52 | // Audio 53 | IO_SOUND1CNT_L = 0x60, 54 | IO_SOUND1CNT_H = 0x62, 55 | IO_SOUND1CNT_X = 0x64, 56 | IO_SOUND2CNT_L = 0x68, 57 | IO_SOUND2CNT_H = 0x6C, 58 | IO_SOUND3CNT_L = 0x70, 59 | IO_SOUND3CNT_H = 0x72, 60 | IO_SOUND3CNT_X = 0x74, 61 | IO_SOUND4CNT_L = 0x78, 62 | IO_SOUND4CNT_H = 0x7C, 63 | 64 | IO_SOUNDCNT_L = 0x80, 65 | IO_SOUNDCNT_H = 0x82, 66 | IO_SOUNDCNT_X = 0x84, 67 | IO_SOUNDBIAS = 0x88, 68 | 69 | // 90-9F wave ram 70 | 71 | // these are 32-bit 72 | IO_FIFO_A = 0xA0, 73 | IO_FIFO_B = 0xA4, 74 | 75 | // DMA (mostly 32-bit) 76 | IO_DMA0SAD = 0xB0, 77 | IO_DMA0DAD = 0xB4, 78 | IO_DMA0CNT_L = 0xB8, 79 | IO_DMA0CNT_H = 0xBA, 80 | IO_DMA1SAD = 0xBC, 81 | IO_DMA1DAD = 0xC0, 82 | IO_DMA1CNT_L = 0xC4, 83 | IO_DMA1CNT_H = 0xC6, 84 | IO_DMA2SAD = 0xC8, 85 | IO_DMA2DAD = 0xCC, 86 | IO_DMA2CNT_L = 0xD0, 87 | IO_DMA2CNT_H = 0xD2, 88 | IO_DMA3SAD = 0xD4, 89 | IO_DMA3DAD = 0xD8, 90 | IO_DMA3CNT_L = 0xDC, 91 | IO_DMA3CNT_H = 0xDE, 92 | 93 | // timers 94 | IO_TM0CNT_L = 0x100, // counter 95 | IO_TM0CNT_H = 0x102, // control 96 | IO_TM1CNT_L = 0x104, 97 | IO_TM1CNT_H = 0x106, 98 | IO_TM2CNT_L = 0x108, 99 | IO_TM2CNT_H = 0x10A, 100 | IO_TM3CNT_L = 0x10C, 101 | IO_TM3CNT_H = 0x10E, 102 | 103 | // input 104 | IO_KEYINPUT = 0x130, 105 | 106 | // system/interrupts 107 | IO_IE = 0x200, 108 | IO_IF = 0x202, 109 | IO_WAITCNT = 0x204, 110 | IO_IME = 0x208, 111 | 112 | IO_HALTCNT = 0x301, // yeah, odd addr 113 | }; 114 | 115 | enum DISPCNTBits 116 | { 117 | DISPCNT_Mode = 0x7, 118 | DISPCNT_CGBMode = (1 << 3), 119 | DISPCNT_Frame = (1 << 4), 120 | DISPCNT_HBlankFree = (1 << 5), 121 | DISPCNT_OBJChar1D = (1 << 6), 122 | DISPCNT_ForceBlank = (1 << 7), 123 | DISPCNT_BG0On = (1 << 8), 124 | DISPCNT_BG1On = (1 << 9), 125 | DISPCNT_BG2On = (1 << 10), 126 | DISPCNT_BG3On = (1 << 11), 127 | DISPCNT_OBJOn = (1 << 12), 128 | DISPCNT_Window0On = (1 << 13), 129 | DISPCNT_Window1On = (1 << 14), 130 | DISPCNT_OBJWindowOn = (1 << 15), 131 | }; 132 | 133 | enum DISPSTATBits 134 | { 135 | DISPSTAT_VBlank = 1 << 0, 136 | DISPSTAT_HBlank = 1 << 1, 137 | DISPSTAT_VCount = 1 << 2, 138 | DISPSTAT_VBlankInt = 1 << 3, 139 | DISPSTAT_HBlankInt = 1 << 4, 140 | DISPSTAT_VCountInt = 1 << 5 141 | 142 | //8-15 is VCount value to compare 143 | }; 144 | 145 | enum BGCNTBits 146 | { 147 | BGCNT_Priority = 0x3, 148 | BGCNT_CharBase = 0x3 << 2, 149 | BGCNT_Mosaic = 1 << 6, 150 | BGCNT_SinglePal = 1 << 7, 151 | BGCNT_ScreenBase = 0x1F << 8, 152 | BGCNT_Wrap = 1 << 13, 153 | BGCNT_ScreenSize = 0x3 << 14 154 | }; 155 | 156 | enum WININBits 157 | { 158 | WININ_Win0BG0 = 1 << 0, 159 | WININ_Win0BG1 = 1 << 1, 160 | WININ_Win0BG2 = 1 << 2, 161 | WININ_Win0BG3 = 1 << 3, 162 | WININ_Win0Object = 1 << 4, 163 | WININ_Win0Effect = 1 << 5, 164 | 165 | WININ_Win1BG0 = 1 << 8, 166 | WININ_Win1BG1 = 1 << 9, 167 | WININ_Win1BG2 = 1 << 10, 168 | WININ_Win1BG3 = 1 << 11, 169 | WININ_Win1Object = 1 << 12, 170 | WININ_Win1Effect = 1 << 13, 171 | }; 172 | 173 | enum WINOUTBits 174 | { 175 | WINOUT_OutsideBG0 = 1 << 0, 176 | WINOUT_OutsideBG1 = 1 << 1, 177 | WINOUT_OutsideBG2 = 1 << 2, 178 | WINOUT_OutsideBG3 = 1 << 3, 179 | WINOUT_OutsideObject = 1 << 4, 180 | WINOUT_OutsideEffect = 1 << 5, 181 | 182 | WINOUT_ObjWinBG0 = 1 << 8, 183 | WINOUT_ObjWinBG1 = 1 << 9, 184 | WINOUT_ObjWinBG2 = 1 << 10, 185 | WINOUT_ObjWinBG3 = 1 << 11, 186 | WINOUT_ObjWinObject = 1 << 12, 187 | WINOUT_ObjWinEffect = 1 << 13, 188 | }; 189 | 190 | enum BLDCNTBits 191 | { 192 | BLDCNT_SrcBG0 = 1 << 0, 193 | BLDCNT_SrcBG1 = 1 << 1, 194 | BLDCNT_SrcBG2 = 1 << 2, 195 | BLDCNT_SrcBG3 = 1 << 3, 196 | BLDCNT_SrcObject = 1 << 4, 197 | BLDCNT_SrcBackdrop = 1 << 5, 198 | 199 | BLDCNT_Effect = 3 << 6, 200 | 201 | BLDCNT_DstBG0 = 1 << 8, 202 | BLDCNT_DstBG1 = 1 << 9, 203 | BLDCNT_DstBG2 = 1 << 10, 204 | BLDCNT_DstBG3 = 1 << 11, 205 | BLDCNT_DstObject = 1 << 12, 206 | BLDCNT_DstBackdrop = 1 << 13, 207 | }; 208 | 209 | enum SOUND1CNTBits 210 | { 211 | SOUND1CNT_L_Shift = 0x7, 212 | SOUND1CNT_L_Negate = 1 << 3, 213 | SOUND1CNT_L_Period = 0x70 214 | }; 215 | 216 | // common bits for all channels 217 | enum SOUNDxCNTBits // sometimes H, sometimes X... 218 | { 219 | SOUNDxCNT_Length = 1 << 14, 220 | SOUNDxCNT_Trigger = 1 << 15 221 | }; 222 | 223 | enum SOUNDCNTBits 224 | { 225 | SOUNDCNT_X_Ch1On = 1 << 0, 226 | SOUNDCNT_X_Ch2On = 1 << 1, 227 | SOUNDCNT_X_Ch3On = 1 << 2, 228 | SOUNDCNT_X_Ch4On = 1 << 3, 229 | 230 | SOUNDCNT_X_Enable = 1 << 7 231 | }; 232 | 233 | 234 | enum DMACNTHBits 235 | { 236 | DMACNTH_DestMode = 0x3 << 5, 237 | DMACNTH_SrcMode = 0x3 << 7, 238 | DAMCNTH_Repeat = 1 << 9, 239 | DMACNTH_32Bit = 1 << 10, 240 | DMACNTH_GamePak = 1 << 11, // 3 only 241 | DMACNTH_Start = 0x3 << 12, 242 | DMACNTH_IRQEnable = 1 << 14, 243 | DMACNTH_Enable = 1 << 15 244 | }; 245 | 246 | enum TMCNTHBits 247 | { 248 | TMCNTH_Prescaler = 0x3, 249 | TMCNTH_CountUp = 1 << 2, 250 | TMCNTH_IRQEnable = 1 << 6, 251 | TMCNTH_Enable = 1 << 7 252 | }; 253 | 254 | enum WAITCNTBits 255 | { 256 | WAITCNT_SRAM = 3, 257 | WAITCNT_ROMWS0N = 3 << 2, 258 | WAITCNT_ROMWS0S = 1 << 4, 259 | WAITCNT_ROMWS1N = 3 << 5, 260 | WAITCNT_ROMWS1S = 1 << 7, 261 | WAITCNT_ROMWS2N = 3 << 8, 262 | WAITCNT_ROMWS2S = 1 << 10, 263 | WAITCNT_PHI = 3 << 11, 264 | WAITCNT_Prefetch = 1 << 14, 265 | WAITCNT_GameType = 1 << 15 266 | }; -------------------------------------------------------------------------------- /core/AGBMemory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | class AGBCPU; 6 | 7 | class AGBMemory 8 | { 9 | public: 10 | enum class SaveType : uint8_t 11 | { 12 | Unknown, 13 | EEPROM_512, 14 | EEPROM_8K, 15 | RAM, 16 | Flash_64K, 17 | Flash_128K 18 | }; 19 | 20 | AGBMemory(AGBCPU &cpu); 21 | 22 | //using CartRamUpdateCallback = void(*)(uint8_t *, unsigned int); 23 | 24 | void setBIOSROM(const uint8_t *rom); 25 | bool hasBIOS() const {return biosROM;} 26 | 27 | void setCartROM(const uint8_t *rom, uint32_t size); 28 | void loadCartridgeSave(const uint8_t *data, uint32_t len); 29 | void reset(); 30 | 31 | template 32 | T read(uint32_t addr, int &cycles, bool sequential) const; 33 | template 34 | void write(uint32_t addr, T data, int &cycles, bool sequential); 35 | 36 | // fast access to IO regs 37 | uint16_t readIOReg(uint16_t addr) const {return *reinterpret_cast(ioRegs + addr);} 38 | uint16_t &getIOReg(uint16_t addr) {return *reinterpret_cast(ioRegs + addr);} 39 | void writeIOReg(uint16_t addr, uint16_t val) {*reinterpret_cast(ioRegs + addr) = val;} 40 | 41 | uint8_t *getPalRAM() {return palRAM;} 42 | uint8_t *getVRAM() {return vram;} 43 | uint8_t *getOAM() {return oam;} 44 | 45 | uint8_t *getCartridgeSave() {return cartSaveData;} 46 | SaveType getCartridgeSaveType() {return saveType;} 47 | /*void setCartRamUpdateCallback(CartRamUpdateCallback callback);*/ 48 | 49 | const uint8_t *mapAddress(uint32_t addr) const; 50 | uint8_t *mapAddress(uint32_t addr); 51 | 52 | int getAccessCycles(uint32_t addr, int width, bool sequential) const; 53 | 54 | void updateWaitControl(uint16_t waitcnt); 55 | void updatePC(uint32_t pc); 56 | 57 | inline int iCycle(int i = 1) 58 | { 59 | prefetchCycles -= i; 60 | return i; 61 | } 62 | 63 | int prefetchTiming16(int cycles, int bugCycles = 0) 64 | { 65 | // not in ROM 66 | if(!pcInROM) 67 | return cycles; 68 | 69 | if(!cartPrefetchEnabled) 70 | { 71 | // prefetch disable bug, N cycle instead of S 72 | if(bugCycles) 73 | return bugCycles; 74 | 75 | return cycles; 76 | } 77 | 78 | prefetchCycles--; 79 | 80 | while(prefetchCycles <= 0) 81 | { 82 | if(prefetchedHalfWords < 8) 83 | prefetchedHalfWords++; 84 | prefetchCycles += prefetchSCycles; 85 | } 86 | 87 | if(prefetchedHalfWords) 88 | { 89 | prefetchedHalfWords--; 90 | return 1; 91 | } 92 | 93 | // prefetch isn't done... wait for it 94 | cycles = prefetchCycles + 1; 95 | prefetchCycles = prefetchSCycles; 96 | return cycles; 97 | } 98 | 99 | int prefetchTiming32(int cycles, int bugCycles = 0) 100 | { 101 | // not in ROM 102 | if(!pcInROM) 103 | return cycles; 104 | 105 | if(!cartPrefetchEnabled) 106 | { 107 | // prefetch disable bug, N cycle instead of S 108 | if(bugCycles) 109 | return bugCycles; 110 | 111 | return cycles; 112 | } 113 | 114 | prefetchCycles--; 115 | 116 | while(prefetchCycles <= 0) 117 | { 118 | if(prefetchedHalfWords < 8) 119 | prefetchedHalfWords++; 120 | prefetchCycles += prefetchSCycles; 121 | } 122 | 123 | if(prefetchedHalfWords > 1) 124 | { 125 | prefetchedHalfWords -= 2; 126 | return 1; // can fetch a word in one cycle 127 | } 128 | 129 | // prefetch isn't done... wait for it 130 | if(prefetchedHalfWords) 131 | cycles = 1 /*first half*/ + prefetchCycles/* + 1 */; // cycle for first half allows prefetch to continue? 132 | else 133 | cycles = prefetchCycles + 1 /*first half*/ + prefetchSCycles; 134 | 135 | prefetchedHalfWords = 0; 136 | prefetchCycles = prefetchSCycles; 137 | return cycles; 138 | } 139 | 140 | // verify that pointer returns the same as a regular read to the address 141 | // without affecting prefetch (used for asserts) 142 | template 143 | bool verifyPointer(const T *ptr, uint32_t addr) 144 | { 145 | auto tmpCycles = prefetchCycles; 146 | auto tmpHalfWords = prefetchedHalfWords; 147 | 148 | int tmp; 149 | bool ret = read(addr, tmp, false) == *ptr; 150 | 151 | prefetchCycles = tmpCycles; 152 | prefetchedHalfWords = tmpHalfWords; 153 | 154 | return ret; 155 | } 156 | 157 | private: 158 | 159 | enum class FlashState : uint8_t 160 | { 161 | Read, 162 | ID, 163 | Erase, 164 | Write, 165 | Bank, 166 | }; 167 | 168 | template 169 | T doRead(const uint8_t (&mem)[size], uint32_t addr) const; 170 | template 171 | void doWrite(uint8_t (&mem)[size], uint32_t addr, T data); 172 | 173 | template 174 | T doBIOSRead(uint32_t addr) const; 175 | 176 | template 177 | T doIORead(uint32_t addr) const; 178 | template 179 | void doIOWrite(uint32_t addr, T data); 180 | 181 | template 182 | void doPalRAMWrite(uint32_t addr, T data); 183 | 184 | template 185 | T doVRAMRead(uint32_t addr) const; 186 | template 187 | void doVRAMWrite(uint32_t addr, T data); 188 | 189 | template 190 | void doOAMWrite(uint32_t addr, T data); 191 | 192 | template 193 | T doROMRead(uint32_t addr) const; 194 | template 195 | T doROMOrEEPROMRead(uint32_t addr) const; 196 | template 197 | void doEEPROMWrite(uint32_t addr, T data); 198 | 199 | template 200 | T doSRAMRead(uint32_t addr) const; 201 | template 202 | void doSRAMWrite(uint32_t addr, T data); 203 | 204 | template 205 | T doOpenRead(uint32_t addr) const; 206 | 207 | void writeFlash(uint32_t addr, uint8_t data); 208 | 209 | AGBCPU &cpu; 210 | 211 | // prefetch state 212 | bool cartPrefetchEnabled = false, pcInROM = false; 213 | int prefetchSCycles = 0; 214 | mutable int prefetchCycles = 0; 215 | int prefetchedHalfWords = 0; 216 | 217 | const uint8_t *biosROM = nullptr; 218 | uint8_t ewram[0x40000]; // external wram, two wait states, 16bit bus 219 | uint8_t iwram[0x8000]; // internal wram 220 | 221 | uint8_t ioRegs[0x400]; 222 | 223 | uint8_t palRAM[0x400]; // 16bit bus 224 | uint8_t vram[0x18000]; // 16bit bus 225 | uint8_t oam[0x400]; 226 | 227 | const uint8_t *cartROM = nullptr; 228 | uint32_t cartROMSize = 0; 229 | 230 | SaveType saveType = SaveType::Unknown; 231 | 232 | uint64_t eepromCommandData[2]; // max 81 bits 233 | uint64_t eepromReadData; 234 | uint8_t cartSaveData[128 * 1024]; // RAM/flash 235 | 236 | FlashState flashState = FlashState::Read; 237 | uint8_t flashCmdState = 0; 238 | uint8_t flashBank = 0; 239 | uint8_t flashID[2]; 240 | 241 | uint32_t dummy = 0xBADADD55; 242 | 243 | int8_t cartAccessN[4], cartAccessS[4]; // ROM and RAM 244 | 245 | //CartRamUpdateCallback cartRamUpdateCallback; 246 | }; -------------------------------------------------------------------------------- /tests/runner.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "png.h" 10 | 11 | #include "DMGAPU.h" 12 | #include "DMGCPU.h" 13 | #include "DMGDisplay.h" 14 | #include "DMGMemory.h" 15 | #include "DMGRegs.h" 16 | 17 | static std::ifstream romFile; 18 | static size_t romSize; 19 | 20 | static DMGCPU *cpu; 21 | static uint16_t screenData[160 * 144]; 22 | static uint8_t romBankCache[0x4000]; // need at least one bank 23 | 24 | static bool takeScreenshot = false; 25 | 26 | static void getROMBank(uint8_t bank, uint8_t *ptr) 27 | { 28 | auto addr = (bank * 0x4000); 29 | 30 | romFile.seekg(addr); 31 | romFile.read((char *)ptr, 0x4000); 32 | } 33 | 34 | // PNG load/save 35 | // assuming 160 * 144 36 | // and very little error checking 37 | static uint8_t *loadPNG(const std::string &filename) 38 | { 39 | auto f = fopen(filename.c_str(), "rb"); 40 | 41 | if(!f) 42 | return nullptr; 43 | 44 | png_byte header[8]; 45 | if(fread(header, 1, 8, f) != 8 || png_sig_cmp(header, 0, 8)) 46 | return nullptr; 47 | 48 | auto pngRead = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); 49 | auto info = png_create_info_struct(pngRead); 50 | auto endInfo = png_create_info_struct(pngRead); 51 | 52 | if(setjmp(png_jmpbuf(pngRead))) 53 | { 54 | png_destroy_read_struct(&pngRead, &info, &endInfo); 55 | return nullptr; 56 | } 57 | 58 | png_init_io(pngRead, f); 59 | png_set_sig_bytes(pngRead, 8); 60 | 61 | png_read_png(pngRead, info, PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_ALPHA | PNG_TRANSFORM_GRAY_TO_RGB, nullptr); 62 | 63 | auto rows = png_get_rows(pngRead, info); 64 | 65 | png_uint_32 width, height; 66 | 67 | int bitDepth, colourType; 68 | 69 | png_get_IHDR(pngRead, info, &width, &height, &bitDepth, &colourType, nullptr, nullptr, nullptr); 70 | 71 | if(width != 160 || height != 144 || colourType != PNG_COLOR_TYPE_RGB) 72 | { 73 | png_destroy_read_struct(&pngRead, &info, &endInfo); 74 | fclose(f); 75 | return nullptr; 76 | } 77 | 78 | auto data = new uint8_t[width * height * 3]; 79 | 80 | for(unsigned int y = 0; y < height; y++) 81 | memcpy(data + (y * width * 3), rows[y], width * 3); 82 | 83 | png_destroy_read_struct(&pngRead, &info, &endInfo); 84 | fclose(f); 85 | return data; 86 | } 87 | 88 | static void savePNG(const std::string &filename, const uint8_t *data) 89 | { 90 | auto f = fopen(filename.c_str(), "wb"); 91 | 92 | if(!f) 93 | return; 94 | 95 | auto pngWrite = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); 96 | auto info = png_create_info_struct(pngWrite); 97 | 98 | png_init_io(pngWrite, f); 99 | 100 | int width = 160, height = 144; 101 | 102 | png_set_IHDR(pngWrite, info, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); 103 | 104 | auto rows = static_cast(png_malloc(pngWrite, height * sizeof(png_bytep))); 105 | for(int y = 0; y < height; y++) 106 | rows[y] = const_cast(data + y * width * 3); 107 | 108 | png_set_rows(pngWrite, info, rows); 109 | png_write_png(pngWrite, info, PNG_TRANSFORM_IDENTITY, nullptr); 110 | 111 | png_free(pngWrite, rows); 112 | 113 | png_destroy_write_struct(&pngWrite, &info); 114 | 115 | fclose(f); 116 | } 117 | 118 | static void dumpImage(const std::string &filename, const uint8_t *data) 119 | { 120 | savePNG("./test-results/" + filename + ".png", data); 121 | } 122 | 123 | static void screenToRGB(DMGDisplay &display, uint8_t *outRGB) 124 | { 125 | for(int i = 0; i < 160 * 144; i++) 126 | { 127 | auto pixel = screenData[i]; 128 | outRGB[i * 3 + 0] = (pixel & 0x1F) << 3; 129 | outRGB[i * 3 + 1] = (pixel & 0x3E0) >> 2; 130 | outRGB[i * 3 + 2] = (pixel & 0x7C00) >> 7; 131 | } 132 | } 133 | 134 | static bool runTest(const std::string &rom, DMGCPU::Console console = DMGCPU::Console::Auto) 135 | { 136 | // find/open ROM 137 | std::string paths[]{ 138 | "", 139 | }; 140 | 141 | std::string basePath; 142 | 143 | for(auto path : paths) 144 | { 145 | basePath = path; 146 | romFile.open(path + rom); 147 | if(romFile) 148 | break; 149 | } 150 | 151 | if(!romFile) 152 | { 153 | std::cerr << "Failed to load " << rom << "\n"; 154 | return false; 155 | } 156 | 157 | romFile.seekg(0, std::ios::end); 158 | romSize = romFile.tellg(); 159 | romFile.seekg(0, std::ios::beg); 160 | 161 | // clean instance 162 | cpu = new DMGCPU; 163 | cpu->getDisplay().setFramebuffer(screenData); 164 | 165 | auto &mem = cpu->getMem(); 166 | mem.setROMBankCallback(getROMBank); 167 | mem.addROMCache(romBankCache, sizeof(romBankCache)); 168 | 169 | cpu->setConsole(console); 170 | 171 | cpu->reset(); 172 | 173 | uint8_t rgbDisplay[160 * 144 * 3]; 174 | 175 | unsigned int time = 0; 176 | bool result = false; 177 | 178 | while(!result) 179 | { 180 | // flush apu 181 | auto &apu = cpu->getAPU(); 182 | apu.update(); 183 | while(apu.getNumSamples()) 184 | apu.getSample(); 185 | cpu->run(10); 186 | 187 | time += 10; 188 | 189 | bool convertDisplay = takeScreenshot || cpu->getBreakpointTriggered(); 190 | 191 | if(convertDisplay) 192 | { 193 | cpu->getDisplay().update(); 194 | screenToRGB(cpu->getDisplay(), rgbDisplay); 195 | } 196 | 197 | if(takeScreenshot) 198 | { 199 | dumpImage("output", rgbDisplay); 200 | takeScreenshot = false; 201 | } 202 | } 203 | 204 | result = true; 205 | 206 | delete cpu; 207 | 208 | romFile.close(); 209 | return result; 210 | } 211 | 212 | static void replayLog(const std::string &logFilename, DMGCPU::Console console = DMGCPU::Console::Auto, bool record = false) 213 | { 214 | // find/open ROM 215 | std::string rom, logPath; 216 | 217 | auto index = logFilename.find_last_of("\\/"); 218 | if(index != std::string::npos) 219 | logPath = logFilename.substr(0, index + 1); 220 | 221 | 222 | std::ifstream logFile(logFilename); 223 | 224 | if(!logFile) 225 | { 226 | std::cerr << "Failed to open log " << logFilename << "!\n"; 227 | return; 228 | } 229 | 230 | // get rom from first line 231 | std::getline(logFile, rom); 232 | 233 | romFile.open(logPath + rom); 234 | 235 | if(!romFile) 236 | { 237 | std::cerr << "Failed to load " << logPath + rom << "\n"; 238 | return; 239 | } 240 | 241 | romFile.seekg(0, std::ios::end); 242 | romSize = romFile.tellg(); 243 | romFile.seekg(0, std::ios::beg); 244 | 245 | // clean instance 246 | cpu = new DMGCPU; 247 | cpu->getDisplay().setFramebuffer(screenData); 248 | 249 | auto &mem = cpu->getMem(); 250 | mem.setROMBankCallback(getROMBank); 251 | mem.addROMCache(romBankCache, sizeof(romBankCache)); 252 | 253 | cpu->setConsole(console); 254 | 255 | cpu->reset(); 256 | 257 | uint8_t rgbDisplay[160 * 144 * 3]; 258 | 259 | unsigned int tick = 0, imageIndex = 0; 260 | unsigned int nextInputTick; 261 | int nextInputValue; 262 | 263 | logFile >> nextInputTick; 264 | logFile >> nextInputValue; 265 | 266 | auto start = std::chrono::steady_clock::now(); 267 | 268 | while(!logFile.eof()) 269 | { 270 | // flush apu 271 | auto &apu = cpu->getAPU(); 272 | apu.update(); 273 | while(apu.getNumSamples()) 274 | apu.getSample(); 275 | 276 | bool didInput = false; 277 | if(tick == nextInputTick) 278 | { 279 | cpu->setInputs(nextInputValue); 280 | didInput = true; 281 | logFile >> nextInputTick; 282 | logFile >> nextInputValue; 283 | } 284 | 285 | cpu->run(10); 286 | 287 | if(record && didInput) 288 | { 289 | cpu->getDisplay().update(); 290 | screenToRGB(cpu->getDisplay(), rgbDisplay); 291 | char name[10]; 292 | snprintf(name, 10, "f%06i", imageIndex++); 293 | dumpImage(name, rgbDisplay); 294 | } 295 | 296 | tick++; 297 | } 298 | 299 | auto end = std::chrono::steady_clock::now(); 300 | 301 | auto realTime = std::chrono::duration_cast(end - start).count(); 302 | 303 | std::cout << "Ran " << rom << " for " << (tick * 10) << "ms in " << realTime << "us\n"; 304 | 305 | delete cpu; 306 | 307 | romFile.close(); 308 | } 309 | 310 | static void handleSignal(int signal) 311 | { 312 | takeScreenshot = true; 313 | } 314 | 315 | int main(int argc, char *argv[]) 316 | { 317 | std::filesystem::create_directory("./test-results"); 318 | 319 | if(argc > 1) 320 | { 321 | // run a test (wrapper for an external test runner) 322 | int i = 1; 323 | // options 324 | auto console = DMGCPU::Console::Auto; 325 | bool recordReplay = false; 326 | 327 | for(; i < argc; i++) 328 | { 329 | std::string_view arg = argv[i]; 330 | if(arg == "--cgb") 331 | console = DMGCPU::Console::CGB; 332 | else if(arg == "--dmg") 333 | console = DMGCPU::Console::DMG; 334 | else if(arg == "--record") 335 | recordReplay = true; 336 | else 337 | break; 338 | } 339 | 340 | std::string filename = argv[i]; 341 | std::string ext = filename.substr(filename.find_last_of('.')); 342 | 343 | if(ext == ".gb" || ext == ".gbc") 344 | { 345 | signal(SIGUSR1, handleSignal); 346 | runTest(filename, console); 347 | } 348 | else if(ext == ".log") 349 | { 350 | // replay from a log file 351 | // assume all remaining args are log files 352 | for(; i < argc; i++) 353 | replayLog(argv[i], console, recordReplay); 354 | } 355 | return 0; 356 | } 357 | return 0; 358 | } -------------------------------------------------------------------------------- /core/AGBCPU.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include "AGBAPU.h" 5 | #include "AGBDisplay.h" 6 | #include "AGBMemory.h" 7 | 8 | class AGBCPU final 9 | { 10 | public: 11 | enum Interrupts 12 | { 13 | Int_LCDVBlank = 1 << 0, 14 | Int_LCDHBlank = 1 << 1, 15 | Int_LCDVCount = 1 << 2, 16 | Int_Timer0 = 1 << 3, 17 | Int_Timer1 = 1 << 4, 18 | Int_Timer2 = 1 << 5, 19 | Int_Timer3 = 1 << 6, 20 | Int_Serial = 1 << 7, 21 | Int_DMA0 = 1 << 8, 22 | Int_DMA1 = 1 << 9, 23 | Int_DMA2 = 1 << 10, 24 | Int_DMA3 = 1 << 11, 25 | Int_Keypad = 1 << 12, 26 | Int_External = 1 << 13 27 | }; 28 | 29 | enum DMATrigger 30 | { 31 | Trig_VBlank = 1, 32 | Trig_HBlank, 33 | Trig_SoundA, 34 | Trig_SoundB 35 | }; 36 | 37 | AGBCPU(); 38 | 39 | void reset(); 40 | 41 | void run(int ms); 42 | void runFrame(); 43 | 44 | void flagInterrupt(int interrupt, bool recalculateUpdate = true); 45 | void triggerDMA(int trigger); 46 | 47 | uint16_t readReg(uint32_t addr, uint16_t val); 48 | bool writeReg(uint32_t addr, uint16_t data, uint16_t mask); 49 | 50 | AGBAPU &getAPU() {return apu;} 51 | AGBDisplay &getDisplay() {return display;} 52 | AGBMemory &getMem() {return mem;} 53 | 54 | uint32_t getCycleCount() const {return cycleCount;} 55 | 56 | void setInputs(uint16_t newInputs); 57 | 58 | private: 59 | enum class Reg 60 | { 61 | R0 = 0, 62 | R1, 63 | R2, 64 | R3, 65 | R4, 66 | R5, 67 | R6, 68 | R7, 69 | // ARM mode/high 70 | R8, 71 | R9, 72 | R10, 73 | R11, 74 | R12, 75 | R13, 76 | R14, 77 | R15, 78 | 79 | // banked 80 | R8_fiq, 81 | R9_fiq, 82 | R10_fiq, 83 | R11_fiq, 84 | R12_fiq, 85 | R13_fiq, 86 | R14_fiq, 87 | 88 | R13_svc, 89 | R14_svc, 90 | 91 | R13_abt, 92 | R14_abt, 93 | 94 | R13_irq, 95 | R14_irq, 96 | 97 | R13_und, 98 | R14_und, 99 | 100 | // aliases 101 | SP = R13, // also banked aliases... 102 | LR = R14, 103 | PC = R15 104 | }; 105 | 106 | enum Flags 107 | { 108 | // control 109 | Flag_T = (1 << 5), // thumb 110 | Flag_F = (1 << 6), // FIQ disable 111 | Flag_I = (1 << 7), // IRQ disable 112 | 113 | // condition codes 114 | Flag_V = (1 << 28), 115 | Flag_C = (1 << 29), 116 | Flag_Z = (1 << 30), 117 | Flag_N = (1 << 31) 118 | }; 119 | 120 | Reg mapReg(Reg r) const 121 | { 122 | int iReg = static_cast(r); 123 | if(!regBankOffset || iReg < 8 || r == Reg::PC) 124 | return r; 125 | 126 | if(regBankOffset != 8/*FIQ*/ && iReg < 13) 127 | return r; 128 | 129 | return static_cast(iReg + regBankOffset); 130 | } 131 | 132 | uint32_t reg(Reg r) const {return regs[static_cast(mapReg(r))];} 133 | uint32_t ®(Reg r) {return regs[static_cast(mapReg(r))];} 134 | 135 | // THUMB, first 8 regs, also used when we don't want to map 136 | uint32_t loReg(Reg r) const {return regs[static_cast(r)];} 137 | uint32_t &loReg(Reg r) {return regs[static_cast(r)];} 138 | 139 | uint32_t &getSPSR() 140 | { 141 | switch(cpsr & 0x1F) 142 | { 143 | case 0x1F: // System 144 | return spsr[5]; // system/user modes don't have a SPSR reg, but seems this needs to do something 145 | case 0x11: // FIQ 146 | return spsr[0]; 147 | case 0x12: // IRQ 148 | return spsr[3]; 149 | case 0x13: // SVC 150 | return spsr[1]; 151 | case 0x17: // ABT 152 | return spsr[2]; 153 | case 0x1B: // UND 154 | return spsr[4]; 155 | } 156 | 157 | assert(!"Bad CPSR mode!"); 158 | return spsr[5]; // invalid mode! 159 | } 160 | 161 | void modeChanged() // possibly 162 | { 163 | switch(cpsr & 0x1F) 164 | { 165 | case 0x10: // User 166 | case 0x1F: // System 167 | regBankOffset = 0; 168 | break; 169 | case 0x11: // FIQ 170 | regBankOffset = static_cast(Reg::R8_fiq) - static_cast(Reg::R8); 171 | break; 172 | case 0x13: // SVC 173 | regBankOffset = static_cast(Reg::R13_svc) - static_cast(Reg::R13); 174 | break; 175 | case 0x17: // ABT 176 | regBankOffset = static_cast(Reg::R13_abt) - static_cast(Reg::R13); 177 | break; 178 | case 0x12: // IRQ 179 | regBankOffset = static_cast(Reg::R13_irq) - static_cast(Reg::R13); 180 | break; 181 | case 0x1B: // UND 182 | regBankOffset = static_cast(Reg::R13_und) - static_cast(Reg::R13); 183 | break; 184 | default: 185 | assert(!"Bad CPSR mode"); 186 | } 187 | 188 | curSP = mapReg(Reg::SP); 189 | curLR = mapReg(Reg::LR); 190 | } 191 | 192 | uint8_t readMem8(uint32_t addr, int &cycles, bool sequential = false) const; 193 | uint32_t readMem16(uint32_t addr, int &cycles, bool sequential = false); 194 | uint16_t readMem16Aligned(uint32_t addr, int &cycles, bool sequential = false); 195 | uint32_t readMem32(uint32_t addr, int &cycles, bool sequential = false); 196 | uint32_t readMem32Aligned(uint32_t addr, int &cycles, bool sequential = false); 197 | void writeMem8(uint32_t addr, uint8_t data, int &cycles, bool sequential = false); 198 | void writeMem16(uint32_t addr, uint16_t data, int &cycles, bool sequential = false); 199 | void writeMem32(uint32_t addr, uint32_t data, int &cycles, bool sequential = false); 200 | 201 | int runCycles(int cycles); 202 | 203 | int executeARMInstruction(); 204 | int executeTHUMBInstruction(); 205 | 206 | bool checkARMCondition(int cond) const; 207 | 208 | uint32_t getARMShiftedReg(uint16_t shift, bool &carry); 209 | int doARMHalfwordTransfer(uint32_t opcode, bool isPre); 210 | int doARMMultiply(uint32_t opcode); 211 | int doARMSwap(uint32_t opcode); 212 | int doARMPSRTransfer(uint32_t opcode, uint32_t op2 = 0); 213 | int doARMSingleDataTransfer(uint32_t opcode, bool isReg, bool isPre); 214 | int doARMBlockDataTransfer(uint32_t opcode, bool isPre); 215 | 216 | int doALUOp(int op, Reg destReg, uint32_t op1, uint32_t op2, bool carry); 217 | int doALUOpNoCond(int op, Reg destReg, uint32_t op1, uint32_t op2); 218 | 219 | int doTHUMB01MoveShifted(uint16_t opcode, uint32_t pc); 220 | int doTHUMB0102(uint16_t opcode, uint32_t pc); 221 | int doTHUMB03(uint16_t opcode, uint32_t pc); 222 | int doTHUMB040506(uint16_t opcode, uint32_t pc); 223 | int doTHUMB04ALU(uint16_t opcode, uint32_t pc); 224 | int doTHUMB05HiReg(uint16_t opcode, uint32_t pc); 225 | int doTHUMB06PCRelLoad(uint16_t opcode, uint32_t pc); 226 | int doTHUMB0708(uint16_t opcode, uint32_t pc); 227 | int doTHUMB09LoadStoreWord(uint16_t opcode, uint32_t pc); 228 | int doTHUMB09LoadStoreByte(uint16_t opcode, uint32_t pc); 229 | int doTHUMB10LoadStoreHalf(uint16_t opcode, uint32_t pc); 230 | int doTHUMB11SPRelLoadStore(uint16_t opcode, uint32_t pc); 231 | int doTHUMB12LoadAddr(uint16_t opcode, uint32_t pc); 232 | int doTHUMB1314(uint16_t opcode, uint32_t pc); 233 | int doTHUMB13SPOffset(uint16_t opcode, uint32_t pc); 234 | int doTHUMB14PushPop(uint16_t opcode, uint32_t pc); 235 | int doTHUMB15MultiLoadStore(uint16_t opcode, uint32_t pc); 236 | int doTHUMB1617(uint16_t opcode, uint32_t pc); 237 | int doTHUMB18UncondBranch(uint16_t opcode, uint32_t pc); 238 | int doTHUMB19LongBranchLink(uint16_t opcode, uint32_t pc); 239 | 240 | void updateARMPC(uint32_t pc); 241 | void updateTHUMBPC(uint32_t pc); 242 | 243 | int serviceInterrupts(); 244 | 245 | int dmaTransfer(int channel); 246 | 247 | void updateTimers(); 248 | void calculateNextTimerOverflow(uint32_t cycleCount); 249 | 250 | void calculateNextUpdate(uint32_t cycleCount); 251 | 252 | void handleBIOSBranch(uint32_t pc); 253 | void handleSWI(int num); 254 | 255 | void swiSoftReset(); 256 | void swiRegisterRAMReset(); 257 | bool swiIntrWait(bool discardFlags, uint16_t flags); 258 | void swiDiv(); 259 | uint32_t swiArcTan(int tan); 260 | uint32_t swiArcTan2(); 261 | void swiCPUSet(); 262 | void swiCPUFastSet(); 263 | void swiBgAffineSet(); 264 | void swiObjAffineSet(); 265 | void swiBitUnpack(); 266 | void swiLZ77Write8(); 267 | void swiLZ77Write16(); 268 | void swiHuffmanDecode(); 269 | 270 | static const uint32_t clockSpeed = 16*1024*1024; 271 | static const uint32_t signBit = 0x80000000; 272 | 273 | // registers 274 | uint32_t regs[31]{}; 275 | uint32_t cpsr; 276 | uint32_t spsr[6]; // fiq, svc, abt, irq, und 277 | 278 | Reg curSP = Reg::SP, curLR = Reg::LR; 279 | int regBankOffset = 0; 280 | 281 | const uint8_t *pcPtr = nullptr; 282 | int pcSCycles = 0, pcNCycles = 0; 283 | 284 | // pipeline 285 | uint32_t fetchOp = 0, decodeOp = 0; 286 | 287 | // internal state 288 | //bool stopped, halted; 289 | bool halted; 290 | uint16_t swiWaitFlags = 0; // interrupt flags for IntrWait 291 | 292 | uint16_t currentInterrupts = 0; // IME ? (IE & IF) : 0 293 | uint16_t enabledInterrutps = 0; 294 | uint8_t interruptDelay = 0; 295 | 296 | // dma 297 | uint8_t dmaTriggered = 0, dmaActive = 0; 298 | uint16_t dmaCount[4]; // internal values 299 | uint32_t dmaSrc[4], dmaDst[4]; // reloaded on enable 300 | uint16_t dmaCurCount[4]; // tmp values while DMA is in progress 301 | uint32_t dmaCurDst[4]; // 302 | uint32_t dmaLastVal = 0; // last value copied by any DMA 303 | 304 | uint32_t cycleCount = 0; 305 | int lastExtraCycles = 0; // used to keep runFrame in sync 306 | 307 | uint32_t nextUpdateCycle = 0; // next cycle where something needs updated 308 | 309 | // timers 310 | uint32_t lastTimerUpdate = 0; 311 | uint32_t nextTimerUpdate = 0; 312 | uint8_t timerEnabled = 0, timerInterruptEnabled = 0; 313 | 314 | uint16_t timerCounters[4]{}; 315 | int timerPrescalers[4]{}; 316 | 317 | uint16_t inputs = 0; 318 | 319 | AGBAPU apu; 320 | AGBDisplay display; 321 | AGBMemory mem; 322 | }; -------------------------------------------------------------------------------- /minsdl/Main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "AGBCPU.h" 10 | #include "DMGCPU.h" 11 | 12 | static bool quit = false; 13 | static bool turbo = false; 14 | 15 | static bool isAGB = false; 16 | 17 | static DMGCPU dmgCPU; 18 | static AGBCPU agbCPU; 19 | 20 | static uint16_t inputs = 0; 21 | static uint16_t screenData[240 * 160]; 22 | static uint8_t romBankCache[0x4000]; 23 | 24 | static uint8_t agbBIOSROM[0x4000]; 25 | 26 | static std::ifstream romFile; 27 | 28 | static SDL_Texture *texture; // display texture 29 | 30 | static const std::unordered_map dmgKeyMap { 31 | {SDLK_RIGHT, 1 << 0}, 32 | {SDLK_LEFT, 1 << 1}, 33 | {SDLK_UP, 1 << 2}, 34 | {SDLK_DOWN, 1 << 3}, 35 | 36 | {SDLK_Z, 1 << 4}, 37 | {SDLK_X, 1 << 5}, 38 | {SDLK_C, 1 << 6}, 39 | {SDLK_V, 1 << 7}, 40 | }; 41 | 42 | // maybe swap dpad/buttons on DMG to share mappings? 43 | static const std::unordered_map agbKeyMap { 44 | {SDLK_RIGHT, 1 << 4}, 45 | {SDLK_LEFT, 1 << 5}, 46 | {SDLK_UP, 1 << 6}, 47 | {SDLK_DOWN, 1 << 7}, 48 | 49 | {SDLK_Z, 1 << 0}, 50 | {SDLK_X, 1 << 1}, 51 | {SDLK_C, 1 << 2}, 52 | {SDLK_V, 1 << 3}, 53 | 54 | {SDLK_Q, 1 << 9}, 55 | {SDLK_E, 1 << 8}, 56 | }; 57 | 58 | static void getROMBank(uint8_t bank, uint8_t *ptr) 59 | { 60 | auto addr = bank * 0x4000; 61 | 62 | romFile.seekg(addr); 63 | romFile.read((char *)ptr, 0x4000); 64 | } 65 | 66 | static void audioCallback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) 67 | { 68 | // TODO: refactor core so we can push data directly 69 | if(isAGB) 70 | { 71 | auto &apu = agbCPU.getAPU(); 72 | 73 | for(int i = 0; i < additional_amount; i++) 74 | { 75 | if(!apu.hasSample()) 76 | break; 77 | 78 | int16_t v = apu.getSample(); 79 | SDL_PutAudioStreamData(stream, &v, 2); 80 | } 81 | } 82 | else 83 | { 84 | auto &apu = dmgCPU.getAPU(); 85 | 86 | for(int i = 0; i < additional_amount; i++) 87 | { 88 | if(!apu.getNumSamples()) 89 | break; 90 | 91 | int16_t v = apu.getSample(); 92 | SDL_PutAudioStreamData(stream, &v, 2); 93 | } 94 | } 95 | } 96 | 97 | static uint8_t *readSave(const std::string &savePath, size_t &saveSize) 98 | { 99 | std::ifstream saveFile(savePath, std::ios::binary); 100 | 101 | if(!saveFile) 102 | { 103 | std::cout << "Could not find " << savePath << ", no save loaded.\n"; 104 | return nullptr; 105 | } 106 | 107 | saveFile.seekg(0, std::ios::end); 108 | saveSize = saveFile.tellg(); 109 | saveFile.seekg(0); 110 | 111 | auto saveData = new uint8_t[saveSize]; 112 | saveFile.read(reinterpret_cast(saveData), saveSize); 113 | std::cout << "Read " << saveSize << " bytes from " << savePath << "\n"; 114 | 115 | return saveData; 116 | } 117 | 118 | static void dmgVBlankCallback() 119 | { 120 | SDL_UpdateTexture(texture, nullptr, screenData, 160 * 2); 121 | } 122 | 123 | static void pollEvents() 124 | { 125 | auto &keyMap = isAGB ? agbKeyMap : dmgKeyMap; 126 | 127 | SDL_Event event; 128 | while(SDL_PollEvent(&event)) 129 | { 130 | switch(event.type) 131 | { 132 | case SDL_EVENT_KEY_DOWN: 133 | { 134 | auto it = keyMap.find(event.key.key); 135 | if(it != keyMap.end()) 136 | inputs |= it->second; 137 | break; 138 | } 139 | case SDL_EVENT_KEY_UP: 140 | { 141 | auto it = keyMap.find(event.key.key); 142 | if(it != keyMap.end()) 143 | inputs &= ~it->second; 144 | break; 145 | } 146 | 147 | case SDL_EVENT_QUIT: 148 | quit = true; 149 | break; 150 | } 151 | } 152 | } 153 | 154 | int main(int argc, char *argv[]) 155 | { 156 | int screenWidth = 160; 157 | int screenHeight = 144; 158 | int screenScale = 5; 159 | bool useBIOS = true; 160 | 161 | uint32_t timeToRun = 0; 162 | bool timeLimit = false; 163 | std::string romFilename; 164 | 165 | int i = 1; 166 | 167 | for(; i < argc; i++) 168 | { 169 | std::string arg(argv[i]); 170 | 171 | if(arg == "--scale" && i + 1 < argc) 172 | screenScale = std::stoi(argv[++i]); 173 | else if(arg == "--turbo") 174 | turbo = true; 175 | else if(arg == "--time" && i + 1 < argc) 176 | { 177 | timeLimit = true; 178 | timeToRun = std::stoi(argv[++i]) * 1000; 179 | } 180 | else if(arg == "--no-bios") 181 | useBIOS = false; 182 | else 183 | break; 184 | } 185 | 186 | if(i == argc) 187 | { 188 | std::cerr << "No ROM specified!\n"; 189 | return 1; 190 | } 191 | 192 | // get base path 193 | std::string basePath; 194 | auto tmp = SDL_GetBasePath(); 195 | if(tmp) 196 | basePath = tmp; 197 | 198 | romFilename = argv[i]; 199 | 200 | std::string ext = romFilename.substr(romFilename.find_last_of('.')); 201 | isAGB = ext == ".gba"; 202 | 203 | romFile.open(romFilename, std::ios::binary); 204 | 205 | if(!romFile) 206 | { 207 | std::cerr << "Failed to open ROM \"" << romFilename << "\"\n"; 208 | return 1; 209 | } 210 | 211 | // emu init 212 | if(isAGB) 213 | { 214 | screenWidth = 240; 215 | screenHeight = 160; 216 | 217 | auto &mem = agbCPU.getMem(); 218 | 219 | // need the bios for now 220 | if(useBIOS) 221 | { 222 | std::ifstream biosFile(basePath + "bios.gba", std::ios::binary); 223 | if(biosFile) 224 | { 225 | biosFile.read(reinterpret_cast(agbBIOSROM), sizeof(agbBIOSROM)); 226 | 227 | mem.setBIOSROM(agbBIOSROM); 228 | } 229 | else 230 | { 231 | std::cerr << "bios.gba not found use --no-bios to run without\n"; 232 | return 1; 233 | } 234 | } 235 | else 236 | std::cout << "BIOS emulation is unfinished and likely inaccurate!\n"; 237 | 238 | // read the entire ROM, this one doesn't have the load callback/caching setup 239 | romFile.seekg(0, std::ios::end); 240 | auto romSize = romFile.tellg(); 241 | romFile.seekg(0); 242 | 243 | auto romData = new uint8_t[romSize]; // FIXME: leaky 244 | romFile.read(reinterpret_cast(romData), romSize); 245 | 246 | agbCPU.getDisplay().setFramebuffer(screenData); 247 | 248 | mem.setCartROM(romData, romSize); 249 | 250 | agbCPU.reset(); 251 | 252 | // attempt to read save 253 | if(!turbo) 254 | { 255 | size_t size; 256 | auto saveData = readSave(romFilename.substr(0, romFilename.length() - 3) + "sav", size); 257 | 258 | if(saveData) 259 | { 260 | mem.loadCartridgeSave(saveData, size); 261 | delete[] saveData; 262 | } 263 | } 264 | } 265 | else 266 | { 267 | dmgCPU.getDisplay().setFramebuffer(screenData); 268 | dmgCPU.getDisplay().setVBlankCallback(dmgVBlankCallback); 269 | 270 | auto &mem = dmgCPU.getMem(); 271 | mem.setROMBankCallback(getROMBank); 272 | mem.addROMCache(romBankCache, sizeof(romBankCache)); 273 | 274 | dmgCPU.reset(); 275 | 276 | // attempt to read save 277 | size_t size; 278 | auto saveData = readSave(romFilename + ".ram", size); 279 | 280 | if(saveData) 281 | { 282 | mem.loadCartridgeRAM(saveData, size); 283 | delete[] saveData; 284 | } 285 | } 286 | 287 | // SDL init 288 | if(!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) 289 | { 290 | std::cerr << "Failed to init SDL!\n"; 291 | return 1; 292 | } 293 | 294 | auto window = SDL_CreateWindow("DaftBoySDL", screenWidth * screenScale, screenHeight * screenScale, 295 | SDL_WINDOW_RESIZABLE); 296 | 297 | auto renderer = SDL_CreateRenderer(window, nullptr); 298 | if(!turbo) 299 | SDL_SetRenderVSync(renderer, 1); 300 | SDL_SetRenderLogicalPresentation(renderer, screenWidth, screenHeight, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); 301 | 302 | texture = SDL_CreateTexture(renderer, isAGB ? SDL_PIXELFORMAT_BGR565 : SDL_PIXELFORMAT_XBGR1555, SDL_TEXTUREACCESS_STREAMING, screenWidth, screenHeight); 303 | SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); 304 | 305 | // audio 306 | SDL_AudioSpec spec{}; 307 | 308 | spec.freq = isAGB ? 32768 : 22050; 309 | spec.format = SDL_AUDIO_S16LE; 310 | spec.channels = 1; 311 | 312 | auto stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audioCallback, nullptr); 313 | 314 | if(!stream) 315 | { 316 | std::cerr << "Failed to open audio: " << SDL_GetError() << "\n"; 317 | quit = true; 318 | } 319 | 320 | if(!turbo) 321 | SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(stream)); 322 | 323 | auto lastTick = SDL_GetTicks(); 324 | auto startTime = SDL_GetTicks(); 325 | 326 | auto checkTimeLimit = [timeLimit, &timeToRun]() 327 | { 328 | // fixed length benchmark 329 | if(timeLimit) 330 | { 331 | timeToRun -= 10; 332 | if(timeToRun == 0) 333 | { 334 | quit = true; 335 | return true; 336 | } 337 | } 338 | return false; 339 | }; 340 | 341 | while(!quit) 342 | { 343 | pollEvents(); 344 | 345 | auto now = SDL_GetTicks(); 346 | 347 | if(isAGB) 348 | { 349 | agbCPU.setInputs(inputs); 350 | 351 | // run frames until there isn't any room left for audio 352 | while(true) 353 | { 354 | // 548-549 samples per update 355 | if(agbCPU.getAPU().getNumSamples() > 2047 - 549) 356 | break; 357 | 358 | if(turbo) 359 | { 360 | agbCPU.run(10); 361 | 362 | agbCPU.getAPU().update(); 363 | while(agbCPU.getAPU().getNumSamples()) 364 | agbCPU.getAPU().getSample(); 365 | 366 | if(now - lastTick >= 10 || checkTimeLimit()) 367 | break; 368 | 369 | now = SDL_GetTicks(); 370 | } 371 | else 372 | { 373 | agbCPU.runFrame(); 374 | agbCPU.getAPU().update(); 375 | agbCPU.getDisplay().update(); 376 | } 377 | } 378 | 379 | // should be at vblank if not turbo mode, otherwise we don't care 380 | SDL_UpdateTexture(texture, nullptr, screenData, screenWidth * 2); 381 | } 382 | else 383 | { 384 | if(turbo) 385 | { 386 | dmgCPU.setInputs(inputs); 387 | 388 | // push as fast as possible 389 | // avoid doing SDL stuff between updates 390 | while(now - lastTick < 10) 391 | { 392 | dmgCPU.run(10); 393 | dmgCPU.getAPU().update(); 394 | 395 | // discard audio 396 | auto &apu = dmgCPU.getAPU(); 397 | while(apu.getNumSamples()) 398 | apu.getSample(); 399 | 400 | now = SDL_GetTicks(); 401 | 402 | if(checkTimeLimit()) 403 | break; 404 | } 405 | } 406 | else 407 | { 408 | dmgCPU.setInputs(inputs); 409 | dmgCPU.run(now - lastTick); 410 | dmgCPU.getAPU().update(); 411 | dmgCPU.getDisplay().update(); 412 | } 413 | 414 | // we'll update the texture in the vblank callback, so it's synced 415 | } 416 | 417 | lastTick = now; 418 | 419 | SDL_RenderClear(renderer); 420 | SDL_RenderTexture(renderer, texture, nullptr, nullptr); 421 | SDL_RenderPresent(renderer); 422 | } 423 | 424 | // write save 425 | if(isAGB) 426 | { 427 | int size = 0; 428 | switch(agbCPU.getMem().getCartridgeSaveType()) 429 | { 430 | case AGBMemory::SaveType::Unknown: 431 | break; 432 | case AGBMemory::SaveType::EEPROM_512: 433 | size = 512; 434 | break; 435 | case AGBMemory::SaveType::EEPROM_8K: 436 | size = 8 * 1024; 437 | break; 438 | case AGBMemory::SaveType::RAM: 439 | size = 32 * 1024; 440 | break; 441 | case AGBMemory::SaveType::Flash_64K: 442 | size = 64 * 1024; 443 | break; 444 | case AGBMemory::SaveType::Flash_128K: 445 | size = 128 * 1024; 446 | break; 447 | } 448 | if(size && !turbo) 449 | { 450 | auto saveFilename = romFilename.substr(0, romFilename.length() - 3) + "sav"; 451 | std::ofstream saveFile(saveFilename, std::ios::out | std::ios::binary); 452 | saveFile.write(reinterpret_cast(agbCPU.getMem().getCartridgeSave()), size); 453 | 454 | if(!saveFile) 455 | std::cerr << "Failed to write " << saveFilename << "\n"; 456 | else 457 | std::cout << "Wrote " << size << " bytes to " << saveFilename << "\n"; 458 | } 459 | } 460 | else 461 | { 462 | std::ofstream saveFile(romFilename + ".ram", std::ios::out | std::ios::binary); 463 | saveFile.write(reinterpret_cast(dmgCPU.getMem().getCartridgeRAM()), dmgCPU.getMem().getCartridgeRAMSize()); 464 | } 465 | 466 | if(timeLimit) 467 | { 468 | auto runTime = SDL_GetTicks() - startTime; 469 | printf("Ran for %ums\n", runTime); 470 | } 471 | 472 | SDL_DestroyAudioStream(stream); 473 | 474 | SDL_DestroyTexture(texture); 475 | SDL_DestroyRenderer(renderer); 476 | SDL_DestroyWindow(window); 477 | 478 | return 0; 479 | } 480 | -------------------------------------------------------------------------------- /32blit/gameblit.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "gameblit.hpp" 4 | #include "assets.hpp" 5 | 6 | #include "DMGAPU.h" 7 | #include "DMGDisplay.h" 8 | #include "DMGCPU.h" 9 | #include "DMGMemory.h" 10 | #include "DMGRegs.h" 11 | #include "file-browser.hpp" 12 | #include "menu.hpp" 13 | 14 | #ifdef PROFILER 15 | #include "engine/profiler.hpp" 16 | 17 | blit::Profiler profiler; 18 | blit::ProfilerProbe *profilerUpdateProbe, *profilerRenderProbe; 19 | #endif 20 | 21 | // catch running out of memory 22 | #ifdef TARGET_32BLIT_HW 23 | extern "C" void *_sbrk(ptrdiff_t incr) 24 | { 25 | extern char end, __ltdc_start; 26 | static char *heap_end; 27 | 28 | if(!heap_end) 29 | heap_end = &end; 30 | 31 | // ltdc is at the end of the heap 32 | if(heap_end + incr > &__ltdc_start) 33 | return (void *)-1; 34 | 35 | char *ret = heap_end; 36 | heap_end += incr; 37 | 38 | return (void *)ret; 39 | } 40 | #endif 41 | 42 | void addAppendedFiles() 43 | { 44 | #if defined(TARGET_32BLIT_HW) 45 | extern char _flash_end; 46 | auto appFilesPtr = &_flash_end; 47 | #elif defined(PICO_BUILD) 48 | extern char __flash_binary_end; 49 | auto appFilesPtr = &__flash_binary_end; 50 | appFilesPtr = (char *)(((uintptr_t)appFilesPtr) + 0xFF & ~0xFF); // round up to 256 byte boundary 51 | #else 52 | char *appFilesPtr = nullptr; 53 | return; 54 | #endif 55 | 56 | if(memcmp(appFilesPtr, "APPFILES", 8) != 0) 57 | return; 58 | 59 | uint32_t numFiles = *reinterpret_cast(appFilesPtr + 8); 60 | 61 | const int headerSize = 12, fileHeaderSize = 8; 62 | 63 | auto dataPtr = appFilesPtr + headerSize + fileHeaderSize * numFiles; 64 | 65 | for(auto i = 0u; i < numFiles; i++) 66 | { 67 | auto filenameLength = *reinterpret_cast(appFilesPtr + headerSize + i * fileHeaderSize); 68 | auto fileLength = *reinterpret_cast(appFilesPtr + headerSize + i * fileHeaderSize + 4); 69 | 70 | blit::File::add_buffer_file("/" + std::string(dataPtr, filenameLength), reinterpret_cast(dataPtr + filenameLength), fileLength); 71 | 72 | dataPtr += filenameLength + fileLength; 73 | } 74 | } 75 | 76 | const blit::Font tallFont(tall_font); 77 | duh::FileBrowser fileBrowser(tallFont); 78 | 79 | DMGCPU cpu; 80 | 81 | #ifndef BLIT_BOARD_PIMORONI_PICOSYSTEM 82 | static uint16_t screenData[160 * 144]; 83 | #else 84 | static uint8_t renderCounter = 0; 85 | #endif 86 | 87 | #ifdef BLIT_BOARD_PIMORONI_PICOVISION 88 | static blit::Surface dmgScreen((uint8_t *)screenData, blit::PixelFormat::BGR555, {160, 144}); 89 | #endif 90 | 91 | // ROM cache 92 | #ifdef BLIT_BOARD_PIMORONI_PICOVISION 93 | static const int romBankCacheSize = 5; 94 | #elif defined(PICO_RP2350) // generic device with 320x240 framebuffer 95 | static const int romBankCacheSize = 10; // 11 if standalone 96 | #elif defined(PICO_BUILD) // really picosystem 97 | static const int romBankCacheSize = 0; // could fit 2 with GBC removed 98 | #else 99 | static const int romBankCacheSize = 11; 100 | static const int extraROMBankCacheSize = 4; 101 | 102 | static uint8_t extraROMBankCache[0x4000 * extraROMBankCacheSize]{1}; // sneakily steal some of DTCMRAM 103 | #endif 104 | 105 | static uint8_t romBankCache[0x4000 * romBankCacheSize]; 106 | 107 | bool loaded = false; 108 | std::string loadedFilename; 109 | blit::File romFile; 110 | 111 | bool turbo = false; 112 | bool awfulScale = false; 113 | 114 | void updateCartRAM(uint8_t *cartRam, unsigned int size); 115 | 116 | // menu 117 | enum class MenuItem 118 | { 119 | SaveRAM, 120 | LoadState, 121 | SaveState, 122 | Reset, 123 | SwitchGame, 124 | }; 125 | 126 | Menu menu("Menu", 127 | { 128 | {static_cast(MenuItem::SaveRAM), "Save Cart RAM"}, 129 | {static_cast(MenuItem::LoadState), "Load State"}, 130 | {static_cast(MenuItem::SaveState), "Save State"}, 131 | {static_cast(MenuItem::Reset), "Reset"}, 132 | {static_cast(MenuItem::SwitchGame), "Switch Game"} 133 | }, tallFont); 134 | 135 | bool menuOpen = false; 136 | 137 | int redrawBG = 2; 138 | uint32_t lastUpdate = 0; 139 | 140 | #ifdef _MSC_VER 141 | #include 142 | static int log2i(unsigned int x) 143 | { 144 | unsigned long idx = 0; 145 | _BitScanReverse(&idx, x); 146 | return idx; 147 | } 148 | #else 149 | static int log2i(unsigned int x) 150 | { 151 | return 8 * sizeof(unsigned int) - __builtin_clz(x) - 1; 152 | } 153 | #endif 154 | 155 | // assuming RLE/palette and that data is the right size 156 | void packedToRGB(const uint8_t *packed_data, blit::Surface &surf) 157 | { 158 | auto image = *(const blit::packed_image *)packed_data; 159 | 160 | int palette_entry_count = image.palette_entry_count; 161 | if(palette_entry_count == 0) 162 | palette_entry_count = 256; 163 | 164 | uint8_t bit_depth = log2i(std::max(1, palette_entry_count - 1)) + 1; 165 | 166 | auto palette = (const blit::Pen *)(packed_data + sizeof(blit::packed_image)); 167 | auto image_data = packed_data + sizeof(blit::packed_image) + palette_entry_count * 4; 168 | auto end = packed_data + image.byte_count; 169 | 170 | uint32_t offset = 0; 171 | uint32_t surf_len = surf.bounds.area(); 172 | int parse_state = 0; 173 | uint8_t count = 0, col = 0, bit = 0; 174 | 175 | for (auto bytes = image_data; bytes < end; ++bytes) { 176 | uint8_t b = *bytes; 177 | 178 | for (auto j = 0; j < 8; j++) 179 | { 180 | switch (parse_state) 181 | { 182 | case 0: // flag 183 | if(b & (0b10000000 >> j)) 184 | parse_state = 1; 185 | else 186 | parse_state = 2; 187 | break; 188 | case 1: // repeat count 189 | count <<= 1; 190 | count |= ((0b10000000 >> j) & b) ? 1 : 0; 191 | if(++bit == 8) 192 | { 193 | parse_state = 2; 194 | bit = 0; 195 | } 196 | break; 197 | 198 | case 2: // value 199 | col <<= 1; 200 | col |= ((0b10000000 >> j) & b) ? 1 : 0; 201 | 202 | if(++bit == bit_depth) 203 | { 204 | blit::Pen p{palette[col].r, palette[col].g, palette[col].b}; // attempting to avoid unaligned load... 205 | 206 | surf.pbf(&p, &surf, offset, count + 1); 207 | offset += count + 1; 208 | 209 | bit = 0; col = 0; 210 | parse_state = 0; 211 | count = 0; 212 | 213 | // done, skip any remaining padding 214 | if(offset == surf_len) 215 | parse_state = 3; 216 | } 217 | break; 218 | } 219 | } 220 | } 221 | } 222 | 223 | void onMenuItemPressed(const Menu::Item &item) 224 | { 225 | switch(static_cast(item.id)) 226 | { 227 | case MenuItem::SaveRAM: 228 | updateCartRAM(cpu.getMem().getCartridgeRAM(), cpu.getMem().getCartridgeRAMSize()); 229 | break; 230 | 231 | case MenuItem::LoadState: 232 | { 233 | auto filename = loadedFilename.substr(0, loadedFilename.find_last_of('.') + 1) + "s0"; 234 | blit::File file(filename); 235 | 236 | if(file.is_open()) 237 | { 238 | cpu.loadSaveState(file.get_length(), [&file](uint32_t off, uint32_t len, uint8_t *buf) -> uint32_t 239 | { 240 | auto ret = file.read(off, len, reinterpret_cast(buf)); 241 | return ret < 0 ? 0 : ret; 242 | }); 243 | } 244 | break; 245 | } 246 | 247 | case MenuItem::SaveState: 248 | { 249 | auto filename = loadedFilename.substr(0, loadedFilename.find_last_of('.') + 1) + "s0"; 250 | blit::File file(filename, blit::OpenMode::write); 251 | 252 | cpu.saveSaveState([&file](uint32_t off, uint32_t len, const uint8_t *buf) -> uint32_t 253 | { 254 | auto ret = file.write(off, len, reinterpret_cast(buf)); 255 | return ret < 0 ? 0 : ret; 256 | }); 257 | break; 258 | } 259 | 260 | case MenuItem::Reset: 261 | cpu.reset(); 262 | break; 263 | 264 | case MenuItem::SwitchGame: 265 | loaded = false; 266 | break; 267 | } 268 | 269 | menuOpen = false; 270 | } 271 | 272 | int loadedBanks = 0; 273 | int bankLoadTime = 0; 274 | 275 | void getROMBank(uint8_t bank, uint8_t *ptr) 276 | { 277 | //blit::debugf("loading bank %i\n", bank); 278 | loadedBanks++; 279 | auto start = blit::now_us(); 280 | romFile.read(bank * 0x4000, 0x4000, (char *)ptr); 281 | bankLoadTime += blit::us_diff(start, blit::now_us()); 282 | } 283 | 284 | void updateCartRAM(uint8_t *cartRam, unsigned int size) 285 | { 286 | auto saveFile = loadedFilename.substr(0, loadedFilename.find_last_of('.') + 1) + "sav"; 287 | 288 | blit::File f(saveFile + ".tmp", blit::OpenMode::write); 289 | 290 | if(f.write(0, size, (const char *)cartRam) != int(size)) 291 | return; 292 | 293 | // save RTC 294 | if(cpu.getMem().hasRTC()) 295 | { 296 | uint32_t rtc[12]; 297 | cpu.getMem().getRTCData(rtc); 298 | 299 | if(f.write(size, sizeof(rtc), (const char *)rtc) != sizeof(rtc)) 300 | return; 301 | } 302 | 303 | f.close(); 304 | 305 | blit::remove_file(saveFile + ".old"); // remove old 306 | blit::rename_file(saveFile, saveFile + ".old"); // move current -> old 307 | 308 | if(!blit::rename_file(saveFile + ".tmp", saveFile)) // move new -> current 309 | return; 310 | 311 | // cleanup for .gb.ram -> .sav 312 | if(blit::file_exists(loadedFilename + ".ram")) 313 | { 314 | blit::remove_file(loadedFilename + ".ram"); 315 | blit::remove_file(loadedFilename + ".ram.old"); 316 | } 317 | } 318 | 319 | void updateAudio(blit::AudioChannel &channel) 320 | { 321 | auto &apu = cpu.getAPU(); 322 | if(apu.getNumSamples() < 64) 323 | { 324 | //underrun 325 | memset(channel.wave_buffer, 0, 64 * 2); 326 | return; 327 | } 328 | 329 | for(int i = 0; i < 64; i++) 330 | channel.wave_buffer[i] = apu.getSample(); 331 | } 332 | 333 | void openROM(std::string filename) 334 | { 335 | romFile.open(filename); 336 | 337 | // use flash cache for anything bigger than 256K 338 | if(romFile.get_length() > 256 * 1024) 339 | romFile.open(filename, blit::OpenMode::read | blit::OpenMode::cached); 340 | 341 | auto &mem = cpu.getMem(); 342 | 343 | mem.setCartROM(romFile.get_ptr()); 344 | 345 | cpu.reset(); 346 | 347 | auto saveFile = filename.substr(0, filename.find_last_of('.') + 1) + "sav"; 348 | 349 | // fallback 350 | if(!blit::file_exists(saveFile)) 351 | saveFile = filename + ".ram"; 352 | 353 | if(blit::file_exists(saveFile)) 354 | { 355 | blit::File file(saveFile); 356 | int ramLen = file.get_length(); 357 | 358 | if(file.get_ptr()) 359 | mem.loadCartridgeRAM(file.get_ptr(), std::min(ramLen, mem.getCartridgeRAMSize())); 360 | else 361 | file.read(0, mem.getCartridgeRAMSize(), (char *)mem.getCartridgeRAM()); 362 | 363 | // save has RTC 364 | const int rtcDataSize = 48; 365 | if(ramLen % 8192 == rtcDataSize) 366 | { 367 | uint32_t rtc[12]; 368 | file.read(ramLen - rtcDataSize, rtcDataSize, reinterpret_cast(rtc)); 369 | mem.setRTCData(rtc); 370 | } 371 | } 372 | 373 | loaded = true; 374 | loadedFilename = filename; 375 | } 376 | 377 | void init() 378 | { 379 | #ifdef BLIT_BOARD_PIMORONI_PICOVISION 380 | blit::set_screen_mode(blit::ScreenMode::hires); 381 | #else 382 | blit::set_screen_mode(blit::ScreenMode::hires, blit::PixelFormat::RGB565); 383 | #endif 384 | 385 | cpu.getMem().addROMCache(romBankCache, romBankCacheSize * 0x4000); 386 | 387 | #ifndef BLIT_BOARD_PIMORONI_PICOSYSTEM 388 | cpu.getDisplay().setFramebuffer(screenData); 389 | #endif 390 | 391 | #ifndef PICO_BUILD 392 | // 32blit extra cache 393 | cpu.getMem().addROMCache(extraROMBankCache, extraROMBankCacheSize * 0x4000); 394 | #endif 395 | 396 | blit::channels[0].waveforms = blit::Waveform::WAVE; 397 | blit::channels[0].wave_buffer_callback = &updateAudio; 398 | 399 | if(!turbo) 400 | { 401 | blit::channels[0].adsr = 0xFFFF00; 402 | blit::channels[0].trigger_sustain(); 403 | } 404 | 405 | fileBrowser.set_extensions({".gb", ".gbc"}); 406 | fileBrowser.set_on_file_open(openROM); 407 | 408 | // embed test ROM 409 | #if 0 410 | blit::File::add_buffer_file("auto.gb", test_rom, test_rom_length); 411 | blit::File::add_buffer_file("auto.sav", test_ram, test_ram_length); 412 | #endif 413 | 414 | addAppendedFiles(); 415 | 416 | fileBrowser.init(); 417 | 418 | menu.set_display_rect(blit::Rect(0, 0, 100, blit::screen.bounds.h)); 419 | menu.set_on_item_activated(onMenuItemPressed); 420 | 421 | auto &mem = cpu.getMem(); 422 | 423 | mem.setROMBankCallback(getROMBank); 424 | mem.setCartRamUpdateCallback(updateCartRAM); 425 | 426 | // autostart 427 | auto launchPath = blit::get_launch_path(); 428 | if(launchPath) 429 | openROM(launchPath); 430 | else if(blit::file_exists("auto.gb")) 431 | openROM("auto.gb"); 432 | 433 | #ifdef PROFILER 434 | profiler.set_display_size(blit::screen.bounds.w, blit::screen.bounds.h); 435 | profiler.set_rows(5); 436 | profiler.set_alpha(200); 437 | profiler.display_history(true); 438 | 439 | profiler.setup_graph_element(blit::Profiler::dmCur, true, true, blit::Pen(0, 255, 0)); 440 | profiler.setup_graph_element(blit::Profiler::dmAvg, true, true, blit::Pen(0, 255, 255)); 441 | profiler.setup_graph_element(blit::Profiler::dmMax, true, true, blit::Pen(255, 0, 0)); 442 | profiler.setup_graph_element(blit::Profiler::dmMin, true, true, blit::Pen(255, 255, 0)); 443 | 444 | profilerUpdateProbe = profiler.add_probe("Update", 300); 445 | profilerRenderProbe = profiler.add_probe("Render", 300); 446 | #endif 447 | } 448 | 449 | void render(uint32_t time_ms) 450 | { 451 | #ifdef PROFILER 452 | profilerRenderProbe->start(); 453 | #endif 454 | 455 | #ifdef BLIT_BOARD_PIMORONI_PICOSYSTEM 456 | if(renderCounter < 2) 457 | { 458 | if(renderCounter == 0) 459 | { 460 | // render the background while we still have a full framebuffer 461 | packedToRGB(asset_background_square, blit::screen); 462 | } 463 | else if(renderCounter == 1) 464 | { 465 | // bg render should be done, reconfigure screen 466 | auto origSize = blit::screen.row_stride * blit::screen.bounds.h; 467 | 468 | // 182 height is a hack to avoid double-buffering 469 | blit::set_screen_mode(blit::ScreenMode::hires, {160, /*144*/182}); 470 | 471 | cpu.getDisplay().setFramebuffer(reinterpret_cast(blit::screen.data) + 160 * 19); 472 | 473 | // ... and steal that extra RAM 474 | auto newSize = blit::screen.row_stride * blit::screen.bounds.h; 475 | cpu.getMem().addROMCache(blit::screen.data + newSize, origSize - newSize); 476 | } 477 | renderCounter++; 478 | return; 479 | } 480 | else if(loaded && renderCounter == 2) 481 | { 482 | // clear on game load 483 | blit::screen.pen = blit::Pen(145, 142, 147); 484 | blit::screen.clear(); 485 | // could try to repair the header but... 486 | renderCounter++; 487 | } 488 | #endif 489 | 490 | if(!loaded) 491 | { 492 | fileBrowser.render(); 493 | #ifdef PROFILER 494 | profilerRenderProbe->store_elapsed_us(); 495 | #endif 496 | return; 497 | } 498 | 499 | #ifndef BLIT_BOARD_PIMORONI_PICOSYSTEM 500 | bool updateRunning = time_ms - lastUpdate < 30; 501 | 502 | if(redrawBG || !updateRunning) 503 | { 504 | if(awfulScale) 505 | { 506 | blit::screen.pen = blit::Pen(145, 142, 147); 507 | blit::screen.clear(); 508 | } 509 | else 510 | packedToRGB(asset_background, blit::screen); // unpack directly to the screen 511 | 512 | if(updateRunning) 513 | redrawBG--; 514 | } 515 | #endif 516 | 517 | // PicoVision display is implemented with a blit (it supports RGB555) 518 | #ifdef BLIT_BOARD_PIMORONI_PICOVISION 519 | blit::screen.blit(&dmgScreen, {0, 0, 160, 144}, {80, 48}); 520 | #endif 521 | 522 | #ifndef PICO_RP2040 523 | auto gbScreen = screenData; 524 | 525 | auto expandCol = [](uint16_t rgb555, uint8_t &r, uint8_t &g, uint8_t &b) 526 | { 527 | r = (rgb555 & 0x1F) << 3; 528 | g = (rgb555 & 0x3E0) >> 2; 529 | b = (rgb555 & 0x7C00) >> 7; 530 | }; 531 | 532 | // TODO: always enable conversion in DMGDisplay? 533 | auto expandCol565 = [](uint16_t rgb555) 534 | { 535 | return (rgb555 & 0x1F)| (rgb555 & 0x7FE0) << 1; 536 | }; 537 | 538 | if(awfulScale) 539 | { 540 | int oy = 0; 541 | 542 | auto copyLine = [gbScreen, &expandCol](int y, int y1, int oy) 543 | { 544 | auto packCol565 = [](uint8_t r, uint8_t g, uint8_t b) 545 | { 546 | return (r >> 3) | ((g >> 2) << 5) | ((b >> 3) << 11); 547 | }; 548 | 549 | auto ptr = reinterpret_cast(blit::screen.ptr(27, oy++)); 550 | for(int x = 0; x < 158; x += 3) 551 | { 552 | uint8_t tmpR, tmpG, tmpB; 553 | 554 | uint8_t r1, r2, r3, g1, g2, g3, b1, b2, b3; 555 | expandCol(gbScreen[x + y * 160], r1, g1, b1); 556 | expandCol(gbScreen[x + y1 * 160], tmpR, tmpG, tmpB); 557 | r1 = (r1 + tmpR) / 2; 558 | g1 = (g1 + tmpG) / 2; 559 | b1 = (b1 + tmpB) / 2; 560 | 561 | expandCol(gbScreen[(x + 1) + y * 160], r2, g2, b2); 562 | expandCol(gbScreen[(x + 1) + y1 * 160], tmpR, tmpG, tmpB); 563 | r2 = (r2 + tmpR) / 2; 564 | g2 = (g2 + tmpG) / 2; 565 | b2 = (b2 + tmpB) / 2; 566 | 567 | expandCol(gbScreen[(x + 2) + y * 160], r3, g3, b3); 568 | expandCol(gbScreen[(x + 2) + y1 * 160], tmpR, tmpG, tmpB); 569 | r3 = (r3 + tmpR) / 2; 570 | g3 = (g3 + tmpG) / 2; 571 | b3 = (b3 + tmpB) / 2; 572 | 573 | *ptr++ = packCol565(r1, g1, b1); 574 | *ptr++ = packCol565((r1 + r2) / 2, (g1 + g2) / 2, (b1 + b2) / 2); 575 | *ptr++ = packCol565(r2, g2, b2); 576 | *ptr++ = packCol565((r2 + r3) / 2, (g2 + g3) / 2, (b2 + b3) / 2); 577 | *ptr++ = packCol565(r3, g3, b3); 578 | } 579 | 580 | // one pixel left 581 | uint8_t r1, g1, b1, r2, g2, b2; 582 | expandCol(gbScreen[159 + y * 160], r1, g1, b1); 583 | expandCol(gbScreen[159 + y1 * 160], r2, g2, b2); 584 | 585 | *ptr++ = packCol565((r1 + r2) / 2, (g1 + g2) / 2, (b1 + b2) / 2); 586 | }; 587 | for(int y = 0; y < 144; y += 3) 588 | { 589 | copyLine(y, y, oy++); 590 | copyLine(y, y + 1, oy++); 591 | copyLine(y + 1, y + 1, oy++); 592 | copyLine(y + 1, y + 2, oy++); 593 | copyLine(y + 2, y + 2, oy++); 594 | } 595 | } 596 | else 597 | { 598 | for(int y = 0; y < 144; y++) 599 | { 600 | auto ptr = reinterpret_cast(blit::screen.ptr(80, y + 48)); 601 | for(int x = 0; x < 160; x++) 602 | *ptr++ = expandCol565(gbScreen[x + y * 160]); 603 | } 604 | } 605 | #endif 606 | 607 | if(menuOpen) 608 | { 609 | menu.render(); 610 | redrawBG = 2; 611 | } 612 | 613 | #ifdef PROFILER 614 | profilerRenderProbe->store_elapsed_us(); 615 | 616 | profiler.set_graph_time(profilerRenderProbe->elapsed_metrics().uMaxElapsedUs); 617 | #endif 618 | } 619 | 620 | void update(uint32_t time_ms) 621 | { 622 | #ifdef PROFILER 623 | blit::ScopedProfilerProbe scopedProbe(profilerUpdateProbe); 624 | #endif 625 | 626 | lastUpdate = time_ms; 627 | 628 | if(!loaded) 629 | { 630 | fileBrowser.update(time_ms); 631 | return; 632 | } 633 | 634 | // menu 635 | if(blit::buttons.released & blit::Button::MENU) 636 | menuOpen = !menuOpen; 637 | 638 | if(menuOpen) 639 | { 640 | menu.update(time_ms); 641 | return; 642 | } 643 | 644 | #if defined(TARGET_32BLIT_HW) || defined(PICO_BUILD) 645 | if(blit::now() - time_ms >= 20) 646 | return; 647 | #endif 648 | 649 | auto start = blit::now(); 650 | 651 | // translate inputs 652 | uint8_t inputs = (blit::buttons & 0xFC) | // UP/DOWN/A/B match, select -> X, start -> Y 653 | ((blit::buttons & blit::Button::DPAD_RIGHT) >> 1) | 654 | ((blit::buttons & blit::Button::DPAD_LEFT) << 1); 655 | 656 | cpu.setInputs(inputs); 657 | 658 | // toggle the awful 1.5x scale 659 | if(blit::buttons.released & blit::Button::JOYSTICK) 660 | { 661 | awfulScale = !awfulScale; 662 | redrawBG = 2; 663 | } 664 | 665 | loadedBanks = 0; 666 | bankLoadTime = 0; 667 | 668 | if(cpu.getAPU().getNumSamples() < 1024 - 225) // single update generates ~220 samples 669 | { 670 | cpu.run(10); 671 | cpu.getAPU().update(); 672 | } 673 | else 674 | printf("CPU stalled, no audio room!\n"); 675 | 676 | auto end = blit::now(); 677 | 678 | if(end - start > 10) 679 | blit::debugf("running slow! %ims %i banks/%ius\n", end-start, loadedBanks, bankLoadTime); 680 | 681 | // SPEEEEEEEED 682 | while(turbo && blit::now() - start < 9) 683 | { 684 | // discard audio 685 | auto &apu = cpu.getAPU(); 686 | while(apu.getNumSamples()) 687 | apu.getSample(); 688 | cpu.run(1); 689 | } 690 | 691 | #ifdef PROFILER 692 | static int lastLogTime = time_ms; 693 | 694 | if(time_ms - lastLogTime >= 1000) 695 | { 696 | profiler.log_probes(); 697 | lastLogTime = time_ms; 698 | } 699 | #endif 700 | } 701 | -------------------------------------------------------------------------------- /core/AGBMemory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "AGBMemory.h" 5 | 6 | #include "AGBCPU.h" 7 | #include "AGBRegs.h" 8 | #include "GCCBuiltin.h" 9 | 10 | enum MemoryRegion 11 | { 12 | Region_BIOS = 0x0, 13 | Region_Unused = 0x1, 14 | Region_EWRAM = 0x2, 15 | Region_IWRAM = 0x3, 16 | Region_IO = 0x4, 17 | Region_Palette = 0x5, 18 | Region_VRAM = 0x6, 19 | Region_OAM = 0x7, 20 | Region_ROMWait0L = 0x8, 21 | Region_ROMWait0H = 0x9, 22 | Region_ROMWait1L = 0xA, 23 | Region_ROMWait1H = 0xB, 24 | Region_ROMWait2L = 0xC, 25 | Region_ROMWait2H = 0xD, 26 | Region_SaveL = 0xE, 27 | Region_SaveH = 0xF, 28 | }; 29 | 30 | template uint8_t AGBMemory::read(uint32_t addr, int &cycles, bool sequential) const; 31 | template uint16_t AGBMemory::read(uint32_t addr, int &cycles, bool sequential) const; 32 | template uint32_t AGBMemory::read(uint32_t addr, int &cycles, bool sequential) const; 33 | template void AGBMemory::write(uint32_t addr, uint8_t val, int &cycles, bool sequential); 34 | template void AGBMemory::write(uint32_t addr, uint16_t val, int &cycles, bool sequential); 35 | template void AGBMemory::write(uint32_t addr, uint32_t val, int &cycles, bool sequential); 36 | 37 | AGBMemory::AGBMemory(AGBCPU &cpu) : cpu(cpu){} 38 | 39 | void AGBMemory::setBIOSROM(const uint8_t *rom) 40 | { 41 | biosROM = rom; 42 | } 43 | 44 | void AGBMemory::setCartROM(const uint8_t *rom, uint32_t size) 45 | { 46 | cartROM = rom; 47 | cartROMSize = size; 48 | } 49 | 50 | void AGBMemory::loadCartridgeSave(const uint8_t *data, uint32_t len) 51 | { 52 | if(data) 53 | memcpy(cartSaveData, data, len); 54 | 55 | // determine the type of save from the size 56 | if(len == 512) 57 | saveType = SaveType::EEPROM_512; 58 | else if(len == 8 * 1024) 59 | saveType = SaveType::EEPROM_8K; 60 | else if(len == 32 * 1024) 61 | saveType = SaveType::RAM; 62 | else if(len == 64 * 1024) 63 | saveType = SaveType::Flash_64K; 64 | else if(len == 128 * 1024) 65 | saveType = SaveType::Flash_128K; 66 | } 67 | 68 | void AGBMemory::reset() 69 | { 70 | saveType = SaveType::Unknown; 71 | flashState = FlashState::Read; 72 | flashBank = 0; 73 | 74 | memset(cartSaveData, 0xFF, sizeof(cartSaveData)); 75 | 76 | cartAccessN[0] = 5; 77 | cartAccessS[0] = 3; 78 | cartAccessN[1] = 5; 79 | cartAccessS[1] = 5; 80 | cartAccessN[2] = 5; 81 | cartAccessS[2] = 9; 82 | 83 | cartAccessN[3] = cartAccessS[3] = 5; 84 | } 85 | 86 | template 87 | T AGBMemory::read(uint32_t addr, int &cycles, bool sequential) const 88 | { 89 | auto accessCycles = [&cycles, this](int c) 90 | { 91 | cycles += c; 92 | prefetchCycles -= c; 93 | }; 94 | 95 | switch(addr >> 24) 96 | { 97 | case Region_BIOS: 98 | accessCycles(1); 99 | return doBIOSRead(addr); 100 | case Region_Unused: 101 | accessCycles(1); 102 | return doOpenRead(addr); 103 | case Region_EWRAM: 104 | accessCycles(sizeof(T) == 4 ? 6 : 3); 105 | return doRead(ewram, addr); 106 | case Region_IWRAM: 107 | accessCycles(1); 108 | return doRead(iwram, addr); 109 | case Region_IO: 110 | accessCycles(1); 111 | return doIORead(addr); 112 | case Region_Palette: 113 | accessCycles(sizeof(T) == 4 ? 2 : 1); 114 | return doRead(palRAM, addr); 115 | case Region_VRAM: 116 | accessCycles(sizeof(T) == 4 ? 2 : 1); 117 | return doVRAMRead(addr); 118 | case Region_OAM: 119 | accessCycles(1); 120 | return doRead(oam, addr); 121 | 122 | case Region_ROMWait0L: 123 | case Region_ROMWait0H: 124 | case Region_ROMWait1L: 125 | case Region_ROMWait1H: 126 | case Region_ROMWait2L: 127 | cycles += (sequential ? cartAccessS[(addr >> 25) - 4] : cartAccessN[(addr >> 25) - 4]) 128 | + (sizeof(T) == 4 ? cartAccessS[(addr >> 25) - 4] : 0); 129 | 130 | prefetchCycles = cartAccessN[(addr >> 25) - 4] + 1; // cart bus active, interrupt prefetch (+1 seems hacky but improves results?) 131 | return doROMRead(addr); 132 | case Region_ROMWait2H: 133 | cycles += (sequential ? cartAccessS[2] : cartAccessN[2]) + (sizeof(T) == 4 ? cartAccessS[2] : 0); 134 | 135 | prefetchCycles = cartAccessN[2] + 1; 136 | return doROMOrEEPROMRead(addr); 137 | 138 | case Region_SaveL: 139 | case Region_SaveH: 140 | cycles += (sequential ? cartAccessS[3] : cartAccessN[3]) + (sizeof(T) == 4 ? cartAccessS[3] : 0); 141 | return doSRAMRead(addr); 142 | } 143 | 144 | return doOpenRead(addr); 145 | } 146 | 147 | template 148 | void AGBMemory::write(uint32_t addr, T data, int &cycles, bool sequential) 149 | { 150 | auto accessCycles = [&cycles, this](int c) 151 | { 152 | cycles += c; 153 | prefetchCycles -= c; 154 | }; 155 | 156 | switch(addr >> 24) 157 | { 158 | case Region_BIOS: 159 | case Region_Unused: 160 | accessCycles(1); 161 | return; 162 | case Region_EWRAM: 163 | accessCycles(sizeof(T) == 4 ? 6 : 3); 164 | doWrite(ewram, addr, data); 165 | return; 166 | case Region_IWRAM: 167 | accessCycles(1); 168 | doWrite(iwram, addr, data); 169 | return; 170 | case Region_IO: 171 | accessCycles(1); 172 | doIOWrite(addr, data); 173 | return; 174 | case Region_Palette: 175 | accessCycles(sizeof(T) == 4 ? 2 : 1); 176 | doPalRAMWrite(addr, data); 177 | return; 178 | case Region_VRAM: 179 | accessCycles(sizeof(T) == 4 ? 2 : 1); 180 | doVRAMWrite(addr, data); 181 | return; 182 | case Region_OAM: 183 | accessCycles(1); 184 | doOAMWrite(addr, data); 185 | return; 186 | 187 | case Region_ROMWait0L: 188 | case Region_ROMWait0H: 189 | case Region_ROMWait1L: 190 | case Region_ROMWait1H: 191 | case Region_ROMWait2L: 192 | cycles += (sequential ? cartAccessS[(addr >> 25) - 4] : cartAccessN[(addr >> 25) - 4]) 193 | + (sizeof(T) == 4 ? cartAccessS[(addr >> 25) - 4] : 0); 194 | return; 195 | case Region_ROMWait2H: 196 | cycles += (sequential ? cartAccessS[2] : cartAccessN[2]) + (sizeof(T) == 4 ? cartAccessS[2] : 0); 197 | return doEEPROMWrite(addr, data); 198 | 199 | case Region_SaveL: 200 | case Region_SaveH: 201 | cycles += (sequential ? cartAccessS[3] : cartAccessN[3]) + (sizeof(T) == 4 ? cartAccessS[3] : 0); 202 | doSRAMWrite(addr, data); 203 | return; 204 | } 205 | } 206 | 207 | const uint8_t *AGBMemory::mapAddress(uint32_t addr) const 208 | { 209 | switch(addr >> 24) 210 | { 211 | case Region_BIOS: 212 | return biosROM ? biosROM + (addr & 0x3FFF) : nullptr; 213 | 214 | case Region_EWRAM: 215 | return ewram + (addr & 0x3FFFF); 216 | case Region_IWRAM: 217 | return iwram + (addr & 0x7FFF); 218 | 219 | case Region_Palette: 220 | return palRAM + (addr & 0x3FF); 221 | case Region_VRAM: 222 | addr &= 0x1FFFF; 223 | if(addr >= 0x18000) 224 | addr &= ~0x8000; // last 32K is the previous 32K 225 | return vram + addr; 226 | case Region_OAM: 227 | return oam + (addr & 0x3FF); 228 | case Region_ROMWait0L: 229 | case Region_ROMWait0H: 230 | case Region_ROMWait1L: 231 | case Region_ROMWait1H: 232 | case Region_ROMWait2L: 233 | case Region_ROMWait2H: 234 | addr &= 0x1FFFFFF; 235 | if(addr >= cartROMSize) 236 | return nullptr; 237 | return cartROM + addr; 238 | } 239 | 240 | return reinterpret_cast(&dummy); 241 | } 242 | 243 | uint8_t *AGBMemory::mapAddress(uint32_t addr) 244 | { 245 | switch(addr >> 24) 246 | { 247 | case Region_EWRAM: 248 | return ewram + (addr & 0x3FFFF); 249 | case Region_IWRAM: 250 | return iwram + (addr & 0x7FFF); 251 | 252 | case Region_Palette: 253 | return palRAM + (addr & 0x3FF); 254 | case Region_VRAM: 255 | addr &= 0x1FFFF; 256 | if(addr >= 0x18000) 257 | addr &= ~0x8000; // last 32K is the previous 32K 258 | return vram + addr; 259 | case Region_OAM: 260 | return oam + (addr & 0x3FF); 261 | } 262 | 263 | return nullptr; 264 | } 265 | 266 | int AGBMemory::getAccessCycles(uint32_t addr, int width, bool sequential) const 267 | { 268 | switch(addr >> 24) 269 | { 270 | case Region_BIOS: 271 | case Region_IWRAM: 272 | case Region_IO: 273 | case Region_OAM: 274 | return 1; 275 | 276 | case Region_EWRAM: 277 | return width == 4 ? 6 : 3; 278 | 279 | case Region_Palette: 280 | case Region_VRAM: 281 | return width == 4 ? 2 : 1; 282 | 283 | case Region_ROMWait0L: 284 | case Region_ROMWait0H: 285 | case Region_ROMWait1L: 286 | case Region_ROMWait1H: 287 | case Region_ROMWait2L: 288 | case Region_ROMWait2H: 289 | case Region_SaveL: 290 | case Region_SaveH: 291 | return (sequential ? cartAccessS[(addr >> 25) - 4] : cartAccessN[(addr >> 25) - 4]) 292 | + (width == 4 ? cartAccessS[(addr >> 25) - 4] : 0); // extra time for reading 32bit value is always sequential 293 | 294 | } 295 | 296 | return 1; 297 | } 298 | 299 | void AGBMemory::updateWaitControl(uint16_t waitcnt) 300 | { 301 | // update ROM access times 302 | static const int nTimings[]{4, 3, 2, 8}; 303 | cartAccessN[0] = nTimings[(waitcnt & WAITCNT_ROMWS0N) >> 2] + 1; 304 | cartAccessN[1] = nTimings[(waitcnt & WAITCNT_ROMWS1N) >> 5] + 1; 305 | cartAccessN[2] = nTimings[(waitcnt & WAITCNT_ROMWS1N) >> 8] + 1; 306 | 307 | cartAccessS[0] = (waitcnt & WAITCNT_ROMWS0S) ? 2 : 3; 308 | cartAccessS[1] = (waitcnt & WAITCNT_ROMWS1S) ? 2 : 5; 309 | cartAccessS[2] = (waitcnt & WAITCNT_ROMWS2S) ? 2 : 9; 310 | 311 | // ... and SRAM/flash 312 | cartAccessN[3] = cartAccessS[3] = nTimings[waitcnt & WAITCNT_SRAM] + 1; 313 | 314 | cartPrefetchEnabled = waitcnt & WAITCNT_Prefetch; 315 | } 316 | 317 | void AGBMemory::updatePC(uint32_t pc) 318 | { 319 | // can't execure from save area anyway... 320 | pcInROM = (pc >> 24) >= Region_ROMWait0L; 321 | 322 | if(pcInROM) 323 | { 324 | prefetchCycles = prefetchSCycles = cartAccessS[(pc >> 25) - 4]; // pipeline refill makes next access S 325 | prefetchedHalfWords = 0; 326 | } 327 | } 328 | 329 | template 330 | T AGBMemory::doRead(const uint8_t (&mem)[size], uint32_t addr) const 331 | { 332 | // use size of type for alignment 333 | return *reinterpret_cast(mem + (addr & (size - sizeof(T)))); 334 | } 335 | 336 | template 337 | void AGBMemory::doWrite(uint8_t (&mem)[size], uint32_t addr, T data) 338 | { 339 | // use size of type for alignment 340 | *reinterpret_cast< T *>(mem + (addr & (size - sizeof(T)))) = data; 341 | } 342 | 343 | template 344 | T AGBMemory::doBIOSRead(uint32_t addr) const 345 | { 346 | if(!biosROM) 347 | return 0; // TODO: (this must be from outside if we have no BIOS) 348 | 349 | // TODO: reading from outside BIOS 350 | const size_t size = 0x4000; 351 | return *reinterpret_cast(biosROM + (addr & (size - 1))); 352 | } 353 | 354 | template 355 | T AGBMemory::doIORead(uint32_t addr) const 356 | { 357 | // IO regs don't mirror 358 | if(addr >= 0x4000400) 359 | return doOpenRead(addr); 360 | 361 | return cpu.readReg(addr & 0xFFFFFF, doRead(ioRegs, addr)); 362 | } 363 | 364 | template<> 365 | uint8_t AGBMemory::doIORead(uint32_t addr) const 366 | { 367 | // promote to 16-bit 368 | return doIORead(addr) >> (addr & 1) * 8; 369 | } 370 | 371 | template<> 372 | [[gnu::noinline]] 373 | uint32_t AGBMemory::doIORead(uint32_t addr) const 374 | { 375 | // split for the callback 376 | return doIORead(addr & ~3) | doIORead((addr & ~3) + 2) << 16; 377 | } 378 | 379 | template 380 | void AGBMemory::doIOWrite(uint32_t addr, T data) 381 | { 382 | if(addr >= 0x4000400) 383 | return; 384 | 385 | if(cpu.writeReg(addr & 0x3FE, data, 0xFFFF)) 386 | return; 387 | 388 | doWrite(ioRegs, addr, data); 389 | } 390 | 391 | template<> 392 | void AGBMemory::doIOWrite(uint32_t addr, uint8_t data) 393 | { 394 | if(addr >= 0x4000400) 395 | return; 396 | 397 | // promote to 16-bit 398 | auto tmp = readIOReg(addr & 0x3FE); 399 | uint16_t data16 = addr & 1 ? (tmp & 0xFF) | data << 8 : (tmp & 0xFF00) | data; 400 | 401 | // set mask to the byte actually getting written (important in some cases) 402 | if(cpu.writeReg(addr & 0x3FE, data16, addr & 1 ? 0xFF00 : 0xFF)) 403 | return; 404 | 405 | doWrite(ioRegs, addr, data); 406 | } 407 | 408 | template<> 409 | [[gnu::noinline]] 410 | void AGBMemory::doIOWrite(uint32_t addr, uint32_t data) 411 | { 412 | // split 413 | doIOWrite(addr, data); 414 | doIOWrite(addr + 2, data >> 16); 415 | } 416 | 417 | template 418 | void AGBMemory::doPalRAMWrite(uint32_t addr, T data) 419 | { 420 | doWrite(palRAM, addr, data); 421 | } 422 | 423 | template<> 424 | void AGBMemory::doPalRAMWrite(uint32_t addr, uint8_t data) 425 | { 426 | // writes byte value to halfword 427 | doWrite(palRAM, addr, data | data << 8); 428 | } 429 | 430 | template 431 | T AGBMemory::doVRAMRead(uint32_t addr) const 432 | { 433 | addr &= (0x20000 - sizeof(T)); 434 | if(addr >= sizeof(vram)) 435 | addr &= ~0x8000; // last 32K is the previous 32K 436 | 437 | return *reinterpret_cast(vram + addr); 438 | } 439 | 440 | template 441 | void AGBMemory::doVRAMWrite(uint32_t addr, T data) 442 | { 443 | addr &= (0x20000 - sizeof(T)); 444 | if(addr >= sizeof(vram)) 445 | addr &= ~0x8000; // last 32K is the previous 32K 446 | 447 | *reinterpret_cast(vram + addr) = data; 448 | } 449 | 450 | template<> 451 | void AGBMemory::doVRAMWrite(uint32_t addr, uint8_t data) 452 | { 453 | if((addr & 0x1FFFF) < 0x10000) // "background" VRAM, same as palette ram 454 | *reinterpret_cast(vram + (addr & 0xFFFE)) = data | data << 8; 455 | // else ignored 456 | } 457 | 458 | template 459 | void AGBMemory::doOAMWrite(uint32_t addr, T data) 460 | { 461 | doWrite(oam, addr, data); 462 | } 463 | 464 | template<> 465 | void AGBMemory::doOAMWrite(uint32_t addr, uint8_t data) 466 | { 467 | // ignored 468 | } 469 | 470 | template 471 | T AGBMemory::doROMRead(uint32_t addr) const 472 | { 473 | addr &= (0x2000000 - sizeof(T)); 474 | if(addr >= cartROMSize) 475 | { 476 | // out of bounds rom access returns low bits of address 477 | auto addrLow = (addr >> 1) & 0xFFFF; 478 | 479 | if(sizeof(T) == 1) 480 | return addrLow >> (addr & 1) * 8; 481 | 482 | return addrLow | (addrLow + 1) << 16; 483 | } 484 | 485 | return *reinterpret_cast(cartROM + addr); 486 | } 487 | 488 | template 489 | T AGBMemory::doROMOrEEPROMRead(uint32_t addr) const 490 | { 491 | // usually just ROM 492 | return doROMRead(addr); 493 | } 494 | 495 | template<> 496 | uint16_t AGBMemory::doROMOrEEPROMRead(uint32_t addr) const 497 | { 498 | // 16-bit read from high ROM addr could be EEPROM 499 | if(saveType == SaveType::EEPROM_512 || saveType == SaveType::EEPROM_8K) 500 | { 501 | int bit = (addr & 0xFF) >> 1; 502 | if(bit < 4) 503 | return 1; // first 4 bits are ignored for reads, write waits for 1 at bit 0 504 | 505 | return (eepromReadData >> (67 - bit)) & 1; 506 | } 507 | 508 | return doROMRead(addr); 509 | } 510 | 511 | template 512 | void AGBMemory::doEEPROMWrite(uint32_t addr, T data) 513 | { 514 | // 16-bit only 515 | } 516 | 517 | template<> 518 | void AGBMemory::doEEPROMWrite(uint32_t addr, uint16_t data) 519 | { 520 | if(saveType == SaveType::Unknown) 521 | saveType = SaveType::EEPROM_512; 522 | 523 | if(saveType != SaveType::EEPROM_512 && saveType != SaveType::EEPROM_8K) 524 | return; 525 | 526 | // set/clear command bit 527 | int bit = (addr & 0xFF) >> 1; 528 | uint64_t mask = 1ull << (63 - bit % 64); 529 | eepromCommandData[bit / 64] = (data & 1) ? (eepromCommandData[bit / 64] | mask) : (eepromCommandData[bit / 64] & ~mask); 530 | 531 | // long read request, must be an 8K EEPROM 532 | if(bit > 8 && (eepromCommandData[0] >> 62) == 3) 533 | saveType = SaveType::EEPROM_8K; 534 | 535 | // end of read request for 512b 536 | if(bit == 8 && (eepromCommandData[0] >> 62) == 3 && saveType == SaveType::EEPROM_512) 537 | { 538 | uint16_t eepromAddr = (eepromCommandData[0] >> 56) & 0x3F; 539 | 540 | uint64_t data = reinterpret_cast(cartSaveData)[eepromAddr]; 541 | eepromReadData = __builtin_bswap64(data); 542 | } 543 | // end of write request for 512b 544 | else if(bit == 72 && (eepromCommandData[0] >> 62) == 2 && saveType == SaveType::EEPROM_512) 545 | { 546 | uint16_t eepromAddr = (eepromCommandData[0] >> 56) & 0x3F; 547 | 548 | uint64_t data = eepromCommandData[0] << 8 | eepromCommandData[1] >> 56; 549 | 550 | reinterpret_cast(cartSaveData)[eepromAddr] = __builtin_bswap64(data); 551 | } 552 | // end of read request for 8k 553 | else if(bit == 16 && (eepromCommandData[0] >> 62) == 3) 554 | { 555 | uint16_t eepromAddr = (eepromCommandData[0] >> 48) & 0x3FF; 556 | 557 | uint64_t data = reinterpret_cast(cartSaveData)[eepromAddr]; 558 | eepromReadData = __builtin_bswap64(data); 559 | } 560 | // end write request for 8k 561 | else if(bit == 80 && (eepromCommandData[0] >> 62) == 2) 562 | { 563 | uint16_t eepromAddr = (eepromCommandData[0] >> 48) & 0x3FF; 564 | uint64_t data = eepromCommandData[0] << 16 | eepromCommandData[1] >> 48; 565 | 566 | reinterpret_cast(cartSaveData)[eepromAddr] = __builtin_bswap64(data); 567 | } 568 | } 569 | 570 | template 571 | T AGBMemory::doSRAMRead(uint32_t addr) const 572 | { 573 | uint8_t byte = doSRAMRead(addr); 574 | 575 | return byte | byte << 8 | byte << 16 | byte << 24; 576 | } 577 | 578 | template<> 579 | uint8_t AGBMemory::doSRAMRead(uint32_t addr) const 580 | { 581 | // the only valid width 582 | switch(saveType) 583 | { 584 | case SaveType::Unknown: 585 | case SaveType::EEPROM_512: 586 | case SaveType::EEPROM_8K: 587 | return 0xFF; 588 | case SaveType::RAM: 589 | return cartSaveData[addr & 0x7FFF]; // SRAM is 32k 590 | case SaveType::Flash_64K: 591 | case SaveType::Flash_128K: 592 | if(flashState == FlashState::ID) 593 | return flashID[addr & 1]; 594 | 595 | return cartSaveData[(addr & 0xFFFF) + (flashBank << 16)]; // 1-2 64k banks 596 | } 597 | 598 | __builtin_unreachable(); 599 | } 600 | 601 | template 602 | void AGBMemory::doSRAMWrite(uint32_t addr, T data) 603 | { 604 | int shift = (addr & (sizeof(T) - 1)) * 8; 605 | doSRAMWrite(addr, data >> shift); 606 | } 607 | 608 | template<> 609 | void AGBMemory::doSRAMWrite(uint32_t addr, uint8_t data) 610 | { 611 | if(saveType == SaveType::Unknown) 612 | { 613 | if(addr == 0xE005555 && data == 0xAA) 614 | { 615 | saveType = SaveType::Flash_128K; 616 | 617 | // scan ROM for save type marker 618 | for(uint32_t off = 0; off < cartROMSize - 10; off++) 619 | { 620 | auto ptr = cartROM + off; 621 | if(*ptr != 'F') 622 | continue; 623 | 624 | // override to smaller size 625 | if(memcmp(ptr, "FLASH_V", 7) == 0 || memcmp(ptr, "FLASH512_V", 10) == 0) 626 | { 627 | saveType = SaveType::Flash_64K; 628 | break; 629 | } 630 | 631 | // stop searching if we can confirm the larger size 632 | if(memcmp(ptr, "FLASH1M_V", 9) == 0) 633 | break; 634 | } 635 | } 636 | else 637 | saveType = SaveType::RAM; 638 | } 639 | 640 | if(saveType == SaveType::Flash_64K || saveType == SaveType::Flash_128K) 641 | writeFlash(addr, data); 642 | else if(saveType == SaveType::RAM) 643 | cartSaveData[addr & 0x7FFF] = data; 644 | } 645 | 646 | template 647 | T AGBMemory::doOpenRead(uint32_t addr) const 648 | { 649 | return static_cast(0xBADADD55); // TODO 650 | } 651 | 652 | void AGBMemory::writeFlash(uint32_t addr, uint8_t data) 653 | { 654 | // bank switch 655 | if(flashState == FlashState::Bank && addr == 0xE000000) 656 | { 657 | flashBank = data; 658 | flashState = FlashState::Read; 659 | return; 660 | } 661 | // write a byte 662 | else if(flashState == FlashState::Write) 663 | { 664 | cartSaveData[(addr & 0xFFFF) + (flashBank << 16)] = data; 665 | flashState = FlashState::Read; 666 | return; 667 | } 668 | 669 | // parse commands 670 | if(flashCmdState == 0 && addr == 0xE005555 && data == 0xAA) 671 | flashCmdState = 1; 672 | else if(flashCmdState == 1 && addr == 0xE002AAA && data == 0x55) 673 | flashCmdState = 2; 674 | else if(flashCmdState == 2) 675 | { 676 | if(data == 0x10 && addr == 0xE005555 && flashState == FlashState::Erase) 677 | { 678 | // erase all 679 | memset(cartSaveData, 0xFF, sizeof(cartSaveData)); 680 | flashState = FlashState::Read; 681 | } 682 | else if(data == 0x30 && flashState == FlashState::Erase) 683 | { 684 | // erase 4k sector 685 | memset(cartSaveData + (addr & 0xF000) + (flashBank << 16), 0xFF, 0x1000); 686 | flashState = FlashState::Read; 687 | } 688 | else if(data == 0x80 && addr == 0xE005555) 689 | flashState = FlashState::Erase; // actual erase happens later 690 | else if(data == 0x90 && addr == 0xE005555) 691 | { 692 | if(saveType == SaveType::Flash_64K) 693 | { 694 | // 64k sst 695 | flashID[0] = 0xBF; 696 | flashID[1] = 0xD4; 697 | } 698 | else 699 | { 700 | // 128k sanyo 701 | flashID[0] = 0x62; 702 | flashID[1] = 0x13; 703 | } 704 | flashState = FlashState::ID; 705 | } 706 | else if(data == 0xA0 && addr == 0xE005555) 707 | flashState = FlashState::Write; 708 | else if(data == 0xB0 && addr == 0xE005555) 709 | flashState = FlashState::Bank; 710 | else if(data == 0xF0 && addr == 0xE005555) 711 | flashState = FlashState::Read; 712 | else 713 | printf("Flash CMD %02X\n", data); 714 | 715 | flashCmdState = 0; 716 | } 717 | else 718 | flashCmdState = 0; 719 | } -------------------------------------------------------------------------------- /core/DMGMemory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "DMGMemory.h" 5 | #include "DMGCPU.h" 6 | #include "DMGRegs.h" 7 | #include "DMGSaveState.h" 8 | 9 | DMGMemory::DMGMemory(DMGCPU &cpu) : cpu(cpu) 10 | { 11 | } 12 | 13 | void DMGMemory::setROMBankCallback(ROMBankCallback callback) 14 | { 15 | this->romBankCallback = callback; 16 | } 17 | 18 | void DMGMemory::setCartROM(const uint8_t *rom) 19 | { 20 | cartROM = rom; 21 | } 22 | 23 | void DMGMemory::loadCartridgeRAM(const uint8_t *ram, uint32_t len) 24 | { 25 | memcpy(cartRam, ram, len); 26 | } 27 | 28 | void DMGMemory::addROMCache(uint8_t *ptr, uint32_t size) 29 | { 30 | auto end = ptr + size; 31 | 32 | while(ptr + 0x4000 <= end) 33 | { 34 | cachedROMBanks.emplace_back(ROMCacheEntry{ptr, 0}); 35 | ptr += 0x4000; 36 | } 37 | } 38 | 39 | void DMGMemory::reset() 40 | { 41 | vramBank = 0; 42 | wramBank = 1; 43 | 44 | // io regs 45 | memset(iohram, 0xFF, 0x80); 46 | 47 | iohram[IO_JOYP] = 0xC0; 48 | iohram[IO_SB ] = 0x00; 49 | iohram[IO_SC ] = 0x7E; 50 | iohram[IO_TIMA] = 0x00; 51 | iohram[IO_TMA ] = 0x00; 52 | iohram[IO_TAC ] = 0xF8; 53 | iohram[IO_IF ] = 0xE1; 54 | iohram[IO_NR10] = 0x80; 55 | iohram[IO_NR11] = 0xBF; 56 | iohram[IO_NR12] = 0xF3; 57 | iohram[IO_NR14] = 0xBF; 58 | iohram[IO_NR21] = 0x3F; 59 | iohram[IO_NR22] = 0x00; 60 | iohram[IO_NR24] = 0xBF; 61 | iohram[IO_NR30] = 0x7F; 62 | iohram[IO_NR31] = 0xFF; 63 | iohram[IO_NR32] = 0x9F; 64 | iohram[IO_NR34] = 0xBF; 65 | iohram[IO_NR41] = 0xFF; 66 | iohram[IO_NR42] = 0x00; 67 | iohram[IO_NR43] = 0x00; 68 | iohram[IO_NR44] = 0xBF; 69 | iohram[IO_NR50] = 0x77; 70 | iohram[IO_NR51] = 0xF3; 71 | iohram[IO_NR52] = 0xF1; 72 | iohram[IO_LCDC] = 0x91; 73 | iohram[IO_STAT] = 0x80; 74 | iohram[IO_SCY ] = 0x00; 75 | iohram[IO_SCX ] = 0x00; 76 | iohram[IO_LY ] = 0x00; 77 | iohram[IO_LYC ] = 0x00; 78 | iohram[IO_BGP ] = 0xFC; 79 | iohram[IO_OBP0] = 0xFF; 80 | iohram[IO_OBP1] = 0xFF; 81 | iohram[IO_WY ] = 0x00; 82 | iohram[IO_WX ] = 0x00; 83 | iohram[IO_IE ] = 0x00; 84 | 85 | // clear vram 86 | memset(vram, 0, sizeof(vram)); 87 | 88 | // reset RTC 89 | for(auto ® : rtcRegs) 90 | reg = 0; 91 | rtcRegs[4] = 0x40; // start stopped 92 | 93 | // load first ROM bank for reading headers 94 | romBankCallback(0, cartROMBank0); 95 | 96 | // reset cache 97 | for(auto it = cachedROMBanks.begin(); it != cachedROMBanks.end();) 98 | { 99 | // remove cart ram/wram, will re-add later if possible 100 | if(it->ptr == cartRam || it->ptr == cartRam + 0x4000 || it->ptr == wram + 0x4000) 101 | { 102 | it = cachedROMBanks.erase(it); 103 | continue; 104 | } 105 | 106 | it->bank = 0; 107 | ++it; 108 | } 109 | 110 | // get ROM size 111 | int size = cartROMBank0[0x148]; 112 | if(size <= 8) 113 | cartROMBanks = 2 << size; 114 | else if(size == 0x52) 115 | cartROMBanks = 72; 116 | else if(size == 0x53) 117 | cartROMBanks = 80; 118 | else if(size == 0x54) 119 | cartROMBanks = 96; 120 | else 121 | cartROMBanks = 0; // uhoh 122 | 123 | // check cart ram size 124 | static const unsigned int ramSizes[]{ 125 | 0, 2048, 8 * 1024, 32 * 1024, 128 * 1024, 64 * 1024 126 | }; 127 | 128 | cartRamSize = ramSizes[cartROMBank0[0x149]]; 129 | 130 | if(cartRamSize > sizeof(cartRam)) 131 | printf("Too much cart RAM! (%i > %i)\n", cartRamSize, sizeof(cartRam)); 132 | 133 | switch(cartROMBank0[0x147]) 134 | { 135 | case 0: 136 | mbcType = MBCType::None; 137 | break; 138 | case 1: 139 | case 2: // + RAM 140 | case 3: // + RAM + Battery 141 | mbcType = MBCType::MBC1; 142 | break; 143 | 144 | case 5: 145 | case 6: // + Battery 146 | mbcType = MBCType::MBC2; 147 | break; 148 | 149 | case 0x0F: // + Timer + Battery 150 | case 0x10: // + Timer + RAM + Battery 151 | case 0x11: 152 | case 0x12: // + RAM 153 | case 0x13: // + RAM + Battery 154 | mbcType = MBCType::MBC3; 155 | break; 156 | 157 | case 0x19: 158 | case 0x1A: // + RAM 159 | case 0x1B: // + RAM + Battery 160 | case 0x1C: // + Rumble 161 | case 0x1D: // + Rumble + RAM 162 | case 0x1E: // + Rumble + RAM + Battery 163 | mbcType = MBCType::MBC5; 164 | break; 165 | 166 | default: 167 | printf("unhandled cartridge type %x\n", cartROMBank0[0x147]); 168 | mbcType = MBCType::None; 169 | } 170 | 171 | // use spare RAM as rom cache 172 | if(cartRamSize == 0 && mbcType != MBCType::MBC2) 173 | cachedROMBanks.emplace_back(ROMCacheEntry{cartRam, 0}); 174 | if(cartRamSize <= 0x4000) 175 | cachedROMBanks.emplace_back(ROMCacheEntry{cartRam + 0x4000, 0}); 176 | 177 | if(!(cartROMBank0[0x143] & 0x80))// CGB flag 178 | cachedROMBanks.emplace_back(ROMCacheEntry{wram + 0x4000, 0}); // spare WRAM (really 0x6000) 179 | 180 | // grab the first bank to use for bank 1 181 | auto cartROMBank1 = cachedROMBanks.front().ptr; 182 | 183 | if(mbcType == MBCType::MBC1 && cartROMBanks == 64) 184 | { 185 | // 1M MBC1 may be a multicart 186 | for(int i = 1; i < 4; i++) 187 | { 188 | romBankCallback(i << 4, cartROMBank1); 189 | 190 | // compare logos 191 | if(memcmp(cartROMBank0 + 0x104, cartROMBank1 + 0x104, 48) == 0) 192 | { 193 | mbcType = MBCType::MBC1M; 194 | break; 195 | } 196 | } 197 | } 198 | 199 | // load the second bank too 200 | romBankCallback(1, cartROMBank1); 201 | 202 | mbcRAMEnabled = false; 203 | mbcROMBank = 1; 204 | mbcRAMBank = 0; 205 | mbcRAMBankMode = false; 206 | 207 | regions[0x0] = 208 | regions[0x1] = 209 | regions[0x2] = 210 | regions[0x3] = cartROMBank0; 211 | regions[0x4] = 212 | regions[0x5] = 213 | regions[0x6] = 214 | regions[0x7] = cartROMBank1 - 0x4000; 215 | regions[0x8] = vram - 0x8000; // banked 216 | regions[0x9] = vram - 0x8000; // banked 217 | regions[0xA] = nullptr; // cart RAM - banked 218 | regions[0xB] = nullptr; // cart RAM - banked 219 | regions[0xC] = wram - 0xC000; 220 | regions[0xD] = wram - 0xC000; // banked 221 | regions[0xE] = wram - 0xE000; 222 | regions[0xF] = nullptr; 223 | } 224 | 225 | void DMGMemory::saveMBCState(std::function writeFunc, uint32_t &offset) 226 | { 227 | BESSHeader head; 228 | memcpy(head.id, "MBC ", 4); 229 | head.len = 0; 230 | 231 | uint8_t data[3 * 4]; 232 | 233 | if(mbcType != MBCType::None) 234 | { 235 | // ram enable 236 | data[head.len++] = 0x00; 237 | data[head.len++] = 0x00; // 0000 238 | data[head.len++] = mbcRAMEnabled ? 0xA : 0; 239 | } 240 | 241 | // MBC/RTC BESS blocks 242 | switch(mbcType) 243 | { 244 | case MBCType::None: 245 | break; 246 | 247 | case MBCType::MBC1: 248 | // rom bank low 249 | data[head.len++] = 0x00; 250 | data[head.len++] = 0x20; // 2000 251 | data[head.len++] = mbcROMBank & 0x1F; 252 | 253 | // rom bank high/ram bank 254 | data[head.len++] = 0x00; 255 | data[head.len++] = 0x40; // 4000 256 | data[head.len++] = (mbcROMBank >> 5) & 0x3; 257 | 258 | data[head.len++] = 0x00; 259 | data[head.len++] = 0x60; // 6000 260 | data[head.len++] = mbcRAMBankMode ? 1 : 0; 261 | break; 262 | 263 | case MBCType::MBC1M: 264 | // rom bank low 265 | data[head.len++] = 0x00; 266 | data[head.len++] = 0x20; // 2000 267 | data[head.len++] = mbcROMBank & 0xF; 268 | 269 | // rom bank high/ram bank 270 | data[head.len++] = 0x00; 271 | data[head.len++] = 0x40; // 4000 272 | data[head.len++] = (mbcROMBank >> 4) & 0x3; 273 | 274 | data[head.len++] = 0x00; 275 | data[head.len++] = 0x60; // 6000 276 | data[head.len++] = mbcRAMBankMode ? 1 : 0; 277 | break; 278 | 279 | case MBCType::MBC2: 280 | // rom bank 281 | data[head.len++] = 0x00; 282 | data[head.len++] = 0x01; // 0100 283 | data[head.len++] = mbcROMBank; 284 | break; 285 | 286 | case MBCType::MBC3: 287 | // rom bank 288 | data[head.len++] = 0x00; 289 | data[head.len++] = 0x20; // 2000 290 | data[head.len++] = mbcROMBank & 0x7F; 291 | 292 | // ram bank 293 | data[head.len++] = 0x00; 294 | data[head.len++] = 0x40; // 4000 295 | data[head.len++] = mbcRAMBank; 296 | break; 297 | 298 | case MBCType::MBC5: 299 | // rom bank low 300 | data[head.len++] = 0x00; 301 | data[head.len++] = 0x20; // 2000 302 | data[head.len++] = mbcROMBank & 0xFF; 303 | 304 | // rom bank high 305 | data[head.len++] = 0x00; 306 | data[head.len++] = 0x30; // 3000 307 | data[head.len++] = mbcROMBank >> 8; 308 | 309 | // ram bank 310 | data[head.len++] = 0x00; 311 | data[head.len++] = 0x40; // 4000 312 | data[head.len++] = mbcRAMBank; 313 | break; 314 | } 315 | 316 | if(head.len) 317 | { 318 | writeFunc(offset, sizeof(head), reinterpret_cast(&head)); 319 | writeFunc(offset + sizeof(head), head.len, data); 320 | offset += sizeof(head) + head.len; 321 | } 322 | 323 | if(hasRTC()) 324 | { 325 | // MBC3 RTC 326 | uint32_t rtcData[12]; 327 | 328 | memcpy(head.id, "RTC ", 4); 329 | head.len = sizeof(rtcData); 330 | 331 | getRTCData(rtcData); 332 | 333 | writeFunc(offset, sizeof(head), reinterpret_cast(&head)); 334 | writeFunc(offset + sizeof(head), head.len, reinterpret_cast(rtcData)); 335 | offset += sizeof(head) + head.len; 336 | } 337 | } 338 | 339 | uint8_t DMGMemory::read(uint16_t addr) const 340 | { 341 | int region = addr >> 12; 342 | 343 | if(regions[region]) 344 | return regions[region][addr]; 345 | 346 | if(region != 0xF) 347 | { 348 | // handle disabled RAM 349 | if(!mbcRAMEnabled) 350 | return 0xFF; 351 | 352 | if(mbcType == MBCType::MBC3) 353 | { 354 | // RTC read 355 | if(mbcRAMBank >= 8 && mbcRAMBank <= 0xC) 356 | return rtcRegs[mbcRAMBank - 8 + 5]; 357 | 358 | return 0xFF; // invalid bank 359 | } 360 | 361 | // only other way to get here is MBC2 362 | return cartRam[addr & 0x1FF]; 363 | } 364 | 365 | // must be Fxxx 366 | 367 | if(addr >= 0xFF00) 368 | { 369 | auto val = iohram[addr & 0xFF]; 370 | val = cpu.readReg(addr, val); 371 | 372 | return val; 373 | } 374 | 375 | if(addr < 0xFE00) 376 | return regions[0xD][addr - 0x2000]; // echo of D 377 | if(addr < 0xFEA0) 378 | return oam[addr & 0xFF]; 379 | 380 | return 0; // FEA0 - FF00 = unusable 381 | } 382 | 383 | const uint8_t *DMGMemory::mapAddress(uint16_t addr) const 384 | { 385 | int region = addr >> 12; 386 | 387 | if(regions[region]) 388 | return regions[region] + addr; 389 | 390 | if(addr < 0xFE00) 391 | return regions[0xD] + addr - 0x2000; //echo (?) 392 | if(addr < 0xFEA0) 393 | return oam + (addr & 0xFF); 394 | if(addr < 0xFF00) 395 | return nullptr; 396 | 397 | return iohram + (addr & 0xFF); 398 | } 399 | 400 | void DMGMemory::write(uint16_t addr, uint8_t data) 401 | { 402 | int region = addr >> 12; 403 | if(region < 8) 404 | writeMBC(addr, data); // cart rom 405 | else if(region == 0xA || region == 0xB) 406 | { 407 | if(mbcRAMEnabled) 408 | { 409 | // 512 4-bit values 410 | if(mbcType == MBCType::MBC2) 411 | cartRam[addr & 0x1FF] = data | 0xF0; 412 | else if(mbcType == MBCType::MBC3 && mbcRAMBank >= 8) 413 | { 414 | // RTC write 415 | updateRTC(); 416 | 417 | switch(mbcRAMBank - 8) 418 | { 419 | case 0: // seconds 420 | rtcMilliseconds = 0; // writing to seconds resets the millisecond counter? 421 | [[fallthrough]]; 422 | case 1: // minutes 423 | data &= 0x3F; 424 | break; 425 | case 2: // hours 426 | data &= 0x1F; 427 | break; 428 | case 4: // control 429 | data &= 0xC1; 430 | break; 431 | } 432 | 433 | rtcRegs[mbcRAMBank - 8] = data; 434 | } 435 | else if(regions[region]) 436 | const_cast(regions[region])[addr] = data; 437 | cartRamWritten = true; 438 | } 439 | } 440 | else if(regions[region]) 441 | const_cast(regions[region])[addr] = data; // these are the non-const ones... 442 | else 443 | { 444 | // must be Fxxx 445 | 446 | if(addr < 0xFE00) // echo 447 | { 448 | const_cast(regions[0xD])[addr - 0x2000] = data; 449 | return; 450 | } 451 | 452 | if(addr < 0xFEA0) 453 | { 454 | oam[addr & 0xFF] = data; 455 | return; 456 | } 457 | if(addr < 0xFF00) 458 | return; //unusable 459 | 460 | if(addr == 0xFF50) // boot flag, not writable 461 | return; 462 | else if((addr & 0xFF) == IO_VBK) 463 | { 464 | if(!isGBC) return; 465 | 466 | vramBank = data & 1; 467 | data |= 0xFE; // make sure only the low bit reads back 468 | regions[0x8] = regions[0x9] = vram + (vramBank * 0x2000) - 0x8000; 469 | } 470 | else if((addr & 0xFF) == IO_SVBK) 471 | { 472 | if(!isGBC) return; 473 | 474 | wramBank = (data & 0x7) ? (data & 0x7) : 1; // 0 is also 1 475 | regions[0xD] = wram + (wramBank * 0x1000) - 0xD000; 476 | } 477 | else if(cpu.writeReg(addr, data)) 478 | return; 479 | 480 | iohram[addr & 0xFF] = data; 481 | } 482 | } 483 | 484 | void DMGMemory::setCartRamUpdateCallback(CartRamUpdateCallback callback) 485 | { 486 | cartRamUpdateCallback = callback; 487 | } 488 | 489 | bool DMGMemory::hasRTC() const 490 | { 491 | return mbcType == MBCType::MBC3 && (cartROMBank0[0x147] == 0x0F || cartROMBank0[0x147] == 0x10); 492 | } 493 | 494 | void DMGMemory::getRTCData(uint32_t buf[12]) 495 | { 496 | updateRTC(); 497 | 498 | // the "VBA-M" format 499 | for(int i = 0; i < 10; i++) 500 | buf[i] = rtcRegs[i]; 501 | 502 | // invalid timestamp 503 | buf[10] = buf[11] = 0x7FFFFFFF; 504 | } 505 | 506 | void DMGMemory::setRTCData(uint32_t buf[12]) 507 | { 508 | // the "VBA-M" format 509 | for(int i = 0; i < 10; i++) 510 | rtcRegs[i] = buf[i]; 511 | 512 | // TODO: get elapsed time from timestamp 513 | } 514 | 515 | void DMGMemory::writeMBC(uint16_t addr, uint8_t data) 516 | { 517 | if(mbcType == MBCType::None) 518 | return; 519 | 520 | auto updateRAMBank = [this]() 521 | { 522 | if(!mbcRAMEnabled) 523 | regions[0xA] = regions[0xB] = nullptr; // RAM disabled 524 | else if((mbcType == MBCType::MBC1 || mbcType == MBCType::MBC1M) && !mbcRAMBankMode) // no banking, use bank 0 525 | regions[0xA] = regions[0xB] = cartRam - 0xA000; 526 | else 527 | { 528 | int off = (mbcRAMBank * 0x2000) & (cartRamSize - 1); // limit to RAM size 529 | regions[0xA] = regions[0xB] = cartRam + off - 0xA000; 530 | } 531 | }; 532 | 533 | // MBC2 is a bit different (and simpler) 534 | if(mbcType == MBCType::MBC2) 535 | { 536 | if(addr < 0x4000) 537 | { 538 | if(addr & 0x100) // ROM bank 539 | { 540 | mbcROMBank = data & 0xF; // 4 bit rom bank 541 | if(mbcROMBank == 0) 542 | mbcROMBank = 1; // bank 0 is handled as bank 1 543 | 544 | updateCurrentROMBank(mbcROMBank, 4); 545 | } 546 | else // RAM enable 547 | { 548 | mbcRAMEnabled = (data & 0xF) == 0xA; 549 | // don't set the pointers as the RAM is tiny and weird 550 | } 551 | } 552 | return; 553 | } 554 | 555 | // MBC1/3/5 556 | 557 | if(addr < 0x2000) 558 | { 559 | if(mbcType == MBCType::MBC5) // MBC5 doesn't ignore the top bits 560 | mbcRAMEnabled = data == 0xA; 561 | else 562 | mbcRAMEnabled = (data & 0xF) == 0xA; 563 | 564 | updateRAMBank(); 565 | 566 | // on disable sync the ram if changed 567 | if(!mbcRAMEnabled) 568 | { 569 | if(cartRamWritten && cartRamUpdateCallback) 570 | cartRamUpdateCallback(cartRam, cartRamSize); 571 | 572 | cartRamWritten = false; 573 | } 574 | } 575 | else if(addr < 0x4000) 576 | { 577 | if(mbcType == MBCType::MBC1) // low 5 bits of rom bank 578 | { 579 | mbcROMBank = (mbcROMBank & 0xE0) | (data & 0x1F); 580 | 581 | if((mbcROMBank & 0x1F) == 0) 582 | mbcROMBank++; // bank 0 is handled as bank 1 (+ some bugs) 583 | } 584 | else if(mbcType == MBCType::MBC1M) // low 4 bits of rom bank (game bank) 585 | { 586 | mbcROMBank = (mbcROMBank & 0xF0) | (data & 0xF); 587 | 588 | if((data & 0x1F) == 0) 589 | mbcROMBank++; // bank 0 is handled as bank 1 (+ some bugs) 590 | } 591 | else if(mbcType == MBCType::MBC3) 592 | { 593 | mbcROMBank = data & 0x7F; // 7 bit rom bank 594 | if(mbcROMBank == 0) 595 | mbcROMBank = 1; // bank 0 is handled as bank 1 596 | } 597 | else if(mbcType == MBCType::MBC5) 598 | { 599 | if(addr < 0x3000) // low 8 600 | mbcROMBank = (mbcROMBank & 0x100) | data; 601 | else // high 1 602 | mbcROMBank = (mbcROMBank & 0xFF) | ((data & 1) << 8); 603 | } 604 | 605 | updateCurrentROMBank(mbcROMBank, 4); 606 | } 607 | else if(addr < 0x6000) 608 | { 609 | // MBC3/5 RAM banking 610 | if(mbcType == MBCType::MBC5 || mbcType == MBCType::MBC3) 611 | { 612 | mbcRAMBank = data & 0xF; 613 | 614 | // valid ranges for MBC3 are 0-3, 8-C (the second range being the RTC) 615 | if(mbcType == MBCType::MBC3 && mbcRAMBank > 3) 616 | regions[0xA] = regions[0xB] = nullptr; 617 | else 618 | updateRAMBank(); 619 | } 620 | else // MBC1 high 2 bits of rom bank / ram bank 621 | { 622 | if(mbcRAMBankMode) 623 | { 624 | mbcRAMBank = data & 0x3; 625 | updateRAMBank(); 626 | 627 | // also affetcts the bank at 0-3FFF 628 | if(mbcType == MBCType::MBC1M) 629 | updateCurrentROMBank((data & 0x3) << 4, 0); 630 | else 631 | updateCurrentROMBank((data & 0x3) << 5, 0); 632 | } 633 | 634 | // ROM is always banked 635 | if(mbcType == MBCType::MBC1M) 636 | mbcROMBank = (mbcROMBank & 0xF) | ((data & 0x3) << 4); 637 | else 638 | mbcROMBank = (mbcROMBank & 0x1F) | ((data & 0x3) << 5); 639 | updateCurrentROMBank(mbcROMBank, 4); 640 | } 641 | } 642 | else // < 0x8000 643 | { 644 | if(mbcType == MBCType::MBC1 || mbcType == MBCType::MBC1M) 645 | { 646 | mbcRAMBankMode = data == 1; 647 | updateRAMBank(); 648 | 649 | // update bank at 0-3FFF 650 | if(mbcRAMBankMode) 651 | updateCurrentROMBank(mbcROMBank & 0x60, 0); 652 | else 653 | updateCurrentROMBank(0, 0); 654 | } 655 | else if(mbcType == MBCType::MBC3) 656 | { 657 | // RTC latch 658 | if(data) 659 | { 660 | updateRTC(); 661 | 662 | for(int i = 0; i < 5; i++) 663 | rtcRegs[i + 5] = rtcRegs[i]; 664 | } 665 | } 666 | } 667 | } 668 | 669 | void DMGMemory::updateCurrentROMBank(unsigned int bank, int region) 670 | { 671 | // region is either 0 or 4 672 | int offset = region * 0x1000; 673 | 674 | bank %= cartROMBanks; 675 | 676 | if(bank == 0) 677 | { 678 | for(int i = 0; i < 4; i++) 679 | regions[region + i] = cartROMBank0 - offset; 680 | return; 681 | } 682 | 683 | // entire ROM is loaded 684 | if(cartROM) 685 | { 686 | for(int i = 0; i < 4; i++) 687 | regions[region + i] = cartROM + bank * 0x4000 - offset; 688 | return; 689 | } 690 | 691 | for(auto it = cachedROMBanks.begin(); it != cachedROMBanks.end(); ++it) 692 | { 693 | if(it->bank == bank) 694 | { 695 | for(int i = 0; i < 4; i++) 696 | regions[region + i] = it->ptr - offset; 697 | 698 | cachedROMBanks.splice(cachedROMBanks.begin(), cachedROMBanks, it); // move it to the top 699 | return; 700 | } 701 | } 702 | 703 | // reuse the last (least recently used) bank 704 | auto it = std::prev(cachedROMBanks.end()); 705 | 706 | // make sure it isn't being used by the other region 707 | if(mbcType == MBCType::MBC1 && ((region == 0 && regions[4] == it->ptr - 0x4000) || (region == 4 && regions[0] == it->ptr))) 708 | --it; // use the next one instead 709 | 710 | for(int i = 0; i < 4; i++) 711 | regions[region + i] = it->ptr - offset; 712 | 713 | romBankCallback(bank, it->ptr); 714 | it->bank = bank; 715 | cachedROMBanks.splice(cachedROMBanks.begin(), cachedROMBanks, it); // move it to the top 716 | } 717 | 718 | void DMGMemory::updateRTC() 719 | { 720 | auto curTime = cpu.getCycleCount(); 721 | 722 | if((rtcRegs[4] & (1 << 6))) // halted 723 | { 724 | rtcUpdateTime = curTime; 725 | return; 726 | } 727 | 728 | auto cpuClock = 4 * 1024 * 1024 * (cpu.getDoubleSpeedMode() ? 2 : 1); 729 | auto elapsed = (curTime - rtcUpdateTime) * 1000 / cpuClock; 730 | 731 | rtcUpdateTime += elapsed * cpuClock / 1000; 732 | 733 | while(elapsed) 734 | { 735 | int step = std::min(elapsed, static_cast(1000 - rtcMilliseconds)); 736 | elapsed -= step; 737 | 738 | rtcMilliseconds += step; 739 | 740 | if(rtcMilliseconds != 1000) 741 | continue; 742 | 743 | rtcMilliseconds = 0; 744 | 745 | // seconds 746 | rtcRegs[0] = (rtcRegs[0] + 1) & 0x3F; 747 | 748 | if(rtcRegs[0] != 60) 749 | continue; 750 | 751 | // minutes 752 | rtcRegs[0] = 0; 753 | rtcRegs[1] = (rtcRegs[1] + 1) & 0x3F; 754 | 755 | if(rtcRegs[1] != 60) 756 | continue; 757 | 758 | // hours 759 | rtcRegs[1] = 0; 760 | rtcRegs[2] = (rtcRegs[2] + 1) & 0x1F; 761 | 762 | if(rtcRegs[2] != 24) 763 | continue; 764 | 765 | // days 766 | rtcRegs[2] = 0; 767 | int days = rtcRegs[3] | (rtcRegs[4] & 1) << 8; 768 | days++; 769 | 770 | rtcRegs[3] = days & 0xFF; 771 | rtcRegs[4] = (rtcRegs[4] & 0xC0) | ((days >> 8) & 1); 772 | 773 | if(days > 0x1FF) 774 | rtcRegs[4] |= 1 << 7; // day carry 775 | } 776 | } -------------------------------------------------------------------------------- /core/DMGDisplay.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "DMGDisplay.h" 5 | 6 | #include "DMGCPU.h" 7 | #include "DMGMemory.h" 8 | #include "DMGRegs.h" 9 | #include "DMGSaveState.h" 10 | 11 | enum SpriteFlags 12 | { 13 | // 0-2 is colour palette 14 | Sprite_Bank = (1 << 3), 15 | Sprite_Palette = (1 << 4), // non-colour 16 | Sprite_XFlip = (1 << 5), 17 | Sprite_YFlip = (1 << 6), 18 | Sprite_BGPriority = (1 << 7), 19 | }; 20 | 21 | enum TileFlags 22 | { 23 | // 0-2 is palette 24 | Tile_Bank = (1 << 3), 25 | Tile_XFlip = (1 << 5), 26 | Tile_YFlip = (1 << 6), 27 | Tile_BGPriority = (1 << 7) 28 | }; 29 | 30 | // constants 31 | static const int tileDataSize = 16; 32 | static const int screenSizeTiles = 32; // 32x32 tiles 33 | 34 | static const int numSprites = 40; 35 | 36 | // reverses the bits in each byte, but not the bytes 37 | static uint16_t reverseBitsPerByte(uint16_t v) 38 | { 39 | v = (((v & 0xAAAA) >> 1) | ((v & 0x5555) << 1)); 40 | v = (((v & 0xCCCC) >> 2) | ((v & 0x3333) << 2)); 41 | v = (((v & 0xF0F0) >> 4) | ((v & 0x0F0F) << 4)); 42 | return v; 43 | } 44 | 45 | DMGDisplay::DMGDisplay(DMGCPU &cpu) : cpu(cpu), mem(cpu.getMem()) 46 | { 47 | 48 | } 49 | 50 | void DMGDisplay::reset() 51 | { 52 | memset(bgPalette, 0xFF, sizeof(bgPalette)); 53 | memset(objPalette, 0xFF, sizeof(objPalette)); // not initialised 54 | 55 | lastUpdateCycle = 0; 56 | 57 | enabled = true; 58 | y = 0; 59 | statMode = 0; 60 | compareMatch = false; 61 | windowY = 0; 62 | firstFrame = false; 63 | 64 | remainingScanlineCycles = scanlineCycles; 65 | remainingModeCycles = 4; 66 | 67 | // make sure the default palette gets set up for !GBC 68 | for(int i = IO_BGP; i <= IO_OBP1; i++) 69 | mem.write(0xFF00 | i, mem.readIOReg(i)); 70 | } 71 | 72 | void DMGDisplay::loadSaveState(BESSCore &bess, DaftState &state, std::function readFunc) 73 | { 74 | readFunc(bess.bgPalOff, bess.bgPalSize, reinterpret_cast(bgPalette)); 75 | readFunc(bess.objPalOff, bess.objPalSize, reinterpret_cast(objPalette)); 76 | 77 | // sync with loaded regs 78 | enabled = bess.ioRegs[IO_LCDC] & LCDC_DisplayEnable; 79 | y = bess.ioRegs[IO_LY]; 80 | statMode = bess.ioRegs[IO_STAT] & STAT_Mode; 81 | compareMatch = bess.ioRegs[IO_STAT] & STAT_Coincidence; 82 | 83 | if(state.version) 84 | { 85 | windowY = state.windowY; 86 | firstFrame = state.flags & State_DispFirstFrame; 87 | statInterruptActive = state.statInterruptActive; 88 | remainingScanlineCycles = state.remainingScalineCycles; 89 | remainingModeCycles = state.remainingModeCycles; 90 | } 91 | 92 | // interrupts 93 | const auto statInts = STAT_HBlankInt | STAT_VBlankInt | STAT_OAMInt | STAT_CoincidenceInt; 94 | interruptsEnabled = (bess.ie & Int_VBlank) || ((bess.ie & Int_LCDStat) && (bess.ioRegs[IO_STAT] & statInts)); 95 | 96 | lastUpdateCycle = cpu.getCycleCount(); 97 | } 98 | 99 | void DMGDisplay::saveState(DaftState &state) 100 | { 101 | state.windowY = windowY; 102 | state.statInterruptActive = statInterruptActive; 103 | state.remainingScalineCycles = remainingScanlineCycles; 104 | state.remainingModeCycles = remainingModeCycles; 105 | 106 | if(firstFrame) 107 | state.flags |= State_DispFirstFrame; 108 | } 109 | 110 | void DMGDisplay::savePaletteState(BESSCore &bess, std::function writeFunc, uint32_t &offset) 111 | { 112 | bool isColour = cpu.getColourMode(); 113 | 114 | if(!isColour) 115 | { 116 | bess.bgPalOff = bess.bgPalSize = 0; 117 | bess.bgPalOff = bess.objPalOff = 0; 118 | return; 119 | } 120 | 121 | bess.bgPalOff = offset; 122 | bess.bgPalSize = sizeof(bgPalette); 123 | writeFunc(offset, bess.bgPalSize, reinterpret_cast(bgPalette)); 124 | offset += bess.bgPalSize; 125 | 126 | bess.objPalOff = offset; 127 | bess.objPalSize = sizeof(objPalette); 128 | writeFunc(offset, bess.objPalSize, reinterpret_cast(objPalette)); 129 | offset += bess.objPalSize; 130 | } 131 | 132 | void DMGDisplay::update() 133 | { 134 | auto curCycle = cpu.getCycleCount(); 135 | if(enabled) 136 | { 137 | auto passed = curCycle - lastUpdateCycle; 138 | 139 | // blank out the screen if we're stopped 140 | if(cpu.getStopped()) 141 | { 142 | // check if CGB, even in DMG mode 143 | bool isCGB = cpu.getConsole() == DMGCPU::Console::CGB || cpu.getColourMode(); 144 | 145 | // mode 3 on CGB keeps the old image 146 | if(!isCGB || statMode != 3) 147 | memset(screenData, isCGB ? 0 : 0xFF, screenWidth * screenHeight * 2); 148 | 149 | lastUpdateCycle = curCycle; 150 | return; 151 | } 152 | 153 | if(cpu.getDoubleSpeedMode()) 154 | passed >>= 1; 155 | 156 | while(passed) 157 | { 158 | auto step = std::min(remainingModeCycles, passed); 159 | 160 | passed -= step; 161 | 162 | remainingScanlineCycles -= step; 163 | remainingModeCycles -= step; 164 | 165 | // update STAT if not vblank 166 | if(remainingModeCycles == 0) 167 | { 168 | // new mode 169 | if(statMode <= 1 && remainingScanlineCycles) 170 | { 171 | // start of line - update compare 172 | updateCompare(y == mem.readIOReg(IO_LYC)); 173 | 174 | auto stat = mem.readIOReg(IO_STAT); 175 | 176 | // in vblank 177 | if(y >= screenHeight) 178 | { 179 | // start of vblank 180 | if(y == screenHeight) 181 | { 182 | cpu.flagInterrupt(Int_VBlank); 183 | statMode = 1; 184 | remainingModeCycles = scanlineCycles; 185 | 186 | // line 144 still generates the OAM/mode 2 interrupt 187 | // (do other vblank lines do this?) 188 | if((stat & (STAT_VBlankInt | STAT_OAMInt))) 189 | { 190 | if(!statInterruptActive) 191 | cpu.flagInterrupt(Int_LCDStat); 192 | 193 | statInterruptActive |= (stat & (STAT_VBlankInt | STAT_OAMInt)); 194 | } 195 | statInterruptActive &= ~STAT_HBlankInt; 196 | 197 | if(vBlankCallback) 198 | vBlankCallback(); 199 | } 200 | 201 | // line 153 has some additional wierdness... (switches to y = 0 early) 202 | 203 | remainingModeCycles = remainingScanlineCycles; 204 | 205 | continue; 206 | } 207 | 208 | // first line after enabling 209 | if(firstFrame) 210 | { 211 | // skips mode 2 212 | statMode = 3; 213 | remainingModeCycles = 172; // minimal time (skipped the bit that reads sprites) 214 | firstFrame = false; 215 | continue; 216 | } 217 | 218 | // line start -> mode 2 219 | statMode = 2; // oam search 220 | remainingModeCycles = 80; 221 | 222 | if(stat & STAT_OAMInt) 223 | { 224 | if(!statInterruptActive) 225 | cpu.flagInterrupt(Int_LCDStat); 226 | 227 | statInterruptActive |= STAT_OAMInt; 228 | } 229 | statInterruptActive &= ~(STAT_HBlankInt | STAT_VBlankInt); 230 | 231 | continue; 232 | } 233 | else if(statMode == 2) 234 | { 235 | // mode 2 -> 3 236 | statMode = 3; 237 | statInterruptActive &= ~(STAT_HBlankInt | STAT_OAMInt); 238 | 239 | // guess something semi-accurate 240 | remainingModeCycles = 172 + (mem.getIOReg(IO_SCX) & 7); 241 | 242 | // more if sprites 243 | const int spriteHeight = (mem.getIOReg(IO_LCDC) & LCDC_Sprite8x16) ? 16 : 8; 244 | int numLineSprites = 0; 245 | auto oam = mem.getOAM(); 246 | 247 | for(int i = 0; i < numSprites && numLineSprites < 10; i++) 248 | { 249 | const int spriteY = oam[i * 4] - 16; 250 | 251 | // not on this line 252 | if(y < spriteY || y >= spriteY + spriteHeight) 253 | continue; 254 | 255 | numLineSprites++; 256 | remainingModeCycles += 11; // not really accurate, does result in the right range though 257 | } 258 | 259 | continue; 260 | } 261 | else if(statMode == 3) 262 | { 263 | // mode 3 -> 0 (hblank) 264 | statMode = 0; 265 | drawScanLine(y); // draw right before hblank 266 | 267 | auto stat = mem.readIOReg(IO_STAT); 268 | if((stat & STAT_HBlankInt)) 269 | { 270 | if(!statInterruptActive) 271 | cpu.flagInterrupt(Int_LCDStat); 272 | 273 | statInterruptActive |= STAT_HBlankInt; 274 | } 275 | 276 | remainingModeCycles = remainingScanlineCycles; 277 | continue; 278 | } 279 | else if(statMode == 1) 280 | remainingModeCycles = scanlineCycles; 281 | } 282 | else 283 | continue; 284 | 285 | // next scanline 286 | remainingScanlineCycles = scanlineCycles; 287 | remainingModeCycles = 4; 288 | y++; 289 | 290 | compareMatch = false; // cleared for one cycle 291 | 292 | if(y > 153) 293 | { 294 | y = windowY = 0; // end vblank 295 | statMode = 0; 296 | } 297 | } 298 | } 299 | 300 | lastUpdateCycle = curCycle; 301 | } 302 | 303 | void DMGDisplay::updateForInterrupts() 304 | { 305 | if(!interruptsEnabled) 306 | return; // nothing to do 307 | 308 | auto passed = cpu.getCycleCount() - lastUpdateCycle; 309 | bool doubleSpeed = cpu.getDoubleSpeedMode(); 310 | 311 | if(doubleSpeed) 312 | passed >>= 1; 313 | 314 | if(passed < remainingModeCycles) 315 | return; 316 | 317 | update(); 318 | } 319 | 320 | int DMGDisplay::getCyclesToNextUpdate() const 321 | { 322 | if(!interruptsEnabled) 323 | return 0x7FFFFFFF; // this is only used when halted and clamped to something reasonable in the CPU 324 | 325 | auto passed = cpu.getCycleCount() - lastUpdateCycle; 326 | bool doubleSpeed = cpu.getDoubleSpeedMode(); 327 | 328 | if(doubleSpeed) 329 | passed >>= 1; 330 | 331 | return (remainingModeCycles - passed) * (doubleSpeed ? 2 : 1); 332 | } 333 | 334 | void DMGDisplay::setFramebuffer(uint16_t *data) 335 | { 336 | screenData = data; 337 | } 338 | 339 | uint8_t DMGDisplay::readReg(uint16_t addr, uint8_t val) 340 | { 341 | switch(addr & 0xFF) 342 | { 343 | case IO_STAT: 344 | update(); 345 | return (val & 0xF8) | (compareMatch ? STAT_Coincidence : 0) | statMode | 0x80; 346 | case IO_LY: 347 | update(); 348 | return y; 349 | } 350 | 351 | return val; 352 | } 353 | 354 | bool DMGDisplay::writeReg(uint16_t addr, uint8_t data) 355 | { 356 | // pre-converting the DMG palette is easy 357 | #ifdef DISPLAY_RGB565 358 | const uint16_t colMap[]{0xFFFF, 0xAD55, 0x528A, 0}; 359 | #else 360 | const uint16_t colMap[]{0xFFFF, 0x56B5, 0x294A, 0}; 361 | #endif 362 | 363 | const auto statInts = STAT_HBlankInt | STAT_VBlankInt | STAT_OAMInt | STAT_CoincidenceInt; 364 | 365 | switch(addr & 0xFF) 366 | { 367 | case IO_STAT: 368 | { 369 | update(); 370 | 371 | auto ie = mem.readIOReg(IO_IE); 372 | auto stat = data; 373 | 374 | // trigger vblank stat (and others?) 375 | if(statMode == 1 && (stat & STAT_VBlankInt) && !statInterruptActive) 376 | { 377 | cpu.flagInterrupt(Int_LCDStat); 378 | statInterruptActive |= STAT_VBlankInt; 379 | } 380 | 381 | statInterruptActive &= stat; 382 | 383 | interruptsEnabled = (ie & Int_VBlank) || ((ie & Int_LCDStat) && (stat & statInts)); 384 | break; 385 | } 386 | 387 | case IO_LCDC: 388 | { 389 | update(); 390 | if(!(data & LCDC_DisplayEnable)) 391 | { 392 | // reset 393 | remainingScanlineCycles = scanlineCycles - 4; // running behind 394 | statMode = 0; 395 | remainingModeCycles = 80; // no mode 2 396 | y = windowY = 0; 397 | firstFrame = true; 398 | } 399 | else if(!enabled) // enabling 400 | updateCompare(y == mem.readIOReg(IO_LYC)); 401 | 402 | enabled = data & LCDC_DisplayEnable; 403 | break; 404 | } 405 | 406 | case IO_LY: 407 | return true; 408 | case IO_LYC: 409 | if(enabled) 410 | { 411 | update(); 412 | updateCompare(y == data); 413 | } 414 | 415 | break; 416 | 417 | // grey palettes 418 | case IO_BGP: 419 | { 420 | if(cpu.getColourMode()) 421 | break; 422 | 423 | update(); 424 | 425 | for(int i = 0; i < 4; i++) 426 | bgPalette[i] = colMap[(data >> (2 * i)) & 0x3]; 427 | break; 428 | } 429 | case IO_OBP0: 430 | { 431 | if(cpu.getColourMode()) 432 | break; 433 | 434 | update(); 435 | 436 | for(int i = 0; i < 4; i++) 437 | objPalette[i] = colMap[(data >> (2 * i)) & 0x3]; 438 | break; 439 | } 440 | case IO_OBP1: 441 | { 442 | if(cpu.getColourMode()) 443 | break; 444 | 445 | update(); 446 | 447 | for(int i = 0; i < 4; i++) 448 | objPalette[i + 4] = colMap[(data >> (2 * i)) & 0x3]; 449 | break; 450 | } 451 | 452 | case IO_BCPS: 453 | case IO_OCPS: 454 | // ignore write on !GBC, force bit 6 otherwise 455 | if(cpu.getColourMode()) 456 | mem.writeIOReg(addr & 0xFF, data | 0x40); 457 | return true; 458 | 459 | // colour palettes 460 | case IO_BCPD: 461 | { 462 | if(!cpu.getColourMode()) 463 | return true; 464 | 465 | update(); 466 | 467 | auto bcps = mem.readIOReg(IO_BCPS); 468 | 469 | #if defined(DISPLAY_RGB565) || defined(DISPLAY_RB_SWAP) 470 | reinterpret_cast(bgPaletteRaw)[bcps & 0x3F] = data; 471 | bgPaletteDirty = true; 472 | #else 473 | reinterpret_cast(bgPalette)[bcps & 0x3F] = data; 474 | #endif 475 | // auto inc 476 | if(bcps & 0x80) 477 | mem.writeIOReg(IO_BCPS, ((bcps & 0x3F) + 1) | 0xC0); 478 | 479 | return true; 480 | } 481 | 482 | case IO_OCPD: 483 | { 484 | if(!cpu.getColourMode()) 485 | return true; 486 | 487 | update(); 488 | 489 | auto ocps = mem.readIOReg(IO_OCPS); 490 | 491 | #if defined(DISPLAY_RGB565) || defined(DISPLAY_RB_SWAP) 492 | reinterpret_cast(objPaletteRaw)[ocps & 0x3F] = data; 493 | objPaletteDirty = true; 494 | #else 495 | reinterpret_cast(objPalette)[ocps & 0x3F] = data; 496 | #endif 497 | 498 | // auto inc 499 | if(ocps & 0x80) 500 | mem.writeIOReg(IO_OCPS, ((ocps & 0x3F) + 1) | 0xC0); 501 | 502 | return true; 503 | } 504 | 505 | case IO_OPRI: 506 | { 507 | if(!cpu.getColourMode()) 508 | return true; 509 | // this is set by the bios 510 | break; 511 | } 512 | 513 | // update interrupt status 514 | case IO_IE: 515 | { 516 | update(); 517 | auto ie = data; 518 | auto stat = mem.readIOReg(IO_STAT); 519 | interruptsEnabled = (ie & Int_VBlank) || ((ie & Int_LCDStat) && (stat & statInts)); 520 | break; 521 | } 522 | 523 | } 524 | 525 | return false; 526 | } 527 | 528 | // get the two data bytes for a tile row 529 | // handles x/y flips 530 | static uint16_t getTileRow(uint8_t lcdc, uint8_t *mapPtr, uint8_t *tileDataPtr, int tileY, int &attrs) 531 | { 532 | attrs = mapPtr[0x2000]; // GBC, bank 1 533 | 534 | // tile id is signed if addr == 0x8800 535 | int tileId = (lcdc & LCDC_TileData8000) ? *mapPtr : (int8_t)(*mapPtr) + 128; 536 | 537 | auto tileAddr = tileId * tileDataSize; 538 | 539 | if(attrs & Tile_Bank) 540 | tileAddr += 0x2000; 541 | 542 | if(attrs & Tile_YFlip) 543 | tileY = 7 - tileY; 544 | 545 | auto d = reinterpret_cast(tileDataPtr + tileAddr)[tileY]; 546 | 547 | if(attrs & Tile_XFlip) 548 | d = reverseBitsPerByte(d); 549 | 550 | return d; 551 | }; 552 | 553 | static uint16_t getTileRow(uint8_t lcdc, uint8_t *mapPtr, uint8_t *tileDataPtr, int x, int y, int tileY, int &attrs) 554 | { 555 | int tileId = x + y * screenSizeTiles; 556 | 557 | return getTileRow(lcdc, mapPtr + tileId, tileDataPtr, tileY, attrs); 558 | }; 559 | 560 | // gets the two bit index from the top of the two bytes 561 | inline int getPalIndex(uint16_t d) 562 | { 563 | return ((d & 0x80) >> 7) | ((d >> 15) << 1); 564 | }; 565 | 566 | static void copyPartialTile(uint8_t lcdc, int &x, int endX, uint16_t d, int tileAttrs, uint16_t *bgPalette, uint16_t *&out, uint8_t *&rawOut) 567 | { 568 | uint8_t tilePriority = (tileAttrs & Tile_BGPriority) ? 0x80 : 0; 569 | 570 | // palette 571 | const auto bgPal = bgPalette + (tileAttrs & 0x7) * 4; 572 | 573 | for(; x < endX; x++, d <<= 1) 574 | { 575 | int palIndex = getPalIndex(d); 576 | 577 | if(lcdc & LCDC_BGDisp) 578 | *(rawOut++) = palIndex | tilePriority; 579 | 580 | *(out++) = bgPal[palIndex]; 581 | } 582 | }; 583 | 584 | static void copyFullTile(uint8_t lcdc, uint16_t d, int tileAttrs, uint16_t *bgPalette, uint16_t *&out, uint8_t *&rawOut) 585 | { 586 | uint8_t tilePriority = (tileAttrs & Tile_BGPriority) ? 0x80 : 0; 587 | 588 | // palette 589 | const auto bgPal = bgPalette + (tileAttrs & 0x7) * 4; 590 | 591 | for(int i = 0; i < 8; i++, d <<= 1) 592 | { 593 | int palIndex = getPalIndex(d); 594 | 595 | if(lcdc & LCDC_BGDisp) 596 | *(rawOut++) = palIndex | tilePriority; 597 | 598 | *(out++) = bgPal[palIndex]; 599 | } 600 | }; 601 | 602 | static void copyTiles(uint8_t lcdc, uint8_t *tileDataPtr, uint8_t *mapPtr, uint16_t *bgPalette, int &x, int xLimit, int offsetX, uint8_t oy, uint16_t *&out, uint8_t *&rawOut) 603 | { 604 | // full tiles 605 | uint8_t ox = x + offsetX; // this is a uint8 so that it wraps 606 | 607 | auto rowMapPtr = mapPtr + (oy / 8) * screenSizeTiles; 608 | 609 | while(x + 7 < xLimit) 610 | { 611 | int mapAttrs; 612 | auto d = getTileRow(lcdc, rowMapPtr + ox / 8, tileDataPtr, oy & 7, mapAttrs); 613 | 614 | copyFullTile(lcdc, d, mapAttrs, bgPalette, out, rawOut); 615 | x += 8; 616 | ox += 8; 617 | } 618 | 619 | if(x < xLimit) 620 | { 621 | int mapAttrs; 622 | auto d = getTileRow(lcdc, rowMapPtr + ox / 8, tileDataPtr, oy & 7, mapAttrs); 623 | 624 | copyPartialTile(lcdc, x, xLimit, d, mapAttrs, bgPalette, out, rawOut); 625 | } 626 | }; 627 | 628 | void DMGDisplay::drawScanLine(int y) 629 | { 630 | // contains palette index + a tile priority flag 631 | uint8_t bgRaw[screenWidth]{0}; 632 | 633 | #ifdef BLIT_BOARD_PIMORONI_PICOSYSTEM 634 | // need to avoid writing unfinished lines directly to the framebuffer as we're not synced 635 | uint16_t scanLine[screenWidth]; 636 | #else 637 | auto scanLine = screenData + y * screenWidth; 638 | #endif 639 | 640 | auto lcdc = mem.readIOReg(IO_LCDC); 641 | 642 | const bool isColour = cpu.getColourMode(); 643 | 644 | // sync palettes 645 | #if defined(DISPLAY_RGB565) || defined(DISPLAY_RB_SWAP) 646 | 647 | const auto convert = [](uint16_t col) 648 | { 649 | #ifdef DISPLAY_RB_SWAP 650 | col = col << 10 | (col & 0x3E0) | (col << 1) >> 11; 651 | #endif 652 | #ifdef DISPLAY_RGB565 653 | col = (col & 0x1F) | (col & 0x7FE0) << 1; 654 | #endif 655 | return col; 656 | }; 657 | 658 | if(bgPaletteDirty) 659 | { 660 | auto outCol = bgPalette; 661 | for(auto col : bgPaletteRaw) 662 | *outCol++ = convert(col); 663 | 664 | bgPaletteDirty = false; 665 | } 666 | 667 | if(objPaletteDirty) 668 | { 669 | auto outCol = objPalette; 670 | for(auto col : objPaletteRaw) 671 | *outCol++ = convert(col); 672 | 673 | objPaletteDirty = false; 674 | } 675 | 676 | #endif 677 | 678 | // active scanline 679 | // this is reduced to a priority flag on GBC 680 | if(lcdc & LCDC_BGDisp || isColour) 681 | drawBackground(scanLine, bgRaw); 682 | 683 | if(lcdc & LCDC_OBJDisp) 684 | drawSprites(scanLine, bgRaw); 685 | 686 | #ifdef BLIT_BOARD_PIMORONI_PICOSYSTEM 687 | memcpy(screenData + y * screenWidth, scanLine, screenWidth * 2); 688 | #endif 689 | } 690 | 691 | void DMGDisplay::drawBackground(uint16_t *scanLine, uint8_t *bgRaw) 692 | { 693 | auto lcdc = mem.readIOReg(IO_LCDC); 694 | 695 | auto vram = mem.getVRAM(); 696 | auto tileDataPtr = (lcdc & LCDC_TileData8000) ? vram : vram + 0x800; 697 | auto bgMapPtr = (lcdc & LCDC_BGTileMap9C00) ? vram + 0x1C00 : vram + 0x1800; 698 | auto winMapPtr = (lcdc & LCDC_WindowTileMap9C00) ? vram + 0x1C00 : vram + 0x1800; 699 | 700 | int windowX = screenWidth; 701 | 702 | int x = 0; 703 | auto out = scanLine; 704 | auto rawOut = bgRaw; 705 | 706 | if(lcdc & LCDC_WindowEnable) 707 | { 708 | int windowY = mem.readIOReg(IO_WY); 709 | if(y >= windowY) 710 | windowX = mem.readIOReg(IO_WX) - 7; 711 | } 712 | 713 | // background 714 | if(windowX > 0) 715 | { 716 | auto scrollX = mem.readIOReg(IO_SCX); 717 | auto scrollY = mem.readIOReg(IO_SCY); 718 | 719 | // partial tile at the start of the line 720 | if(scrollX & 7) 721 | { 722 | uint8_t oy = y + scrollY; 723 | int mapAttrs; 724 | auto d = getTileRow(lcdc, bgMapPtr, tileDataPtr, scrollX / 8, oy / 8, oy & 7, mapAttrs); 725 | 726 | // skip bits 727 | d <<= scrollX & 7; 728 | 729 | copyPartialTile(lcdc, x, 8 - (scrollX & 7), d, mapAttrs, bgPalette, out, rawOut); 730 | } 731 | 732 | int xEnd = windowX < screenWidth ? windowX : screenWidth; 733 | copyTiles(lcdc, tileDataPtr, bgMapPtr, bgPalette, x, xEnd, scrollX, y + scrollY, out, rawOut); 734 | } 735 | 736 | // window 737 | if(x < screenWidth) 738 | { 739 | copyTiles(lcdc, tileDataPtr, winMapPtr, bgPalette, x, screenWidth, -windowX, windowY, out, rawOut); 740 | windowY++; 741 | } 742 | } 743 | 744 | void DMGDisplay::drawSprites(uint16_t *scanLine, uint8_t *bgRaw) 745 | { 746 | auto lcdc = mem.readIOReg(IO_LCDC); 747 | const bool isColour = cpu.getColourMode(); 748 | 749 | const int spriteHeight = (lcdc & LCDC_Sprite8x16) ? 16 : 8; 750 | 751 | // sprites 752 | auto oam = mem.getOAM(); 753 | auto spriteDataPtr = mem.getVRAM(); 754 | 755 | // 10 sprites per line limit 756 | uint8_t lineSprites[10]; 757 | int numLineSprites = 0; 758 | 759 | for(int i = 0; i < numSprites && numLineSprites < 10; i++) 760 | { 761 | const int spriteY = oam[i * 4] - 16; 762 | 763 | // not on this line 764 | if(y < spriteY || y >= spriteY + spriteHeight) 765 | continue; 766 | 767 | lineSprites[numLineSprites++] = i; 768 | } 769 | 770 | if(!isColour && numLineSprites > 1) 771 | { 772 | // sort sprites by x 773 | for(int i = 1; i < numLineSprites; i++) 774 | { 775 | int j = i; 776 | 777 | while(j && oam[lineSprites[j] * 4 + 1] < oam[lineSprites[j - 1] * 4 + 1]) 778 | { 779 | std::swap(lineSprites[j], lineSprites[j - 1]); 780 | j--; 781 | } 782 | } 783 | } 784 | 785 | // TODO: the bg priority handling here isn't quite right 786 | for(int i = numLineSprites - 1; i >= 0; i--) 787 | { 788 | const int spriteId = lineSprites[i]; 789 | const int spriteY = oam[spriteId * 4] - 16; 790 | const int spriteX = oam[spriteId * 4 + 1] - 8; 791 | int tileId = oam[spriteId * 4 + 2]; 792 | const int attrs = oam[spriteId * 4 + 3]; 793 | 794 | // tall sprites 795 | if(spriteHeight != 8) 796 | tileId &= 0xFE; 797 | 798 | const uint16_t *spritePal; 799 | if(isColour) 800 | spritePal = objPalette + (attrs & 0x7) * 4; 801 | else // OBP0 or 1 802 | spritePal = objPalette + ((attrs & Sprite_Palette) ? 4 : 0); 803 | 804 | // TODO: priority 805 | 806 | int ty = y - spriteY; 807 | 808 | if(attrs & Sprite_YFlip) 809 | ty = (spriteHeight - 1) - ty; 810 | 811 | auto tileAddr = tileId * tileDataSize; 812 | 813 | if(attrs & Sprite_Bank) 814 | tileAddr += 0x2000; 815 | 816 | // get the two tile data bytes for this line 817 | uint16_t d = reinterpret_cast(spriteDataPtr + tileAddr)[ty]; 818 | 819 | if(attrs & Sprite_XFlip) 820 | d = reverseBitsPerByte(d); 821 | 822 | int x = std::max(0, -spriteX); 823 | int end = std::min(8, screenWidth - spriteX); 824 | 825 | d <<= x; 826 | 827 | auto out = scanLine + (x + spriteX); 828 | auto bgIn = bgRaw + x + spriteX; 829 | for(; x < end; x++, out++, bgIn++, d <<= 1) 830 | { 831 | // background has priority 832 | if(((attrs & Sprite_BGPriority) || (*bgIn & 0x80)/*tile has priority flag*/) && (*bgIn & 0x7F)) 833 | continue; 834 | 835 | int palIndex = ((d & 0x80) >> 7) | ((d & 0x8000) >> 14); 836 | 837 | if(!palIndex) 838 | continue; 839 | 840 | *out = spritePal[palIndex]; 841 | } 842 | } 843 | } 844 | 845 | void DMGDisplay::updateCompare(bool newVal) 846 | { 847 | if((mem.readIOReg(IO_STAT) & STAT_CoincidenceInt) && !compareMatch && newVal) 848 | { 849 | if(!statInterruptActive) 850 | cpu.flagInterrupt(Int_LCDStat); 851 | 852 | statInterruptActive |= STAT_CoincidenceInt; 853 | } 854 | else if(!newVal) 855 | statInterruptActive &= ~STAT_CoincidenceInt; 856 | 857 | compareMatch = newVal; 858 | } 859 | -------------------------------------------------------------------------------- /core/AGBAPU.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "AGBAPU.h" 5 | 6 | #include "AGBCPU.h" 7 | #include "AGBMemory.h" 8 | #include "AGBRegs.h" 9 | 10 | AGBAPU::AGBAPU(AGBCPU &cpu) : cpu(cpu) 11 | {} 12 | 13 | void AGBAPU::reset() 14 | { 15 | enabled = true; 16 | 17 | lastUpdateCycle = 0; 18 | 19 | frameSeqClock = 0; 20 | channelEnabled = 1; 21 | 22 | ch4FreqTimerPeriod = 8; 23 | 24 | //... incomplete 25 | 26 | sampleClock = 0; 27 | readOff = 0, writeOff = 64; 28 | 29 | // init wave RAM 30 | /*for(int i = 0x30; i < 0x40;) 31 | { 32 | cpu.getMem().writeIOReg(i++, 0x00); 33 | cpu.getMem().writeIOReg(i++, 0xFF); 34 | }*/ 35 | } 36 | 37 | void AGBAPU::update() 38 | { 39 | auto curCycle = cpu.getCycleCount(); 40 | if(lastUpdateCycle == curCycle) 41 | return; 42 | 43 | auto passed = curCycle - lastUpdateCycle; 44 | 45 | auto oldCycle = lastUpdateCycle >> 2; 46 | 47 | passed >>= 2; 48 | lastUpdateCycle += passed << 2; 49 | 50 | // output clock 51 | const int clocksPerSample = 4194304 / 32768; 52 | 53 | while(passed) 54 | { 55 | // clamp update step (min of next sample and next seq update) 56 | uint32_t nextSample = clocksPerSample - sampleClock; 57 | uint32_t nextFrameSeqUpdate = 8192u - (oldCycle & 0x1FFF); 58 | auto step = std::min(nextSample, std::min(nextFrameSeqUpdate, passed)); 59 | 60 | updateFreq(step); 61 | 62 | // update frame sequencer clock 63 | if((oldCycle & 0x1FFF) + step >= 8192) 64 | updateFrameSequencer(); 65 | 66 | // output 67 | sampleClock += step; 68 | 69 | if(sampleClock >= clocksPerSample) 70 | { 71 | sampleClock -= clocksPerSample; 72 | sampleOutput(); 73 | } 74 | 75 | passed -= step; 76 | oldCycle += step; 77 | } 78 | } 79 | 80 | // timer 0 or 1 overflow, may need to update DMA channels 81 | void AGBAPU::timerOverflow(int timer, uint32_t cycle) 82 | { 83 | auto control = cpu.getMem().readIOReg(IO_SOUNDCNT_H); 84 | 85 | if(!(control & 0x3300)) 86 | return; // no DMA channels enabled 87 | 88 | if(timer == 0 && ((control & 0x4400) == 0x4400)) 89 | return; // both channels using timer 1 90 | 91 | if(timer == 1 && !(control & 0x4400)) 92 | return; // both channels using timer 0 93 | 94 | update(); //FIXME: to passed in cycle? 95 | 96 | // enabled and using this timer 97 | if(control & 0x300 && ((control & (1 << 10)) != 0) == (timer == 1)) 98 | { 99 | if(fifoAFilled) 100 | { 101 | dmaAVal = dmaAFIFO[fifoARead]; 102 | fifoARead = (fifoARead + 1) & 0x1F; 103 | fifoAFilled--; 104 | } 105 | 106 | // trigger DMA for more data 107 | if(fifoAFilled <= 16) 108 | cpu.triggerDMA(AGBCPU::Trig_SoundA); 109 | } 110 | 111 | if(control & 0x3000 && ((control & (1 << 14)) != 0) == (timer == 1)) 112 | { 113 | if(fifoBFilled) 114 | { 115 | dmaBVal = dmaBFIFO[fifoBRead]; 116 | fifoBRead = (fifoBRead + 1) & 0x1F; 117 | fifoBFilled--; 118 | } 119 | 120 | if(fifoBFilled <= 16) 121 | cpu.triggerDMA(AGBCPU::Trig_SoundB); 122 | } 123 | } 124 | 125 | int16_t AGBAPU::getSample() 126 | { 127 | auto ret = sampleData[readOff]; 128 | 129 | readOff = (readOff + 1) % bufferSize; 130 | 131 | return ret; 132 | } 133 | 134 | int AGBAPU::getNumSamples() const 135 | { 136 | int avail = writeOff - readOff; 137 | if(avail < 0) 138 | avail += bufferSize; 139 | 140 | return avail; 141 | } 142 | 143 | uint16_t AGBAPU::readReg(uint32_t addr, uint16_t val) 144 | { 145 | update(); 146 | 147 | switch(addr) 148 | { 149 | case IO_SOUND1CNT_L: 150 | return val & 0x7F; // other bits are unused 151 | 152 | case IO_SOUND1CNT_H: 153 | case IO_SOUND2CNT_L: 154 | return val & ~0x3F; // mask out length 155 | 156 | case IO_SOUND1CNT_X: 157 | case IO_SOUND2CNT_H: 158 | case IO_SOUND3CNT_X: 159 | return val & SOUNDxCNT_Length; // only length is readable 160 | 161 | case IO_SOUND3CNT_L: 162 | return val & 0xE0; // other bits unused 163 | 164 | case IO_SOUND3CNT_H: 165 | return val & 0xE000; // mask out length and unused 166 | 167 | case IO_SOUND4CNT_L: 168 | return val & 0xFF00; // mask out length 169 | 170 | case IO_SOUND4CNT_H: 171 | return val & (SOUNDxCNT_Length | 0xFF); // freq bits are readable here too 172 | 173 | case IO_SOUNDCNT_L: 174 | return val & ~0x88; // usused bits above each volume 175 | 176 | case IO_SOUNDCNT_H: 177 | return val & 0x770F; // reset bits are write only, 4 unused bits 178 | 179 | case IO_SOUNDCNT_X: 180 | return (val & SOUNDCNT_X_Enable) | channelEnabled; // enabled channels is read only, enable is only writable bit 181 | 182 | // invalid, unwritable 183 | case 0x66: 184 | case 0x6E: 185 | case 0x76: 186 | case 0x7A: 187 | case 0x7E: 188 | case 0x86: 189 | case 0x8A: 190 | return 0; 191 | 192 | // TODO: wave RAM 193 | } 194 | 195 | 196 | return val; 197 | } 198 | 199 | bool AGBAPU::writeReg(uint32_t addr, uint16_t data, uint16_t mask) 200 | { 201 | const uint8_t dutyPatterns[]{0b01000000, 0b11000000, 0b11110000, 0b00111111}; 202 | 203 | if(addr < IO_SOUND1CNT_L || addr > IO_FIFO_B + 2) 204 | return false; 205 | 206 | update(); 207 | 208 | auto &mem = cpu.getMem(); 209 | 210 | // ignore the write (?) 211 | if(!enabled && addr >= IO_SOUND1CNT_L && addr < IO_SOUNDCNT_X) 212 | return true; 213 | 214 | switch(addr) 215 | { 216 | case IO_SOUND1CNT_L: // sweep 217 | 218 | // switching negate off after an update with it on kills the channel 219 | if(!(data & SOUND1CNT_L_Negate) && (mem.readIOReg(IO_SOUND1CNT_L) & SOUND1CNT_L_Negate) && ch1SweepCalcWithNeg) 220 | channelEnabled &= ~1; 221 | break; 222 | 223 | case IO_SOUND1CNT_H: // length/duty/env 224 | ch1DutyPattern = dutyPatterns[(data >> 6) & 3]; 225 | ch1Len = 64 - (data & 0x3F); 226 | 227 | // disable if DAC off 228 | if((data & 0xF800) == 0) 229 | channelEnabled &= ~(1 << 0); 230 | 231 | if(!enabled) 232 | return true; // don't store it if disabled 233 | break; 234 | 235 | case IO_SOUND1CNT_X: // freq/trigger/counter 236 | { 237 | auto freq = data & 0x7FF; 238 | ch1FreqTimerPeriod = (2048 - freq) * 4; 239 | 240 | // enabling counter can cause an extra clock 241 | if((data & SOUNDxCNT_Length) && !(mem.readIOReg(IO_SOUND1CNT_X) & SOUNDxCNT_Length) && !(frameSeqClock & 1)) 242 | { 243 | if(ch1Len && --ch1Len == 0) 244 | channelEnabled &= ~(1 << 0); // done, disable 245 | } 246 | 247 | if(data & mask & SOUNDxCNT_Trigger) 248 | { 249 | // init sweep 250 | auto sweepReg = mem.readIOReg(IO_SOUND1CNT_L); 251 | auto sweepShift = sweepReg & SOUND1CNT_L_Shift; 252 | ch1SweepTimer = (sweepReg & SOUND1CNT_L_Period) >> 4; 253 | ch1SweepFreq = freq; 254 | ch1SweepEnable = ch1SweepTimer || sweepShift; 255 | 256 | if(ch1SweepTimer == 0) 257 | ch1SweepTimer = 8; 258 | 259 | // reload envelope 260 | ch1EnvVolume = mem.readIOReg(IO_SOUND1CNT_H) >> 12; 261 | ch1EnvTimer = (mem.readIOReg(IO_SOUND1CNT_H) >> 8) & 0x7; 262 | 263 | ch1FreqTimer = ch1FreqTimerPeriod + 8; // delay 264 | 265 | // slightly smaller delay on restart 266 | if(channelEnabled & (1 << 0)) 267 | ch1FreqTimer -= 4; 268 | 269 | if(ch1Len == 0) 270 | { 271 | // triggering resets length to max 272 | // but also clocks if the counter is enabled 273 | if((data & SOUNDxCNT_Length) && !(frameSeqClock & 1)) 274 | ch1Len = 63; 275 | else 276 | ch1Len = 64; 277 | } 278 | 279 | if(mem.readIOReg(IO_SOUND1CNT_H) & 0xF800) 280 | channelEnabled |= (1 << 0); 281 | 282 | ch1SweepCalcWithNeg = false; 283 | 284 | // update sweep now if shift is set 285 | if(sweepShift) 286 | { 287 | int newFreq = ch1SweepFreq >> sweepShift; 288 | 289 | if(sweepReg & SOUND1CNT_L_Negate) // negate 290 | { 291 | newFreq = ch1SweepFreq - newFreq; 292 | ch1SweepCalcWithNeg = true; 293 | } 294 | else 295 | newFreq = ch1SweepFreq + newFreq; 296 | 297 | if(newFreq >= 2048) 298 | channelEnabled &= ~(1 << 0); 299 | } 300 | } 301 | break; 302 | } 303 | 304 | case IO_SOUND2CNT_L: // length/duty/env 305 | ch2DutyPattern = dutyPatterns[(data >> 6) & 3]; 306 | ch2Len = 64 - (data & 0x3F); 307 | 308 | // disable if DAC off 309 | if((data & 0xF800) == 0) 310 | channelEnabled &= ~(1 << 1); 311 | 312 | if(!enabled) 313 | return true; // don't store it if disabled 314 | break; 315 | 316 | case IO_SOUND2CNT_H: // freq/trigger/counter 317 | { 318 | auto freq = data & 0x7FF; 319 | ch2FreqTimerPeriod = (2048 - freq) * 4; 320 | 321 | // enabling counter can cause an extra clock 322 | if((data & SOUNDxCNT_Length) && !(mem.readIOReg(IO_SOUND2CNT_H) & SOUNDxCNT_Length) && !(frameSeqClock & 1)) 323 | { 324 | if(ch2Len && --ch2Len == 0) 325 | channelEnabled &= ~(1 << 1); // done, disable 326 | } 327 | 328 | if(data & mask & SOUNDxCNT_Trigger) 329 | { 330 | // reload envelope 331 | ch2EnvVolume = mem.readIOReg(IO_SOUND2CNT_L) >> 12; 332 | ch2EnvTimer = (mem.readIOReg(IO_SOUND2CNT_L) >> 8) & 0x7; 333 | 334 | ch2FreqTimer = ch2FreqTimerPeriod + 8; // delay 335 | 336 | // slightly smaller delay on restart 337 | if(channelEnabled & (1 << 1)) 338 | ch2FreqTimer -= 4; 339 | 340 | if(ch2Len == 0) 341 | { 342 | // triggering resets length to max 343 | // but also clocks if the counter is enabled 344 | if((data & SOUNDxCNT_Length) && !(frameSeqClock & 1)) 345 | ch2Len = 63; 346 | else 347 | ch2Len = 64; 348 | } 349 | 350 | if(mem.readIOReg(IO_SOUND2CNT_L) & 0xF800) 351 | channelEnabled |= (1 << 1); 352 | } 353 | break; 354 | } 355 | 356 | case IO_SOUND3CNT_L: // enable 357 | // disable if DAC off 358 | if(!(data & 0x80)) 359 | channelEnabled &= ~(1 << 2); 360 | 361 | if(!(channelEnabled & (1 << 2))) 362 | ch3BankIndex = (data >> 6) & 1; 363 | break; 364 | 365 | case IO_SOUND3CNT_H: // length 366 | ch3Len = 256 - (data & 0xFF); 367 | break; 368 | 369 | case IO_SOUND3CNT_X: // freq/trigger/counter 370 | { 371 | auto freq = data & 0x7FF; 372 | ch3FreqTimerPeriod = (2048 - freq) * 2; 373 | 374 | // enabling counter can cause an extra clock 375 | if((data & SOUNDxCNT_Length) && !(mem.readIOReg(IO_SOUND3CNT_X) & SOUNDxCNT_Length) && !(frameSeqClock & 1)) 376 | { 377 | if(ch3Len && --ch3Len == 0) 378 | channelEnabled &= ~(1 << 2); // done, disable 379 | } 380 | 381 | if(data & mask & SOUNDxCNT_Trigger) 382 | { 383 | ch3SampleIndex = 0; 384 | ch3BankIndex = (mem.readIOReg(IO_SOUND3CNT_L) >> 6) & 1; 385 | ch3FreqTimer = ch3FreqTimerPeriod + 6; // there is a small delay 386 | 387 | if(ch3Len == 0) 388 | { 389 | // triggering resets length to max 390 | // but also clocks if the counter is enabled 391 | if((data & SOUNDxCNT_Length) && !(frameSeqClock & 1)) 392 | ch3Len = 255; 393 | else 394 | ch3Len = 256; 395 | } 396 | 397 | if(mem.readIOReg(IO_SOUND3CNT_L) & 0x80) 398 | channelEnabled |= (1 << 2); 399 | } 400 | break; 401 | } 402 | 403 | case IO_SOUND4CNT_L: // length/envelope/volume 404 | ch4Len = 64 - (data & 0x3F); 405 | 406 | // disable if DAC off 407 | if((data & 0xF800) == 0) 408 | channelEnabled &= ~(1 << 3); 409 | break; 410 | 411 | case IO_SOUND4CNT_H: // clock/width/trigger/counter 412 | { 413 | const int divisors[]{8, 16, 32, 48, 64, 80, 96, 112}; 414 | auto shift = (data >> 4) & 0xF; 415 | auto divCode = data & 0x7; 416 | ch4FreqTimerPeriod = divisors[divCode] << shift; 417 | ch4Narrow = data & (1 << 3); 418 | 419 | // enabling counter can cause an extra clock 420 | if((data & SOUNDxCNT_Length) && !(mem.readIOReg(IO_SOUND4CNT_H) & SOUNDxCNT_Length) && !(frameSeqClock & 1)) 421 | { 422 | if(ch4Len && --ch4Len == 0) 423 | channelEnabled &= ~(1 << 3); // done, disable 424 | } 425 | 426 | if(data & mask & SOUNDxCNT_Trigger) 427 | { 428 | // reload envelope 429 | ch4EnvVolume = mem.readIOReg(IO_SOUND4CNT_L) >> 12; 430 | ch4EnvTimer = (mem.readIOReg(IO_SOUND4CNT_L) >> 8) & 0x7; 431 | 432 | ch4FreqTimer = ch4FreqTimerPeriod; 433 | 434 | ch4LFSRBits = 0x7FFF; 435 | 436 | if(ch4Len == 0) 437 | { 438 | // triggering resets length to max 439 | // but also clocks if the counter is enabled 440 | if((data & SOUNDxCNT_Length) && !(frameSeqClock & 1)) 441 | ch4Len = 63; 442 | else 443 | ch4Len = 64; 444 | } 445 | 446 | if(mem.readIOReg(IO_SOUND4CNT_L) & 0xF800) 447 | channelEnabled |= (1 << 3); 448 | } 449 | break; 450 | } 451 | 452 | // ordering should work out for these... 453 | case IO_FIFO_A: 454 | case IO_FIFO_A + 2: 455 | if(fifoAFilled < 31) 456 | { 457 | dmaAFIFO[fifoAWrite++] = data & 0xFF; 458 | fifoAWrite &= 0x1F; 459 | dmaAFIFO[fifoAWrite++] = data >> 8; 460 | fifoAWrite &= 0x1F; 461 | fifoAFilled += 2; 462 | } 463 | break; 464 | 465 | case IO_FIFO_B: 466 | case IO_FIFO_B + 2: 467 | if(fifoBFilled < 31) 468 | { 469 | dmaBFIFO[fifoBWrite++] = data & 0xFF; 470 | fifoBWrite &= 0x1F; 471 | dmaBFIFO[fifoBWrite++] = data >> 8; 472 | fifoBWrite &= 0x1F; 473 | fifoBFilled += 2; 474 | } 475 | break; 476 | 477 | case IO_SOUNDCNT_H: 478 | 479 | // FIFO reset 480 | if(data & (1 << 11)) 481 | fifoAFilled = fifoARead = fifoAWrite = 0; 482 | if(data & (1 << 15)) 483 | fifoBFilled = fifoBRead = fifoBWrite = 0; 484 | break; 485 | 486 | case IO_SOUNDCNT_X: 487 | // disabling 488 | if(enabled && !(data & SOUNDCNT_X_Enable)) 489 | { 490 | // disabled 491 | for(int i = IO_SOUND1CNT_L; i < IO_SOUNDCNT_X; i++) 492 | { 493 | writeReg(i, 0, 0xFFFF); 494 | mem.writeIOReg(i, 0); 495 | } 496 | 497 | channelEnabled = 0; 498 | frameSeqClock = 0; 499 | 500 | ch1DutyStep = ch2DutyStep = 0; 501 | ch1Val = ch2Val = false; 502 | ch3SampleIndex = 0; 503 | ch3Sample = 0; 504 | ch4Val = 0; 505 | } 506 | else if(!enabled && (data & SOUNDCNT_X_Enable)) 507 | { 508 | frameSeqClock = 7; // make the next frame be 0 509 | } 510 | 511 | enabled = data & SOUNDCNT_X_Enable; 512 | break; 513 | 514 | // wave RAM 515 | case 0x90: 516 | case 0x92: 517 | case 0x94: 518 | case 0x96: 519 | case 0x98: 520 | case 0x9A: 521 | case 0x9C: 522 | case 0x9E: 523 | { 524 | // the bank that isn't currently in use 525 | int bank = 1 - ch3BankIndex; 526 | int off64 = bank * 2 + ((addr & 0xF) / 8); 527 | int byte = addr & 7; 528 | 529 | // swap everything around 530 | uint16_t swapped = data << 8 | data >> 8; 531 | 532 | int shift = (6 - byte) * 8; 533 | ch3WaveBuf[off64] = (ch3WaveBuf[off64] & ~(0xFFFFull << shift)) | (uint64_t(swapped) << shift); 534 | break; 535 | } 536 | } 537 | 538 | return false; 539 | } 540 | 541 | void AGBAPU::updateFrameSequencer() 542 | { 543 | auto &mem = cpu.getMem(); 544 | 545 | frameSeqClock = (frameSeqClock + 1) & 7; 546 | 547 | if((frameSeqClock & 1) == 0) 548 | { 549 | // length 550 | 551 | const auto ch1FreqHi = mem.readIOReg(IO_SOUND1CNT_X); 552 | const auto ch2FreqHi = mem.readIOReg(IO_SOUND2CNT_H); 553 | const auto ch3FreqHi = mem.readIOReg(IO_SOUND3CNT_X); 554 | const auto ch4FreqHi = mem.readIOReg(IO_SOUND4CNT_H); 555 | 556 | if((ch1FreqHi & SOUNDxCNT_Length) && ch1Len) 557 | { 558 | if(--ch1Len == 0) 559 | channelEnabled &= ~(1 << 0); // done, disable 560 | } 561 | 562 | if((ch2FreqHi & SOUNDxCNT_Length) && ch2Len) 563 | { 564 | if(--ch2Len == 0) 565 | channelEnabled &= ~(1 << 1); // done, disable 566 | } 567 | 568 | if((ch3FreqHi & SOUNDxCNT_Length) && ch3Len) 569 | { 570 | if(--ch3Len == 0) 571 | channelEnabled &= ~(1 << 2); // done, disable 572 | } 573 | 574 | if((ch4FreqHi & SOUNDxCNT_Length) && ch4Len) 575 | { 576 | if(--ch4Len == 0) 577 | channelEnabled &= ~(1 << 3); // done, disable 578 | } 579 | } 580 | 581 | if(frameSeqClock == 7) 582 | { 583 | // envelope 584 | const auto ch1EnvVol = mem.readIOReg(IO_SOUND1CNT_H); 585 | const auto ch2EnvVol = mem.readIOReg(IO_SOUND2CNT_L); 586 | const auto ch4EnvVol = mem.readIOReg(IO_SOUND4CNT_L); 587 | 588 | ch1EnvTimer--; 589 | 590 | if(ch1EnvTimer == 0 && (ch1EnvVol & 0x700)) 591 | { 592 | if(ch1EnvVol & (1 << 11) && ch1EnvVolume < 15) 593 | ch1EnvVolume++; 594 | else if(ch1EnvVolume > 0) 595 | ch1EnvVolume--; 596 | 597 | ch1EnvTimer = (ch1EnvVol >> 8) & 0x7; 598 | } 599 | 600 | ch2EnvTimer--; 601 | 602 | if(ch2EnvTimer == 0 && (ch2EnvVol & 0x700)) 603 | { 604 | if(ch2EnvVol & (1 << 11) && ch2EnvVolume < 15) 605 | ch2EnvVolume++; 606 | else if(ch2EnvVolume > 0) 607 | ch2EnvVolume--; 608 | 609 | ch2EnvTimer = (ch2EnvVol >> 8) & 0x7; 610 | } 611 | 612 | ch4EnvTimer--; 613 | 614 | if(ch4EnvTimer == 0 && (ch4EnvVol & 0x700)) 615 | { 616 | if(ch4EnvVol & (1 << 11) && ch4EnvVolume < 15) 617 | ch4EnvVolume++; 618 | else if(ch4EnvVolume > 0) 619 | ch4EnvVolume--; 620 | 621 | ch4EnvTimer = (ch4EnvVol >> 8) & 0x7; 622 | } 623 | } 624 | 625 | if((frameSeqClock & 0x3) == 2) 626 | { 627 | // sweep 628 | ch1SweepTimer--; 629 | 630 | if(ch1SweepEnable && ch1SweepTimer == 0) 631 | { 632 | const auto ch1Sweep = mem.readIOReg(IO_SOUND1CNT_L); 633 | auto sweepPeriod = (ch1Sweep & SOUND1CNT_L_Period) >> 4; 634 | if(sweepPeriod) 635 | { 636 | auto shift = ch1Sweep & SOUND1CNT_L_Shift; 637 | int newFreq = ch1SweepFreq >> shift; 638 | 639 | if(ch1Sweep & SOUND1CNT_L_Negate) // negate 640 | newFreq = ch1SweepFreq - newFreq; 641 | else 642 | newFreq = ch1SweepFreq + newFreq; 643 | 644 | if(shift && newFreq < 2048) 645 | { 646 | ch1SweepFreq = newFreq; 647 | mem.writeIOReg(IO_SOUND1CNT_X, (mem.readIOReg(IO_SOUND1CNT_X) & 0xF100) | newFreq); 648 | ch1FreqTimerPeriod = (2048 - newFreq) * 4; 649 | } 650 | 651 | ch1SweepTimer = (ch1Sweep & SOUND1CNT_L_Period) >> 4; 652 | 653 | // calculate again for overflow check 654 | newFreq = ch1SweepFreq >> shift; 655 | 656 | if(ch1Sweep & SOUND1CNT_L_Negate) // negate 657 | { 658 | newFreq = ch1SweepFreq - newFreq; 659 | ch1SweepCalcWithNeg = true; 660 | } 661 | else 662 | newFreq = ch1SweepFreq + newFreq; 663 | 664 | if(newFreq >= 2048) 665 | channelEnabled &= ~(1 << 0); 666 | } 667 | else // period is 0, reset to 8 668 | ch1SweepTimer = 8; 669 | } 670 | } 671 | } 672 | 673 | void AGBAPU::updateFreq(int cyclesPassed) 674 | { 675 | // channel 1 676 | if(channelEnabled & (1 << 0) && ch1EnvVolume) 677 | { 678 | int timer = ch1FreqTimer; 679 | timer -= cyclesPassed; 680 | while(timer <= 0) 681 | { 682 | timer += ch1FreqTimerPeriod; 683 | ch1Val = ch1DutyPattern & (1 << ch1DutyStep); 684 | ch1DutyStep++; 685 | ch1DutyStep &= 7; 686 | } 687 | ch1FreqTimer = timer; 688 | } 689 | 690 | // channel 2 691 | if(channelEnabled & (1 << 1) && ch2EnvVolume) 692 | { 693 | int timer = ch2FreqTimer; 694 | timer -= cyclesPassed; 695 | while(timer <= 0) 696 | { 697 | timer += ch2FreqTimerPeriod; 698 | ch2Val = ch2DutyPattern & (1 << ch2DutyStep); 699 | ch2DutyStep++; 700 | ch2DutyStep &= 7; 701 | } 702 | ch2FreqTimer = timer; 703 | } 704 | 705 | // channel 3 706 | if(channelEnabled & (1 << 2)) 707 | { 708 | int timer = ch3FreqTimer; 709 | timer -= cyclesPassed; 710 | while(timer <= 0) 711 | { 712 | timer += ch3FreqTimerPeriod; 713 | 714 | ch3SampleIndex++; 715 | 716 | if(ch3SampleIndex == 32) 717 | { 718 | // two bank mode - switch bank 719 | if(cpu.getMem().readIOReg(IO_SOUND3CNT_L) & (1 << 5)) 720 | ch3BankIndex ^= 1; 721 | ch3SampleIndex = 0; 722 | } 723 | 724 | int bank = ch3BankIndex; 725 | 726 | // rotate the sample 727 | auto tmp = ch3WaveBuf[bank * 2] >> 60; 728 | ch3WaveBuf[bank * 2 + 0] = ch3WaveBuf[bank * 2 + 0] << 4 | ch3WaveBuf[bank * 2 + 1] >> 60; 729 | ch3WaveBuf[bank * 2 + 1] = ch3WaveBuf[bank * 2 + 1] << 4 | tmp; 730 | 731 | ch3Sample = ch3WaveBuf[bank * 2] >> 60; 732 | } 733 | 734 | ch3FreqTimer = timer; 735 | } 736 | 737 | // channel 4 738 | if(channelEnabled & (1 << 3) && ch4EnvVolume) 739 | { 740 | int timer = ch4FreqTimer; 741 | timer -= cyclesPassed; 742 | while(timer <= 0) 743 | { 744 | timer += ch4FreqTimerPeriod; 745 | // make noise 746 | int bit = ((ch4LFSRBits >> 1) ^ ch4LFSRBits) & 1; 747 | ch4LFSRBits >>= 1; 748 | ch4LFSRBits |= bit << 14; // bit 15 749 | 750 | if(ch4Narrow) 751 | ch4LFSRBits = (ch4LFSRBits & ~(1 << 6)) | (bit << 6); // also set bit 7 752 | 753 | ch4Val = !(ch4LFSRBits & 1); 754 | } 755 | ch4FreqTimer = timer; 756 | } 757 | } 758 | 759 | void AGBAPU::sampleOutput() 760 | { 761 | auto &mem = cpu.getMem(); 762 | 763 | // wait for the audio thread/interrupt to catch up 764 | while((writeOff + 1) % bufferSize == readOff) {} 765 | 766 | // TODO: master left/right volume in low byte 767 | auto outputSelect = mem.readIOReg(IO_SOUNDCNT_L) >> 8; 768 | auto dmaControl = mem.readIOReg(IO_SOUNDCNT_H); 769 | 770 | auto vol = ch1EnvVolume; 771 | auto ch1Val = (channelEnabled & 1) && this->ch1Val ? vol : -vol; 772 | 773 | vol = ch2EnvVolume; 774 | auto ch2Val = (channelEnabled & 2) && this->ch2Val ? vol : -vol; 775 | 776 | vol = (mem.readIOReg(IO_SOUND3CNT_H) >> 13) & 0x3; 777 | auto ch3Val = (channelEnabled & 4) && vol ? ((ch3Sample ^ 0xF) * 2) - 0xF : 0; 778 | 779 | // 75% vol 780 | if(mem.readIOReg(IO_SOUND3CNT_H) & 0x8000) 781 | ch3Val = (ch3Val * 3) / 4; 782 | else if(vol) 783 | ch3Val >>= vol - 1; 784 | 785 | vol = ch4EnvVolume; 786 | auto ch4Val = (channelEnabled & 8) && this->ch4Val ? vol : -vol; 787 | 788 | int left = 0, right = 0; 789 | 790 | int scale = 2 << (dmaControl & 3); // 2, 4, 8 (25, 50 and 100%) 791 | 792 | if(outputSelect & 0x01) 793 | right += ch1Val * scale; 794 | if(outputSelect & 0x10) 795 | left += ch1Val * scale; 796 | 797 | if(outputSelect & 0x02) 798 | right += ch2Val * scale; 799 | if(outputSelect & 0x20) 800 | left += ch2Val * scale; 801 | 802 | if(outputSelect & 0x04) 803 | right += ch3Val * scale; 804 | if(outputSelect & 0x40) 805 | left += ch3Val * scale; 806 | 807 | if(outputSelect & 0x08) 808 | right += ch4Val * scale; 809 | if(outputSelect & 0x80) 810 | left += ch4Val * scale; 811 | 812 | // shiny new DMA channels 813 | int dmaAVal = this->dmaAVal * 4; 814 | int dmaBVal = this->dmaBVal * 4; 815 | 816 | // 50% vol 817 | if(!(dmaControl & (1 << 2))) 818 | dmaAVal /= 2; 819 | if(!(dmaControl & (1 << 3))) 820 | dmaBVal /= 2; 821 | 822 | if(dmaControl & (1 << 8)) 823 | right += dmaAVal; 824 | if(dmaControl & (1 << 9)) 825 | left += dmaAVal; 826 | 827 | if(dmaControl & (1 << 12)) 828 | right += dmaBVal; 829 | if(dmaControl & (1 << 13)) 830 | left += dmaBVal; 831 | 832 | auto bias = mem.readIOReg(IO_SOUNDBIAS) & 0x3FE; 833 | 834 | // bias to unsigned and clamp to 10-bit 835 | left = std::min(0x3FF, std::max(0, left + bias)); 836 | right = std::min(0x3FF, std::max(0, right + bias)); 837 | 838 | // ... and go back to mono signed 16-bit for output... 839 | sampleData[writeOff] = ((left - 0x200) + (right - 0x200)) * 16; 840 | writeOff = (writeOff + 1) % bufferSize; 841 | } --------------------------------------------------------------------------------