├── .astylerc ├── .editorconfig ├── .github └── workflows │ ├── peanut-sdl.yml │ └── tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── examples ├── benchmark │ ├── CMakeLists.txt │ ├── Makefile │ └── peanut-benchmark.c ├── debug-nuklear │ ├── .gitignore │ ├── CMakeLists.txt │ ├── CPM.cmake │ ├── Makefile │ ├── inc │ │ ├── nuklear.h │ │ ├── nuklear_proj.h │ │ └── nuklear_sdl_renderer.h │ └── src │ │ ├── main.c │ │ ├── nuklear.c │ │ └── overview.c ├── debug │ ├── Makefile │ ├── peanut-debug-simple.c │ └── peanut-debug.c ├── mini_fb │ ├── Makefile │ ├── MiniFB.h │ ├── README.md │ ├── X11MiniFB.c │ └── peanut_minifb.c └── sdl2 │ ├── CMakeLists.txt │ ├── CPM.cmake │ ├── Makefile │ ├── gamecontrollerdb.txt │ ├── meta │ ├── cleffa.svg │ ├── icon.ico │ └── winres.rc │ ├── minigb_apu │ ├── LICENSE │ ├── minigb_apu.c │ └── minigb_apu.h │ └── peanut_sdl.c ├── peanut_gb.h ├── screencaps ├── DRAGONBALL_BBZP.png ├── MEGAMANV.png ├── PKMN_BLUE.gif ├── README.md ├── SHANTAE.png └── ZELDA.gif ├── test ├── .gitignore ├── Makefile ├── cpu_instrs.h ├── dmg-acid2.gb.h ├── instr_timing.h ├── minctest.h ├── test.c └── test_external_rom.c └── version.all /.astylerc: -------------------------------------------------------------------------------- 1 | --style=allman 2 | --indent=force-tab=8 3 | --attach-closing-while 4 | --indent-preproc-block 5 | --indent-preproc-define 6 | --indent-col1-comments 7 | --break-blocks 8 | --pad-oper 9 | --unpad-paren 10 | --align-reference=name 11 | --remove-braces 12 | --max-code-length=80 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Set tab indentation 13 | [*.{c,h}] 14 | indent_style = tab 15 | indent_size = 8 16 | max_line_length = 80 17 | -------------------------------------------------------------------------------- /.github/workflows/peanut-sdl.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches-ignore: 9 | - '**pages**' 10 | 11 | env: 12 | CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache 13 | 14 | jobs: 15 | build: 16 | name: ${{ matrix.os }}-${{ matrix.build_type }} 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | build_type: RelWithDebInfo 24 | generator: "Ninja" 25 | nice-arch: "x86_64" 26 | nice-name: "Ubuntu-Linux" 27 | 28 | - os: macos-latest 29 | build_type: RelWithDebInfo 30 | generator: "Ninja" 31 | nice-arch: "arm64" 32 | nice-name: "macOS" 33 | 34 | - os: windows-2022 35 | build_type: RelWithDebInfo 36 | generator: "Visual Studio 17 2022" 37 | arch: x64 38 | additional-cmake-args: -T v143 -A x64 39 | nice-arch: "x86_64" 40 | nice-name: "Windows7" 41 | 42 | - os: windows-2022 43 | build_type: RelWithDebInfo 44 | generator: "Visual Studio 17 2022" 45 | arch: x86 46 | additional-cmake-args: -T v142 -A Win32 47 | nice-arch: "i386" 48 | nice-name: "Windows7" 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | with: 53 | fetch-depth: 0 54 | 55 | - name: Initialise MSVC environment 56 | if: "contains(matrix.os, 'windows')" 57 | uses: ilammy/msvc-dev-cmd@v1 58 | with: 59 | arch: ${{ matrix.arch }} 60 | 61 | - uses: lukka/get-cmake@latest 62 | 63 | - run: mkdir ${{ env.CPM_SOURCE_CACHE }} 64 | 65 | - name: Cache multiple paths 66 | uses: actions/cache@v4 67 | with: 68 | path: | 69 | ${{ env.CPM_SOURCE_CACHE }} 70 | ${{ github.workspace }}/build/_deps 71 | ${{ github.workspace }}/build/**/cmake_pch.* 72 | key: ${{ matrix.os }}-${{ hashFiles('examples/sdl2/CMakeLists.txt','.github/workflows/peanut-sdl.yml') }} 73 | 74 | - name: Build 75 | run: | 76 | cmake -S ./examples/sdl2/ -B ${{github.workspace}}/build -G "${{matrix.generator}}" -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DLIBRARY_DISCOVER_METHOD=CPM ${{matrix.additional-cmake-args}} 77 | cmake --build build --config ${{matrix.build_type}} 78 | 79 | - name: Tar output on Unix systems 80 | if: "contains(matrix.os, 'windows') == false" 81 | run: | 82 | cd ${{github.workspace}}/build 83 | tar cf peanut-sdl.tar peanut-sdl* 84 | 85 | - name: Get Short SHA 86 | id: vars 87 | shell: bash 88 | run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 89 | 90 | - name: Upload output 91 | uses: actions/upload-artifact@v4 92 | with: 93 | name: ${{ matrix.nice-name }}-${{ matrix.nice-arch }}-${{ matrix.build_type }}-${{ steps.vars.outputs.sha_short }} 94 | path: | 95 | ${{ github.workspace }}/build/peanut-sdl.tar 96 | ${{ github.workspace }}/build/**/peanut-sdl.exe 97 | ${{ github.workspace }}/build/**/peanut-sdl.pdb 98 | 99 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches-ignore: 8 | - '**pages**' 9 | 10 | jobs: 11 | run-tests: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Build tests 16 | run: make -C test 17 | - name: Run tests 18 | id: run_tests 19 | run: | 20 | set +e 21 | ./test/test > test_output.txt 2>&1 22 | echo "exit_code=$?" >> "$GITHUB_OUTPUT" 23 | echo 'output<> "$GITHUB_OUTPUT" 24 | cat test_output.txt >> "$GITHUB_OUTPUT" 25 | echo 'EOF' >> "$GITHUB_OUTPUT" 26 | - name: Comment on failure 27 | if: github.event_name == 'pull_request' && steps.run_tests.outputs.exit_code != '0' 28 | uses: actions/github-script@v7 29 | with: 30 | github-token: ${{ github.token }} 31 | script: | 32 | const body = `Tests failed:\n\`\`\`\n${{ steps.run_tests.outputs.output }}\n\`\`\``; 33 | github.rest.issues.createComment({ 34 | issue_number: context.issue.number, 35 | owner: context.repo.owner, 36 | repo: context.repo.repo, 37 | body 38 | }); 39 | core.setFailed('Tests failed'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build items 2 | *.o 3 | *.a 4 | *.exe 5 | peanut-sdl* 6 | peanut-debug 7 | peanut-minifb 8 | tags 9 | build*/ 10 | bin*/ 11 | # Profile items 12 | *.html 13 | *.css 14 | *.gch 15 | *.gcda 16 | *.gcno 17 | # Run items 18 | *.sav 19 | *.bmp 20 | *.gb 21 | *.gbc 22 | *.bin 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please contribute. Before you make a pull request though, please consider the 4 | following: 5 | 6 | - Commit messages must be clear. No "minor fix" or "oopsie woopsie uwu". 7 | - Code style must be consistent. Use `astyle` with the .astylerc file provided 8 | within this repo to format your changes. 9 | - Each commit should fix only one thing. Editing a README should all be in one 10 | commit, but adding CGB and fixing Tetris should be in separate commits. 11 | 12 | If you aren't sure, make the pull request and I'll happily review the changes 13 | made. :smiley: 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peanut-GB 2 | 3 | Peanut-GB is a single file header Game Boy emulator library based off of [this 4 | gameboy emulator](https://github.com/gregtour/gameboy). The aim is to make a 5 | high speed and portable Game Boy (DMG) emulator library that may be used for any 6 | platform that has a C99 compiler. 7 | 8 | This emulator is *very fast*. So much so that it can run at 9 | [full speed on the Raspberry Pi Pico](https://github.com/deltabeard/RP2040-GB)! 10 | 11 | Only the original Game Boy (DMG) is supported at this time, but preliminary work 12 | has been completed to support Game Boy Color 13 | (see https://github.com/deltabeard/Peanut-GB/issues/50). 14 | 15 | This emulator is a work in progress and can be inaccurate (although it does pass 16 | Blargg's CPU instructions and instruction timing tests). As such, some games may 17 | run incorrectly or not run at all. Please seek an alternative emulator if 18 | accuracy is important. 19 | 20 | ## Features 21 | 22 | - Game Boy (DMG) Support 23 | - Very fast; fast enough to run on a RP2040 ARM Cortex M0+ microcontroller at 24 | full speed. 25 | - MBC1, MBC2, MBC3, and MBC5 support 26 | - Real Time Clock (RTC) support 27 | - Serial connection support 28 | - Can be used with or without a bootrom 29 | - Allows different palettes on background and sprites 30 | - Frame skip and interlacing modes (useful for slow LCDs) 31 | - Simple to use and comes with examples 32 | - LCD and sound can be disabled at compile time. 33 | - If sound is enabled, an external audio processing unit (APU) library is 34 | required. 35 | A fast audio processing unit (APU) library is included in this repository at 36 | https://github.com/deltabeard/Peanut-GB/tree/master/examples/sdl2/minigb_apu . 37 | 38 | ## Caveats 39 | 40 | - The LCD rendering is performed line by line, so certain animations will not 41 | render properly (such as in Prehistorik Man). 42 | - Some games may not be playable due to emulation inaccuracy 43 | (see https://github.com/deltabeard/Peanut-GB/issues/31). 44 | - MiniGB APU runs in a separate thread, and so the timing is not accurate. If 45 | accurate APU timing and emulation is required, then Blargg's Gb_Snd_Emu 46 | library (or an alternative) can be used instead. 47 | 48 | ## SDL2 Example 49 | 50 | The flagship example implementation is given in peanut_sdl.c, which uses SDL2 to 51 | draw the screen and take input. Run `cmake` or `make` in the ./examples/sdl2/ 52 | folder to compile it. 53 | 54 | Run `peanut-sdl`, which creates a *drop-zone* window that you can drag and drop 55 | a ROM file into. Alternatively, run in a terminal using `peanut-sdl game.gb`, 56 | which will automatically create the save file `game.sav` for the game if one 57 | isn't found. Or, run with `peanut-sdl game.gb save.sav` to specify a save file. 58 | 59 | ### Screenshot 60 | 61 | ![Pokemon Blue - Main screen animation](/screencaps/PKMN_BLUE.gif) 62 | ![Legend of Zelda: Links Awakening - animation](/screencaps/ZELDA.gif) 63 | ![Megaman V](/screencaps/MEGAMANV.png) 64 | 65 | ![Shantae](/screencaps/SHANTAE.png) 66 | ![Dragon Ball Z](/screencaps/DRAGONBALL_BBZP.png) 67 | 68 | Note: Animated GIFs shown here are limited to 50fps, whilst the emulation was 69 | running at the native ~60fps. This is because popular GIF decoders limit the 70 | maximum FPS to 50. 71 | 72 | ### Controls 73 | 74 | | Action | Keyboard | Joypad | 75 | |-------------------|------------|--------| 76 | | A | z | A | 77 | | B | x | B | 78 | | Start | Return | START | 79 | | Select | Backspace | BACK | 80 | | D-Pad | Arrow Keys | DPAD | 81 | | Repeat A | a | | 82 | | Repeat B | s | | 83 | | Normal Speed | 1 | | 84 | | Turbo x2 (Hold) | Space | | 85 | | Turbo X2 (Toggle) | 2 | | 86 | | Turbo X3 (Toggle) | 3 | | 87 | | Turbo X4 (Toggle) | 4 | | 88 | | Reset | r | | 89 | | Change Palette | p | | 90 | | Reset Palette | Shift + p | | 91 | | Fullscreen | F11 / f | | 92 | | Frameskip (Toggle)| o | | 93 | | Interlace (Toggle)| i | | 94 | | Dump BMP (Toggle) | b | | 95 | 96 | Frameskip and Interlaced modes are both off by default. The Frameskip toggles 97 | between 60 FPS and 30 FPS. 98 | 99 | Pressing 'b' will dump each frame as a 24-bit bitmap file in the current 100 | folder. See /screencaps/README.md for more information. 101 | 102 | ## Projects Using Peanut-GB 103 | 104 | In no particular order, and a non-exhaustive list, the following projects use Peanut-GB. 105 | 106 | * [Pico-GB](https://github.com/YouMakeTech/Pico-GB) - Game Boy emulation on the Raspberry Pi RP2040 microcontroller. 107 | * [Peanut_gb-RGFW](https://github.com/ColleagueRiley/Peanut_gb-RGFW) - A Gameboy emulator example for [RGFW](https://github.com/ColleagueRiley/RGFW). 108 | * [CPBoy](https://github.com/diddyholz/CPBoy) - A Game Boy emulator for the Classpad II (fx-CP400). 109 | * [PlayGB](https://github.com/risolvipro/PlayGB) - A Game Boy emulator for Playdate. 110 | * [AcolyteHandPICd32](https://github.com/stevenchadburrow/AcolyteHandPICd32) - Game Boy emulation on the PIC32MZ2048EFH144 32-bit 150MHz microcontroller. 111 | 112 | ## Getting Started 113 | 114 | Documentation of function prototypes can be found at the bottom of [peanut_gb.h](peanut_gb.h#L3960). 115 | 116 | ### Required Functions 117 | 118 | The front-end implementation must provide a number of functions to the library. 119 | These functions are set when calling gb_init. 120 | 121 | - gb_rom_read 122 | - gb_cart_ram_read 123 | - gb_cart_ram_write 124 | - gb_error 125 | 126 | ### Optional Functions 127 | 128 | The following optional functions may be defined for further functionality. 129 | 130 | #### lcd_draw_line 131 | 132 | This function is required for LCD drawing. Set this function using gb_init_lcd 133 | and enable LCD functionality within Peanut-GB by defining ENABLE_LCD to 1 before 134 | including peanut_gb.h. ENABLE_LCD is set to 1 by default if it was not 135 | previously defined. If gb_init_lcd is not called or lcd_draw_line is set to 136 | NULL, then LCD drawing is disabled. 137 | 138 | The pixel data sent to lcd_draw_line comes with both shade and layer data. The 139 | first two least significant bits are the shade data (black, dark, light, white). 140 | Bits 4 and 5 are layer data (OBJ0, OBJ1, BG), which can be used to add more 141 | colours to the game in the same way that the Game Boy Color does to older Game 142 | Boy games. 143 | 144 | #### audio_read and audio_write 145 | 146 | These functions are required for audio emulation and output. Peanut-GB does not 147 | include audio emulation, so an external library must be used. These functions 148 | must be defined and audio output must be enabled by defining ENABLE_SOUND to 1 149 | before including peanut_gb.h. 150 | 151 | #### gb_serial_tx and gb_serial_rx 152 | 153 | These functions are required for serial communication. Set these functions using 154 | gb_init_serial. If these functions are not set, then the emulation will act as 155 | though no link cable is connected. 156 | 157 | ### Useful Functions 158 | 159 | These functions are provided by Peanut-GB. 160 | 161 | #### gb_reset 162 | 163 | This function resets the game being played, as though the console had been 164 | powered off and on. gb_reset is called by gb_init to initialise the CPU 165 | registers. 166 | 167 | #### gb_get_save_size 168 | 169 | This function returns the save size of the game being played. This function 170 | returns 0 if the game does not use any save data. 171 | 172 | #### gb_run_frame 173 | 174 | This function runs the CPU until a full frame is rendered to the LCD. 175 | 176 | #### gb_colour_hash 177 | 178 | This function calculates a hash of the game title. This hash is calculated in 179 | the same way as the Game Boy Color to add colour to Game Boy games. 180 | 181 | #### gb_get_rom_name 182 | 183 | This function returns the name of the game. 184 | 185 | #### gb_set_rtc 186 | 187 | Set the time of the real time clock (RTC). Some games use this RTC data. 188 | 189 | #### gb_tick_rtc 190 | 191 | Deprecated: do not use. The RTC is ticked internally. 192 | 193 | #### gb_set_bootrom 194 | 195 | Execute a bootrom image on reset. A reset must be performed after calling 196 | gb_set_bootrom for these changes to take effect. This is because gb_init calls 197 | gb_reset, but gb_set_bootrom must be called after gb_init. 198 | The bootrom must be either a DMG or a MGB bootrom. 199 | 200 | ## License 201 | 202 | This project is licensed under the MIT License. 203 | -------------------------------------------------------------------------------- /examples/benchmark/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # vim: ts=4:sw=4:expandtab 2 | CMAKE_MINIMUM_REQUIRED(VERSION 3.20...3.23) 3 | 4 | ## Check user set options. 5 | IF(NOT CMAKE_BUILD_TYPE) 6 | MESSAGE(STATUS "CMAKE_BUILD_TYPE was not set by user; setting build type to Debug") 7 | SET(CMAKE_BUILD_TYPE "Debug") 8 | ELSE() 9 | # List of valid build types 10 | SET(VALID_BUILD_TYPES Debug Release RelWithDebInfo MinSizeRel) 11 | LIST(FIND VALID_BUILD_TYPES ${CMAKE_BUILD_TYPE} IS_VALID_BUILD_TYPE) 12 | IF(IS_VALID_BUILD_TYPE EQUAL -1) 13 | MESSAGE(FATAL_ERROR "CMAKE_BUILD_TYPE was '${CMAKE_BUILD_TYPE}' but can only be set to one of ${VALID_BUILD_TYPES}") 14 | ENDIF() 15 | ENDIF() 16 | 17 | # Obtain version 18 | INCLUDE(../../version.all) 19 | 20 | # Initialise project information. 21 | PROJECT(peanut-benchmark 22 | LANGUAGES C 23 | VERSION 24 | ${PEANUTGB_VERSION_MAJOR}.${PEANUTGB_VERSION_MINOR}.${PEANUTGB_VERSION_PATCH} 25 | DESCRIPTION "Peanut-GB benchmark application" 26 | HOMEPAGE_URL "https://github.com/deltabeard/peanut-gb") 27 | 28 | # Add dependencies to project. 29 | IF(NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug" AND APPLE) 30 | SET(EXE_TARGET_TYPE MACOSX_BUNDLE) 31 | MESSAGE(VERBOSE "Setting EXE type to MACOSX Bundle") 32 | ELSEIF(NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug" AND MSVC) 33 | SET(EXE_TARGET_TYPE WIN32) 34 | MESSAGE(VERBOSE "Setting EXE type to WIN32") 35 | ENDIF() 36 | 37 | ADD_EXECUTABLE(peanut-benchmark ${EXE_TARGET_TYPE}) 38 | TARGET_SOURCES(peanut-benchmark PRIVATE peanut-benchmark.c 39 | ../../peanut_gb.h 40 | ) 41 | TARGET_INCLUDE_DIRECTORIES(peanut-benchmark PRIVATE ../../) 42 | 43 | ADD_EXECUTABLE(peanut-benchmark-sep ${EXE_TARGET_TYPE}) 44 | ADD_LIBRARY(peanut-gb OBJECT peanut_gb.c) 45 | TARGET_COMPILE_DEFINITIONS(peanut-gb PRIVATE ENABLE_SOUND=0 ENABLE_LCD=1 46 | PEANUT_GB_12_COLOUR=1 PEANUT_GB_HEADER_ONLY=1) 47 | TARGET_COMPILE_DEFINITIONS(peanut-benchmark-sep PRIVATE ENABLE_SOUND=0 ENABLE_LCD=1 48 | PEANUT_GB_12_COLOUR=1) 49 | TARGET_SOURCES(peanut-benchmark-sep PRIVATE peanut-benchmark.c) 50 | TARGET_LINK_LIBRARIES(peanut-benchmark-sep peanut-gb) 51 | 52 | MESSAGE(STATUS " CC: ${CMAKE_C_COMPILER} '${CMAKE_C_COMPILER_ID}' on '${CMAKE_SYSTEM_NAME}'") 53 | MESSAGE(STATUS " CFLAGS: ${CMAKE_C_FLAGS}") 54 | MESSAGE(STATUS " LDFLAGS: ${CMAKE_EXE_LINKER_FLAGS}") 55 | -------------------------------------------------------------------------------- /examples/benchmark/Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | CC := cc 3 | OPT := -g2 -O2 4 | CFLAGS = $(OPT) -std=c99 -Wall -Wextra -DPEANUT_GB_12_COLOUR=1 5 | CP := cp 6 | 7 | peanut-benchmark-sep.o: override CFLAGS += -DPEANUT_GB_HEADER_ONLY 8 | 9 | override CFLAGS += -DENABLE_SOUND=0 -DENABLE_LCD=1 10 | 11 | all: peanut-benchmark peanut-benchmark-sep 12 | peanut-benchmark: peanut-benchmark.c ../../peanut_gb.h 13 | $(CC) $(CFLAGS) $(LDFLAGS) -o$@ $< $(LDLIBS) 14 | 15 | # Separate objects linked to a single executable. 16 | peanut-benchmark-sep: peanut-benchmark-sep.o peanut_gb.o 17 | $(CC) $(CFLAGS) $(LDFLAGS) -o$@ $^ $(LDLIBS) 18 | 19 | peanut-benchmark-sep.o: peanut-benchmark.c 20 | $(CC) -c $(CFLAGS) -o$@ $< 21 | 22 | peanut_gb.o: ../../peanut_gb.h 23 | $(CP) ../../peanut_gb.h peanut_gb.c 24 | $(CC) -c $(CFLAGS) -o$@ peanut_gb.c 25 | 26 | peanut-benchmark.S: peanut-benchmark.c ../../peanut_gb.h 27 | $(CC) -S $(CFLAGS) $(LDFLAGS) -o$@ $< $(LDLIBS) 28 | 29 | clean: 30 | $(RM) peanut-benchmark$(EXT) 31 | -------------------------------------------------------------------------------- /examples/benchmark/peanut-benchmark.c: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2018-2023 Mahyar Koshkouei 4 | * 5 | * Performs a benchmark of Peanut-GB with a specified ROM. 6 | * Plays the ROM five times and prints the FPS for each play. 7 | */ 8 | #ifndef ENABLE_LCD 9 | # define ENABLE_LCD 1 10 | #endif 11 | 12 | /* Sound is disabled for this project. */ 13 | #ifndef ENABLE_LCD 14 | # define ENABLE_SOUND 0 15 | #endif 16 | 17 | /* Import emulator library. */ 18 | #include "../../peanut_gb.h" 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | struct priv_t 27 | { 28 | /* Pointer to allocated memory holding GB file. */ 29 | uint8_t *rom; 30 | /* Pointer to allocated memory holding save file. */ 31 | uint8_t *cart_ram; 32 | 33 | /* Frame buffer */ 34 | uint16_t fb[LCD_HEIGHT][LCD_WIDTH]; 35 | }; 36 | 37 | /** 38 | * Returns a byte from the ROM file at the given address. 39 | */ 40 | static uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) 41 | { 42 | const struct priv_t * const p = gb->direct.priv; 43 | return p->rom[addr]; 44 | } 45 | 46 | /** 47 | * Returns a byte from the cartridge RAM at the given address. 48 | */ 49 | static uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) 50 | { 51 | const struct priv_t * const p = gb->direct.priv; 52 | return p->cart_ram[addr]; 53 | } 54 | 55 | /** 56 | * Writes a given byte to the cartridge RAM at the given address. 57 | */ 58 | static void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, 59 | const uint8_t val) 60 | { 61 | const struct priv_t * const p = gb->direct.priv; 62 | p->cart_ram[addr] = val; 63 | } 64 | 65 | /** 66 | * Returns a pointer to the allocated space containing the ROM. Must be freed. 67 | */ 68 | static uint8_t *read_rom_to_ram(const char *file_name) 69 | { 70 | FILE *rom_file = fopen(file_name, "rb"); 71 | size_t rom_size; 72 | uint8_t *rom = NULL; 73 | 74 | if(rom_file == NULL) 75 | return NULL; 76 | 77 | fseek(rom_file, 0, SEEK_END); 78 | rom_size = ftell(rom_file); 79 | rewind(rom_file); 80 | rom = malloc(rom_size); 81 | 82 | if(fread(rom, sizeof(uint8_t), rom_size, rom_file) != rom_size) 83 | { 84 | free(rom); 85 | fclose(rom_file); 86 | return NULL; 87 | } 88 | 89 | fclose(rom_file); 90 | return rom; 91 | } 92 | 93 | /** 94 | * Ignore all errors. 95 | */ 96 | static void gb_error(struct gb_s *gb, const enum gb_error_e gb_err, const uint16_t addr) 97 | { 98 | const char* gb_err_str[GB_INVALID_MAX] = { 99 | "UNKNOWN", 100 | "INVALID OPCODE", 101 | "INVALID READ", 102 | "INVALID WRITE", 103 | "HALT FOREVER" 104 | }; 105 | struct priv_t *priv = gb->direct.priv; 106 | 107 | fprintf(stderr, "Error %d occurred: %s at %04X\n. Exiting.\n", 108 | gb_err, gb_err_str[gb_err], addr); 109 | 110 | /* Free memory and then exit. */ 111 | free(priv->cart_ram); 112 | free(priv->rom); 113 | exit(EXIT_FAILURE); 114 | } 115 | 116 | #if ENABLE_LCD 117 | /** 118 | * Draws scanline into framebuffer. 119 | */ 120 | static void lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], 121 | const uint_fast8_t line) 122 | { 123 | struct priv_t *priv = gb->direct.priv; 124 | const uint16_t palette[] = { 0x7FFF, 0x5294, 0x294A, 0x0000 }; 125 | 126 | for (unsigned int x = 0; x < LCD_WIDTH; x++) 127 | { 128 | #if PEANUT_GB_16BIT_COLOUR 129 | priv->fb[line][x] = palette[pixels[x] & 3]; 130 | #else 131 | priv->fb[line][x] = palette[pixels[x]]; 132 | #endif 133 | } 134 | } 135 | #endif 136 | 137 | int main(int argc, char **argv) 138 | { 139 | uint_fast32_t frames_per_run = 64 * 1024; 140 | char *rom_file_name = NULL; 141 | 142 | for(int i = 1; i < argc; i++) { 143 | if(strcmp(argv[i], "--frames") == 0) 144 | if (++i == argc) 145 | frames_per_run = 0; 146 | else 147 | frames_per_run = atoi(argv[i]); 148 | else 149 | rom_file_name = argv[i]; 150 | } 151 | 152 | if(!rom_file_name || !frames_per_run) { 153 | fprintf(stderr, "Syntax: %s [--frames ] \n", argv[0]); 154 | exit(EXIT_FAILURE); 155 | } 156 | 157 | for(unsigned int i = 0; i < 5; i++) 158 | { 159 | /* Start benchmark. */ 160 | struct gb_s gb; 161 | struct priv_t priv; 162 | 163 | clock_t start_time; 164 | uint_fast32_t frames = 0; 165 | enum gb_init_error_e ret; 166 | 167 | /* Copy input ROM file to allocated memory. */ 168 | if((priv.rom = read_rom_to_ram(rom_file_name)) == NULL) 169 | { 170 | printf("%d: %s\n", __LINE__, strerror(errno)); 171 | exit(EXIT_FAILURE); 172 | } 173 | 174 | /* Initialise context. */ 175 | ret = gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, 176 | &gb_cart_ram_write, &gb_error, &priv); 177 | 178 | if(ret != GB_INIT_NO_ERROR) 179 | { 180 | fprintf(stderr, "Peanut-GB failed to initialise: %d\n", 181 | ret); 182 | exit(EXIT_FAILURE); 183 | } 184 | 185 | printf("Run %u: ", i); 186 | priv.cart_ram = malloc(gb_get_save_size(&gb)); 187 | 188 | #if ENABLE_LCD 189 | gb_init_lcd(&gb, &lcd_draw_line); 190 | // gb.direct.interlace = true; 191 | #endif 192 | 193 | start_time = clock(); 194 | 195 | do 196 | { 197 | /* Execute CPU cycles until the screen has to be 198 | * redrawn. */ 199 | gb_run_frame(&gb); 200 | } 201 | while(++frames < frames_per_run); 202 | 203 | { 204 | double duration = 205 | (double)(clock() - start_time) / CLOCKS_PER_SEC; 206 | double fps = frames / duration; 207 | printf("%f FPS, dur: %f\n", fps, duration); 208 | } 209 | 210 | free(priv.cart_ram); 211 | free(priv.rom); 212 | } 213 | 214 | return EXIT_SUCCESS; 215 | } 216 | -------------------------------------------------------------------------------- /examples/debug-nuklear/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build directory 2 | build-* 3 | 4 | -------------------------------------------------------------------------------- /examples/debug-nuklear/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | CMAKE_MINIMUM_REQUIRED(VERSION 3.16...3.24 FATAL_ERROR) 2 | 3 | PROJECT(peanutgb-debugger) 4 | ADD_EXECUTABLE(peanutgb-debugger src/main.c src/nuklear.c src/overview.c 5 | ../sdl2/minigb_apu/minigb_apu.c 6 | ../../peanut_gb.h) 7 | TARGET_INCLUDE_DIRECTORIES(peanutgb-debugger PRIVATE inc) 8 | 9 | set_property(TARGET peanutgb-debugger PROPERTY VS_DPI_AWARE "PerMonitor") 10 | 11 | #sdl2 12 | INCLUDE(CPM.cmake) 13 | CPMADDPACKAGE(GITHUB_REPOSITORY libsdl-org/SDL 14 | NAME SDL2 15 | GIT_TAG release-2.26.1 16 | OPTIONS 17 | "SDL_SHARED_ENABLED_BY_DEFAULT OFF" 18 | "SDL_STATIC_ENABLED_BY_DEFAULT ON") 19 | ADD_COMPILE_DEFINITIONS(SDL_MAIN_HANDLED SDL_LEAN_AND_MEAN MINIGB_APU_AUDIO_FORMAT_S16SYS) 20 | TARGET_LINK_LIBRARIES(peanutgb-debugger PRIVATE SDL2-static) 21 | -------------------------------------------------------------------------------- /examples/debug-nuklear/CPM.cmake: -------------------------------------------------------------------------------- 1 | # CPM.cmake - CMake's missing package manager 2 | # =========================================== 3 | # See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. 4 | # 5 | # MIT License 6 | # ----------- 7 | #[[ 8 | Copyright (c) 2019-2022 Lars Melchior and contributors 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | ]] 28 | 29 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 30 | 31 | set(CURRENT_CPM_VERSION 0.36.0) 32 | 33 | get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) 34 | if(CPM_DIRECTORY) 35 | if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) 36 | if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) 37 | message( 38 | AUTHOR_WARNING 39 | "${CPM_INDENT} \ 40 | A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ 41 | It is recommended to upgrade CPM to the most recent version. \ 42 | See https://github.com/cpm-cmake/CPM.cmake for more information." 43 | ) 44 | endif() 45 | if(${CMAKE_VERSION} VERSION_LESS "3.17.0") 46 | include(FetchContent) 47 | endif() 48 | return() 49 | endif() 50 | 51 | get_property( 52 | CPM_INITIALIZED GLOBAL "" 53 | PROPERTY CPM_INITIALIZED 54 | SET 55 | ) 56 | if(CPM_INITIALIZED) 57 | return() 58 | endif() 59 | endif() 60 | 61 | if(CURRENT_CPM_VERSION MATCHES "development-version") 62 | message(WARNING "Your project is using an unstable development version of CPM.cmake. \ 63 | Please update to a recent release if possible. \ 64 | See https://github.com/cpm-cmake/CPM.cmake for details." 65 | ) 66 | endif() 67 | 68 | set_property(GLOBAL PROPERTY CPM_INITIALIZED true) 69 | 70 | macro(cpm_set_policies) 71 | # the policy allows us to change options without caching 72 | cmake_policy(SET CMP0077 NEW) 73 | set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) 74 | 75 | # the policy allows us to change set(CACHE) without caching 76 | if(POLICY CMP0126) 77 | cmake_policy(SET CMP0126 NEW) 78 | set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) 79 | endif() 80 | 81 | # The policy uses the download time for timestamp, instead of the timestamp in the archive. This 82 | # allows for proper rebuilds when a projects url changes 83 | if(POLICY CMP0135) 84 | cmake_policy(SET CMP0135 NEW) 85 | set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) 86 | endif() 87 | endmacro() 88 | cpm_set_policies() 89 | 90 | option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" 91 | $ENV{CPM_USE_LOCAL_PACKAGES} 92 | ) 93 | option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" 94 | $ENV{CPM_LOCAL_PACKAGES_ONLY} 95 | ) 96 | option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) 97 | option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" 98 | $ENV{CPM_DONT_UPDATE_MODULE_PATH} 99 | ) 100 | option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" 101 | $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} 102 | ) 103 | option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK 104 | "Add all packages added through CPM.cmake to the package lock" 105 | $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} 106 | ) 107 | option(CPM_USE_NAMED_CACHE_DIRECTORIES 108 | "Use additional directory of package name in cache on the most nested level." 109 | $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} 110 | ) 111 | 112 | set(CPM_VERSION 113 | ${CURRENT_CPM_VERSION} 114 | CACHE INTERNAL "" 115 | ) 116 | set(CPM_DIRECTORY 117 | ${CPM_CURRENT_DIRECTORY} 118 | CACHE INTERNAL "" 119 | ) 120 | set(CPM_FILE 121 | ${CMAKE_CURRENT_LIST_FILE} 122 | CACHE INTERNAL "" 123 | ) 124 | set(CPM_PACKAGES 125 | "" 126 | CACHE INTERNAL "" 127 | ) 128 | set(CPM_DRY_RUN 129 | OFF 130 | CACHE INTERNAL "Don't download or configure dependencies (for testing)" 131 | ) 132 | 133 | if(DEFINED ENV{CPM_SOURCE_CACHE}) 134 | set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) 135 | else() 136 | set(CPM_SOURCE_CACHE_DEFAULT OFF) 137 | endif() 138 | 139 | set(CPM_SOURCE_CACHE 140 | ${CPM_SOURCE_CACHE_DEFAULT} 141 | CACHE PATH "Directory to download CPM dependencies" 142 | ) 143 | 144 | if(NOT CPM_DONT_UPDATE_MODULE_PATH) 145 | set(CPM_MODULE_PATH 146 | "${CMAKE_BINARY_DIR}/CPM_modules" 147 | CACHE INTERNAL "" 148 | ) 149 | # remove old modules 150 | file(REMOVE_RECURSE ${CPM_MODULE_PATH}) 151 | file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) 152 | # locally added CPM modules should override global packages 153 | set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") 154 | endif() 155 | 156 | if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) 157 | set(CPM_PACKAGE_LOCK_FILE 158 | "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" 159 | CACHE INTERNAL "" 160 | ) 161 | file(WRITE ${CPM_PACKAGE_LOCK_FILE} 162 | "# CPM Package Lock\n# This file should be committed to version control\n\n" 163 | ) 164 | endif() 165 | 166 | include(FetchContent) 167 | 168 | # Try to infer package name from git repository uri (path or url) 169 | function(cpm_package_name_from_git_uri URI RESULT) 170 | if("${URI}" MATCHES "([^/:]+)/?.git/?$") 171 | set(${RESULT} 172 | ${CMAKE_MATCH_1} 173 | PARENT_SCOPE 174 | ) 175 | else() 176 | unset(${RESULT} PARENT_SCOPE) 177 | endif() 178 | endfunction() 179 | 180 | # Try to infer package name and version from a url 181 | function(cpm_package_name_and_ver_from_url url outName outVer) 182 | if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") 183 | # We matched an archive 184 | set(filename "${CMAKE_MATCH_1}") 185 | 186 | if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") 187 | # We matched - (ie foo-1.2.3) 188 | set(${outName} 189 | "${CMAKE_MATCH_1}" 190 | PARENT_SCOPE 191 | ) 192 | set(${outVer} 193 | "${CMAKE_MATCH_2}" 194 | PARENT_SCOPE 195 | ) 196 | elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") 197 | # We couldn't find a name, but we found a version 198 | # 199 | # In many cases (which we don't handle here) the url would look something like 200 | # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly 201 | # distinguish the package name from the irrelevant bits. Moreover if we try to match the 202 | # package name from the filename, we'd get bogus at best. 203 | unset(${outName} PARENT_SCOPE) 204 | set(${outVer} 205 | "${CMAKE_MATCH_1}" 206 | PARENT_SCOPE 207 | ) 208 | else() 209 | # Boldly assume that the file name is the package name. 210 | # 211 | # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but 212 | # such cases should be quite rare. No popular service does this... we think. 213 | set(${outName} 214 | "${filename}" 215 | PARENT_SCOPE 216 | ) 217 | unset(${outVer} PARENT_SCOPE) 218 | endif() 219 | else() 220 | # No ideas yet what to do with non-archives 221 | unset(${outName} PARENT_SCOPE) 222 | unset(${outVer} PARENT_SCOPE) 223 | endif() 224 | endfunction() 225 | 226 | # Initialize logging prefix 227 | if(NOT CPM_INDENT) 228 | set(CPM_INDENT 229 | "CPM:" 230 | CACHE INTERNAL "" 231 | ) 232 | endif() 233 | 234 | function(cpm_find_package NAME VERSION) 235 | string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") 236 | find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) 237 | if(${CPM_ARGS_NAME}_FOUND) 238 | if(DEFINED ${CPM_ARGS_NAME}_VERSION) 239 | set(VERSION ${${CPM_ARGS_NAME}_VERSION}) 240 | endif() 241 | message(STATUS "${CPM_INDENT} using local package ${CPM_ARGS_NAME}@${VERSION}") 242 | CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") 243 | set(CPM_PACKAGE_FOUND 244 | YES 245 | PARENT_SCOPE 246 | ) 247 | else() 248 | set(CPM_PACKAGE_FOUND 249 | NO 250 | PARENT_SCOPE 251 | ) 252 | endif() 253 | endfunction() 254 | 255 | # Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from 256 | # finding the system library 257 | function(cpm_create_module_file Name) 258 | if(NOT CPM_DONT_UPDATE_MODULE_PATH) 259 | # erase any previous modules 260 | file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake 261 | "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" 262 | ) 263 | endif() 264 | endfunction() 265 | 266 | # Find a package locally or fallback to CPMAddPackage 267 | function(CPMFindPackage) 268 | set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) 269 | 270 | cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) 271 | 272 | if(NOT DEFINED CPM_ARGS_VERSION) 273 | if(DEFINED CPM_ARGS_GIT_TAG) 274 | cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) 275 | endif() 276 | endif() 277 | 278 | set(downloadPackage ${CPM_DOWNLOAD_ALL}) 279 | if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) 280 | set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) 281 | elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) 282 | set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) 283 | endif() 284 | if(downloadPackage) 285 | CPMAddPackage(${ARGN}) 286 | cpm_export_variables(${CPM_ARGS_NAME}) 287 | return() 288 | endif() 289 | 290 | cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") 291 | if(CPM_PACKAGE_ALREADY_ADDED) 292 | cpm_export_variables(${CPM_ARGS_NAME}) 293 | return() 294 | endif() 295 | 296 | cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) 297 | 298 | if(NOT CPM_PACKAGE_FOUND) 299 | CPMAddPackage(${ARGN}) 300 | cpm_export_variables(${CPM_ARGS_NAME}) 301 | endif() 302 | 303 | endfunction() 304 | 305 | # checks if a package has been added before 306 | function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) 307 | if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) 308 | CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) 309 | if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") 310 | message( 311 | WARNING 312 | "${CPM_INDENT} requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." 313 | ) 314 | endif() 315 | cpm_get_fetch_properties(${CPM_ARGS_NAME}) 316 | set(${CPM_ARGS_NAME}_ADDED NO) 317 | set(CPM_PACKAGE_ALREADY_ADDED 318 | YES 319 | PARENT_SCOPE 320 | ) 321 | cpm_export_variables(${CPM_ARGS_NAME}) 322 | else() 323 | set(CPM_PACKAGE_ALREADY_ADDED 324 | NO 325 | PARENT_SCOPE 326 | ) 327 | endif() 328 | endfunction() 329 | 330 | # Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of 331 | # arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted 332 | # to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 333 | function(cpm_parse_add_package_single_arg arg outArgs) 334 | # Look for a scheme 335 | if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") 336 | string(TOLOWER "${CMAKE_MATCH_1}" scheme) 337 | set(uri "${CMAKE_MATCH_2}") 338 | 339 | # Check for CPM-specific schemes 340 | if(scheme STREQUAL "gh") 341 | set(out "GITHUB_REPOSITORY;${uri}") 342 | set(packageType "git") 343 | elseif(scheme STREQUAL "gl") 344 | set(out "GITLAB_REPOSITORY;${uri}") 345 | set(packageType "git") 346 | elseif(scheme STREQUAL "bb") 347 | set(out "BITBUCKET_REPOSITORY;${uri}") 348 | set(packageType "git") 349 | # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine 350 | # type 351 | elseif(arg MATCHES ".git/?(@|#|$)") 352 | set(out "GIT_REPOSITORY;${arg}") 353 | set(packageType "git") 354 | else() 355 | # Fall back to a URL 356 | set(out "URL;${arg}") 357 | set(packageType "archive") 358 | 359 | # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. 360 | # We just won't bother with the additional complexity it will induce in this function. SVN is 361 | # done by multi-arg 362 | endif() 363 | else() 364 | if(arg MATCHES ".git/?(@|#|$)") 365 | set(out "GIT_REPOSITORY;${arg}") 366 | set(packageType "git") 367 | else() 368 | # Give up 369 | message(FATAL_ERROR "CPM: Can't determine package type of '${arg}'") 370 | endif() 371 | endif() 372 | 373 | # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs 374 | # containing '@' can be used 375 | string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") 376 | 377 | # Parse the rest according to package type 378 | if(packageType STREQUAL "git") 379 | # For git repos we interpret #... as a tag or branch or commit hash 380 | string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") 381 | elseif(packageType STREQUAL "archive") 382 | # For archives we interpret #... as a URL hash. 383 | string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") 384 | # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url 385 | # should do this at a later point 386 | else() 387 | # We should never get here. This is an assertion and hitting it means there's a bug in the code 388 | # above. A packageType was set, but not handled by this if-else. 389 | message(FATAL_ERROR "CPM: Unsupported package type '${packageType}' of '${arg}'") 390 | endif() 391 | 392 | set(${outArgs} 393 | ${out} 394 | PARENT_SCOPE 395 | ) 396 | endfunction() 397 | 398 | # Check that the working directory for a git repo is clean 399 | function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) 400 | 401 | find_package(Git REQUIRED) 402 | 403 | if(NOT GIT_EXECUTABLE) 404 | # No git executable, assume directory is clean 405 | set(${isClean} 406 | TRUE 407 | PARENT_SCOPE 408 | ) 409 | return() 410 | endif() 411 | 412 | # check for uncommitted changes 413 | execute_process( 414 | COMMAND ${GIT_EXECUTABLE} status --porcelain 415 | RESULT_VARIABLE resultGitStatus 416 | OUTPUT_VARIABLE repoStatus 417 | OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET 418 | WORKING_DIRECTORY ${repoPath} 419 | ) 420 | if(resultGitStatus) 421 | # not supposed to happen, assume clean anyway 422 | message(WARNING "Calling git status on folder ${repoPath} failed") 423 | set(${isClean} 424 | TRUE 425 | PARENT_SCOPE 426 | ) 427 | return() 428 | endif() 429 | 430 | if(NOT "${repoStatus}" STREQUAL "") 431 | set(${isClean} 432 | FALSE 433 | PARENT_SCOPE 434 | ) 435 | return() 436 | endif() 437 | 438 | # check for committed changes 439 | execute_process( 440 | COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} 441 | RESULT_VARIABLE resultGitDiff 442 | OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET 443 | WORKING_DIRECTORY ${repoPath} 444 | ) 445 | 446 | if(${resultGitDiff} EQUAL 0) 447 | set(${isClean} 448 | TRUE 449 | PARENT_SCOPE 450 | ) 451 | else() 452 | set(${isClean} 453 | FALSE 454 | PARENT_SCOPE 455 | ) 456 | endif() 457 | 458 | endfunction() 459 | 460 | # method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload 461 | # FetchContent calls. As these are internal cmake properties, this method should be used carefully 462 | # and may need modification in future CMake versions. Source: 463 | # https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 464 | function(cpm_override_fetchcontent contentName) 465 | cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") 466 | if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") 467 | message(FATAL_ERROR "Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") 468 | endif() 469 | 470 | string(TOLOWER ${contentName} contentNameLower) 471 | set(prefix "_FetchContent_${contentNameLower}") 472 | 473 | set(propertyName "${prefix}_sourceDir") 474 | define_property( 475 | GLOBAL 476 | PROPERTY ${propertyName} 477 | BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" 478 | FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" 479 | ) 480 | set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") 481 | 482 | set(propertyName "${prefix}_binaryDir") 483 | define_property( 484 | GLOBAL 485 | PROPERTY ${propertyName} 486 | BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" 487 | FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" 488 | ) 489 | set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") 490 | 491 | set(propertyName "${prefix}_populated") 492 | define_property( 493 | GLOBAL 494 | PROPERTY ${propertyName} 495 | BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" 496 | FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" 497 | ) 498 | set_property(GLOBAL PROPERTY ${propertyName} TRUE) 499 | endfunction() 500 | 501 | # Download and add a package from source 502 | function(CPMAddPackage) 503 | cpm_set_policies() 504 | 505 | list(LENGTH ARGN argnLength) 506 | if(argnLength EQUAL 1) 507 | cpm_parse_add_package_single_arg("${ARGN}" ARGN) 508 | 509 | # The shorthand syntax implies EXCLUDE_FROM_ALL 510 | set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES") 511 | endif() 512 | 513 | set(oneValueArgs 514 | NAME 515 | FORCE 516 | VERSION 517 | GIT_TAG 518 | DOWNLOAD_ONLY 519 | GITHUB_REPOSITORY 520 | GITLAB_REPOSITORY 521 | BITBUCKET_REPOSITORY 522 | GIT_REPOSITORY 523 | SOURCE_DIR 524 | DOWNLOAD_COMMAND 525 | FIND_PACKAGE_ARGUMENTS 526 | NO_CACHE 527 | GIT_SHALLOW 528 | EXCLUDE_FROM_ALL 529 | SOURCE_SUBDIR 530 | ) 531 | 532 | set(multiValueArgs URL OPTIONS) 533 | 534 | cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") 535 | 536 | # Set default values for arguments 537 | 538 | if(NOT DEFINED CPM_ARGS_VERSION) 539 | if(DEFINED CPM_ARGS_GIT_TAG) 540 | cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) 541 | endif() 542 | endif() 543 | 544 | if(CPM_ARGS_DOWNLOAD_ONLY) 545 | set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) 546 | else() 547 | set(DOWNLOAD_ONLY NO) 548 | endif() 549 | 550 | if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) 551 | set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") 552 | elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) 553 | set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") 554 | elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) 555 | set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") 556 | endif() 557 | 558 | if(DEFINED CPM_ARGS_GIT_REPOSITORY) 559 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) 560 | if(NOT DEFINED CPM_ARGS_GIT_TAG) 561 | set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) 562 | endif() 563 | 564 | # If a name wasn't provided, try to infer it from the git repo 565 | if(NOT DEFINED CPM_ARGS_NAME) 566 | cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) 567 | endif() 568 | endif() 569 | 570 | set(CPM_SKIP_FETCH FALSE) 571 | 572 | if(DEFINED CPM_ARGS_GIT_TAG) 573 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) 574 | # If GIT_SHALLOW is explicitly specified, honor the value. 575 | if(DEFINED CPM_ARGS_GIT_SHALLOW) 576 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) 577 | endif() 578 | endif() 579 | 580 | if(DEFINED CPM_ARGS_URL) 581 | # If a name or version aren't provided, try to infer them from the URL 582 | list(GET CPM_ARGS_URL 0 firstUrl) 583 | cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) 584 | # If we fail to obtain name and version from the first URL, we could try other URLs if any. 585 | # However multiple URLs are expected to be quite rare, so for now we won't bother. 586 | 587 | # If the caller provided their own name and version, they trump the inferred ones. 588 | if(NOT DEFINED CPM_ARGS_NAME) 589 | set(CPM_ARGS_NAME ${nameFromUrl}) 590 | endif() 591 | if(NOT DEFINED CPM_ARGS_VERSION) 592 | set(CPM_ARGS_VERSION ${verFromUrl}) 593 | endif() 594 | 595 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") 596 | endif() 597 | 598 | # Check for required arguments 599 | 600 | if(NOT DEFINED CPM_ARGS_NAME) 601 | message( 602 | FATAL_ERROR 603 | "CPM: 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" 604 | ) 605 | endif() 606 | 607 | # Check if package has been added before 608 | cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") 609 | if(CPM_PACKAGE_ALREADY_ADDED) 610 | cpm_export_variables(${CPM_ARGS_NAME}) 611 | return() 612 | endif() 613 | 614 | # Check for manual overrides 615 | if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") 616 | set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) 617 | set(CPM_${CPM_ARGS_NAME}_SOURCE "") 618 | CPMAddPackage( 619 | NAME "${CPM_ARGS_NAME}" 620 | SOURCE_DIR "${PACKAGE_SOURCE}" 621 | EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" 622 | OPTIONS "${CPM_ARGS_OPTIONS}" 623 | SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" 624 | DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" 625 | FORCE True 626 | ) 627 | cpm_export_variables(${CPM_ARGS_NAME}) 628 | return() 629 | endif() 630 | 631 | # Check for available declaration 632 | if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") 633 | set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) 634 | set(CPM_DECLARATION_${CPM_ARGS_NAME} "") 635 | CPMAddPackage(${declaration}) 636 | cpm_export_variables(${CPM_ARGS_NAME}) 637 | # checking again to ensure version and option compatibility 638 | cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") 639 | return() 640 | endif() 641 | 642 | if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) 643 | cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) 644 | 645 | if(CPM_PACKAGE_FOUND) 646 | cpm_export_variables(${CPM_ARGS_NAME}) 647 | return() 648 | endif() 649 | 650 | if(CPM_LOCAL_PACKAGES_ONLY) 651 | message( 652 | SEND_ERROR 653 | "CPM: ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" 654 | ) 655 | endif() 656 | endif() 657 | 658 | CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") 659 | 660 | if(DEFINED CPM_ARGS_GIT_TAG) 661 | set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") 662 | elseif(DEFINED CPM_ARGS_SOURCE_DIR) 663 | set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") 664 | else() 665 | set(PACKAGE_INFO "${CPM_ARGS_VERSION}") 666 | endif() 667 | 668 | if(DEFINED FETCHCONTENT_BASE_DIR) 669 | # respect user's FETCHCONTENT_BASE_DIR if set 670 | set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) 671 | else() 672 | set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) 673 | endif() 674 | 675 | if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) 676 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) 677 | elseif(DEFINED CPM_ARGS_SOURCE_DIR) 678 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) 679 | if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) 680 | # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work 681 | # for relative paths. 682 | get_filename_component( 683 | source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} 684 | ) 685 | else() 686 | set(source_directory ${CPM_ARGS_SOURCE_DIR}) 687 | endif() 688 | if(NOT EXISTS ${source_directory}) 689 | string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) 690 | # remove timestamps so CMake will re-download the dependency 691 | file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") 692 | endif() 693 | elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) 694 | string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) 695 | set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) 696 | list(SORT origin_parameters) 697 | if(CPM_USE_NAMED_CACHE_DIRECTORIES) 698 | string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") 699 | set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) 700 | else() 701 | string(SHA1 origin_hash "${origin_parameters}") 702 | set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) 703 | endif() 704 | # Expand `download_directory` relative path. This is important because EXISTS doesn't work for 705 | # relative paths. 706 | get_filename_component(download_directory ${download_directory} ABSOLUTE) 707 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) 708 | if(EXISTS ${download_directory}) 709 | cpm_store_fetch_properties( 710 | ${CPM_ARGS_NAME} "${download_directory}" 711 | "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" 712 | ) 713 | cpm_get_fetch_properties("${CPM_ARGS_NAME}") 714 | 715 | if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) 716 | # warn if cache has been changed since checkout 717 | cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) 718 | if(NOT ${IS_CLEAN}) 719 | message(WARNING "Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty") 720 | endif() 721 | endif() 722 | 723 | cpm_add_subdirectory( 724 | "${CPM_ARGS_NAME}" "${DOWNLOAD_ONLY}" 725 | "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" "${${CPM_ARGS_NAME}_BINARY_DIR}" 726 | "${CPM_ARGS_EXCLUDE_FROM_ALL}" "${CPM_ARGS_OPTIONS}" 727 | ) 728 | set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") 729 | 730 | # As the source dir is already cached/populated, we override the call to FetchContent. 731 | set(CPM_SKIP_FETCH TRUE) 732 | cpm_override_fetchcontent( 733 | "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" 734 | BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" 735 | ) 736 | 737 | else() 738 | # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but 739 | # it should guarantee no commit hash get mis-detected. 740 | if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) 741 | cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) 742 | if(NOT ${IS_HASH}) 743 | list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) 744 | endif() 745 | endif() 746 | 747 | # remove timestamps so CMake will re-download the dependency 748 | file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) 749 | set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") 750 | endif() 751 | endif() 752 | 753 | cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") 754 | 755 | if(CPM_PACKAGE_LOCK_ENABLED) 756 | if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) 757 | cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") 758 | elseif(CPM_ARGS_SOURCE_DIR) 759 | cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") 760 | else() 761 | cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") 762 | endif() 763 | endif() 764 | 765 | message( 766 | STATUS "${CPM_INDENT} adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" 767 | ) 768 | 769 | if(NOT CPM_SKIP_FETCH) 770 | cpm_declare_fetch( 771 | "${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}" "${PACKAGE_INFO}" "${CPM_ARGS_UNPARSED_ARGUMENTS}" 772 | ) 773 | cpm_fetch_package("${CPM_ARGS_NAME}" populated) 774 | if(${populated}) 775 | cpm_add_subdirectory( 776 | "${CPM_ARGS_NAME}" "${DOWNLOAD_ONLY}" 777 | "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" "${${CPM_ARGS_NAME}_BINARY_DIR}" 778 | "${CPM_ARGS_EXCLUDE_FROM_ALL}" "${CPM_ARGS_OPTIONS}" 779 | ) 780 | endif() 781 | cpm_get_fetch_properties("${CPM_ARGS_NAME}") 782 | endif() 783 | 784 | set(${CPM_ARGS_NAME}_ADDED YES) 785 | cpm_export_variables("${CPM_ARGS_NAME}") 786 | endfunction() 787 | 788 | # Fetch a previously declared package 789 | macro(CPMGetPackage Name) 790 | if(DEFINED "CPM_DECLARATION_${Name}") 791 | CPMAddPackage(NAME ${Name}) 792 | else() 793 | message(SEND_ERROR "Cannot retrieve package ${Name}: no declaration available") 794 | endif() 795 | endmacro() 796 | 797 | # export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set 798 | macro(cpm_export_variables name) 799 | set(${name}_SOURCE_DIR 800 | "${${name}_SOURCE_DIR}" 801 | PARENT_SCOPE 802 | ) 803 | set(${name}_BINARY_DIR 804 | "${${name}_BINARY_DIR}" 805 | PARENT_SCOPE 806 | ) 807 | set(${name}_ADDED 808 | "${${name}_ADDED}" 809 | PARENT_SCOPE 810 | ) 811 | set(CPM_LAST_PACKAGE_NAME 812 | "${name}" 813 | PARENT_SCOPE 814 | ) 815 | endmacro() 816 | 817 | # declares a package, so that any call to CPMAddPackage for the package name will use these 818 | # arguments instead. Previous declarations will not be overridden. 819 | macro(CPMDeclarePackage Name) 820 | if(NOT DEFINED "CPM_DECLARATION_${Name}") 821 | set("CPM_DECLARATION_${Name}" "${ARGN}") 822 | endif() 823 | endmacro() 824 | 825 | function(cpm_add_to_package_lock Name) 826 | if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) 827 | cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) 828 | file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") 829 | endif() 830 | endfunction() 831 | 832 | function(cpm_add_comment_to_package_lock Name) 833 | if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) 834 | cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) 835 | file(APPEND ${CPM_PACKAGE_LOCK_FILE} 836 | "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" 837 | ) 838 | endif() 839 | endfunction() 840 | 841 | # includes the package lock file if it exists and creates a target `cpm-update-package-lock` to 842 | # update it 843 | macro(CPMUsePackageLock file) 844 | if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) 845 | get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) 846 | if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) 847 | include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) 848 | endif() 849 | if(NOT TARGET cpm-update-package-lock) 850 | add_custom_target( 851 | cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} 852 | ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} 853 | ) 854 | endif() 855 | set(CPM_PACKAGE_LOCK_ENABLED true) 856 | endif() 857 | endmacro() 858 | 859 | # registers a package that has been added to CPM 860 | function(CPMRegisterPackage PACKAGE VERSION) 861 | list(APPEND CPM_PACKAGES ${PACKAGE}) 862 | set(CPM_PACKAGES 863 | ${CPM_PACKAGES} 864 | CACHE INTERNAL "" 865 | ) 866 | set("CPM_PACKAGE_${PACKAGE}_VERSION" 867 | ${VERSION} 868 | CACHE INTERNAL "" 869 | ) 870 | endfunction() 871 | 872 | # retrieve the current version of the package to ${OUTPUT} 873 | function(CPMGetPackageVersion PACKAGE OUTPUT) 874 | set(${OUTPUT} 875 | "${CPM_PACKAGE_${PACKAGE}_VERSION}" 876 | PARENT_SCOPE 877 | ) 878 | endfunction() 879 | 880 | # declares a package in FetchContent_Declare 881 | function(cpm_declare_fetch PACKAGE VERSION INFO) 882 | if(${CPM_DRY_RUN}) 883 | message(STATUS "${CPM_INDENT} package not declared (dry run)") 884 | return() 885 | endif() 886 | 887 | FetchContent_Declare(${PACKAGE} ${ARGN}) 888 | endfunction() 889 | 890 | # returns properties for a package previously defined by cpm_declare_fetch 891 | function(cpm_get_fetch_properties PACKAGE) 892 | if(${CPM_DRY_RUN}) 893 | return() 894 | endif() 895 | 896 | set(${PACKAGE}_SOURCE_DIR 897 | "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" 898 | PARENT_SCOPE 899 | ) 900 | set(${PACKAGE}_BINARY_DIR 901 | "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" 902 | PARENT_SCOPE 903 | ) 904 | endfunction() 905 | 906 | function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) 907 | if(${CPM_DRY_RUN}) 908 | return() 909 | endif() 910 | 911 | set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR 912 | "${source_dir}" 913 | CACHE INTERNAL "" 914 | ) 915 | set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR 916 | "${binary_dir}" 917 | CACHE INTERNAL "" 918 | ) 919 | endfunction() 920 | 921 | # adds a package as a subdirectory if viable, according to provided options 922 | function( 923 | cpm_add_subdirectory 924 | PACKAGE 925 | DOWNLOAD_ONLY 926 | SOURCE_DIR 927 | BINARY_DIR 928 | EXCLUDE 929 | OPTIONS 930 | ) 931 | if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) 932 | if(EXCLUDE) 933 | set(addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) 934 | else() 935 | set(addSubdirectoryExtraArgs "") 936 | endif() 937 | if(OPTIONS) 938 | foreach(OPTION ${OPTIONS}) 939 | cpm_parse_option("${OPTION}") 940 | set(${OPTION_KEY} "${OPTION_VALUE}") 941 | endforeach() 942 | endif() 943 | set(CPM_OLD_INDENT "${CPM_INDENT}") 944 | set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") 945 | add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) 946 | set(CPM_INDENT "${CPM_OLD_INDENT}") 947 | endif() 948 | endfunction() 949 | 950 | # downloads a previously declared package via FetchContent and exports the variables 951 | # `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope 952 | function(cpm_fetch_package PACKAGE populated) 953 | set(${populated} 954 | FALSE 955 | PARENT_SCOPE 956 | ) 957 | if(${CPM_DRY_RUN}) 958 | message(STATUS "${CPM_INDENT} package ${PACKAGE} not fetched (dry run)") 959 | return() 960 | endif() 961 | 962 | FetchContent_GetProperties(${PACKAGE}) 963 | 964 | string(TOLOWER "${PACKAGE}" lower_case_name) 965 | 966 | if(NOT ${lower_case_name}_POPULATED) 967 | FetchContent_Populate(${PACKAGE}) 968 | set(${populated} 969 | TRUE 970 | PARENT_SCOPE 971 | ) 972 | endif() 973 | 974 | cpm_store_fetch_properties( 975 | ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} 976 | ) 977 | 978 | set(${PACKAGE}_SOURCE_DIR 979 | ${${lower_case_name}_SOURCE_DIR} 980 | PARENT_SCOPE 981 | ) 982 | set(${PACKAGE}_BINARY_DIR 983 | ${${lower_case_name}_BINARY_DIR} 984 | PARENT_SCOPE 985 | ) 986 | endfunction() 987 | 988 | # splits a package option 989 | function(cpm_parse_option OPTION) 990 | string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") 991 | string(LENGTH "${OPTION}" OPTION_LENGTH) 992 | string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) 993 | if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) 994 | # no value for key provided, assume user wants to set option to "ON" 995 | set(OPTION_VALUE "ON") 996 | else() 997 | math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") 998 | string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) 999 | endif() 1000 | set(OPTION_KEY 1001 | "${OPTION_KEY}" 1002 | PARENT_SCOPE 1003 | ) 1004 | set(OPTION_VALUE 1005 | "${OPTION_VALUE}" 1006 | PARENT_SCOPE 1007 | ) 1008 | endfunction() 1009 | 1010 | # guesses the package version from a git tag 1011 | function(cpm_get_version_from_git_tag GIT_TAG RESULT) 1012 | string(LENGTH ${GIT_TAG} length) 1013 | if(length EQUAL 40) 1014 | # GIT_TAG is probably a git hash 1015 | set(${RESULT} 1016 | 0 1017 | PARENT_SCOPE 1018 | ) 1019 | else() 1020 | string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) 1021 | set(${RESULT} 1022 | ${CMAKE_MATCH_1} 1023 | PARENT_SCOPE 1024 | ) 1025 | endif() 1026 | endfunction() 1027 | 1028 | # guesses if the git tag is a commit hash or an actual tag or a branch name. 1029 | function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) 1030 | string(LENGTH "${GIT_TAG}" length) 1031 | # full hash has 40 characters, and short hash has at least 7 characters. 1032 | if(length LESS 7 OR length GREATER 40) 1033 | set(${RESULT} 1034 | 0 1035 | PARENT_SCOPE 1036 | ) 1037 | else() 1038 | if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") 1039 | set(${RESULT} 1040 | 1 1041 | PARENT_SCOPE 1042 | ) 1043 | else() 1044 | set(${RESULT} 1045 | 0 1046 | PARENT_SCOPE 1047 | ) 1048 | endif() 1049 | endif() 1050 | endfunction() 1051 | 1052 | function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) 1053 | set(oneValueArgs 1054 | NAME 1055 | FORCE 1056 | VERSION 1057 | GIT_TAG 1058 | DOWNLOAD_ONLY 1059 | GITHUB_REPOSITORY 1060 | GITLAB_REPOSITORY 1061 | GIT_REPOSITORY 1062 | SOURCE_DIR 1063 | DOWNLOAD_COMMAND 1064 | FIND_PACKAGE_ARGUMENTS 1065 | NO_CACHE 1066 | GIT_SHALLOW 1067 | ) 1068 | set(multiValueArgs OPTIONS) 1069 | cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 1070 | 1071 | foreach(oneArgName ${oneValueArgs}) 1072 | if(DEFINED CPM_ARGS_${oneArgName}) 1073 | if(${IS_IN_COMMENT}) 1074 | string(APPEND PRETTY_OUT_VAR "#") 1075 | endif() 1076 | if(${oneArgName} STREQUAL "SOURCE_DIR") 1077 | string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} 1078 | ${CPM_ARGS_${oneArgName}} 1079 | ) 1080 | endif() 1081 | string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") 1082 | endif() 1083 | endforeach() 1084 | foreach(multiArgName ${multiValueArgs}) 1085 | if(DEFINED CPM_ARGS_${multiArgName}) 1086 | if(${IS_IN_COMMENT}) 1087 | string(APPEND PRETTY_OUT_VAR "#") 1088 | endif() 1089 | string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") 1090 | foreach(singleOption ${CPM_ARGS_${multiArgName}}) 1091 | if(${IS_IN_COMMENT}) 1092 | string(APPEND PRETTY_OUT_VAR "#") 1093 | endif() 1094 | string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") 1095 | endforeach() 1096 | endif() 1097 | endforeach() 1098 | 1099 | if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") 1100 | if(${IS_IN_COMMENT}) 1101 | string(APPEND PRETTY_OUT_VAR "#") 1102 | endif() 1103 | string(APPEND PRETTY_OUT_VAR " ") 1104 | foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) 1105 | string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") 1106 | endforeach() 1107 | string(APPEND PRETTY_OUT_VAR "\n") 1108 | endif() 1109 | 1110 | set(${OUT_VAR} 1111 | ${PRETTY_OUT_VAR} 1112 | PARENT_SCOPE 1113 | ) 1114 | 1115 | endfunction() 1116 | -------------------------------------------------------------------------------- /examples/debug-nuklear/Makefile: -------------------------------------------------------------------------------- 1 | # Simple Makefile for building peanutgb-debugger on Unix-like systems. 2 | # SDL2 is required. 3 | 4 | ifneq ($(W64DEVKIT),) 5 | # w64devkit uses pkg-config out of the box. 6 | SDL2_CONFIG := pkg-config sdl2 7 | endif 8 | 9 | SDL2_CONFIG ?= sdl2-config 10 | SDL2_CFLAGS := $(shell $(SDL2_CONFIG) --cflags) 11 | SDL2_LDLIBS := $(shell $(SDL2_CONFIG) --libs) 12 | CFLAGS := -std=c99 -Wall -Wextra -Og -g3 13 | 14 | override CFLAGS += -Iinc $(SDL2_CFLAGS) 15 | override LDLIBS += $(SDL2_LDLIBS) 16 | 17 | all: peanutgb-debugger 18 | peanutgb-debugger: src/main.o src/nuklear.o src/overview.o 19 | $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(LDLIBS) 20 | -------------------------------------------------------------------------------- /examples/debug-nuklear/inc/nuklear_proj.h: -------------------------------------------------------------------------------- 1 | #include 2 | #define NK_ASSERT SDL_assert 3 | #define NK_MEMSET SDL_memset 4 | #define NK_MEMCPY SDL_memcpy 5 | #define NK_SQRT SDL_sqrt 6 | #define NK_SIN SDL_sin 7 | #define NK_COS SDL_cos 8 | #define NK_STRTOD SDL_strtod 9 | #define NK_VSNPRINTF SDL_vsnprintf 10 | #define STBTT_ifloor SDL_floor 11 | #define STBTT_iceil SDL_ceil 12 | #define STBTT_sqrt SDL_sqrt 13 | #define STBTT_pow SDL_pow 14 | #define STBTT_cos SDL_cos 15 | #define STBTT_acos SDL_acos 16 | #define STBTT_fmod SDL_fmod 17 | #define NK_INCLUDE_FIXED_TYPES 18 | #define NK_INCLUDE_STANDARD_IO 19 | #define NK_INCLUDE_STANDARD_VARARGS 20 | #define NK_INCLUDE_DEFAULT_ALLOCATOR 21 | #define NK_INCLUDE_VERTEX_BUFFER_OUTPUT 22 | #define NK_INCLUDE_FONT_BAKING 23 | #define NK_INCLUDE_DEFAULT_FONT 24 | #include "nuklear.h" 25 | 26 | -------------------------------------------------------------------------------- /examples/debug-nuklear/inc/nuklear_sdl_renderer.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Nuklear - 4.9.4 - public domain 3 | */ 4 | /* 5 | * ============================================================== 6 | * 7 | * API 8 | * 9 | * =============================================================== 10 | */ 11 | #ifndef NK_SDL_RENDERER_H_ 12 | #define NK_SDL_RENDERER_H_ 13 | 14 | #ifndef NK_SDL_RENDERER_SDL_H 15 | #define NK_SDL_RENDERER_SDL_H 16 | #endif 17 | #include NK_SDL_RENDERER_SDL_H 18 | NK_API struct nk_context* nk_sdl_init(SDL_Window *win, SDL_Renderer *renderer); 19 | NK_API void nk_sdl_font_stash_begin(struct nk_font_atlas **atlas); 20 | NK_API void nk_sdl_font_stash_end(void); 21 | NK_API int nk_sdl_handle_event(SDL_Event *evt); 22 | NK_API void nk_sdl_render(enum nk_anti_aliasing); 23 | NK_API void nk_sdl_shutdown(void); 24 | NK_API void nk_sdl_handle_grab(void); 25 | 26 | #if SDL_COMPILEDVERSION < SDL_VERSIONNUM(2, 0, 22) 27 | /* Metal API does not support cliprects with negative coordinates or large 28 | * dimensions. The issue is fixed in SDL2 with version 2.0.22 but until 29 | * that version is released, the NK_SDL_CLAMP_CLIP_RECT flag can be used to 30 | * ensure the cliprect is itself clipped to the viewport. 31 | * See discussion at https://discourse.libsdl.org/t/rendergeometryraw-producing-different-results-in-metal-vs-opengl/34953 32 | */ 33 | #define NK_SDL_CLAMP_CLIP_RECT 34 | #endif 35 | 36 | #endif /* NK_SDL_RENDERER_H_ */ 37 | 38 | /* 39 | * ============================================================== 40 | * 41 | * IMPLEMENTATION 42 | * 43 | * =============================================================== 44 | */ 45 | #ifdef NK_SDL_RENDERER_IMPLEMENTATION 46 | #include 47 | #include 48 | 49 | struct nk_sdl_device { 50 | struct nk_buffer cmds; 51 | struct nk_draw_null_texture tex_null; 52 | SDL_Texture *font_tex; 53 | }; 54 | 55 | struct nk_sdl_vertex { 56 | float position[2]; 57 | float uv[2]; 58 | nk_byte col[4]; 59 | }; 60 | 61 | static struct nk_sdl { 62 | SDL_Window *win; 63 | SDL_Renderer *renderer; 64 | struct nk_sdl_device ogl; 65 | struct nk_context ctx; 66 | struct nk_font_atlas atlas; 67 | Uint64 time_of_last_frame; 68 | } sdl; 69 | 70 | NK_INTERN void 71 | nk_sdl_device_upload_atlas(const void *image, int width, int height) 72 | { 73 | struct nk_sdl_device *dev = &sdl.ogl; 74 | 75 | SDL_Texture *g_SDLFontTexture = SDL_CreateTexture(sdl.renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STATIC, width, height); 76 | if (g_SDLFontTexture == NULL) { 77 | SDL_Log("error creating texture"); 78 | return; 79 | } 80 | SDL_UpdateTexture(g_SDLFontTexture, NULL, image, 4 * width); 81 | SDL_SetTextureBlendMode(g_SDLFontTexture, SDL_BLENDMODE_BLEND); 82 | dev->font_tex = g_SDLFontTexture; 83 | } 84 | 85 | NK_API void 86 | nk_sdl_render(enum nk_anti_aliasing AA) 87 | { 88 | /* setup global state */ 89 | struct nk_sdl_device *dev = &sdl.ogl; 90 | 91 | { 92 | SDL_Rect saved_clip; 93 | #ifdef NK_SDL_CLAMP_CLIP_RECT 94 | SDL_Rect viewport; 95 | #endif 96 | SDL_bool clipping_enabled; 97 | int vs = sizeof(struct nk_sdl_vertex); 98 | size_t vp = offsetof(struct nk_sdl_vertex, position); 99 | size_t vt = offsetof(struct nk_sdl_vertex, uv); 100 | size_t vc = offsetof(struct nk_sdl_vertex, col); 101 | 102 | /* convert from command queue into draw list and draw to screen */ 103 | const struct nk_draw_command *cmd; 104 | const nk_draw_index *offset = NULL; 105 | struct nk_buffer vbuf, ebuf; 106 | 107 | /* fill converting configuration */ 108 | struct nk_convert_config config; 109 | static const struct nk_draw_vertex_layout_element vertex_layout[] = { 110 | {NK_VERTEX_POSITION, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_sdl_vertex, position)}, 111 | {NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, NK_OFFSETOF(struct nk_sdl_vertex, uv)}, 112 | {NK_VERTEX_COLOR, NK_FORMAT_R8G8B8A8, NK_OFFSETOF(struct nk_sdl_vertex, col)}, 113 | {NK_VERTEX_LAYOUT_END} 114 | }; 115 | 116 | Uint64 now = SDL_GetTicks64(); 117 | sdl.ctx.delta_time_seconds = (float)(now - sdl.time_of_last_frame) / 1000; 118 | sdl.time_of_last_frame = now; 119 | 120 | NK_MEMSET(&config, 0, sizeof(config)); 121 | config.vertex_layout = vertex_layout; 122 | config.vertex_size = sizeof(struct nk_sdl_vertex); 123 | config.vertex_alignment = NK_ALIGNOF(struct nk_sdl_vertex); 124 | config.tex_null = dev->tex_null; 125 | config.circle_segment_count = 22; 126 | config.curve_segment_count = 22; 127 | config.arc_segment_count = 22; 128 | config.global_alpha = 1.0f; 129 | config.shape_AA = AA; 130 | config.line_AA = AA; 131 | 132 | /* convert shapes into vertexes */ 133 | nk_buffer_init_default(&vbuf); 134 | nk_buffer_init_default(&ebuf); 135 | nk_convert(&sdl.ctx, &dev->cmds, &vbuf, &ebuf, &config); 136 | 137 | /* iterate over and execute each draw command */ 138 | offset = (const nk_draw_index*)nk_buffer_memory_const(&ebuf); 139 | 140 | clipping_enabled = SDL_RenderIsClipEnabled(sdl.renderer); 141 | SDL_RenderGetClipRect(sdl.renderer, &saved_clip); 142 | #ifdef NK_SDL_CLAMP_CLIP_RECT 143 | SDL_RenderGetViewport(sdl.renderer, &viewport); 144 | #endif 145 | 146 | nk_draw_foreach(cmd, &sdl.ctx, &dev->cmds) 147 | { 148 | if (!cmd->elem_count) continue; 149 | 150 | { 151 | SDL_Rect r; 152 | r.x = cmd->clip_rect.x; 153 | r.y = cmd->clip_rect.y; 154 | r.w = cmd->clip_rect.w; 155 | r.h = cmd->clip_rect.h; 156 | #ifdef NK_SDL_CLAMP_CLIP_RECT 157 | if (r.x < 0) { 158 | r.w += r.x; 159 | r.x = 0; 160 | } 161 | if (r.y < 0) { 162 | r.h += r.y; 163 | r.y = 0; 164 | } 165 | if (r.h > viewport.h) { 166 | r.h = viewport.h; 167 | } 168 | if (r.w > viewport.w) { 169 | r.w = viewport.w; 170 | } 171 | #endif 172 | SDL_RenderSetClipRect(sdl.renderer, &r); 173 | } 174 | 175 | { 176 | const void *vertices = nk_buffer_memory_const(&vbuf); 177 | 178 | SDL_RenderGeometryRaw(sdl.renderer, 179 | (SDL_Texture *)cmd->texture.ptr, 180 | (const float*)((const nk_byte*)vertices + vp), vs, 181 | (const SDL_Color*)((const nk_byte*)vertices + vc), vs, 182 | (const float*)((const nk_byte*)vertices + vt), vs, 183 | (vbuf.needed / vs), 184 | (void *) offset, cmd->elem_count, 2); 185 | 186 | offset += cmd->elem_count; 187 | } 188 | } 189 | 190 | SDL_RenderSetClipRect(sdl.renderer, &saved_clip); 191 | if (!clipping_enabled) { 192 | SDL_RenderSetClipRect(sdl.renderer, NULL); 193 | } 194 | 195 | nk_clear(&sdl.ctx); 196 | nk_buffer_clear(&dev->cmds); 197 | nk_buffer_free(&vbuf); 198 | nk_buffer_free(&ebuf); 199 | } 200 | } 201 | 202 | static void 203 | nk_sdl_clipboard_paste(nk_handle usr, struct nk_text_edit *edit) 204 | { 205 | const char *text = SDL_GetClipboardText(); 206 | if (text) nk_textedit_paste(edit, text, nk_strlen(text)); 207 | (void)usr; 208 | } 209 | 210 | static void 211 | nk_sdl_clipboard_copy(nk_handle usr, const char *text, int len) 212 | { 213 | char *str = 0; 214 | (void)usr; 215 | if (!len) return; 216 | str = (char*)malloc((size_t)len+1); 217 | if (!str) return; 218 | memcpy(str, text, (size_t)len); 219 | str[len] = '\0'; 220 | SDL_SetClipboardText(str); 221 | free(str); 222 | } 223 | 224 | NK_API struct nk_context* 225 | nk_sdl_init(SDL_Window *win, SDL_Renderer *renderer) 226 | { 227 | #ifndef NK_SDL_CLAMP_CLIP_RECT 228 | SDL_RendererInfo info; 229 | SDL_version runtimeVer; 230 | 231 | /* warn for cases where NK_SDL_CLAMP_CLIP_RECT should have been set but isn't */ 232 | SDL_GetRendererInfo(renderer, &info); 233 | SDL_GetVersion(&runtimeVer); 234 | if (strncmp("metal", info.name, 5) == 0 && 235 | SDL_VERSIONNUM(runtimeVer.major, runtimeVer.minor, runtimeVer.patch) < SDL_VERSIONNUM(2, 0, 22)) 236 | { 237 | SDL_LogWarn( 238 | SDL_LOG_CATEGORY_APPLICATION, 239 | "renderer is using Metal API but runtime SDL version %d.%d.%d is older than compiled version %d.%d.%d, " 240 | "which may cause issues with rendering", 241 | runtimeVer.major, runtimeVer.minor, runtimeVer.patch, 242 | SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL 243 | ); 244 | } 245 | #endif 246 | sdl.win = win; 247 | sdl.renderer = renderer; 248 | sdl.time_of_last_frame = SDL_GetTicks64(); 249 | nk_init_default(&sdl.ctx, 0); 250 | sdl.ctx.clip.copy = nk_sdl_clipboard_copy; 251 | sdl.ctx.clip.paste = nk_sdl_clipboard_paste; 252 | sdl.ctx.clip.userdata = nk_handle_ptr(0); 253 | nk_buffer_init_default(&sdl.ogl.cmds); 254 | return &sdl.ctx; 255 | } 256 | 257 | NK_API void 258 | nk_sdl_font_stash_begin(struct nk_font_atlas **atlas) 259 | { 260 | nk_font_atlas_init_default(&sdl.atlas); 261 | nk_font_atlas_begin(&sdl.atlas); 262 | *atlas = &sdl.atlas; 263 | } 264 | 265 | NK_API void 266 | nk_sdl_font_stash_end(void) 267 | { 268 | const void *image; int w, h; 269 | image = nk_font_atlas_bake(&sdl.atlas, &w, &h, NK_FONT_ATLAS_RGBA32); 270 | nk_sdl_device_upload_atlas(image, w, h); 271 | nk_font_atlas_end(&sdl.atlas, nk_handle_ptr(sdl.ogl.font_tex), &sdl.ogl.tex_null); 272 | if (sdl.atlas.default_font) 273 | nk_style_set_font(&sdl.ctx, &sdl.atlas.default_font->handle); 274 | } 275 | 276 | NK_API void 277 | nk_sdl_handle_grab(void) 278 | { 279 | struct nk_context *ctx = &sdl.ctx; 280 | if (ctx->input.mouse.grab) { 281 | SDL_SetRelativeMouseMode(SDL_TRUE); 282 | } else if (ctx->input.mouse.ungrab) { 283 | /* better support for older SDL by setting mode first; causes an extra mouse motion event */ 284 | SDL_SetRelativeMouseMode(SDL_FALSE); 285 | SDL_WarpMouseInWindow(sdl.win, (int)ctx->input.mouse.prev.x, (int)ctx->input.mouse.prev.y); 286 | } else if (ctx->input.mouse.grabbed) { 287 | ctx->input.mouse.pos.x = ctx->input.mouse.prev.x; 288 | ctx->input.mouse.pos.y = ctx->input.mouse.prev.y; 289 | } 290 | } 291 | 292 | NK_API int 293 | nk_sdl_handle_event(SDL_Event *evt) 294 | { 295 | struct nk_context *ctx = &sdl.ctx; 296 | 297 | switch(evt->type) 298 | { 299 | case SDL_KEYUP: /* KEYUP & KEYDOWN share same routine */ 300 | case SDL_KEYDOWN: 301 | { 302 | int down = evt->type == SDL_KEYDOWN; 303 | const Uint8* state = SDL_GetKeyboardState(0); 304 | switch(evt->key.keysym.sym) 305 | { 306 | case SDLK_RSHIFT: /* RSHIFT & LSHIFT share same routine */ 307 | case SDLK_LSHIFT: nk_input_key(ctx, NK_KEY_SHIFT, down); break; 308 | case SDLK_DELETE: nk_input_key(ctx, NK_KEY_DEL, down); break; 309 | case SDLK_RETURN: nk_input_key(ctx, NK_KEY_ENTER, down); break; 310 | case SDLK_TAB: nk_input_key(ctx, NK_KEY_TAB, down); break; 311 | case SDLK_BACKSPACE: nk_input_key(ctx, NK_KEY_BACKSPACE, down); break; 312 | case SDLK_HOME: nk_input_key(ctx, NK_KEY_TEXT_START, down); 313 | nk_input_key(ctx, NK_KEY_SCROLL_START, down); break; 314 | case SDLK_END: nk_input_key(ctx, NK_KEY_TEXT_END, down); 315 | nk_input_key(ctx, NK_KEY_SCROLL_END, down); break; 316 | case SDLK_PAGEDOWN: nk_input_key(ctx, NK_KEY_SCROLL_DOWN, down); break; 317 | case SDLK_PAGEUP: nk_input_key(ctx, NK_KEY_SCROLL_UP, down); break; 318 | case SDLK_z: nk_input_key(ctx, NK_KEY_TEXT_UNDO, down && state[SDL_SCANCODE_LCTRL]); break; 319 | case SDLK_r: nk_input_key(ctx, NK_KEY_TEXT_REDO, down && state[SDL_SCANCODE_LCTRL]); break; 320 | case SDLK_c: nk_input_key(ctx, NK_KEY_COPY, down && state[SDL_SCANCODE_LCTRL]); break; 321 | case SDLK_v: nk_input_key(ctx, NK_KEY_PASTE, down && state[SDL_SCANCODE_LCTRL]); break; 322 | case SDLK_x: nk_input_key(ctx, NK_KEY_CUT, down && state[SDL_SCANCODE_LCTRL]); break; 323 | case SDLK_b: nk_input_key(ctx, NK_KEY_TEXT_LINE_START, down && state[SDL_SCANCODE_LCTRL]); break; 324 | case SDLK_e: nk_input_key(ctx, NK_KEY_TEXT_LINE_END, down && state[SDL_SCANCODE_LCTRL]); break; 325 | case SDLK_UP: nk_input_key(ctx, NK_KEY_UP, down); break; 326 | case SDLK_DOWN: nk_input_key(ctx, NK_KEY_DOWN, down); break; 327 | case SDLK_LEFT: 328 | if (state[SDL_SCANCODE_LCTRL]) 329 | nk_input_key(ctx, NK_KEY_TEXT_WORD_LEFT, down); 330 | else nk_input_key(ctx, NK_KEY_LEFT, down); 331 | break; 332 | case SDLK_RIGHT: 333 | if (state[SDL_SCANCODE_LCTRL]) 334 | nk_input_key(ctx, NK_KEY_TEXT_WORD_RIGHT, down); 335 | else nk_input_key(ctx, NK_KEY_RIGHT, down); 336 | break; 337 | } 338 | } 339 | return 1; 340 | 341 | case SDL_MOUSEBUTTONUP: /* MOUSEBUTTONUP & MOUSEBUTTONDOWN share same routine */ 342 | case SDL_MOUSEBUTTONDOWN: 343 | { 344 | int down = evt->type == SDL_MOUSEBUTTONDOWN; 345 | const int x = evt->button.x, y = evt->button.y; 346 | switch(evt->button.button) 347 | { 348 | case SDL_BUTTON_LEFT: 349 | if (evt->button.clicks > 1) 350 | nk_input_button(ctx, NK_BUTTON_DOUBLE, x, y, down); 351 | nk_input_button(ctx, NK_BUTTON_LEFT, x, y, down); break; 352 | case SDL_BUTTON_MIDDLE: nk_input_button(ctx, NK_BUTTON_MIDDLE, x, y, down); break; 353 | case SDL_BUTTON_RIGHT: nk_input_button(ctx, NK_BUTTON_RIGHT, x, y, down); break; 354 | } 355 | } 356 | return 1; 357 | 358 | case SDL_MOUSEMOTION: 359 | if (ctx->input.mouse.grabbed) { 360 | int x = (int)ctx->input.mouse.prev.x, y = (int)ctx->input.mouse.prev.y; 361 | nk_input_motion(ctx, x + evt->motion.xrel, y + evt->motion.yrel); 362 | } 363 | else nk_input_motion(ctx, evt->motion.x, evt->motion.y); 364 | return 1; 365 | 366 | case SDL_TEXTINPUT: 367 | { 368 | nk_glyph glyph; 369 | memcpy(glyph, evt->text.text, NK_UTF_SIZE); 370 | nk_input_glyph(ctx, glyph); 371 | } 372 | return 1; 373 | 374 | case SDL_MOUSEWHEEL: 375 | nk_input_scroll(ctx,nk_vec2((float)evt->wheel.x,(float)evt->wheel.y)); 376 | return 1; 377 | } 378 | return 0; 379 | } 380 | 381 | NK_API 382 | void nk_sdl_shutdown(void) 383 | { 384 | struct nk_sdl_device *dev = &sdl.ogl; 385 | nk_font_atlas_clear(&sdl.atlas); 386 | nk_free(&sdl.ctx); 387 | SDL_DestroyTexture(dev->font_tex); 388 | /* glDeleteTextures(1, &dev->font_tex); */ 389 | nk_buffer_free(&dev->cmds); 390 | memset(&sdl, 0, sizeof(sdl)); 391 | } 392 | 393 | #endif /* NK_SDL_RENDERER_IMPLEMENTATION */ 394 | -------------------------------------------------------------------------------- /examples/debug-nuklear/src/nuklear.c: -------------------------------------------------------------------------------- 1 | #define NK_IMPLEMENTATION 2 | #include "nuklear_proj.h" 3 | -------------------------------------------------------------------------------- /examples/debug/Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | CC := cc 3 | OPT := -g3 -Og 4 | CFLAGS := $(OPT) -std=c99 -Wall -Wextra -Wdouble-promotion \ 5 | -Wno-unused-parameter -Wno-unused-function -Wno-sign-conversion \ 6 | -fsanitize=undefined -fsanitize-trap 7 | CFLAGS += $(shell sdl2-config --cflags) 8 | LDLIBS += $(shell sdl2-config --libs) 9 | 10 | all: peanut-debug peanut-debug-simple 11 | 12 | clean: 13 | $(RM) peanut-debug peanut-debug-simple 14 | -------------------------------------------------------------------------------- /examples/debug/peanut-debug-simple.c: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2018-2023 Mahyar Koshkouei 4 | * 5 | * A more bare-bones application to help with debugging. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define ENABLE_LCD 1 16 | #define ENABLE_SOUND 0 17 | 18 | #include "../../peanut_gb.h" 19 | 20 | struct priv_t 21 | { 22 | /* Pointer to allocated memory holding GB file. */ 23 | uint8_t *rom; 24 | /* Pointer to allocated memory holding save file. */ 25 | uint8_t *cart_ram; 26 | FILE *log; 27 | uint16_t fb[LCD_HEIGHT][LCD_WIDTH]; 28 | }; 29 | 30 | /** 31 | * Returns a byte from the ROM file at the given address. 32 | */ 33 | uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) 34 | { 35 | const struct priv_t * const p = gb->direct.priv; 36 | return p->rom[addr]; 37 | } 38 | 39 | /** 40 | * Returns a byte from the cartridge RAM at the given address. 41 | */ 42 | uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) 43 | { 44 | const struct priv_t * const p = gb->direct.priv; 45 | return p->cart_ram[addr]; 46 | } 47 | 48 | /** 49 | * Writes a given byte to the cartridge RAM at the given address. 50 | */ 51 | void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, 52 | const uint8_t val) 53 | { 54 | const struct priv_t * const p = gb->direct.priv; 55 | p->cart_ram[addr] = val; 56 | } 57 | 58 | /** 59 | * Returns a pointer to the allocated space containing the ROM. Must be freed. 60 | */ 61 | uint8_t *read_rom_to_ram(const char *file_name) 62 | { 63 | FILE *rom_file = fopen(file_name, "rb"); 64 | size_t rom_size; 65 | uint8_t *rom = NULL; 66 | 67 | if(rom_file == NULL) 68 | return NULL; 69 | 70 | fseek(rom_file, 0, SEEK_END); 71 | rom_size = ftell(rom_file); 72 | rewind(rom_file); 73 | rom = malloc(rom_size); 74 | 75 | if(fread(rom, sizeof(uint8_t), rom_size, rom_file) != rom_size) 76 | { 77 | free(rom); 78 | fclose(rom_file); 79 | return NULL; 80 | } 81 | 82 | fclose(rom_file); 83 | return rom; 84 | } 85 | 86 | void read_cart_ram_file(const char *save_file_name, uint8_t **dest, 87 | const size_t len) 88 | { 89 | FILE *f; 90 | 91 | /* If save file not required. */ 92 | if(len == 0) 93 | { 94 | *dest = NULL; 95 | return; 96 | } 97 | 98 | /* Allocate enough memory to hold save file. */ 99 | if((*dest = malloc(len)) == NULL) 100 | { 101 | printf("%d: %s\n", __LINE__, strerror(errno)); 102 | exit(EXIT_FAILURE); 103 | } 104 | 105 | f = fopen(save_file_name, "rb"); 106 | 107 | /* It doesn't matter if the save file doesn't exist. We initialise the 108 | * save memory allocated above. The save file will be created on exit. */ 109 | if(f == NULL) 110 | { 111 | memset(*dest, 0xFF, len); 112 | return; 113 | } 114 | 115 | /* Read save file to allocated memory. */ 116 | fread(*dest, sizeof(uint8_t), len, f); 117 | fclose(f); 118 | } 119 | 120 | void write_cart_ram_file(const char *save_file_name, uint8_t **dest, 121 | const size_t len) 122 | { 123 | FILE *f; 124 | 125 | if(len == 0 || *dest == NULL) 126 | return; 127 | 128 | if((f = fopen(save_file_name, "wb")) == NULL) 129 | { 130 | puts("Unable to open save file."); 131 | printf("%d: %s\n", __LINE__, strerror(errno)); 132 | exit(EXIT_FAILURE); 133 | } 134 | 135 | /* Record save file. */ 136 | fwrite(*dest, sizeof(uint8_t), len, f); 137 | fclose(f); 138 | } 139 | 140 | /** 141 | * Handles an error reported by the emulator. The emulator context may be used 142 | * to better understand why the error given in gb_err was reported. 143 | */ 144 | void gb_error(struct gb_s *gb, const enum gb_error_e gb_err, const uint16_t val) 145 | { 146 | const char* gb_err_str[GB_INVALID_MAX] = { 147 | "UNKNOWN", 148 | "INVALID OPCODE", 149 | "INVALID READ", 150 | "INVALID WRITE", 151 | "HALT FOREVER" 152 | }; 153 | struct priv_t *priv = gb->direct.priv; 154 | 155 | fprintf(stderr, "Error %d occurred: %s at %04X\n. Exiting.\n", 156 | gb_err, gb_err_str[gb_err], val); 157 | 158 | fflush(priv->log); 159 | 160 | /* Free memory and then exit. */ 161 | free(priv->cart_ram); 162 | free(priv->rom); 163 | exit(EXIT_FAILURE); 164 | } 165 | 166 | #if ENABLE_LCD 167 | /** 168 | * Draws scanline into framebuffer. 169 | */ 170 | void lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], 171 | const uint_fast8_t line) 172 | { 173 | } 174 | #endif 175 | 176 | int main(int argc, char **argv) 177 | { 178 | struct gb_s gb; 179 | struct priv_t priv; 180 | unsigned int running = 1; 181 | char *save_file_name; 182 | enum gb_init_error_e ret; 183 | unsigned int debug_mode = 1; 184 | 185 | /* Make sure a file name is given. */ 186 | if(argc < 2 || argc > 3) 187 | { 188 | printf("Usage: %s FILE [SAVE]\n", argv[0]); 189 | puts("SAVE is set by default if not provided."); 190 | return EXIT_FAILURE; 191 | } 192 | 193 | /* Copy input ROM file to allocated memory. */ 194 | if((priv.rom = read_rom_to_ram(argv[1])) == NULL) 195 | { 196 | printf("%d: %s\n", __LINE__, strerror(errno)); 197 | return EXIT_FAILURE; 198 | } 199 | 200 | /* If no save file is specified, copy save file (with specific name) to 201 | * allocated memory. */ 202 | if(argc == 2) 203 | { 204 | char *str_replace; 205 | const char extension[] = ".sav"; 206 | 207 | /* Allocate enough space for the ROM file name, for the "sav" extension 208 | * and for the null terminator. */ 209 | save_file_name = malloc(strlen(argv[1]) + strlen(extension) + 1); 210 | 211 | if(save_file_name == NULL) 212 | { 213 | printf("%d: %s\n", __LINE__, strerror(errno)); 214 | return EXIT_FAILURE; 215 | } 216 | 217 | /* Copy the ROM file name to allocated space. */ 218 | strcpy(save_file_name, argv[1]); 219 | 220 | /* If the file name does not have a dot, or the only dot is at the start 221 | * of the file name, set the pointer to begin replacing the string to 222 | * the end of the file name, otherwise set it to the dot. */ 223 | if((str_replace = strrchr(save_file_name, '.')) == NULL || 224 | str_replace == save_file_name) 225 | str_replace = save_file_name + strlen(save_file_name); 226 | 227 | /* Copy extension to string including terminating null byte. */ 228 | for(unsigned int i = 0; i <= strlen(extension); i++) 229 | *(str_replace++) = extension[i]; 230 | } 231 | else 232 | save_file_name = argv[2]; 233 | 234 | /* TODO: Sanity check input GB file. */ 235 | priv.log = fopen("log.txt", "w"); 236 | 237 | /* Initialise emulator context. */ 238 | ret = gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write, 239 | &gb_error, &priv); 240 | 241 | if(ret != GB_INIT_NO_ERROR) 242 | { 243 | printf("Unable to initialise context. Returned %d.\n", ret); 244 | exit(EXIT_FAILURE); 245 | } 246 | 247 | /* Load Save File. */ 248 | read_cart_ram_file(save_file_name, &priv.cart_ram, gb_get_save_size(&gb)); 249 | 250 | #if ENABLE_LCD 251 | gb_init_lcd(&gb, &lcd_draw_line); 252 | #endif 253 | 254 | uint8_t pc_log[2] = { 0xFF, 0xFF }; 255 | bool pc_log_count = false; 256 | 257 | while(running) 258 | { 259 | /* Execute CPU cycles until the screen has to be redrawn. */ 260 | //gb_run_frame(&gb); 261 | 262 | gb.gb_frame = false; 263 | while(!gb.gb_frame) 264 | { 265 | const char *lcd_mode_str[4] = { 266 | "HBLANK", "VBLANK", "OAM", "TRANSFER" 267 | }; 268 | __gb_step_cpu(&gb); 269 | 270 | if(debug_mode == 0) 271 | continue; 272 | 273 | /* Debugging */ 274 | fprintf(priv.log, "OP:%02X%s PC:%04X AF:%02X%02X BC:%04X DE:%04X SP:%04X HL:%04X ", 275 | __gb_read(&gb, gb.cpu_reg.pc.reg), 276 | gb.gb_halt ? "(HALTED)" : "", 277 | gb.cpu_reg.pc.reg, 278 | gb.cpu_reg.a, gb.cpu_reg.f.reg, 279 | gb.cpu_reg.bc.reg, 280 | gb.cpu_reg.de.reg, 281 | gb.cpu_reg.sp.reg, 282 | gb.cpu_reg.hl.reg); 283 | fprintf(priv.log, "LCD Mode: %02X (%s), LCD Power: %02X (%s) ", 284 | gb.hram_io[IO_STAT], lcd_mode_str[gb.hram_io[IO_STAT] & STAT_MODE], 285 | gb.hram_io[IO_LCDC], (gb.hram_io[IO_LCDC] >> 7) ? "ON" : "OFF"); 286 | fprintf(priv.log, "IF: %02X, IE: %02X ", 287 | gb.hram_io[IO_IF], gb.hram_io[IO_IE]); 288 | fprintf(priv.log, "ROM%d", gb.selected_rom_bank); 289 | fprintf(priv.log, "\n"); 290 | 291 | pc_log[pc_log_count] = __gb_read(&gb, gb.cpu_reg.pc.reg); 292 | pc_log_count = !pc_log_count; 293 | if(pc_log[0] == 0x00 && pc_log[1] == 0x00) 294 | { 295 | puts("NOP Slide detected."); 296 | goto quit; 297 | } 298 | } 299 | } 300 | 301 | quit: 302 | fclose(priv.log); 303 | /* Record save file. */ 304 | write_cart_ram_file(save_file_name, &priv.cart_ram, gb_get_save_size(&gb)); 305 | 306 | free(priv.rom); 307 | free(priv.cart_ram); 308 | if(argc == 2) 309 | free(save_file_name); 310 | 311 | return EXIT_SUCCESS; 312 | } 313 | -------------------------------------------------------------------------------- /examples/debug/peanut-debug.c: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2018-2023 Mahyar Koshkouei 4 | * 5 | * A more bare-bones application to help with debugging. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "SDL.h" 16 | 17 | #define ENABLE_LCD 1 18 | #define ENABLE_SOUND 0 19 | 20 | #include "../../peanut_gb.h" 21 | 22 | const uint16_t lcd_palette[3][4] = 23 | { 24 | { 0x7FFF, 0x5294, 0x294A, 0x0000 }, 25 | { 0x7FFF, 0x5294, 0x294A, 0x0000 }, 26 | { 0x7FFF, 0x5294, 0x294A, 0x0000 } 27 | }; 28 | 29 | struct priv_t 30 | { 31 | /* Pointer to allocated memory holding GB file. */ 32 | uint8_t *rom; 33 | /* Pointer to allocated memory holding save file. */ 34 | uint8_t *cart_ram; 35 | uint16_t fb[LCD_HEIGHT][LCD_WIDTH]; 36 | }; 37 | 38 | /** 39 | * Returns a byte from the ROM file at the given address. 40 | */ 41 | uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) 42 | { 43 | const struct priv_t * const p = gb->direct.priv; 44 | return p->rom[addr]; 45 | } 46 | 47 | /** 48 | * Returns a byte from the cartridge RAM at the given address. 49 | */ 50 | uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) 51 | { 52 | const struct priv_t * const p = gb->direct.priv; 53 | return p->cart_ram[addr]; 54 | } 55 | 56 | /** 57 | * Writes a given byte to the cartridge RAM at the given address. 58 | */ 59 | void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, 60 | const uint8_t val) 61 | { 62 | const struct priv_t * const p = gb->direct.priv; 63 | p->cart_ram[addr] = val; 64 | } 65 | 66 | /** 67 | * Returns a pointer to the allocated space containing the ROM. Must be freed. 68 | */ 69 | uint8_t *read_rom_to_ram(const char *file_name) 70 | { 71 | FILE *rom_file = fopen(file_name, "rb"); 72 | size_t rom_size; 73 | uint8_t *rom = NULL; 74 | 75 | if(rom_file == NULL) 76 | return NULL; 77 | 78 | fseek(rom_file, 0, SEEK_END); 79 | rom_size = ftell(rom_file); 80 | rewind(rom_file); 81 | rom = malloc(rom_size); 82 | 83 | if(fread(rom, sizeof(uint8_t), rom_size, rom_file) != rom_size) 84 | { 85 | free(rom); 86 | fclose(rom_file); 87 | return NULL; 88 | } 89 | 90 | fclose(rom_file); 91 | return rom; 92 | } 93 | 94 | void read_cart_ram_file(const char *save_file_name, uint8_t **dest, 95 | const size_t len) 96 | { 97 | FILE *f; 98 | 99 | /* If save file not required. */ 100 | if(len == 0) 101 | { 102 | *dest = NULL; 103 | return; 104 | } 105 | 106 | /* Allocate enough memory to hold save file. */ 107 | if((*dest = malloc(len)) == NULL) 108 | { 109 | printf("%d: %s\n", __LINE__, strerror(errno)); 110 | exit(EXIT_FAILURE); 111 | } 112 | 113 | f = fopen(save_file_name, "rb"); 114 | 115 | /* It doesn't matter if the save file doesn't exist. We initialise the 116 | * save memory allocated above. The save file will be created on exit. */ 117 | if(f == NULL) 118 | { 119 | memset(*dest, 0xFF, len); 120 | return; 121 | } 122 | 123 | /* Read save file to allocated memory. */ 124 | fread(*dest, sizeof(uint8_t), len, f); 125 | fclose(f); 126 | } 127 | 128 | void write_cart_ram_file(const char *save_file_name, uint8_t **dest, 129 | const size_t len) 130 | { 131 | FILE *f; 132 | 133 | if(len == 0 || *dest == NULL) 134 | return; 135 | 136 | if((f = fopen(save_file_name, "wb")) == NULL) 137 | { 138 | puts("Unable to open save file."); 139 | printf("%d: %s\n", __LINE__, strerror(errno)); 140 | exit(EXIT_FAILURE); 141 | } 142 | 143 | /* Record save file. */ 144 | fwrite(*dest, sizeof(uint8_t), len, f); 145 | fclose(f); 146 | } 147 | 148 | /** 149 | * Handles an error reported by the emulator. The emulator context may be used 150 | * to better understand why the error given in gb_err was reported. 151 | */ 152 | void gb_error(struct gb_s *gb, const enum gb_error_e gb_err, const uint16_t val) 153 | { 154 | const char* gb_err_str[GB_INVALID_MAX] = { 155 | "UNKNOWN", 156 | "INVALID OPCODE", 157 | "INVALID READ", 158 | "INVALID WRITE", 159 | "HALT FOREVER" 160 | }; 161 | struct priv_t *priv = gb->direct.priv; 162 | 163 | fprintf(stderr, "Error %d occurred: %s at %04X\n. Exiting.\n", 164 | gb_err, gb_err_str[gb_err], val); 165 | 166 | /* Free memory and then exit. */ 167 | free(priv->cart_ram); 168 | free(priv->rom); 169 | exit(EXIT_FAILURE); 170 | } 171 | 172 | #if ENABLE_LCD 173 | /** 174 | * Draws scanline into framebuffer. 175 | */ 176 | void lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], 177 | const uint_fast8_t line) 178 | { 179 | struct priv_t *priv = gb->direct.priv; 180 | 181 | for(unsigned int x = 0; x < LCD_WIDTH; x++) 182 | { 183 | priv->fb[line][x] = lcd_palette 184 | [(pixels[x] & LCD_PALETTE_ALL) >> 4] 185 | [pixels[x] & 3]; 186 | } 187 | } 188 | #endif 189 | 190 | int main(int argc, char **argv) 191 | { 192 | struct gb_s gb; 193 | struct priv_t priv; 194 | const unsigned int height = 144; 195 | const unsigned int width = 160; 196 | unsigned int running = 1; 197 | SDL_Window *window; 198 | SDL_Renderer *renderer; 199 | SDL_Texture *texture; 200 | SDL_Event event; 201 | uint32_t new_ticks, old_ticks; 202 | char *save_file_name; 203 | enum gb_init_error_e ret; 204 | unsigned int fast_mode = 0; 205 | unsigned int debug_mode = 1; 206 | 207 | /* Make sure a file name is given. */ 208 | if(argc < 2 || argc > 3) 209 | { 210 | printf("Usage: %s FILE [SAVE]\n", argv[0]); 211 | puts("SAVE is set by default if not provided."); 212 | return EXIT_FAILURE; 213 | } 214 | 215 | /* Copy input ROM file to allocated memory. */ 216 | if((priv.rom = read_rom_to_ram(argv[1])) == NULL) 217 | { 218 | printf("%d: %s\n", __LINE__, strerror(errno)); 219 | return EXIT_FAILURE; 220 | } 221 | 222 | /* If no save file is specified, copy save file (with specific name) to 223 | * allocated memory. */ 224 | if(argc == 2) 225 | { 226 | char *str_replace; 227 | const char extension[] = ".sav"; 228 | 229 | /* Allocate enough space for the ROM file name, for the "sav" extension 230 | * and for the null terminator. */ 231 | save_file_name = malloc(strlen(argv[1]) + strlen(extension) + 1); 232 | 233 | if(save_file_name == NULL) 234 | { 235 | printf("%d: %s\n", __LINE__, strerror(errno)); 236 | return EXIT_FAILURE; 237 | } 238 | 239 | /* Copy the ROM file name to allocated space. */ 240 | strcpy(save_file_name, argv[1]); 241 | 242 | /* If the file name does not have a dot, or the only dot is at the start 243 | * of the file name, set the pointer to begin replacing the string to 244 | * the end of the file name, otherwise set it to the dot. */ 245 | if((str_replace = strrchr(save_file_name, '.')) == NULL || 246 | str_replace == save_file_name) 247 | str_replace = save_file_name + strlen(save_file_name); 248 | 249 | /* Copy extension to string including terminating null byte. */ 250 | for(unsigned int i = 0; i <= strlen(extension); i++) 251 | *(str_replace++) = extension[i]; 252 | } 253 | else 254 | save_file_name = argv[2]; 255 | 256 | /* TODO: Sanity check input GB file. */ 257 | 258 | /* Initialise emulator context. */ 259 | ret = gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write, 260 | &gb_error, &priv); 261 | 262 | if(ret != GB_INIT_NO_ERROR) 263 | { 264 | printf("Unable to initialise context. Returned %d.\n", ret); 265 | exit(EXIT_FAILURE); 266 | } 267 | 268 | /* Load Save File. */ 269 | read_cart_ram_file(save_file_name, &priv.cart_ram, gb_get_save_size(&gb)); 270 | 271 | #if ENABLE_LCD 272 | gb_init_lcd(&gb, &lcd_draw_line); 273 | #endif 274 | /* Initialise frontend implementation, in this case, SDL2. */ 275 | if(SDL_Init(SDL_INIT_VIDEO) < 0) 276 | { 277 | printf("Could not initialise SDL: %s\n", SDL_GetError()); 278 | return EXIT_FAILURE; 279 | } 280 | 281 | window = SDL_CreateWindow("DMG Emulator", 282 | SDL_WINDOWPOS_UNDEFINED, 283 | SDL_WINDOWPOS_UNDEFINED, 284 | width, height, 285 | 0); 286 | if(window == NULL) 287 | { 288 | printf("Could not create window: %s\n", SDL_GetError()); 289 | return EXIT_FAILURE; 290 | } 291 | 292 | renderer = SDL_CreateRenderer(window, -1, 0); 293 | if(renderer == NULL) 294 | { 295 | printf("Could not create renderer: %s\n", SDL_GetError()); 296 | return EXIT_FAILURE; 297 | } 298 | 299 | if(SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) < 0) 300 | { 301 | printf("Renderer could not draw color: %s\n", SDL_GetError()); 302 | return EXIT_FAILURE; 303 | } 304 | 305 | if(SDL_RenderClear(renderer) < 0) 306 | { 307 | printf("Renderer could not clear: %s\n", SDL_GetError()); 308 | return EXIT_FAILURE; 309 | } 310 | SDL_RenderPresent(renderer); 311 | 312 | texture = SDL_CreateTexture(renderer, 313 | SDL_PIXELFORMAT_RGB565, 314 | SDL_TEXTUREACCESS_STREAMING, 315 | width, height); 316 | if(texture == NULL) 317 | { 318 | printf("Texture could not be created: %s\n", SDL_GetError()); 319 | return EXIT_FAILURE; 320 | } 321 | 322 | new_ticks = SDL_GetTicks(); 323 | 324 | uint8_t pc_log[2] = { 0xFF, 0xFF }; 325 | bool pc_log_count = false; 326 | 327 | while(running) 328 | { 329 | int32_t delay; 330 | 331 | /* TODO: Get joypad input. */ 332 | while(SDL_PollEvent(&event)) 333 | { 334 | switch(event.type) 335 | { 336 | case SDL_QUIT: 337 | running = 0; 338 | break; 339 | 340 | case SDL_KEYDOWN: 341 | switch(event.key.keysym.sym) 342 | { 343 | case SDLK_RETURN: gb.direct.joypad &= ~JOYPAD_START; break; 344 | case SDLK_BACKSPACE: gb.direct.joypad &= ~JOYPAD_SELECT; break; 345 | case SDLK_z: gb.direct.joypad &= ~JOYPAD_A; break; 346 | case SDLK_x: gb.direct.joypad &= ~JOYPAD_B; break; 347 | case SDLK_UP: gb.direct.joypad &= ~JOYPAD_UP; break; 348 | case SDLK_DOWN: gb.direct.joypad &= ~JOYPAD_DOWN; break; 349 | case SDLK_LEFT: gb.direct.joypad &= ~JOYPAD_LEFT; break; 350 | case SDLK_RIGHT: gb.direct.joypad &= ~JOYPAD_RIGHT; break; 351 | case SDLK_SPACE: fast_mode = !fast_mode; break; 352 | case SDLK_d: debug_mode = !debug_mode; break; 353 | default: break; 354 | } 355 | break; 356 | case SDL_KEYUP: 357 | switch(event.key.keysym.sym) 358 | { 359 | case SDLK_RETURN: gb.direct.joypad |= JOYPAD_START; break; 360 | case SDLK_BACKSPACE: gb.direct.joypad |= JOYPAD_SELECT; break; 361 | case SDLK_z: gb.direct.joypad |= JOYPAD_A; break; 362 | case SDLK_x: gb.direct.joypad |= JOYPAD_B; break; 363 | case SDLK_UP: gb.direct.joypad |= JOYPAD_UP; break; 364 | case SDLK_DOWN: gb.direct.joypad |= JOYPAD_DOWN; break; 365 | case SDLK_LEFT: gb.direct.joypad |= JOYPAD_LEFT; break; 366 | case SDLK_RIGHT: gb.direct.joypad |= JOYPAD_RIGHT; break; 367 | default: break; 368 | } 369 | break; 370 | } 371 | if(event.type == SDL_QUIT) 372 | running = 0; 373 | } 374 | 375 | /* Calculate the time taken to draw frame, then later add a delay to cap 376 | * at 60 fps. */ 377 | old_ticks = SDL_GetTicks(); 378 | 379 | /* Execute CPU cycles until the screen has to be redrawn. */ 380 | //gb_run_frame(&gb); 381 | 382 | gb.gb_frame = false; 383 | while(!gb.gb_frame) 384 | { 385 | const char *lcd_mode_str[4] = { 386 | "HBLANK", "VBLANK", "OAM", "TRANSFER" 387 | }; 388 | __gb_step_cpu(&gb); 389 | 390 | if(debug_mode == 0) 391 | continue; 392 | 393 | /* Debugging */ 394 | printf("OP:%02X%s PC:%04X A:%02X BC:%04X DE:%04X SP:%04X HL:%04X ", 395 | __gb_read(&gb, gb.cpu_reg.pc.reg), 396 | gb.gb_halt ? "(HALTED)" : "", 397 | gb.cpu_reg.pc.reg, 398 | gb.cpu_reg.a, 399 | gb.cpu_reg.bc.reg, 400 | gb.cpu_reg.de.reg, 401 | gb.cpu_reg.sp.reg, 402 | gb.cpu_reg.hl.reg); 403 | printf("LCD Mode: %s, LCD Power: %s ", 404 | lcd_mode_str[gb.hram_io[IO_STAT] & STAT_MODE], 405 | (gb.hram_io[IO_LCDC] >> 7) ? "ON" : "OFF"); 406 | printf("ROM%d", gb.selected_rom_bank); 407 | printf("\n"); 408 | 409 | pc_log[pc_log_count] = __gb_read(&gb, gb.cpu_reg.pc.reg); 410 | pc_log_count = !pc_log_count; 411 | if(pc_log[0] == 0x00 && pc_log[1] == 0x00) 412 | { 413 | puts("NOP Slide detected."); 414 | goto quit; 415 | } 416 | } 417 | 418 | /* Copy frame buffer to SDL screen. */ 419 | SDL_UpdateTexture(texture, NULL, priv.fb, width * sizeof(uint16_t)); 420 | SDL_RenderClear(renderer); 421 | SDL_RenderCopy(renderer, texture, NULL, NULL); 422 | SDL_RenderPresent(renderer); 423 | 424 | /* Use a delay that will draw the screen at a rate of 59.73 Hz. */ 425 | new_ticks = SDL_GetTicks(); 426 | 427 | if(fast_mode) 428 | continue; 429 | 430 | delay = 17 - (new_ticks - old_ticks); 431 | SDL_Delay(delay > 0 ? delay : 0); 432 | } 433 | 434 | quit: 435 | SDL_Quit(); 436 | 437 | /* Record save file. */ 438 | write_cart_ram_file(save_file_name, &priv.cart_ram, gb_get_save_size(&gb)); 439 | 440 | free(priv.rom); 441 | free(priv.cart_ram); 442 | free(save_file_name); 443 | 444 | return EXIT_SUCCESS; 445 | } 446 | -------------------------------------------------------------------------------- /examples/mini_fb/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS = -Ofast -s 2 | LDLIBS = -lX11 3 | 4 | all: peanut-minifb 5 | peanut-minifb: peanut_minifb.c X11MiniFB.c 6 | $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) 7 | 8 | clean: 9 | $(RM) peanut-minifb 10 | -------------------------------------------------------------------------------- /examples/mini_fb/MiniFB.h: -------------------------------------------------------------------------------- 1 | #ifndef _MINIFB_H_ 2 | #define _MINIFB_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | /** 9 | * Convert separate RGB values to 32-bit XRGB value. 10 | */ 11 | #define MFB_RGB(r, g, b) (((unsigned)r) << 16) | (((unsigned)g) << 8) | b 12 | 13 | /** 14 | * Create a window that is used to display the buffer sent into the mfb_update 15 | * function, returns 0 if fails 16 | */ 17 | int mfb_open(const char* name, int width, int height); 18 | 19 | /** 20 | * Update the display. Input buffer is assumed to be a 32-bit buffer of the size 21 | * given in the open call. 22 | * 23 | * Will return -1 when ESC key is pressed (later on will return keycode and -1 24 | * on other close signal) 25 | */ 26 | int mfb_update(void* buffer); 27 | 28 | /** 29 | * Close the window 30 | */ 31 | void mfb_close(); 32 | 33 | #ifdef __cplusplus 34 | } 35 | #endif 36 | 37 | #endif // _MINIFB_H_ 38 | -------------------------------------------------------------------------------- /examples/mini_fb/README.md: -------------------------------------------------------------------------------- 1 | # minifb Example 2 | 3 | This example uses [MiniFB](https://github.com/emoon/minifb) to draw the LCD. 4 | Currently only X11 is supported, since that's the only target I can test with. I 5 | also only imported a small part of MiniFB, but it should be simple to add 6 | support for other platforms already provided by MiniFB. 7 | 8 | This example implementation is only a **proof of concept**. This is because it 9 | lacks support for: 10 | - Input 11 | - Audio 12 | - Save file 13 | - Everything else that isn't LCD 14 | 15 | So don't bother using it for actually playing games, because you can't. 16 | 17 | You may be able to use this example as a demonstration of the minimum required 18 | to work with Peanut-GB. 19 | -------------------------------------------------------------------------------- /examples/mini_fb/X11MiniFB.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "MiniFB.h" 5 | 6 | #define KEY_FUNCTION 0xFF 7 | #define KEY_ESC 0x1B 8 | 9 | static Display* s_display; 10 | static int s_screen; 11 | static int s_width; 12 | static int s_height; 13 | static Window s_window; 14 | static GC s_gc; 15 | static XImage *s_ximage; 16 | 17 | int mfb_open(const char* title, int width, int height) 18 | { 19 | int depth, i, formatCount, convDepth = -1; 20 | XPixmapFormatValues* formats; 21 | XSetWindowAttributes windowAttributes; 22 | XSizeHints sizeHints; 23 | Visual* visual; 24 | 25 | s_display = XOpenDisplay(0); 26 | 27 | if (!s_display) 28 | return -1; 29 | 30 | s_screen = DefaultScreen(s_display); 31 | visual = DefaultVisual(s_display, s_screen); 32 | formats = XListPixmapFormats(s_display, &formatCount); 33 | depth = DefaultDepth(s_display, s_screen); 34 | Window defaultRootWindow = DefaultRootWindow(s_display); 35 | 36 | for (i = 0; i < formatCount; ++i) 37 | { 38 | if (depth == formats[i].depth) 39 | { 40 | convDepth = formats[i].bits_per_pixel; 41 | break; 42 | } 43 | } 44 | 45 | XFree(formats); 46 | 47 | // We only support 32-bit right now 48 | if (convDepth != 32) 49 | { 50 | XCloseDisplay(s_display); 51 | return -1; 52 | } 53 | 54 | int screenWidth = DisplayWidth(s_display, s_screen); 55 | int screenHeight = DisplayHeight(s_display, s_screen); 56 | 57 | windowAttributes.border_pixel = BlackPixel(s_display, s_screen); 58 | windowAttributes.background_pixel = BlackPixel(s_display, s_screen); 59 | windowAttributes.backing_store = NotUseful; 60 | 61 | s_window = XCreateWindow(s_display, defaultRootWindow, 62 | (screenWidth - width) / 2, (screenHeight - height) / 2, 63 | width, height, 0, depth, InputOutput, visual, 64 | CWBackPixel | CWBorderPixel | CWBackingStore, 65 | &windowAttributes); 66 | 67 | if (!s_window) 68 | return 0; 69 | 70 | XSelectInput(s_display, s_window, KeyPressMask | KeyReleaseMask); 71 | XStoreName(s_display, s_window, title); 72 | 73 | sizeHints.flags = PPosition | PMinSize | PMaxSize; 74 | sizeHints.x = 0; 75 | sizeHints.y = 0; 76 | sizeHints.min_width = width; 77 | sizeHints.max_width = width; 78 | sizeHints.min_height = height; 79 | sizeHints.max_height = height; 80 | 81 | XSetWMNormalHints(s_display, s_window, &sizeHints); 82 | XClearWindow(s_display, s_window); 83 | XMapRaised(s_display, s_window); 84 | XFlush(s_display); 85 | 86 | s_gc = DefaultGC(s_display, s_screen); 87 | 88 | s_ximage = XCreateImage(s_display, CopyFromParent, depth, ZPixmap, 0, 89 | NULL, width, height, 32, width * 4); 90 | 91 | s_width = width; 92 | s_height = height; 93 | 94 | return 1; 95 | } 96 | 97 | static int processEvents() 98 | { 99 | XEvent event; 100 | KeySym sym; 101 | 102 | if (!XPending(s_display)) 103 | return 0; 104 | 105 | XNextEvent(s_display, &event); 106 | 107 | if (event.type != KeyPress) 108 | return 0; 109 | 110 | sym = XLookupKeysym(&event.xkey, 0); 111 | 112 | if ((sym >> 8) != KEY_FUNCTION) 113 | return 0; 114 | 115 | if ((sym & 0xFF) == KEY_ESC) 116 | return -1; 117 | 118 | return 0; 119 | } 120 | 121 | int mfb_update(void* buffer) 122 | { 123 | s_ximage->data = (char*)buffer; 124 | 125 | XPutImage(s_display, s_window, s_gc, s_ximage, 0, 0, 0, 0, 126 | s_width, s_height); 127 | XFlush(s_display); 128 | 129 | if (processEvents() < 0) 130 | return -1; 131 | 132 | return 0; 133 | } 134 | 135 | void mfb_close (void) 136 | { 137 | s_ximage->data = NULL; 138 | XDestroyImage(s_ximage); 139 | XDestroyWindow(s_display, s_window); 140 | XCloseDisplay(s_display); 141 | } 142 | -------------------------------------------------------------------------------- /examples/mini_fb/peanut_minifb.c: -------------------------------------------------------------------------------- 1 | #define ENABLE_SOUND 0 2 | #define ENABLE_LCD 1 3 | 4 | /* Import emulator library. */ 5 | #include "../../peanut_gb.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "MiniFB.h" 16 | 17 | struct priv_t 18 | { 19 | /* Pointer to allocated memory holding GB file. */ 20 | uint8_t *rom; 21 | /* Pointer to allocated memory holding save file. */ 22 | uint8_t *cart_ram; 23 | 24 | /* Frame buffer */ 25 | uint32_t fb[LCD_HEIGHT][LCD_WIDTH]; 26 | }; 27 | 28 | /** 29 | * Returns a byte from the ROM file at the given address. 30 | */ 31 | uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) 32 | { 33 | const struct priv_t * const p = gb->direct.priv; 34 | return p->rom[addr]; 35 | } 36 | 37 | /** 38 | * Returns a byte from the cartridge RAM at the given address. 39 | */ 40 | uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) 41 | { 42 | const struct priv_t * const p = gb->direct.priv; 43 | return p->cart_ram[addr]; 44 | } 45 | 46 | /** 47 | * Writes a given byte to the cartridge RAM at the given address. 48 | */ 49 | void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, 50 | const uint8_t val) 51 | { 52 | const struct priv_t * const p = gb->direct.priv; 53 | p->cart_ram[addr] = val; 54 | } 55 | 56 | /** 57 | * Returns a pointer to the allocated space containing the ROM. Must be freed. 58 | */ 59 | uint8_t *read_rom_to_ram(const char *file_name) 60 | { 61 | FILE *rom_file = fopen(file_name, "rb"); 62 | size_t rom_size; 63 | uint8_t *rom = NULL; 64 | 65 | if(rom_file == NULL) 66 | return NULL; 67 | 68 | fseek(rom_file, 0, SEEK_END); 69 | rom_size = ftell(rom_file); 70 | rewind(rom_file); 71 | rom = malloc(rom_size); 72 | 73 | if(fread(rom, sizeof(uint8_t), rom_size, rom_file) != rom_size) 74 | { 75 | free(rom); 76 | fclose(rom_file); 77 | return NULL; 78 | } 79 | 80 | fclose(rom_file); 81 | return rom; 82 | } 83 | 84 | /** 85 | * Ignore all errors. 86 | */ 87 | void gb_error(struct gb_s *gb, const enum gb_error_e gb_err, const uint16_t val) 88 | { 89 | const char* gb_err_str[GB_INVALID_MAX] = { 90 | "UNKNOWN", 91 | "INVALID OPCODE", 92 | "INVALID READ", 93 | "INVALID WRITE", 94 | "HALT FOREVER" 95 | }; 96 | struct priv_t *priv = gb->direct.priv; 97 | 98 | fprintf(stderr, "Error %d occurred: %s at %04X\n. Exiting.\n", 99 | gb_err, gb_err_str[gb_err], val); 100 | 101 | /* Free memory and then exit. */ 102 | free(priv->cart_ram); 103 | free(priv->rom); 104 | exit(EXIT_FAILURE); 105 | } 106 | 107 | #if ENABLE_LCD 108 | /** 109 | * Draws scanline into framebuffer. 110 | */ 111 | void lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], 112 | const uint_fast8_t line) 113 | { 114 | struct priv_t *priv = gb->direct.priv; 115 | const uint32_t palette[] = { 0xFFFFFF, 0xA5A5A5, 0x525252, 0x000000 }; 116 | 117 | for(unsigned int x = 0; x < LCD_WIDTH; x++) 118 | priv->fb[line][x] = palette[pixels[x] & 3]; 119 | } 120 | #endif 121 | 122 | int main(int argc, char **argv) 123 | { 124 | /* Must be freed */ 125 | char *rom_file_name = NULL; 126 | static struct gb_s gb; 127 | static struct priv_t priv; 128 | enum gb_init_error_e ret; 129 | 130 | switch(argc) 131 | { 132 | case 2: 133 | rom_file_name = argv[1]; 134 | break; 135 | 136 | default: 137 | fprintf(stderr, "%s ROM\n", argv[0]); 138 | exit(EXIT_FAILURE); 139 | } 140 | 141 | /* Copy input ROM file to allocated memory. */ 142 | if((priv.rom = read_rom_to_ram(rom_file_name)) == NULL) 143 | { 144 | printf("%d: %s\n", __LINE__, strerror(errno)); 145 | exit(EXIT_FAILURE); 146 | } 147 | 148 | /* Initialise context. */ 149 | ret = gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, 150 | &gb_cart_ram_write, &gb_error, &priv); 151 | 152 | if(ret != GB_INIT_NO_ERROR) 153 | { 154 | printf("Error: %d\n", ret); 155 | exit(EXIT_FAILURE); 156 | } 157 | 158 | priv.cart_ram = malloc(gb_get_save_size(&gb)); 159 | 160 | #if ENABLE_LCD 161 | gb_init_lcd(&gb, &lcd_draw_line); 162 | // gb.direct.interlace = true; 163 | #endif 164 | 165 | if(!mfb_open("Peanut-minifb", LCD_WIDTH, LCD_HEIGHT)) 166 | return EXIT_FAILURE; 167 | 168 | while(1) 169 | { 170 | const double target_speed_us = 1000000.0 / VERTICAL_SYNC; 171 | int_fast16_t delay; 172 | unsigned long start, end; 173 | struct timeval timecheck; 174 | int state; 175 | 176 | gettimeofday(&timecheck, NULL); 177 | start = (long)timecheck.tv_sec * 1000000 + 178 | (long)timecheck.tv_usec; 179 | 180 | /* Execute CPU cycles until the screen has to be redrawn. */ 181 | gb_run_frame(&gb); 182 | 183 | state = mfb_update(priv.fb); 184 | 185 | /* ESC pressed */ 186 | if(state < 0) 187 | break; 188 | 189 | gettimeofday(&timecheck, NULL); 190 | end = (long)timecheck.tv_sec * 1000000 + 191 | (long)timecheck.tv_usec; 192 | 193 | delay = target_speed_us - (end - start); 194 | 195 | /* If it took more than the maximum allowed time to draw frame, 196 | * do not delay. 197 | * Interlaced mode could be enabled here to help speed up 198 | * drawing. 199 | */ 200 | if(delay < 0) 201 | continue; 202 | 203 | usleep(delay); 204 | } 205 | 206 | mfb_close(); 207 | free(priv.cart_ram); 208 | free(priv.rom); 209 | 210 | return EXIT_SUCCESS; 211 | } 212 | -------------------------------------------------------------------------------- /examples/sdl2/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # vim: ts=4:sw=4:expandtab 2 | CMAKE_MINIMUM_REQUIRED(VERSION 3.14...3.26 FATAL_ERROR) 3 | 4 | ## Check user set options. 5 | IF(NOT CMAKE_BUILD_TYPE) 6 | MESSAGE(STATUS "CMAKE_BUILD_TYPE was not set by user; setting build type to Debug") 7 | SET(CMAKE_BUILD_TYPE "Debug") 8 | ELSE() 9 | # List of valid build types 10 | SET(VALID_BUILD_TYPES Debug Release RelWithDebInfo MinSizeRel) 11 | LIST(FIND VALID_BUILD_TYPES ${CMAKE_BUILD_TYPE} IS_VALID_BUILD_TYPE) 12 | IF(IS_VALID_BUILD_TYPE EQUAL -1) 13 | MESSAGE(FATAL_ERROR "CMAKE_BUILD_TYPE was '${CMAKE_BUILD_TYPE}' but can only be set to one of ${VALID_BUILD_TYPES}") 14 | ENDIF() 15 | ENDIF() 16 | 17 | # Obtain version 18 | INCLUDE(../../version.all) 19 | 20 | # Initialise project information. 21 | PROJECT(peanut-sdl 22 | LANGUAGES C 23 | VERSION 24 | ${PEANUTGB_VERSION_MAJOR}.${PEANUTGB_VERSION_MINOR}.${PEANUTGB_VERSION_PATCH} 25 | DESCRIPTION "Peanut-GB example with SDL2" 26 | HOMEPAGE_URL "https://github.com/deltabeard/peanut-gb") 27 | 28 | # Add dependencies to project. 29 | IF(NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug" AND APPLE) 30 | SET(EXE_TARGET_TYPE MACOSX_BUNDLE) 31 | MESSAGE(VERBOSE "Setting EXE type to MACOSX Bundle") 32 | ELSEIF(NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug" AND MSVC) 33 | SET(EXE_TARGET_TYPE WIN32) 34 | MESSAGE(VERBOSE "Setting EXE type to WIN32") 35 | ENDIF() 36 | 37 | ADD_EXECUTABLE(${PROJECT_NAME} ${EXE_TARGET_TYPE}) 38 | TARGET_SOURCES(${PROJECT_NAME} PRIVATE peanut_sdl.c 39 | ../../peanut_gb.h 40 | minigb_apu/minigb_apu.c 41 | minigb_apu/minigb_apu.h) 42 | TARGET_INCLUDE_DIRECTORIES(${PROJECT_NAME} PRIVATE ../../) 43 | 44 | # Discover libraries 45 | IF(MSVC) 46 | SET(DEFAULT_LIBRARY_DISCOVER_METHOD "CPM") 47 | ELSE() 48 | SET(DEFAULT_LIBRARY_DISCOVER_METHOD "PKG_CONFIG") 49 | ENDIF() 50 | SET(LIBRARY_DISCOVER_METHOD DEFAULT_LIBRARY_DISCOVER_METHOD CACHE STRING 51 | "Tool to use for discovering dependencies; options are: PKG_CONFIG, CPM") 52 | SET(ENABLE_SOUND ON CACHE BOOL "Enable sound output") 53 | 54 | IF(${LIBRARY_DISCOVER_METHOD} STREQUAL "PKG_CONFIG") 55 | INCLUDE(FindPkgConfig) 56 | PKG_SEARCH_MODULE(SDL2 REQUIRED sdl2) 57 | ELSEIF(${LIBRARY_DISCOVER_METHOD} STREQUAL "CPM") 58 | INCLUDE(CPM.cmake) 59 | CPMADDPACKAGE(GITHUB_REPOSITORY libsdl-org/SDL 60 | NAME SDL2 61 | GIT_TAG release-2.32.4 62 | OPTIONS 63 | "SDL_SHARED_ENABLED_BY_DEFAULT OFF" 64 | "SDL_STATIC_ENABLED_BY_DEFAULT ON") 65 | 66 | TARGET_INCLUDE_DIRECTORIES(${PROJECT_NAME} PUBLIC ${SDL2_SOURCE_DIR}/include) 67 | TARGET_LINK_LIBRARIES(${PROJECT_NAME} PRIVATE SDL2-static SDL2main) 68 | ELSE() 69 | MESSAGE(SEND_ERROR "LIBRARY_DISCOVER_METHOD '${LIBRARY_DISCOVER_METHOD}' is not valid") 70 | ENDIF() 71 | 72 | # Add required dependencies 73 | TARGET_LINK_LIBRARIES(${PROJECT_NAME} PRIVATE ${SDL2_LIBRARIES}) 74 | 75 | # Some FindSDL2 modules use slightly different variables, so we just use both. 76 | TARGET_INCLUDE_DIRECTORIES(${PROJECT_NAME} PUBLIC ${SDL2_INCLUDE_DIRS} ${SDL2_INCLUDE_DIR}) 77 | 78 | IF("${CMAKE_C_BYTE_ORDER}" STREQUAL "LITTLE_ENDIAN") 79 | ADD_COMPILE_DEFINITIONS(PEANUT_GB_IS_LITTLE_ENDIAN=1) 80 | elseif("${CMAKE_C_BYTE_ORDER}" STREQUAL "BIG_ENDIAN") 81 | ADD_COMPILE_DEFINITIONS(PEANUT_GB_IS_LITTLE_ENDIAN=0) 82 | else() 83 | message(FATAL_ERROR "CMAKE_C_BYTE_ORDER has unexpected value '${CMAKE_C_BYTE_ORDER}'") 84 | ENDIF() 85 | 86 | # Add definitions of project information. 87 | ADD_COMPILE_DEFINITIONS(COMPANY=Deltabeard) 88 | ADD_COMPILE_DEFINITIONS(DESCRIPTION=${PROJECT_DESCRIPTION}) 89 | ADD_COMPILE_DEFINITIONS(LICENSE=MIT) 90 | ADD_COMPILE_DEFINITIONS(NAME=${PROJECT_NAME}) 91 | ADD_COMPILE_DEFINITIONS(ICON_FILE=${CMAKE_SOURCE_DIR}/meta/icon.ico) 92 | 93 | IF(${ENABLE_SOUND}) 94 | ADD_COMPILE_DEFINITIONS(ENABLE_SOUND=1) 95 | ADD_COMPILE_DEFINITIONS(ENABLE_SOUND_MINIGB MINIGB_APU_AUDIO_FORMAT_S16SYS) 96 | ELSE() 97 | ADD_COMPILE_DEFINITIONS(ENABLE_SOUND=0) 98 | ENDIF() 99 | 100 | EXECUTE_PROCESS( 101 | COMMAND git describe --dirty --always --tags --long 102 | WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} 103 | RESULT_VARIABLE GIT_RESULT 104 | OUTPUT_VARIABLE GIT_VER 105 | OUTPUT_STRIP_TRAILING_WHITESPACE) 106 | IF(NOT GIT_RESULT EQUAL 0) 107 | SET(GIT_VER "LOCAL") 108 | ENDIF() 109 | 110 | # Platform specific options 111 | IF(MSVC) 112 | ADD_COMPILE_DEFINITIONS(GIT_VER=${GIT_VER}) 113 | ADD_COMPILE_DEFINITIONS(EXE_VER=${PEANUTGB_VERSION_MAJOR},${PEANUTGB_VERSION_MINOR},${PEANUTGB_VERSION_PATCH},0) 114 | ADD_COMPILE_DEFINITIONS(_CRT_SECURE_NO_WARNINGS) 115 | TARGET_SOURCES(${PROJECT_NAME} PRIVATE meta/winres.rc) 116 | SET_PROPERTY(TARGET ${PROJECT_NAME} PROPERTY VS_DPI_AWARE "PerMonitor") 117 | TARGET_COMPILE_OPTIONS(${PROJECT_NAME} PRIVATE /W3) 118 | ENDIF() 119 | 120 | IF(APPLE) 121 | SET_TARGET_PROPERTIES(${PROJECT_NAME} PROPERTIES 122 | MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}" 123 | MACOSX_BUNDLE_BUNDLE_VERSION "${peanut-sdl_VERSION}" 124 | MACOSX_BUNDLE_COPYRIGHT "MIT" 125 | MACOSX_BUNDLE_INFO_STRING "${PROJECT_DESCRIPTION}" 126 | MACOSX_BUNDLE_SHORT_VERSION_STRING "${peanut-sdl_VERSION}" 127 | MACOSX_BUNDLE_LONG_VERSION_STRING "${GIT_VER}") 128 | INSTALL(TARGETS peanut-sdl BUNDLE DESTINATION 129 | ${CMAKE_INSTALL_BINDIR}/${PROJECT_NAME}) 130 | ELSE() 131 | INSTALL(TARGETS peanut-sdl RUNTIME) 132 | ENDIF() 133 | 134 | SET(CPACK_PACKAGE_VENDOR Deltabeard) 135 | SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJECT_DESCRIPTION}) 136 | SET(CPACK_PACKAGE_ICON meta/icon.ico) 137 | SET(CPACK_PACKAGE_EXECUTABLES peanut-sdl;Peanut-SDL) 138 | INCLUDE(CPack) 139 | 140 | MESSAGE(STATUS "Peanut-SDL ${peanut-sdl_VERSION} (git: ${GIT_VER}) will build with the following options:") 141 | MESSAGE(STATUS " CC: ${CMAKE_C_COMPILER} '${CMAKE_C_COMPILER_ID}' on '${CMAKE_SYSTEM_NAME}'") 142 | MESSAGE(STATUS " CFLAGS: ${CMAKE_C_FLAGS}") 143 | MESSAGE(STATUS " LDFLAGS: ${CMAKE_EXE_LINKER_FLAGS}") 144 | -------------------------------------------------------------------------------- /examples/sdl2/Makefile: -------------------------------------------------------------------------------- 1 | NAME := Peanut-GB 2 | DESCRIPTION := A DMG emulator using SDL2 3 | COMPANY := Deltabeard 4 | COPYRIGHT := Copyright (c) 2020 Mahyar Koshkouei 5 | LICENSE_SPDX := MIT 6 | 7 | 8 | CPPFLAGS := -DCOMPANY=Deltabeard \ 9 | -DDESCRIPTION="$(DESCRIPTION)" \ 10 | -DLICENSE="$(LICENSE_SPDX)" \ 11 | -DNAME="$(NAME)" \ 12 | -DICON_FILE=./meta/icon.ico \ 13 | -DENABLE_SOUND -DENABLE_SOUND_MINIGB -DMINIGB_APU_AUDIO_FORMAT_S16SYS 14 | 15 | OPT := -O2 -Wall -Wextra 16 | CFLAGS := $(OPT) $(shell sdl2-config --cflags) 17 | LDLIBS := $(shell sdl2-config --libs) 18 | 19 | SOURCES := peanut_sdl.c minigb_apu/minigb_apu.c 20 | OBJECTS := peanut_sdl.o minigb_apu/minigb_apu.o 21 | 22 | ifeq ($(OS),Windows_NT) 23 | OBJECTS += meta/winres.o 24 | endif 25 | 26 | all: peanut-sdl 27 | peanut-sdl: $(OBJECTS) 28 | $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDLIBS) 29 | 30 | peanut_sdl.o: peanut_sdl.c ../../peanut_gb.h 31 | 32 | meta/winres.o: meta/winres.rc 33 | windres $(CPPFLAGS) $< $@ 34 | -------------------------------------------------------------------------------- /examples/sdl2/meta/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deltabeard/Peanut-GB/de54cb92302d99e4237963b6bdab45e0f808d213/examples/sdl2/meta/icon.ico -------------------------------------------------------------------------------- /examples/sdl2/meta/winres.rc: -------------------------------------------------------------------------------- 1 | #define Q(x) #x 2 | #define QUOTE(x) Q(x) 3 | 4 | #define VS_VERSION_INFO 1 5 | #define IDI_ICON1 101 6 | 7 | #ifndef ICON_FILE 8 | #define ICON_FILE icon.ico 9 | #endif 10 | 11 | IDI_ICON1 ICON QUOTE(ICON_FILE) 12 | 13 | #ifndef EXE_VER 14 | #define EXE_VER 0,0,0,0 15 | #endif 16 | 17 | VS_VERSION_INFO VERSIONINFO 18 | FILEFLAGSMASK 0x3fL 19 | FILEFLAGS 0x0L 20 | FILEOS 0x40004L 21 | FILETYPE 0x1L 22 | FILESUBTYPE 0x0L 23 | FILEVERSION EXE_VER 24 | BEGIN 25 | BLOCK "StringFileInfo" 26 | BEGIN 27 | BLOCK "040004b0" 28 | BEGIN 29 | VALUE "CompanyName", QUOTE(COMPANY) 30 | VALUE "FileDescription", QUOTE(DESCRIPTION) 31 | VALUE "InternalName", QUOTE(NAME) 32 | VALUE "LegalCopyright", QUOTE(LICENSE) 33 | VALUE "OriginalFilename", QUOTE(NAME) 34 | VALUE "ProductName", QUOTE(NAME) 35 | VALUE "ProductVersion", QUOTE(GIT_VER) 36 | END 37 | END 38 | BLOCK "VarFileInfo" 39 | BEGIN 40 | VALUE "Translation", 0x400, 1200 41 | END 42 | END 43 | -------------------------------------------------------------------------------- /examples/sdl2/minigb_apu/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Alex Baines 2 | Copyright (c) 2019 Mahyar Koshkouei 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/sdl2/minigb_apu/minigb_apu.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Game Boy APU emulator. 3 | * Copyright (c) 2019 Mahyar Koshkouei 4 | * Copyright (c) 2017 Alex Baines 5 | * minigb_apu is released under the terms of the MIT license. 6 | * 7 | * minigb_apu emulates the audio processing unit (APU) of the Game Boy. This 8 | * project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "minigb_apu.h" 16 | 17 | #define DMG_CLOCK_FREQ_U ((unsigned)DMG_CLOCK_FREQ) 18 | #define AUDIO_NSAMPLES (AUDIO_SAMPLES_TOTAL) 19 | 20 | #define MAX(a, b) ( a > b ? a : b ) 21 | #define MIN(a, b) ( a <= b ? a : b ) 22 | 23 | /* Factor in which values are multiplied to compensate for fixed-point 24 | * arithmetic. Some hard-coded values in this project must be recreated. */ 25 | #ifndef FREQ_INC_MULT 26 | # define FREQ_INC_MULT 105 27 | #endif 28 | /* Handles time keeping for sound generation. 29 | * FREQ_INC_REF must be equal to, or larger than AUDIO_SAMPLE_RATE in order 30 | * to avoid a division by zero error. 31 | * Using a square of 2 simplifies calculations. */ 32 | #define FREQ_INC_REF (AUDIO_SAMPLE_RATE * FREQ_INC_MULT) 33 | 34 | #define MAX_CHAN_VOLUME 15 35 | 36 | static void set_note_freq(struct chan *c) 37 | { 38 | /* Lowest expected value of freq is 64. */ 39 | uint32_t freq = (DMG_CLOCK_FREQ_U / 4) / (2048 - c->freq); 40 | c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE); 41 | } 42 | 43 | static void chan_enable(struct minigb_apu_ctx *ctx, 44 | const uint_fast8_t i, const bool enable) 45 | { 46 | uint8_t val; 47 | 48 | ctx->chans[i].enabled = enable; 49 | val = (ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) | 50 | (ctx->chans[3].enabled << 3) | (ctx->chans[2].enabled << 2) | 51 | (ctx->chans[1].enabled << 1) | (ctx->chans[0].enabled << 0); 52 | 53 | ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val; 54 | } 55 | 56 | static void update_env(struct chan *c) 57 | { 58 | c->env.counter += c->env.inc; 59 | 60 | while (c->env.counter > FREQ_INC_REF) { 61 | if (c->env.step) { 62 | c->volume += c->env.up ? 1 : -1; 63 | if (c->volume == 0 || c->volume == MAX_CHAN_VOLUME) { 64 | c->env.inc = 0; 65 | } 66 | c->volume = MAX(0, MIN(MAX_CHAN_VOLUME, c->volume)); 67 | } 68 | c->env.counter -= FREQ_INC_REF; 69 | } 70 | } 71 | 72 | static void update_len(struct minigb_apu_ctx *ctx, struct chan *c) 73 | { 74 | if (!c->len.enabled) 75 | return; 76 | 77 | c->len.counter += c->len.inc; 78 | if (c->len.counter > FREQ_INC_REF) { 79 | chan_enable(ctx, c - ctx->chans, 0); 80 | c->len.counter = 0; 81 | } 82 | } 83 | 84 | static bool update_freq(struct chan *c, uint32_t *pos) 85 | { 86 | uint32_t inc = c->freq_inc - *pos; 87 | c->freq_counter += inc; 88 | 89 | if (c->freq_counter > FREQ_INC_REF) { 90 | *pos = c->freq_inc - (c->freq_counter - FREQ_INC_REF); 91 | c->freq_counter = 0; 92 | return true; 93 | } else { 94 | *pos = c->freq_inc; 95 | return false; 96 | } 97 | } 98 | 99 | static void update_sweep(struct chan *c) 100 | { 101 | c->sweep.counter += c->sweep.inc; 102 | 103 | while (c->sweep.counter > FREQ_INC_REF) { 104 | if (c->sweep.shift) { 105 | uint16_t inc = (c->sweep.freq >> c->sweep.shift); 106 | if (c->sweep.down) 107 | inc *= -1; 108 | 109 | c->freq = c->sweep.freq + inc; 110 | if (c->freq > 2047) { 111 | c->enabled = 0; 112 | } else { 113 | set_note_freq(c); 114 | c->sweep.freq = c->freq; 115 | } 116 | } else if (c->sweep.rate) { 117 | c->enabled = 0; 118 | } 119 | c->sweep.counter -= FREQ_INC_REF; 120 | } 121 | } 122 | 123 | static void update_square(struct minigb_apu_ctx *ctx, audio_sample_t *samples, 124 | const bool ch2) 125 | { 126 | struct chan *c = &ctx->chans[ch2]; 127 | 128 | if (!c->powered || !c->enabled) 129 | return; 130 | 131 | set_note_freq(c); 132 | 133 | for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { 134 | update_len(ctx, c); 135 | if (!c->enabled) 136 | return; 137 | 138 | update_env(c); 139 | if (!c->volume) 140 | continue; 141 | 142 | if (!ch2) 143 | update_sweep(c); 144 | 145 | uint32_t pos = 0; 146 | uint32_t prev_pos = 0; 147 | int32_t sample = 0; 148 | 149 | while (update_freq(c, &pos)) { 150 | c->square.duty_counter = (c->square.duty_counter + 1) & 7; 151 | sample += ((pos - prev_pos) / c->freq_inc) * c->val; 152 | c->val = (c->square.duty & (1 << c->square.duty_counter)) ? 153 | VOL_INIT_MAX / MAX_CHAN_VOLUME : 154 | VOL_INIT_MIN / MAX_CHAN_VOLUME; 155 | prev_pos = pos; 156 | } 157 | 158 | sample += c->val; 159 | sample *= c->volume; 160 | sample /= 4; 161 | 162 | samples[i + 0] += sample * c->on_left * ctx->vol_l; 163 | samples[i + 1] += sample * c->on_right * ctx->vol_r; 164 | } 165 | } 166 | 167 | static uint8_t wave_sample(struct minigb_apu_ctx *ctx, 168 | const unsigned int pos, const unsigned int volume) 169 | { 170 | uint8_t sample; 171 | 172 | sample = ctx->audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION]; 173 | if (pos & 1) { 174 | sample &= 0xF; 175 | } else { 176 | sample >>= 4; 177 | } 178 | return volume ? (sample >> (volume - 1)) : 0; 179 | } 180 | 181 | static void update_wave(struct minigb_apu_ctx *ctx, audio_sample_t *samples) 182 | { 183 | struct chan *c = &ctx->chans[2]; 184 | 185 | if (!c->powered || !c->enabled || !c->volume) 186 | return; 187 | 188 | set_note_freq(c); 189 | c->freq_inc *= 2; 190 | 191 | for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { 192 | update_len(ctx, c); 193 | if (!c->enabled) 194 | return; 195 | 196 | uint32_t pos = 0; 197 | uint32_t prev_pos = 0; 198 | audio_sample_t sample = 0; 199 | 200 | c->wave.sample = wave_sample(ctx, c->val, c->volume); 201 | 202 | while (update_freq(c, &pos)) { 203 | c->val = (c->val + 1) & 31; 204 | sample += ((pos - prev_pos) / c->freq_inc) * 205 | ((audio_sample_t)c->wave.sample - 8) * 206 | (AUDIO_SAMPLE_MAX/64); 207 | c->wave.sample = wave_sample(ctx, c->val, c->volume); 208 | prev_pos = pos; 209 | } 210 | 211 | sample += ((audio_sample_t)c->wave.sample - 8) * 212 | (audio_sample_t)(AUDIO_SAMPLE_MAX/64); 213 | { 214 | /* First element is unused. */ 215 | audio_sample_t div[] = { AUDIO_SAMPLE_MAX, 1, 2, 4 }; 216 | sample = sample / (div[c->volume]); 217 | } 218 | 219 | sample /= 4; 220 | samples[i + 0] += sample * c->on_left * ctx->vol_l; 221 | samples[i + 1] += sample * c->on_right * ctx->vol_r; 222 | } 223 | } 224 | 225 | static void update_noise(struct minigb_apu_ctx *ctx, audio_sample_t *samples) 226 | { 227 | struct chan *c = &ctx->chans[3]; 228 | 229 | if (c->freq >= 14) 230 | c->enabled = 0; 231 | 232 | if (!c->powered || !c->enabled) 233 | return; 234 | 235 | { 236 | const uint32_t lfsr_div_lut[] = { 237 | 8, 16, 32, 48, 64, 80, 96, 112 238 | }; 239 | uint32_t freq; 240 | 241 | freq = DMG_CLOCK_FREQ_U / (lfsr_div_lut[c->noise.lfsr_div] << c->freq); 242 | c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE); 243 | } 244 | 245 | for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) { 246 | update_len(ctx, c); 247 | if (!c->enabled) 248 | return; 249 | 250 | update_env(c); 251 | if (!c->volume) 252 | continue; 253 | 254 | uint32_t pos = 0; 255 | uint32_t prev_pos = 0; 256 | int32_t sample = 0; 257 | 258 | while (update_freq(c, &pos)) { 259 | c->noise.lfsr_reg = (c->noise.lfsr_reg << 1) | 260 | (c->val >= VOL_INIT_MAX/MAX_CHAN_VOLUME); 261 | 262 | if (c->noise.lfsr_wide) { 263 | c->val = !(((c->noise.lfsr_reg >> 14) & 1) ^ 264 | ((c->noise.lfsr_reg >> 13) & 1)) ? 265 | VOL_INIT_MAX / MAX_CHAN_VOLUME : 266 | VOL_INIT_MIN / MAX_CHAN_VOLUME; 267 | } else { 268 | c->val = !(((c->noise.lfsr_reg >> 6) & 1) ^ 269 | ((c->noise.lfsr_reg >> 5) & 1)) ? 270 | VOL_INIT_MAX / MAX_CHAN_VOLUME : 271 | VOL_INIT_MIN / MAX_CHAN_VOLUME; 272 | } 273 | 274 | sample += ((pos - prev_pos) / c->freq_inc) * c->val; 275 | prev_pos = pos; 276 | } 277 | 278 | sample += c->val; 279 | sample *= c->volume; 280 | sample /= 4; 281 | 282 | samples[i + 0] += sample * c->on_left * ctx->vol_l; 283 | samples[i + 1] += sample * c->on_right * ctx->vol_r; 284 | } 285 | } 286 | 287 | /** 288 | * SDL2 style audio callback function. 289 | */ 290 | void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx, 291 | audio_sample_t *stream) 292 | { 293 | memset(stream, 0, AUDIO_SAMPLES_TOTAL * sizeof(audio_sample_t)); 294 | update_square(ctx, stream, 0); 295 | update_square(ctx, stream, 1); 296 | update_wave(ctx, stream); 297 | update_noise(ctx, stream); 298 | } 299 | 300 | static void chan_trigger(struct minigb_apu_ctx *ctx, uint_fast8_t i) 301 | { 302 | struct chan *c = &ctx->chans[i]; 303 | 304 | chan_enable(ctx, i, 1); 305 | c->volume = c->volume_init; 306 | 307 | // volume envelope 308 | { 309 | /* LUT created in Julia with: 310 | * `(FREQ_INC_MULT * 64)./vcat(8, 1:7)` 311 | * Must be recreated when FREQ_INC_MULT modified. 312 | */ 313 | const uint32_t inc_lut[8] = { 314 | #if FREQ_INC_MULT == 16 315 | 128, 1024, 512, 341, 316 | 256, 205, 171, 146 317 | #elif FREQ_INC_MULT == 64 318 | 512, 4096, 2048, 1365, 319 | 1024, 819, 683, 585 320 | #elif FREQ_INC_MULT == 105 321 | /* Multiples of 105 provide integer values. */ 322 | 840, 6720, 3360, 2240, 323 | 1680, 1344, 1120, 960 324 | #else 325 | #error "LUT not calculated for this value of FREQ_INC_MULT" 326 | #endif 327 | }; 328 | uint8_t val; 329 | 330 | val = ctx->audio_mem[(0xFF12 + (i * 5)) - AUDIO_ADDR_COMPENSATION]; 331 | 332 | c->env.step = val & 0x7; 333 | c->env.up = val & 0x8; 334 | c->env.inc = inc_lut[c->env.step]; 335 | c->env.counter = 0; 336 | } 337 | 338 | // freq sweep 339 | if (i == 0) { 340 | uint8_t val = ctx->audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION]; 341 | 342 | c->sweep.freq = c->freq; 343 | c->sweep.rate = (val >> 4) & 0x07; 344 | c->sweep.down = (val & 0x08); 345 | c->sweep.shift = (val & 0x07); 346 | c->sweep.inc = c->sweep.rate ? 347 | ((128u * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0; 348 | c->sweep.counter = FREQ_INC_REF; 349 | } 350 | 351 | int len_max = 64; 352 | 353 | if (i == 2) { // wave 354 | len_max = 256; 355 | c->val = 0; 356 | } else if (i == 3) { // noise 357 | c->noise.lfsr_reg = 0xFFFF; 358 | c->val = VOL_INIT_MIN / MAX_CHAN_VOLUME; 359 | } 360 | 361 | c->len.inc = (256u * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load)); 362 | c->len.counter = 0; 363 | } 364 | 365 | /** 366 | * Read audio register. 367 | * \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F. 368 | * This is not checked in this function. 369 | * \return Byte at address. 370 | */ 371 | uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr) 372 | { 373 | static const uint8_t ortab[] = { 374 | 0x80, 0x3f, 0x00, 0xff, 0xbf, 375 | 0xff, 0x3f, 0x00, 0xff, 0xbf, 376 | 0x7f, 0xff, 0x9f, 0xff, 0xbf, 377 | 0xff, 0xff, 0x00, 0x00, 0xbf, 378 | 0x00, 0x00, 0x70, 379 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 380 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 381 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 382 | }; 383 | 384 | return ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] | 385 | ortab[addr - AUDIO_ADDR_COMPENSATION]; 386 | } 387 | 388 | /** 389 | * Write audio register. 390 | * \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F. 391 | * This is not checked in this function. 392 | * \param val Byte to write at address. 393 | */ 394 | void minigb_apu_audio_write(struct minigb_apu_ctx *ctx, 395 | const uint16_t addr, const uint8_t val) 396 | { 397 | /* Find sound channel corresponding to register address. */ 398 | uint_fast8_t i; 399 | 400 | if(addr == 0xFF26) 401 | { 402 | ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val & 0x80; 403 | /* On APU power off, clear all registers apart from wave 404 | * RAM. */ 405 | if((val & 0x80) == 0) 406 | { 407 | memset(ctx->audio_mem, 408 | 0x00, 0xFF26 - AUDIO_ADDR_COMPENSATION); 409 | ctx->chans[0].enabled = false; 410 | ctx->chans[1].enabled = false; 411 | ctx->chans[2].enabled = false; 412 | ctx->chans[3].enabled = false; 413 | } 414 | 415 | return; 416 | } 417 | 418 | /* Ignore register writes if APU powered off. */ 419 | if(ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00) 420 | return; 421 | 422 | ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val; 423 | i = (addr - AUDIO_ADDR_COMPENSATION) / 5; 424 | 425 | switch (addr) { 426 | case 0xFF12: 427 | case 0xFF17: 428 | case 0xFF21: { 429 | ctx->chans[i].volume_init = val >> 4; 430 | ctx->chans[i].powered = (val >> 3) != 0; 431 | 432 | // "zombie mode" stuff, needed for Prehistorik Man and probably 433 | // others 434 | if (ctx->chans[i].powered && ctx->chans[i].enabled) { 435 | if ((ctx->chans[i].env.step == 0 && ctx->chans[i].env.inc != 0)) { 436 | if (val & 0x08) { 437 | ctx->chans[i].volume++; 438 | } else { 439 | ctx->chans[i].volume += 2; 440 | } 441 | } else { 442 | ctx->chans[i].volume = 16 - ctx->chans[i].volume; 443 | } 444 | 445 | ctx->chans[i].volume &= 0x0F; 446 | ctx->chans[i].env.step = val & 0x07; 447 | } 448 | } break; 449 | 450 | case 0xFF1C: 451 | ctx->chans[i].volume = ctx->chans[i].volume_init = (val >> 5) & 0x03; 452 | break; 453 | 454 | case 0xFF11: 455 | case 0xFF16: 456 | case 0xFF20: { 457 | const uint8_t duty_lookup[] = { 0x10, 0x30, 0x3C, 0xCF }; 458 | ctx->chans[i].len.load = val & 0x3f; 459 | ctx->chans[i].square.duty = duty_lookup[val >> 6]; 460 | break; 461 | } 462 | 463 | case 0xFF1B: 464 | ctx->chans[i].len.load = val; 465 | break; 466 | 467 | case 0xFF13: 468 | case 0xFF18: 469 | case 0xFF1D: 470 | ctx->chans[i].freq &= 0xFF00; 471 | ctx->chans[i].freq |= val; 472 | break; 473 | 474 | case 0xFF1A: 475 | ctx->chans[i].powered = (val & 0x80) != 0; 476 | chan_enable(ctx, i, val & 0x80); 477 | break; 478 | 479 | case 0xFF14: 480 | case 0xFF19: 481 | case 0xFF1E: 482 | ctx->chans[i].freq &= 0x00FF; 483 | ctx->chans[i].freq |= ((val & 0x07) << 8); 484 | /* Intentional fall-through. */ 485 | case 0xFF23: 486 | ctx->chans[i].len.enabled = val & 0x40; 487 | if (val & 0x80) 488 | chan_trigger(ctx, i); 489 | 490 | break; 491 | 492 | case 0xFF22: 493 | ctx->chans[3].freq = val >> 4; 494 | ctx->chans[3].noise.lfsr_wide = !(val & 0x08); 495 | ctx->chans[3].noise.lfsr_div = val & 0x07; 496 | break; 497 | 498 | case 0xFF24: 499 | { 500 | ctx->vol_l = ((val >> 4) & 0x07); 501 | ctx->vol_r = (val & 0x07); 502 | break; 503 | } 504 | 505 | case 0xFF25: 506 | for (uint_fast8_t j = 0; j < 4; j++) { 507 | ctx->chans[j].on_left = (val >> (4 + j)) & 1; 508 | ctx->chans[j].on_right = (val >> j) & 1; 509 | } 510 | break; 511 | } 512 | } 513 | 514 | void minigb_apu_audio_init(struct minigb_apu_ctx *ctx) 515 | { 516 | /* Initialise channels and samples. */ 517 | memset(ctx->chans, 0, sizeof(ctx->chans)); 518 | ctx->chans[0].val = ctx->chans[1].val = -1; 519 | 520 | /* Initialise IO registers. */ 521 | { 522 | const uint8_t regs_init[] = { 0x80, 0xBF, 0xF3, 0xFF, 0x3F, 523 | 0xFF, 0x3F, 0x00, 0xFF, 0x3F, 524 | 0x7F, 0xFF, 0x9F, 0xFF, 0x3F, 525 | 0xFF, 0xFF, 0x00, 0x00, 0x3F, 526 | 0x77, 0xF3, 0xF1 }; 527 | 528 | for(uint_fast8_t i = 0; i < sizeof(regs_init); ++i) 529 | minigb_apu_audio_write(ctx, 0xFF10 + i, regs_init[i]); 530 | } 531 | 532 | /* Initialise Wave Pattern RAM. */ 533 | { 534 | const uint8_t wave_init[] = { 0xac, 0xdd, 0xda, 0x48, 535 | 0x36, 0x02, 0xcf, 0x16, 536 | 0x2c, 0x04, 0xe5, 0x2c, 537 | 0xac, 0xdd, 0xda, 0x48 }; 538 | 539 | for(uint_fast8_t i = 0; i < sizeof(wave_init); ++i) 540 | minigb_apu_audio_write(ctx, 0xFF30 + i, wave_init[i]); 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /examples/sdl2/minigb_apu/minigb_apu.h: -------------------------------------------------------------------------------- 1 | /** 2 | * minigb_apu is released under the terms listed within the LICENSE file. 3 | * 4 | * minigb_apu emulates the audio processing unit (APU) of the Game Boy. This 5 | * project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | #ifndef AUDIO_SAMPLE_RATE 13 | # define AUDIO_SAMPLE_RATE 32768 14 | #endif 15 | 16 | /* The audio output format is in platform native endian. */ 17 | #if defined(MINIGB_APU_AUDIO_FORMAT_S16SYS) 18 | typedef int16_t audio_sample_t; 19 | # define AUDIO_SAMPLE_MAX INT16_MAX 20 | # define AUDIO_SAMPLE_MIN INT16_MIN 21 | # define VOL_INIT_MAX (AUDIO_SAMPLE_MAX/8) 22 | # define VOL_INIT_MIN (AUDIO_SAMPLE_MIN/8) 23 | #elif defined(MINIGB_APU_AUDIO_FORMAT_S32SYS) 24 | typedef int32_t audio_sample_t; 25 | # define AUDIO_SAMPLE_MAX INT32_MAX 26 | # define AUDIO_SAMPLE_MIN INT32_MIN 27 | # define VOL_INIT_MAX (INT32_MAX/8) 28 | # define VOL_INIT_MIN (INT32_MIN/8) 29 | #else 30 | #error MiniGB APU: Invalid or unsupported audio format selected 31 | #endif 32 | 33 | #define DMG_CLOCK_FREQ 4194304.0 34 | #define SCREEN_REFRESH_CYCLES 70224.0 35 | #define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES) 36 | 37 | /* Number of audio samples in each channel. */ 38 | #define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC)) 39 | /* Number of audio channels. The audio output is in interleaved stereo format.*/ 40 | #define AUDIO_CHANNELS 2 41 | /* Number of audio samples output in each audio_callback call. */ 42 | #define AUDIO_SAMPLES_TOTAL (AUDIO_SAMPLES * 2) 43 | 44 | #define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1) 45 | #define AUDIO_ADDR_COMPENSATION 0xFF10 46 | 47 | struct chan_len_ctr { 48 | uint8_t load; 49 | uint8_t enabled; 50 | uint32_t counter; 51 | uint32_t inc; 52 | }; 53 | 54 | struct chan_vol_env { 55 | uint8_t step; 56 | uint8_t up; 57 | uint32_t counter; 58 | uint32_t inc; 59 | }; 60 | 61 | struct chan_freq_sweep { 62 | uint8_t rate; 63 | uint8_t shift; 64 | uint8_t down; 65 | uint16_t freq; 66 | uint32_t counter; 67 | uint32_t inc; 68 | }; 69 | 70 | struct chan { 71 | uint8_t enabled; 72 | uint8_t powered; 73 | uint8_t on_left; 74 | uint8_t on_right; 75 | 76 | uint8_t volume; 77 | uint8_t volume_init; 78 | 79 | uint16_t freq; 80 | uint32_t freq_counter; 81 | uint32_t freq_inc; 82 | 83 | int32_t val; 84 | 85 | struct chan_len_ctr len; 86 | struct chan_vol_env env; 87 | struct chan_freq_sweep sweep; 88 | 89 | union { 90 | struct { 91 | uint8_t duty; 92 | uint8_t duty_counter; 93 | } square; 94 | struct { 95 | uint16_t lfsr_reg; 96 | uint8_t lfsr_wide; 97 | uint8_t lfsr_div; 98 | } noise; 99 | struct { 100 | uint8_t sample; 101 | } wave; 102 | }; 103 | }; 104 | 105 | struct minigb_apu_ctx { 106 | struct chan chans[4]; 107 | int32_t vol_l, vol_r; 108 | 109 | /** 110 | * Memory holding audio registers between 0xFF10 and 0xFF3F inclusive. 111 | */ 112 | uint8_t audio_mem[AUDIO_MEM_SIZE]; 113 | }; 114 | 115 | /** 116 | * Fill allocated buffer "stream" with AUDIO_SAMPLES_TOTAL number of 16-bit 117 | * signed samples (native endian order) in stereo interleaved format. 118 | * Each call corresponds to the time taken for each VSYNC in the Game Boy. 119 | * 120 | * \param ctx Library context. Must be initialised with audio_init(). 121 | * \param stream Allocated pointer to store audio samples. Must be at least 122 | * AUDIO_SAMPLES_TOTAL in size. 123 | */ 124 | void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx, 125 | audio_sample_t *stream); 126 | 127 | /** 128 | * Read audio register at given address "addr". 129 | * \param ctx Library context. Must be initialised with audio_init(). 130 | * \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F, 131 | * inclusive. 132 | */ 133 | uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr); 134 | 135 | /** 136 | * Write "val" to audio register at given address "addr". 137 | * \param ctx Library context. Must be initialised with audio_init(). 138 | * \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F, 139 | * inclusive. 140 | * \param val Value to write to address. 141 | */ 142 | void minigb_apu_audio_write(struct minigb_apu_ctx *ctx, 143 | const uint16_t addr, const uint8_t val); 144 | 145 | /** 146 | * Initialise audio driver. 147 | * \param ctx Library context. 148 | */ 149 | void minigb_apu_audio_init(struct minigb_apu_ctx *ctx); 150 | -------------------------------------------------------------------------------- /examples/sdl2/peanut_sdl.c: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2018-2023 Mahyar Koshkouei 4 | * 5 | * An example of using the peanut_gb.h library. This example application uses 6 | * SDL2 to draw the screen and get input. 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | #include "SDL.h" 13 | 14 | #if defined(ENABLE_SOUND_BLARGG) 15 | # include "blargg_apu/audio.h" 16 | #elif defined(ENABLE_SOUND_MINIGB) 17 | # include "minigb_apu/minigb_apu.h" 18 | #endif 19 | 20 | uint8_t audio_read(uint16_t addr); 21 | void audio_write(uint16_t addr, uint8_t val); 22 | 23 | #include "../../peanut_gb.h" 24 | 25 | enum { 26 | LOG_CATERGORY_PEANUTSDL = SDL_LOG_CATEGORY_CUSTOM 27 | }; 28 | 29 | struct priv_t 30 | { 31 | /* Window context used to generate message boxes. */ 32 | SDL_Window *win; 33 | 34 | /* Pointer to allocated memory holding GB file. */ 35 | uint8_t *rom; 36 | /* Pointer to allocated memory holding save file. */ 37 | uint8_t *cart_ram; 38 | /* Size of the cart_ram in bytes. */ 39 | size_t save_size; 40 | /* Pointer to boot ROM binary if available. */ 41 | uint8_t *bootrom; 42 | 43 | /* Colour palette for each BG, OBJ0, and OBJ1. */ 44 | uint16_t selected_palette[3][4]; 45 | uint16_t fb[LCD_HEIGHT][LCD_WIDTH]; 46 | }; 47 | 48 | static struct minigb_apu_ctx apu; 49 | 50 | /** 51 | * Returns a byte from the ROM file at the given address. 52 | */ 53 | uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) 54 | { 55 | const struct priv_t * const p = gb->direct.priv; 56 | return p->rom[addr]; 57 | } 58 | 59 | /** 60 | * Returns a byte from the cartridge RAM at the given address. 61 | */ 62 | uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) 63 | { 64 | const struct priv_t * const p = gb->direct.priv; 65 | return p->cart_ram[addr]; 66 | } 67 | 68 | /** 69 | * Writes a given byte to the cartridge RAM at the given address. 70 | */ 71 | void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, 72 | const uint8_t val) 73 | { 74 | const struct priv_t * const p = gb->direct.priv; 75 | p->cart_ram[addr] = val; 76 | } 77 | 78 | uint8_t gb_bootrom_read(struct gb_s *gb, const uint_fast16_t addr) 79 | { 80 | const struct priv_t * const p = gb->direct.priv; 81 | return p->bootrom[addr]; 82 | } 83 | 84 | uint8_t audio_read(uint16_t addr) 85 | { 86 | return minigb_apu_audio_read(&apu, addr); 87 | } 88 | 89 | void audio_write(uint16_t addr, uint8_t val) 90 | { 91 | minigb_apu_audio_write(&apu, addr, val); 92 | } 93 | 94 | void audio_callback(void *ptr, uint8_t *data, int len) 95 | { 96 | minigb_apu_audio_callback(&apu, (void *)data); 97 | } 98 | 99 | void read_cart_ram_file(const char *save_file_name, uint8_t **dest, 100 | const size_t len) 101 | { 102 | SDL_RWops *f; 103 | 104 | /* If save file not required. */ 105 | if(len == 0) 106 | { 107 | *dest = NULL; 108 | return; 109 | } 110 | 111 | /* Allocate enough memory to hold save file. */ 112 | if((*dest = SDL_malloc(len)) == NULL) 113 | { 114 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 115 | SDL_LOG_PRIORITY_CRITICAL, 116 | "%d: %s", __LINE__, SDL_GetError()); 117 | exit(EXIT_FAILURE); 118 | } 119 | 120 | f = SDL_RWFromFile(save_file_name, "rb"); 121 | 122 | /* It doesn't matter if the save file doesn't exist. We initialise the 123 | * save memory allocated above. The save file will be created on exit. */ 124 | if(f == NULL) 125 | { 126 | SDL_memset(*dest, 0, len); 127 | return; 128 | } 129 | 130 | /* Read save file to allocated memory. */ 131 | SDL_RWread(f, *dest, sizeof(uint8_t), len); 132 | SDL_RWclose(f); 133 | } 134 | 135 | void write_cart_ram_file(const char *save_file_name, uint8_t **dest, 136 | const size_t len) 137 | { 138 | SDL_RWops *f; 139 | 140 | if(len == 0 || *dest == NULL) 141 | return; 142 | 143 | if((f = SDL_RWFromFile(save_file_name, "wb")) == NULL) 144 | { 145 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 146 | SDL_LOG_PRIORITY_CRITICAL, 147 | "Unable to open save file: %s", 148 | SDL_GetError()); 149 | return; 150 | } 151 | 152 | /* Record save file. */ 153 | SDL_RWwrite(f, *dest, sizeof(uint8_t), len); 154 | SDL_RWclose(f); 155 | 156 | return; 157 | } 158 | 159 | /** 160 | * Handles an error reported by the emulator. The emulator context may be used 161 | * to better understand why the error given in gb_err was reported. 162 | */ 163 | void gb_error(struct gb_s *gb, const enum gb_error_e gb_err, const uint16_t addr) 164 | { 165 | const char* gb_err_str[GB_INVALID_MAX] = { 166 | "UNKNOWN", 167 | "INVALID OPCODE", 168 | "INVALID READ", 169 | "INVALID WRITE", 170 | "" 171 | }; 172 | struct priv_t *priv = gb->direct.priv; 173 | char error_msg[256]; 174 | char location[64] = ""; 175 | uint8_t instr_byte; 176 | 177 | /* Record save file. */ 178 | write_cart_ram_file("recovery.sav", &priv->cart_ram, priv->save_size); 179 | 180 | if(addr >= 0x4000 && addr < 0x8000) 181 | { 182 | uint32_t rom_addr; 183 | rom_addr = (uint32_t)addr * (uint32_t)gb->selected_rom_bank; 184 | SDL_snprintf(location, sizeof(location), 185 | " (bank %d mode %d, file offset %u)", 186 | gb->selected_rom_bank, gb->cart_mode_select, rom_addr); 187 | } 188 | 189 | instr_byte = __gb_read(gb, addr); 190 | 191 | SDL_snprintf(error_msg, sizeof(error_msg), 192 | "Error: %s at 0x%04X%s with instruction %02X.\n" 193 | "Cart RAM saved to recovery.sav\n" 194 | "Exiting.\n", 195 | gb_err_str[gb_err], addr, location, instr_byte); 196 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 197 | SDL_LOG_PRIORITY_CRITICAL, 198 | "%s", error_msg); 199 | 200 | SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", error_msg, priv->win); 201 | 202 | /* Free memory and then exit. */ 203 | SDL_free(priv->cart_ram); 204 | SDL_free(priv->rom); 205 | exit(EXIT_FAILURE); 206 | } 207 | 208 | /** 209 | * Automatically assigns a colour palette to the game using a given game 210 | * checksum. 211 | * TODO: Not all checksums are programmed in yet because I'm lazy. 212 | */ 213 | void auto_assign_palette(struct priv_t *priv, uint8_t game_checksum) 214 | { 215 | size_t palette_bytes = 3 * 4 * sizeof(uint16_t); 216 | 217 | switch(game_checksum) 218 | { 219 | /* Balloon Kid and Tetris Blast */ 220 | case 0x71: 221 | case 0xFF: 222 | { 223 | const uint16_t palette[3][4] = 224 | { 225 | { 0x7FFF, 0x7E60, 0x7C00, 0x0000 }, /* OBJ0 */ 226 | { 0x7FFF, 0x7E60, 0x7C00, 0x0000 }, /* OBJ1 */ 227 | { 0x7FFF, 0x7E60, 0x7C00, 0x0000 } /* BG */ 228 | }; 229 | memcpy(priv->selected_palette, palette, palette_bytes); 230 | break; 231 | } 232 | 233 | /* Pokemon Yellow and Tetris */ 234 | case 0x15: 235 | case 0xDB: 236 | case 0x95: /* Not officially */ 237 | { 238 | const uint16_t palette[3][4] = 239 | { 240 | { 0x7FFF, 0x7FE0, 0x7C00, 0x0000 }, /* OBJ0 */ 241 | { 0x7FFF, 0x7FE0, 0x7C00, 0x0000 }, /* OBJ1 */ 242 | { 0x7FFF, 0x7FE0, 0x7C00, 0x0000 } /* BG */ 243 | }; 244 | memcpy(priv->selected_palette, palette, palette_bytes); 245 | break; 246 | } 247 | 248 | /* Donkey Kong */ 249 | case 0x19: 250 | { 251 | const uint16_t palette[3][4] = 252 | { 253 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, /* OBJ0 */ 254 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, /* OBJ1 */ 255 | { 0x7FFF, 0x7E60, 0x7C00, 0x0000 } /* BG */ 256 | }; 257 | memcpy(priv->selected_palette, palette, palette_bytes); 258 | break; 259 | } 260 | 261 | /* Pokemon Blue */ 262 | case 0x61: 263 | case 0x45: 264 | 265 | /* Pokemon Blue Star */ 266 | case 0xD8: 267 | { 268 | const uint16_t palette[3][4] = 269 | { 270 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, /* OBJ0 */ 271 | { 0x7FFF, 0x329F, 0x001F, 0x0000 }, /* OBJ1 */ 272 | { 0x7FFF, 0x329F, 0x001F, 0x0000 } /* BG */ 273 | }; 274 | memcpy(priv->selected_palette, palette, palette_bytes); 275 | break; 276 | } 277 | 278 | /* Pokemon Red */ 279 | case 0x14: 280 | { 281 | const uint16_t palette[3][4] = 282 | { 283 | { 0x7FFF, 0x3FE6, 0x0200, 0x0000 }, /* OBJ0 */ 284 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, /* OBJ1 */ 285 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 } /* BG */ 286 | }; 287 | memcpy(priv->selected_palette, palette, palette_bytes); 288 | break; 289 | } 290 | 291 | /* Pokemon Red Star */ 292 | case 0x8B: 293 | { 294 | const uint16_t palette[3][4] = 295 | { 296 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, /* OBJ0 */ 297 | { 0x7FFF, 0x329F, 0x001F, 0x0000 }, /* OBJ1 */ 298 | { 0x7FFF, 0x3FE6, 0x0200, 0x0000 } /* BG */ 299 | }; 300 | memcpy(priv->selected_palette, palette, palette_bytes); 301 | break; 302 | } 303 | 304 | /* Kirby */ 305 | case 0x27: 306 | case 0x49: 307 | case 0x5C: 308 | case 0xB3: 309 | { 310 | const uint16_t palette[3][4] = 311 | { 312 | { 0x7D8A, 0x6800, 0x3000, 0x0000 }, /* OBJ0 */ 313 | { 0x001F, 0x7FFF, 0x7FEF, 0x021F }, /* OBJ1 */ 314 | { 0x527F, 0x7FE0, 0x0180, 0x0000 } /* BG */ 315 | }; 316 | memcpy(priv->selected_palette, palette, palette_bytes); 317 | break; 318 | } 319 | 320 | /* Donkey Kong Land [1/2/III] */ 321 | case 0x18: 322 | case 0x6A: 323 | case 0x4B: 324 | case 0x6B: 325 | { 326 | const uint16_t palette[3][4] = 327 | { 328 | { 0x7F08, 0x7F40, 0x48E0, 0x2400 }, /* OBJ0 */ 329 | { 0x7FFF, 0x2EFF, 0x7C00, 0x001F }, /* OBJ1 */ 330 | { 0x7FFF, 0x463B, 0x2951, 0x0000 } /* BG */ 331 | }; 332 | memcpy(priv->selected_palette, palette, palette_bytes); 333 | break; 334 | } 335 | 336 | /* Link's Awakening */ 337 | case 0x70: 338 | { 339 | const uint16_t palette[3][4] = 340 | { 341 | { 0x7FFF, 0x03E0, 0x1A00, 0x0120 }, /* OBJ0 */ 342 | { 0x7FFF, 0x329F, 0x001F, 0x001F }, /* OBJ1 */ 343 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 } /* BG */ 344 | }; 345 | memcpy(priv->selected_palette, palette, palette_bytes); 346 | break; 347 | } 348 | 349 | /* Mega Man [1/2/3] & others I don't care about. */ 350 | case 0x01: 351 | case 0x10: 352 | case 0x29: 353 | case 0x52: 354 | case 0x5D: 355 | case 0x68: 356 | case 0x6D: 357 | case 0xF6: 358 | { 359 | const uint16_t palette[3][4] = 360 | { 361 | { 0x7FFF, 0x329F, 0x001F, 0x0000 }, /* OBJ0 */ 362 | { 0x7FFF, 0x3FE6, 0x0200, 0x0000 }, /* OBJ1 */ 363 | { 0x7FFF, 0x7EAC, 0x40C0, 0x0000 } /* BG */ 364 | }; 365 | memcpy(priv->selected_palette, palette, palette_bytes); 366 | break; 367 | } 368 | 369 | default: 370 | { 371 | const uint16_t palette[3][4] = 372 | { 373 | { 0x7FFF, 0x5294, 0x294A, 0x0000 }, 374 | { 0x7FFF, 0x5294, 0x294A, 0x0000 }, 375 | { 0x7FFF, 0x5294, 0x294A, 0x0000 } 376 | }; 377 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 378 | SDL_LOG_PRIORITY_INFO, 379 | "No palette found for 0x%02X.", game_checksum); 380 | memcpy(priv->selected_palette, palette, palette_bytes); 381 | } 382 | } 383 | } 384 | 385 | /** 386 | * Assigns a palette. This is used to allow the user to manually select a 387 | * different colour palette if one was not found automatically, or if the user 388 | * prefers a different colour palette. 389 | * selection is the requestion colour palette. This should be a maximum of 390 | * NUMBER_OF_PALETTES - 1. The default greyscale palette is selected otherwise. 391 | */ 392 | void manual_assign_palette(struct priv_t *priv, uint8_t selection) 393 | { 394 | #define NUMBER_OF_PALETTES 12 395 | size_t palette_bytes = 3 * 4 * sizeof(uint16_t); 396 | 397 | switch(selection) 398 | { 399 | /* 0x05 (Right) */ 400 | case 0: 401 | { 402 | const uint16_t palette[3][4] = 403 | { 404 | { 0x7FFF, 0x2BE0, 0x7D00, 0x0000 }, 405 | { 0x7FFF, 0x2BE0, 0x7D00, 0x0000 }, 406 | { 0x7FFF, 0x2BE0, 0x7D00, 0x0000 } 407 | }; 408 | memcpy(priv->selected_palette, palette, palette_bytes); 409 | break; 410 | } 411 | 412 | /* 0x07 (A + Down) */ 413 | case 1: 414 | { 415 | const uint16_t palette[3][4] = 416 | { 417 | { 0x7FFF, 0x7FE0, 0x7C00, 0x0000 }, 418 | { 0x7FFF, 0x7FE0, 0x7C00, 0x0000 }, 419 | { 0x7FFF, 0x7FE0, 0x7C00, 0x0000 } 420 | }; 421 | memcpy(priv->selected_palette, palette, palette_bytes); 422 | break; 423 | } 424 | 425 | /* 0x12 (Up) */ 426 | case 2: 427 | { 428 | const uint16_t palette[3][4] = 429 | { 430 | { 0x7FFF, 0x7EAC, 0x40C0, 0x0000 }, 431 | { 0x7FFF, 0x7EAC, 0x40C0, 0x0000 }, 432 | { 0x7FFF, 0x7EAC, 0x40C0, 0x0000 } 433 | }; 434 | memcpy(priv->selected_palette, palette, palette_bytes); 435 | break; 436 | } 437 | 438 | /* 0x13 (B + Right) */ 439 | case 3: 440 | { 441 | const uint16_t palette[3][4] = 442 | { 443 | { 0x0000, 0x0210, 0x7F60, 0x7FFF }, 444 | { 0x0000, 0x0210, 0x7F60, 0x7FFF }, 445 | { 0x0000, 0x0210, 0x7F60, 0x7FFF } 446 | }; 447 | memcpy(priv->selected_palette, palette, palette_bytes); 448 | break; 449 | } 450 | 451 | /* 0x16 (B + Left, DMG Palette) */ 452 | default: 453 | case 4: 454 | { 455 | const uint16_t palette[3][4] = 456 | { 457 | { 0x7FFF, 0x5294, 0x294A, 0x0000 }, 458 | { 0x7FFF, 0x5294, 0x294A, 0x0000 }, 459 | { 0x7FFF, 0x5294, 0x294A, 0x0000 } 460 | }; 461 | memcpy(priv->selected_palette, palette, palette_bytes); 462 | break; 463 | } 464 | 465 | /* 0x17 (Down) */ 466 | case 5: 467 | { 468 | const uint16_t palette[3][4] = 469 | { 470 | { 0x7FF4, 0x7E52, 0x4A5F, 0x0000 }, 471 | { 0x7FF4, 0x7E52, 0x4A5F, 0x0000 }, 472 | { 0x7FF4, 0x7E52, 0x4A5F, 0x0000 } 473 | }; 474 | memcpy(priv->selected_palette, palette, palette_bytes); 475 | break; 476 | } 477 | 478 | /* 0x19 (B + Up) */ 479 | case 6: 480 | { 481 | const uint16_t palette[3][4] = 482 | { 483 | { 0x7FFF, 0x7EAC, 0x40C0, 0x0000 }, 484 | { 0x7FFF, 0x7EAC, 0x40C0, 0x0000 }, 485 | { 0x7F98, 0x6670, 0x41A5, 0x2CC1 } 486 | }; 487 | memcpy(priv->selected_palette, palette, palette_bytes); 488 | break; 489 | } 490 | 491 | /* 0x1C (A + Right) */ 492 | case 7: 493 | { 494 | const uint16_t palette[3][4] = 495 | { 496 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, 497 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, 498 | { 0x7FFF, 0x3FE6, 0x0198, 0x0000 } 499 | }; 500 | memcpy(priv->selected_palette, palette, palette_bytes); 501 | break; 502 | } 503 | 504 | /* 0x0D (A + Left) */ 505 | case 8: 506 | { 507 | const uint16_t palette[3][4] = 508 | { 509 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, 510 | { 0x7FFF, 0x7EAC, 0x40C0, 0x0000 }, 511 | { 0x7FFF, 0x463B, 0x2951, 0x0000 } 512 | }; 513 | memcpy(priv->selected_palette, palette, palette_bytes); 514 | break; 515 | } 516 | 517 | /* 0x10 (A + Up) */ 518 | case 9: 519 | { 520 | const uint16_t palette[3][4] = 521 | { 522 | { 0x7FFF, 0x3FE6, 0x0200, 0x0000 }, 523 | { 0x7FFF, 0x329F, 0x001F, 0x0000 }, 524 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 } 525 | }; 526 | memcpy(priv->selected_palette, palette, palette_bytes); 527 | break; 528 | } 529 | 530 | /* 0x18 (Left) */ 531 | case 10: 532 | { 533 | const uint16_t palette[3][4] = 534 | { 535 | { 0x7FFF, 0x7E10, 0x48E7, 0x0000 }, 536 | { 0x7FFF, 0x3FE6, 0x0200, 0x0000 }, 537 | { 0x7FFF, 0x329F, 0x001F, 0x0000 } 538 | }; 539 | memcpy(priv->selected_palette, palette, palette_bytes); 540 | break; 541 | } 542 | 543 | /* 0x1A (B + Down) */ 544 | case 11: 545 | { 546 | const uint16_t palette[3][4] = 547 | { 548 | { 0x7FFF, 0x329F, 0x001F, 0x0000 }, 549 | { 0x7FFF, 0x3FE6, 0x0200, 0x0000 }, 550 | { 0x7FFF, 0x7FE0, 0x3D20, 0x0000 } 551 | }; 552 | memcpy(priv->selected_palette, palette, palette_bytes); 553 | break; 554 | } 555 | } 556 | 557 | return; 558 | } 559 | 560 | #if ENABLE_LCD 561 | /** 562 | * Draws scanline into framebuffer. 563 | */ 564 | void lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], 565 | const uint_fast8_t line) 566 | { 567 | struct priv_t *priv = gb->direct.priv; 568 | 569 | for(unsigned int x = 0; x < LCD_WIDTH; x++) 570 | { 571 | priv->fb[line][x] = priv->selected_palette 572 | [(pixels[x] & LCD_PALETTE_ALL) >> 4] 573 | [pixels[x] & 3]; 574 | } 575 | } 576 | #endif 577 | 578 | /** 579 | * Saves the LCD screen as a 15-bit BMP file. 580 | */ 581 | int save_lcd_bmp(struct gb_s* gb, uint16_t fb[LCD_HEIGHT][LCD_WIDTH]) 582 | { 583 | /* Should be enough to record up to 828 days worth of frames. */ 584 | static uint_fast32_t file_num = 0; 585 | char file_name[32]; 586 | char title_str[16]; 587 | SDL_RWops *f; 588 | int ret = -1; 589 | 590 | SDL_snprintf(file_name, 32, "%.16s_%010ld.bmp", 591 | gb_get_rom_name(gb, title_str), file_num); 592 | 593 | f = SDL_RWFromFile(file_name, "wb"); 594 | if(f == NULL) 595 | goto ret; 596 | 597 | const uint8_t bmp_hdr_rgb555[] = { 598 | 0x42, 0x4d, 0x36, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 599 | 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0xa0, 0x00, 600 | 0x00, 0x00, 0x70, 0xff, 0xff, 0xff, 0x01, 0x00, 0x10, 0x00, 601 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xb4, 0x00, 0x00, 0x00, 0x00, 602 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 603 | 0x00, 0x00, 0x00, 0x00 604 | }; 605 | 606 | SDL_RWwrite(f, bmp_hdr_rgb555, sizeof(uint8_t), sizeof(bmp_hdr_rgb555)); 607 | SDL_RWwrite(f, fb, sizeof(uint16_t), LCD_HEIGHT * LCD_WIDTH); 608 | ret = SDL_RWclose(f); 609 | 610 | file_num++; 611 | 612 | ret: 613 | return ret; 614 | } 615 | 616 | int main(int argc, char **argv) 617 | { 618 | struct gb_s gb; 619 | struct priv_t priv = 620 | { 621 | .rom = NULL, 622 | .cart_ram = NULL 623 | }; 624 | const double target_speed_ms = 1000.0 / VERTICAL_SYNC; 625 | double speed_compensation = 0.0; 626 | SDL_Window *window; 627 | SDL_Renderer *renderer; 628 | SDL_Texture *texture; 629 | SDL_Event event; 630 | SDL_GameController *controller = NULL; 631 | uint_fast32_t new_ticks, old_ticks; 632 | enum gb_init_error_e gb_ret; 633 | unsigned int fast_mode = 1; 634 | unsigned int fast_mode_timer = 1; 635 | /* Record save file every 60 seconds. */ 636 | int save_timer = 60; 637 | /* Must be freed */ 638 | char *rom_file_name = NULL; 639 | char *save_file_name = NULL; 640 | int ret = EXIT_SUCCESS; 641 | 642 | SDL_LogSetPriority(LOG_CATERGORY_PEANUTSDL, SDL_LOG_PRIORITY_INFO); 643 | 644 | /* Enable Hi-DPI to stop blurry game image. */ 645 | #ifdef SDL_HINT_WINDOWS_DPI_AWARENESS 646 | SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); 647 | #endif 648 | 649 | /* Initialise frontend implementation, in this case, SDL2. */ 650 | if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) < 0) 651 | { 652 | char buf[128]; 653 | SDL_snprintf(buf, sizeof(buf), 654 | "Unable to initialise SDL2: %s", SDL_GetError()); 655 | SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", buf, NULL); 656 | ret = EXIT_FAILURE; 657 | goto out; 658 | } 659 | 660 | window = SDL_CreateWindow("Peanut-SDL: Opening File", 661 | SDL_WINDOWPOS_CENTERED, 662 | SDL_WINDOWPOS_CENTERED, 663 | LCD_WIDTH * 2, LCD_HEIGHT * 2, 664 | SDL_WINDOW_RESIZABLE | SDL_WINDOW_INPUT_FOCUS); 665 | 666 | if(window == NULL) 667 | { 668 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 669 | SDL_LOG_PRIORITY_CRITICAL, 670 | "Could not create window: %s", 671 | SDL_GetError()); 672 | ret = EXIT_FAILURE; 673 | goto out; 674 | } 675 | priv.win = window; 676 | 677 | switch(argc) 678 | { 679 | case 1: 680 | SDL_SetWindowTitle(window, "Drag and drop ROM"); 681 | do 682 | { 683 | SDL_Delay(10); 684 | SDL_PollEvent(&event); 685 | 686 | switch(event.type) 687 | { 688 | case SDL_DROPFILE: 689 | rom_file_name = event.drop.file; 690 | break; 691 | 692 | case SDL_QUIT: 693 | ret = EXIT_FAILURE; 694 | goto out; 695 | 696 | default: 697 | break; 698 | } 699 | } while(rom_file_name == NULL); 700 | 701 | break; 702 | 703 | case 2: 704 | /* Apply file name to rom_file_name 705 | * Set save_file_name to NULL. */ 706 | rom_file_name = argv[1]; 707 | break; 708 | 709 | case 3: 710 | /* Apply file name to rom_file_name 711 | * Apply save name to save_file_name */ 712 | rom_file_name = argv[1]; 713 | save_file_name = argv[2]; 714 | break; 715 | 716 | default: 717 | #if ENABLE_FILE_GUI 718 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 719 | SDL_LOG_PRIORITY_CRITICAL, 720 | "Usage: %s [ROM] [SAVE]", argv[0]); 721 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 722 | SDL_LOG_PRIORITY_CRITICAL, 723 | "A file picker is presented if ROM is not given."); 724 | #else 725 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 726 | SDL_LOG_PRIORITY_CRITICAL, 727 | "Usage: %s ROM [SAVE]\n", argv[0]); 728 | #endif 729 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 730 | SDL_LOG_PRIORITY_CRITICAL, 731 | "SAVE is set by default if not provided."); 732 | ret = EXIT_FAILURE; 733 | goto out; 734 | } 735 | 736 | /* Copy input ROM file to allocated memory. */ 737 | if((priv.rom = SDL_LoadFile(rom_file_name, NULL)) == NULL) 738 | { 739 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 740 | SDL_LOG_PRIORITY_CRITICAL, 741 | "%d: %s", __LINE__, SDL_GetError()); 742 | ret = EXIT_FAILURE; 743 | goto out; 744 | } 745 | 746 | /* If no save file is specified, copy save file (with specific name) to 747 | * allocated memory. */ 748 | if(save_file_name == NULL) 749 | { 750 | char *str_replace; 751 | const char extension[] = ".sav"; 752 | 753 | /* Allocate enough space for the ROM file name, for the "sav" 754 | * extension and for the null terminator. */ 755 | save_file_name = SDL_malloc( 756 | SDL_strlen(rom_file_name) + SDL_strlen(extension) + 1); 757 | 758 | if(save_file_name == NULL) 759 | { 760 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 761 | SDL_LOG_PRIORITY_CRITICAL, 762 | "%d: %s", __LINE__, SDL_GetError()); 763 | ret = EXIT_FAILURE; 764 | goto out; 765 | } 766 | 767 | /* Copy the ROM file name to allocated space. */ 768 | strcpy(save_file_name, rom_file_name); 769 | 770 | /* If the file name does not have a dot, or the only dot is at 771 | * the start of the file name, set the pointer to begin 772 | * replacing the string to the end of the file name, otherwise 773 | * set it to the dot. */ 774 | if((str_replace = strrchr(save_file_name, '.')) == NULL || 775 | str_replace == save_file_name) 776 | str_replace = save_file_name + strlen(save_file_name); 777 | 778 | /* Copy extension to string including terminating null byte. */ 779 | for(unsigned int i = 0; i <= strlen(extension); i++) 780 | *(str_replace++) = extension[i]; 781 | } 782 | 783 | /* TODO: Sanity check input GB file. */ 784 | 785 | /* Initialise emulator context. */ 786 | gb_ret = gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write, 787 | &gb_error, &priv); 788 | 789 | switch(gb_ret) 790 | { 791 | case GB_INIT_NO_ERROR: 792 | break; 793 | 794 | case GB_INIT_CARTRIDGE_UNSUPPORTED: 795 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 796 | SDL_LOG_PRIORITY_CRITICAL, 797 | "Unsupported cartridge."); 798 | ret = EXIT_FAILURE; 799 | goto out; 800 | 801 | case GB_INIT_INVALID_CHECKSUM: 802 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 803 | SDL_LOG_PRIORITY_CRITICAL, 804 | "Invalid ROM: Checksum failure."); 805 | ret = EXIT_FAILURE; 806 | goto out; 807 | 808 | default: 809 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 810 | SDL_LOG_PRIORITY_CRITICAL, 811 | "Unknown error: %d", gb_ret); 812 | ret = EXIT_FAILURE; 813 | goto out; 814 | } 815 | 816 | /* Copy dmg_boot.bin boot ROM file to allocated memory. */ 817 | if((priv.bootrom = SDL_LoadFile("dmg_boot.bin", NULL)) == NULL) 818 | { 819 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 820 | SDL_LOG_PRIORITY_INFO, 821 | "No dmg_boot.bin file found; disabling boot ROM"); 822 | } 823 | else 824 | { 825 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 826 | SDL_LOG_PRIORITY_INFO, 827 | "boot ROM enabled"); 828 | gb_set_bootrom(&gb, gb_bootrom_read); 829 | gb_reset(&gb); 830 | } 831 | 832 | /* Load Save File. */ 833 | if(gb_get_save_size_s(&gb, &priv.save_size) < 0) 834 | { 835 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 836 | SDL_LOG_PRIORITY_CRITICAL, 837 | "Unable to get save size: %s", 838 | SDL_GetError()); 839 | ret = EXIT_FAILURE; 840 | goto out; 841 | } 842 | 843 | /* Only attempt to load a save file if the ROM actually supports saves.*/ 844 | if(priv.save_size > 0) 845 | read_cart_ram_file(save_file_name, &priv.cart_ram, priv.save_size); 846 | 847 | /* Set the RTC of the game cartridge. Only used by games that support it. */ 848 | { 849 | time_t rawtime; 850 | time(&rawtime); 851 | #ifdef _POSIX_C_SOURCE 852 | struct tm timeinfo; 853 | localtime_r(&rawtime, &timeinfo); 854 | #else 855 | struct tm *timeinfo; 856 | timeinfo = localtime(&rawtime); 857 | #endif 858 | 859 | /* You could potentially force the game to allow the player to 860 | * reset the time by setting the RTC to invalid values. 861 | * 862 | * Using memset(&gb->cart_rtc, 0xFF, sizeof(gb->cart_rtc)) for 863 | * example causes Pokemon Gold/Silver to say "TIME NOT SET", 864 | * allowing the player to set the time without having some dumb 865 | * password. 866 | * 867 | * The memset has to be done directly to gb->cart_rtc because 868 | * gb_set_rtc() processes the input values, which may cause 869 | * games to not detect invalid values. 870 | */ 871 | 872 | /* Set RTC. Only games that specify support for RTC will use 873 | * these values. */ 874 | #ifdef _POSIX_C_SOURCE 875 | gb_set_rtc(&gb, &timeinfo); 876 | #else 877 | gb_set_rtc(&gb, timeinfo); 878 | #endif 879 | } 880 | 881 | #if ENABLE_SOUND 882 | SDL_AudioDeviceID dev; 883 | #endif 884 | 885 | #if ENABLE_SOUND == 0 886 | // Sound is disabled, so do nothing. 887 | #elif defined(ENABLE_SOUND_BLARGG) 888 | audio_init(&dev); 889 | #elif defined(ENABLE_SOUND_MINIGB) 890 | { 891 | SDL_AudioSpec want, have; 892 | 893 | want.freq = AUDIO_SAMPLE_RATE; 894 | want.format = AUDIO_S16, 895 | want.channels = 2; 896 | want.samples = AUDIO_SAMPLES; 897 | want.callback = audio_callback; 898 | want.userdata = NULL; 899 | 900 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 901 | SDL_LOG_PRIORITY_INFO, 902 | "Audio driver: %s", 903 | SDL_GetAudioDeviceName(0, 0)); 904 | 905 | if((dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0)) == 0) 906 | { 907 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 908 | SDL_LOG_PRIORITY_CRITICAL, 909 | "SDL could not open audio device: %s", 910 | SDL_GetError()); 911 | exit(EXIT_FAILURE); 912 | } 913 | 914 | minigb_apu_audio_init(&apu); 915 | SDL_PauseAudioDevice(dev, 0); 916 | } 917 | #endif 918 | 919 | #if ENABLE_LCD 920 | gb_init_lcd(&gb, &lcd_draw_line); 921 | #endif 922 | 923 | /* Allow the joystick input even if game is in background. */ 924 | SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); 925 | 926 | if(SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt") < 0) 927 | { 928 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 929 | SDL_LOG_PRIORITY_INFO, 930 | "Unable to assign joystick mappings: %s\n", 931 | SDL_GetError()); 932 | } 933 | 934 | /* Open the first available controller. */ 935 | for(int i = 0; i < SDL_NumJoysticks(); i++) 936 | { 937 | if(!SDL_IsGameController(i)) 938 | continue; 939 | 940 | controller = SDL_GameControllerOpen(i); 941 | 942 | if(controller) 943 | { 944 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 945 | SDL_LOG_PRIORITY_INFO, 946 | "Game Controller %s connected.", 947 | SDL_GameControllerName(controller)); 948 | break; 949 | } 950 | else 951 | { 952 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 953 | SDL_LOG_PRIORITY_INFO, 954 | "Could not open game controller %i: %s\n", 955 | i, SDL_GetError()); 956 | } 957 | } 958 | 959 | { 960 | /* 12 for "Peanut-SDL: " and a maximum of 16 for the title. */ 961 | char title_str[28] = "Peanut-SDL: "; 962 | gb_get_rom_name(&gb, title_str + 12); 963 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 964 | SDL_LOG_PRIORITY_INFO, 965 | "%s", 966 | title_str); 967 | SDL_SetWindowTitle(window, title_str); 968 | } 969 | 970 | SDL_SetWindowMinimumSize(window, LCD_WIDTH, LCD_HEIGHT); 971 | 972 | renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC); 973 | if(renderer == NULL) 974 | { 975 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 976 | SDL_LOG_PRIORITY_CRITICAL, 977 | "Could not create renderer: %s", 978 | SDL_GetError()); 979 | ret = EXIT_FAILURE; 980 | goto out; 981 | } 982 | 983 | if(SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) < 0) 984 | { 985 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 986 | SDL_LOG_PRIORITY_CRITICAL, 987 | "Renderer could not draw color: %s", 988 | SDL_GetError()); 989 | ret = EXIT_FAILURE; 990 | goto out; 991 | } 992 | 993 | if(SDL_RenderClear(renderer) < 0) 994 | { 995 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 996 | SDL_LOG_PRIORITY_CRITICAL, 997 | "Renderer could not clear: %s", 998 | SDL_GetError()); 999 | ret = EXIT_FAILURE; 1000 | goto out; 1001 | } 1002 | 1003 | SDL_RenderPresent(renderer); 1004 | 1005 | /* Use integer scale. */ 1006 | SDL_RenderSetLogicalSize(renderer, LCD_WIDTH, LCD_HEIGHT); 1007 | SDL_RenderSetIntegerScale(renderer, 1); 1008 | 1009 | texture = SDL_CreateTexture(renderer, 1010 | SDL_PIXELFORMAT_RGB555, 1011 | SDL_TEXTUREACCESS_STREAMING, 1012 | LCD_WIDTH, LCD_HEIGHT); 1013 | 1014 | if(texture == NULL) 1015 | { 1016 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 1017 | SDL_LOG_PRIORITY_CRITICAL, 1018 | "Texture could not be created: %s", 1019 | SDL_GetError()); 1020 | ret = EXIT_FAILURE; 1021 | goto out; 1022 | } 1023 | 1024 | auto_assign_palette(&priv, gb_colour_hash(&gb)); 1025 | 1026 | while(SDL_QuitRequested() == SDL_FALSE) 1027 | { 1028 | int delay; 1029 | static double rtc_timer = 0; 1030 | static unsigned int selected_palette = 3; 1031 | static unsigned int dump_bmp = 0; 1032 | 1033 | /* Calculate the time taken to draw frame, then later add a 1034 | * delay to cap at 60 fps. */ 1035 | old_ticks = SDL_GetTicks(); 1036 | 1037 | /* Get joypad input. */ 1038 | while(SDL_PollEvent(&event)) 1039 | { 1040 | static int fullscreen = 0; 1041 | 1042 | switch(event.type) 1043 | { 1044 | case SDL_QUIT: 1045 | goto quit; 1046 | 1047 | case SDL_CONTROLLERBUTTONDOWN: 1048 | switch(event.cbutton.button) 1049 | { 1050 | case SDL_CONTROLLER_BUTTON_A: 1051 | gb.direct.joypad &= ~JOYPAD_A; 1052 | break; 1053 | 1054 | case SDL_CONTROLLER_BUTTON_B: 1055 | gb.direct.joypad &= ~JOYPAD_B; 1056 | break; 1057 | 1058 | case SDL_CONTROLLER_BUTTON_BACK: 1059 | gb.direct.joypad &= ~JOYPAD_SELECT; 1060 | break; 1061 | 1062 | case SDL_CONTROLLER_BUTTON_START: 1063 | gb.direct.joypad &= ~JOYPAD_START; 1064 | break; 1065 | 1066 | case SDL_CONTROLLER_BUTTON_DPAD_UP: 1067 | gb.direct.joypad &= ~JOYPAD_UP; 1068 | break; 1069 | 1070 | case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: 1071 | gb.direct.joypad &= ~JOYPAD_RIGHT; 1072 | break; 1073 | 1074 | case SDL_CONTROLLER_BUTTON_DPAD_DOWN: 1075 | gb.direct.joypad &= ~JOYPAD_DOWN; 1076 | break; 1077 | 1078 | case SDL_CONTROLLER_BUTTON_DPAD_LEFT: 1079 | gb.direct.joypad &= ~JOYPAD_LEFT; 1080 | break; 1081 | } 1082 | 1083 | break; 1084 | 1085 | case SDL_CONTROLLERBUTTONUP: 1086 | switch(event.cbutton.button) 1087 | { 1088 | case SDL_CONTROLLER_BUTTON_A: 1089 | gb.direct.joypad |= JOYPAD_A; 1090 | break; 1091 | 1092 | case SDL_CONTROLLER_BUTTON_B: 1093 | gb.direct.joypad |= JOYPAD_B; 1094 | break; 1095 | 1096 | case SDL_CONTROLLER_BUTTON_BACK: 1097 | gb.direct.joypad |= JOYPAD_SELECT; 1098 | break; 1099 | 1100 | case SDL_CONTROLLER_BUTTON_START: 1101 | gb.direct.joypad |= JOYPAD_START; 1102 | break; 1103 | 1104 | case SDL_CONTROLLER_BUTTON_DPAD_UP: 1105 | gb.direct.joypad |= JOYPAD_UP; 1106 | break; 1107 | 1108 | case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: 1109 | gb.direct.joypad |= JOYPAD_RIGHT; 1110 | break; 1111 | 1112 | case SDL_CONTROLLER_BUTTON_DPAD_DOWN: 1113 | gb.direct.joypad |= JOYPAD_DOWN; 1114 | break; 1115 | 1116 | case SDL_CONTROLLER_BUTTON_DPAD_LEFT: 1117 | gb.direct.joypad |= JOYPAD_LEFT; 1118 | break; 1119 | } 1120 | 1121 | break; 1122 | 1123 | case SDL_KEYDOWN: 1124 | switch(event.key.keysym.sym) 1125 | { 1126 | case SDLK_RETURN: 1127 | gb.direct.joypad &= ~JOYPAD_START; 1128 | break; 1129 | 1130 | case SDLK_BACKSPACE: 1131 | gb.direct.joypad &= ~JOYPAD_SELECT; 1132 | break; 1133 | 1134 | case SDLK_z: 1135 | gb.direct.joypad &= ~JOYPAD_A; 1136 | break; 1137 | 1138 | case SDLK_x: 1139 | gb.direct.joypad &= ~JOYPAD_B; 1140 | break; 1141 | 1142 | case SDLK_a: 1143 | gb.direct.joypad ^= JOYPAD_A; 1144 | break; 1145 | 1146 | case SDLK_s: 1147 | gb.direct.joypad ^= JOYPAD_B; 1148 | break; 1149 | 1150 | case SDLK_UP: 1151 | gb.direct.joypad &= ~JOYPAD_UP; 1152 | break; 1153 | 1154 | case SDLK_RIGHT: 1155 | gb.direct.joypad &= ~JOYPAD_RIGHT; 1156 | break; 1157 | 1158 | case SDLK_DOWN: 1159 | gb.direct.joypad &= ~JOYPAD_DOWN; 1160 | break; 1161 | 1162 | case SDLK_LEFT: 1163 | gb.direct.joypad &= ~JOYPAD_LEFT; 1164 | break; 1165 | 1166 | case SDLK_SPACE: 1167 | fast_mode = 2; 1168 | break; 1169 | 1170 | case SDLK_1: 1171 | fast_mode = 1; 1172 | break; 1173 | 1174 | case SDLK_2: 1175 | fast_mode = 2; 1176 | break; 1177 | 1178 | case SDLK_3: 1179 | fast_mode = 3; 1180 | break; 1181 | 1182 | case SDLK_4: 1183 | fast_mode = 4; 1184 | break; 1185 | 1186 | case SDLK_r: 1187 | gb_reset(&gb); 1188 | break; 1189 | #if ENABLE_LCD 1190 | 1191 | case SDLK_i: 1192 | gb.direct.interlace = !gb.direct.interlace; 1193 | break; 1194 | 1195 | case SDLK_o: 1196 | gb.direct.frame_skip = !gb.direct.frame_skip; 1197 | break; 1198 | 1199 | case SDLK_b: 1200 | dump_bmp = ~dump_bmp; 1201 | 1202 | if(dump_bmp) 1203 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 1204 | SDL_LOG_PRIORITY_INFO, 1205 | "Dumping frames"); 1206 | else 1207 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 1208 | SDL_LOG_PRIORITY_INFO, 1209 | "Stopped dumping frames"); 1210 | 1211 | break; 1212 | #endif 1213 | 1214 | case SDLK_p: 1215 | if(event.key.keysym.mod == KMOD_LSHIFT) 1216 | { 1217 | auto_assign_palette(&priv, gb_colour_hash(&gb)); 1218 | break; 1219 | } 1220 | 1221 | if(++selected_palette == NUMBER_OF_PALETTES) 1222 | selected_palette = 0; 1223 | 1224 | manual_assign_palette(&priv, selected_palette); 1225 | break; 1226 | } 1227 | 1228 | break; 1229 | 1230 | case SDL_KEYUP: 1231 | switch(event.key.keysym.sym) 1232 | { 1233 | case SDLK_RETURN: 1234 | gb.direct.joypad |= JOYPAD_START; 1235 | break; 1236 | 1237 | case SDLK_BACKSPACE: 1238 | gb.direct.joypad |= JOYPAD_SELECT; 1239 | break; 1240 | 1241 | case SDLK_z: 1242 | gb.direct.joypad |= JOYPAD_A; 1243 | break; 1244 | 1245 | case SDLK_x: 1246 | gb.direct.joypad |= JOYPAD_B; 1247 | break; 1248 | 1249 | case SDLK_a: 1250 | gb.direct.joypad |= JOYPAD_A; 1251 | break; 1252 | 1253 | case SDLK_s: 1254 | gb.direct.joypad |= JOYPAD_B; 1255 | break; 1256 | 1257 | case SDLK_UP: 1258 | gb.direct.joypad |= JOYPAD_UP; 1259 | break; 1260 | 1261 | case SDLK_RIGHT: 1262 | gb.direct.joypad |= JOYPAD_RIGHT; 1263 | break; 1264 | 1265 | case SDLK_DOWN: 1266 | gb.direct.joypad |= JOYPAD_DOWN; 1267 | break; 1268 | 1269 | case SDLK_LEFT: 1270 | gb.direct.joypad |= JOYPAD_LEFT; 1271 | break; 1272 | 1273 | case SDLK_SPACE: 1274 | fast_mode = 1; 1275 | break; 1276 | 1277 | case SDLK_f: 1278 | if(fullscreen) 1279 | { 1280 | SDL_SetWindowFullscreen(window, 0); 1281 | fullscreen = 0; 1282 | SDL_ShowCursor(SDL_ENABLE); 1283 | } 1284 | else 1285 | { 1286 | SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); 1287 | fullscreen = SDL_WINDOW_FULLSCREEN_DESKTOP; 1288 | SDL_ShowCursor(SDL_DISABLE); 1289 | } 1290 | break; 1291 | 1292 | case SDLK_F11: 1293 | { 1294 | if(fullscreen) 1295 | { 1296 | SDL_SetWindowFullscreen(window, 0); 1297 | fullscreen = 0; 1298 | SDL_ShowCursor(SDL_ENABLE); 1299 | } 1300 | else 1301 | { 1302 | SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN); 1303 | fullscreen = SDL_WINDOW_FULLSCREEN; 1304 | SDL_ShowCursor(SDL_DISABLE); 1305 | } 1306 | } 1307 | break; 1308 | } 1309 | 1310 | break; 1311 | } 1312 | } 1313 | 1314 | /* Execute CPU cycles until the screen has to be redrawn. */ 1315 | gb_run_frame(&gb); 1316 | 1317 | /* Tick the internal RTC when 1 second has passed. */ 1318 | rtc_timer += target_speed_ms / (double) fast_mode; 1319 | 1320 | if(rtc_timer >= 1000.0) 1321 | { 1322 | rtc_timer -= 1000.0; 1323 | gb_tick_rtc(&gb); 1324 | } 1325 | 1326 | /* Skip frames during fast mode. */ 1327 | if(fast_mode_timer > 1) 1328 | { 1329 | fast_mode_timer--; 1330 | /* We continue here since the rest of the logic in the 1331 | * loop is for drawing the screen and delaying. */ 1332 | continue; 1333 | } 1334 | 1335 | fast_mode_timer = fast_mode; 1336 | 1337 | #if ENABLE_SOUND_BLARGG 1338 | /* Process audio. */ 1339 | audio_frame(); 1340 | #endif 1341 | 1342 | #if ENABLE_LCD 1343 | /* Copy frame buffer to SDL screen. */ 1344 | SDL_UpdateTexture(texture, NULL, &priv.fb, LCD_WIDTH * sizeof(uint16_t)); 1345 | SDL_RenderClear(renderer); 1346 | SDL_RenderCopy(renderer, texture, NULL, NULL); 1347 | SDL_RenderPresent(renderer); 1348 | 1349 | if(dump_bmp) 1350 | { 1351 | if(save_lcd_bmp(&gb, priv.fb) != 0) 1352 | { 1353 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 1354 | SDL_LOG_PRIORITY_ERROR, 1355 | "Failure dumping frame: %s", 1356 | SDL_GetError()); 1357 | dump_bmp = 0; 1358 | SDL_LogMessage(LOG_CATERGORY_PEANUTSDL, 1359 | SDL_LOG_PRIORITY_INFO, 1360 | "Stopped dumping frames"); 1361 | } 1362 | } 1363 | 1364 | #endif 1365 | 1366 | /* Use a delay that will draw the screen at a rate of 59.7275 Hz. */ 1367 | new_ticks = SDL_GetTicks(); 1368 | 1369 | /* Since we can only delay for a maximum resolution of 1ms, we 1370 | * can accumulate the error and compensate for the delay 1371 | * accuracy when the delay compensation surpasses 1ms. */ 1372 | speed_compensation += target_speed_ms - (new_ticks - old_ticks); 1373 | 1374 | /* We cast the delay compensation value to an integer, since it 1375 | * is the type used by SDL_Delay. This is where delay accuracy 1376 | * is lost. */ 1377 | delay = (int)(speed_compensation); 1378 | 1379 | /* We then subtract the actual delay value by the requested 1380 | * delay value. */ 1381 | speed_compensation -= delay; 1382 | 1383 | /* Only run delay logic if required. */ 1384 | if(delay > 0) 1385 | { 1386 | uint_fast32_t delay_ticks = SDL_GetTicks(); 1387 | uint_fast32_t after_delay_ticks; 1388 | 1389 | /* Tick the internal RTC when 1 second has passed. */ 1390 | rtc_timer += delay; 1391 | 1392 | if(rtc_timer >= 1000) 1393 | { 1394 | rtc_timer -= 1000; 1395 | gb_tick_rtc(&gb); 1396 | 1397 | /* If 60 seconds has passed, record save file. 1398 | * We do this because the blarrg audio library 1399 | * used contains asserts that will abort the 1400 | * program without save. 1401 | * TODO: Remove all workarounds due to faulty 1402 | * external libraries. */ 1403 | --save_timer; 1404 | 1405 | if(!save_timer) 1406 | { 1407 | #if ENABLE_SOUND_BLARGG 1408 | /* Locking the audio thread to reduce 1409 | * possibility of abort during save. */ 1410 | SDL_LockAudioDevice(dev); 1411 | #endif 1412 | write_cart_ram_file(save_file_name, 1413 | &priv.cart_ram, 1414 | priv.save_size); 1415 | #if ENABLE_SOUND_BLARGG 1416 | SDL_UnlockAudioDevice(dev); 1417 | #endif 1418 | save_timer = 60; 1419 | } 1420 | } 1421 | 1422 | /* This will delay for at least the number of 1423 | * milliseconds requested, so we have to compensate for 1424 | * error here too. */ 1425 | SDL_Delay(delay); 1426 | 1427 | after_delay_ticks = SDL_GetTicks(); 1428 | speed_compensation += (double)delay - 1429 | (int)(after_delay_ticks - delay_ticks); 1430 | } 1431 | } 1432 | 1433 | quit: 1434 | SDL_DestroyRenderer(renderer); 1435 | SDL_DestroyWindow(window); 1436 | SDL_DestroyTexture(texture); 1437 | SDL_GameControllerClose(controller); 1438 | SDL_Quit(); 1439 | #ifdef ENABLE_SOUND_BLARGG 1440 | audio_cleanup(); 1441 | #endif 1442 | 1443 | /* Record save file. */ 1444 | write_cart_ram_file(save_file_name, &priv.cart_ram, priv.save_size); 1445 | 1446 | out: 1447 | SDL_free(priv.rom); 1448 | SDL_free(priv.cart_ram); 1449 | 1450 | /* If the save file name was automatically generated (which required memory 1451 | * allocated on the help), then free it here. */ 1452 | if(argc == 2) 1453 | SDL_free(save_file_name); 1454 | 1455 | if(argc == 1) 1456 | SDL_free(rom_file_name); 1457 | 1458 | return ret; 1459 | } 1460 | -------------------------------------------------------------------------------- /screencaps/DRAGONBALL_BBZP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deltabeard/Peanut-GB/de54cb92302d99e4237963b6bdab45e0f808d213/screencaps/DRAGONBALL_BBZP.png -------------------------------------------------------------------------------- /screencaps/MEGAMANV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deltabeard/Peanut-GB/de54cb92302d99e4237963b6bdab45e0f808d213/screencaps/MEGAMANV.png -------------------------------------------------------------------------------- /screencaps/PKMN_BLUE.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deltabeard/Peanut-GB/de54cb92302d99e4237963b6bdab45e0f808d213/screencaps/PKMN_BLUE.gif -------------------------------------------------------------------------------- /screencaps/README.md: -------------------------------------------------------------------------------- 1 | # Screencaps 2 | 3 | These screencaps were made by using the "Dump BMP" feature of the Peanut-SDL 4 | example. This feature dumps each frame as a 24-bit bitmap in the running folder. 5 | This feature may be toggled on and off by pressing 'b' on the keyboard. 6 | 7 | To convert the output bitmaps to a GIF, I used the following commands: 8 | ``` 9 | # Generate palette for selected bitmap. This bitmap should have all the colours 10 | # that will appear in the sequence. This is to reduce the output file size of 11 | the GIF later. 12 | ffmpeg -y -i MEGAMANV_%010d.bmp -vf palettegen palette.png 13 | 14 | # Convert the bitmaps to GIF. 15 | ffmpeg -y -framerate 4194304/70224 -i MEGAMANV_%010d.bmp -i palette.png -filter_complex "paletteuse" -r 50 MEGAMANV.gif 16 | ``` 17 | 18 | 60 FPS is not possible with the GIF format, hence 50 FPS is used instead. Web browsers are limited to 50 FPS anyway. 19 | 20 | See: https://wunkolo.github.io/post/2020/02/buttery-smooth-10fps/ 21 | -------------------------------------------------------------------------------- /screencaps/SHANTAE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deltabeard/Peanut-GB/de54cb92302d99e4237963b6bdab45e0f808d213/screencaps/SHANTAE.png -------------------------------------------------------------------------------- /screencaps/ZELDA.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deltabeard/Peanut-GB/de54cb92302d99e4237963b6bdab45e0f808d213/screencaps/ZELDA.gif -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | peanut_gb.c 2 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | OPT=-g2 -O2 2 | 3 | override CFLAGS += $(OPT) -Wall -Wextra 4 | 5 | all: test test_so 6 | test: test.o 7 | $(CC) $< -o $@ $(CFLAGS) 8 | 9 | test_so: test.c peanut_gb.o 10 | $(CC) $^ -o $@ -DPEANUT_GB_HEADER_ONLY $(CFLAGS) 11 | 12 | test_external_rom: test_external_rom.c 13 | $(CC) $^ -o $@ $(CFLAGS) 14 | 15 | peanut_gb.o: ../peanut_gb.h 16 | cp ../peanut_gb.h ./peanut_gb.c 17 | $(CC) -c peanut_gb.c -o $@ $(CFLAGS) 18 | $(CC) -c peanut_gb.c -S -o $@.S $(CFLAGS) 19 | -------------------------------------------------------------------------------- /test/minctest.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * MINCTEST - Minimal C Test Library - 0.2.0 4 | * 5 | * Copyright (c) 2014-2017 Lewis Van Winkle 6 | * 7 | * http://CodePlea.com 8 | * 9 | * This software is provided 'as-is', without any express or implied 10 | * warranty. In no event will the authors be held liable for any damages 11 | * arising from the use of this software. 12 | * 13 | * Permission is granted to anyone to use this software for any purpose, 14 | * including commercial applications, and to alter it and redistribute it 15 | * freely, subject to the following restrictions: 16 | * 17 | * 1. The origin of this software must not be misrepresented; you must not 18 | * claim that you wrote the original software. If you use this software 19 | * in a product, an acknowledgement in the product documentation would be 20 | * appreciated but is not required. 21 | * 2. Altered source versions must be plainly marked as such, and must not be 22 | * misrepresented as being the original software. 23 | * 3. This notice may not be removed or altered from any source distribution. 24 | * 25 | */ 26 | 27 | 28 | 29 | /* 30 | * MINCTEST - Minimal testing library for C 31 | * 32 | * 33 | * Example: 34 | * 35 | * void test1() { 36 | * lok('a' == 'a'); 37 | * } 38 | * 39 | * void test2() { 40 | * lequal(5, 6); 41 | * lfequal(5.5, 5.6); 42 | * } 43 | * 44 | * int main() { 45 | * lrun("test1", test1); 46 | * lrun("test2", test2); 47 | * lresults(); 48 | * return lfails != 0; 49 | * } 50 | * 51 | * 52 | * 53 | * Hints: 54 | * All functions/variables start with the letter 'l'. 55 | * 56 | */ 57 | 58 | 59 | #ifndef __MINCTEST_H__ 60 | #define __MINCTEST_H__ 61 | 62 | #include 63 | #include 64 | #include 65 | #include 66 | 67 | 68 | /* How far apart can floats be before we consider them unequal. */ 69 | #ifndef LTEST_FLOAT_TOLERANCE 70 | #define LTEST_FLOAT_TOLERANCE 0.001 71 | #endif 72 | 73 | 74 | /* Track the number of passes, fails. */ 75 | /* NB this is made for all tests to be in one file. */ 76 | static int ltests = 0; 77 | static int lfails = 0; 78 | 79 | 80 | /* Display the test results. */ 81 | #define lresults() do {\ 82 | if (lfails == 0) {\ 83 | printf("ALL TESTS PASSED (%d/%d)\n", ltests, ltests);\ 84 | } else {\ 85 | printf("SOME TESTS FAILED (%d/%d)\n", ltests-lfails, ltests);\ 86 | }\ 87 | } while (0) 88 | 89 | 90 | /* Run a test. Name can be any string to print out, test is the function name to call. */ 91 | #define lrun(name, test) do {\ 92 | const int ts = ltests;\ 93 | const int fs = lfails;\ 94 | const clock_t start = clock();\ 95 | printf("%-14s\t", name);\ 96 | test();\ 97 | printf("\npass:%2d fail:%2d %4dms\n",\ 98 | (ltests-ts)-(lfails-fs), lfails-fs,\ 99 | (int)((clock() - start) * 1000 / CLOCKS_PER_SEC));\ 100 | } while (0) 101 | 102 | 103 | /* Assert a true statement. */ 104 | #define lok(test) do {\ 105 | ++ltests;\ 106 | if (!(test)) {\ 107 | ++lfails;\ 108 | printf("%s:%d error\t", __FILE__, __LINE__);\ 109 | }} while (0) 110 | 111 | 112 | /* Prototype to assert equal. */ 113 | #define lequal_base(equality, a, b, format) do {\ 114 | ++ltests;\ 115 | if (!(equality)) {\ 116 | ++lfails;\ 117 | printf("%s:%d ("format " != " format")\n", __FILE__, __LINE__, (a), (b));\ 118 | }} while (0) 119 | 120 | 121 | /* Assert two integers are equal. */ 122 | #define lequal(a, b)\ 123 | lequal_base((a) == (b), a, b, "%d") 124 | 125 | 126 | /* Assert two floats are equal (Within LTEST_FLOAT_TOLERANCE). */ 127 | #define lfequal(a, b)\ 128 | lequal_base(fabs((double)(a)-(double)(b)) <= LTEST_FLOAT_TOLERANCE\ 129 | && fabs((double)(a)-(double)(b)) == fabs((double)(a)-(double)(b)), (double)(a), (double)(b), "%f") 130 | 131 | 132 | /* Assert two strings are equal. */ 133 | #define lsequal(a, b)\ 134 | lequal_base(strcmp(a, b) == 0, a, b, "%s") 135 | 136 | 137 | #endif /*__MINCTEST_H__*/ 138 | -------------------------------------------------------------------------------- /test/test.c: -------------------------------------------------------------------------------- 1 | #include "minctest.h" 2 | 3 | #define ENABLE_SOUND 0 4 | #define ENABLE_LCD 1 5 | #include "../peanut_gb.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "dmg-acid2.gb.h" /* Generated via `xxd -i` */ 12 | 13 | /* Hash of correct LCD output for DMG-Acid2 Test. */ 14 | #define DMG_ACID2_HASH 0xF91DF416u 15 | 16 | struct priv 17 | { 18 | char str[1024]; 19 | unsigned int count; 20 | }; 21 | 22 | /* Frame buffer storage for the dmg-acid2 test. */ 23 | struct acid_priv 24 | { 25 | uint8_t fb[LCD_HEIGHT][LCD_WIDTH]; 26 | }; 27 | 28 | /* FNV-1a 32-bit hashing function used to check the LCD output. */ 29 | static uint32_t fnv1a_hash(const void *data, size_t len) 30 | { 31 | const uint8_t *bytes = data; 32 | /* 2166136261 is the standard 32‑bit FNV offset basis. */ 33 | uint32_t hash = 2166136261u; 34 | 35 | while(len--) 36 | { 37 | hash ^= *bytes++; 38 | /* 16777619 (0x01000193) is the 32‑bit FNV prime. Decimal is 39 | * used instead of hexadecimal for maximum portability. */ 40 | hash *= 16777619u; 41 | } 42 | 43 | return hash; 44 | } 45 | 46 | /** 47 | * Return byte from blarrg test ROM. 48 | */ 49 | uint8_t gb_rom_read_cpu_instrs(struct gb_s *gb, const uint_fast32_t addr) 50 | { 51 | #include "cpu_instrs.h" 52 | assert(addr < cpu_instrs_gb_len); 53 | return cpu_instrs_gb[addr]; 54 | } 55 | 56 | /** 57 | * Return byte from blarrg test ROM. 58 | */ 59 | uint8_t gb_rom_read_instr_timing(struct gb_s *gb, const uint_fast32_t addr) 60 | { 61 | #include "instr_timing.h" 62 | assert(addr < instr_timing_gb_len); 63 | return instr_timing_gb[addr]; 64 | } 65 | 66 | uint8_t gb_rom_read_acid(struct gb_s *gb, const uint_fast32_t addr) 67 | { 68 | (void)gb; 69 | assert(addr < dmg_acid2_gb_len); 70 | return dmg_acid2_gb[addr]; 71 | } 72 | 73 | /** 74 | * Ignore cart RAM writes, since the test doesn't require it. 75 | */ 76 | void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, const uint8_t val) 77 | { 78 | return; 79 | } 80 | 81 | /** 82 | * Ignore cart RAM reads, since the test doesn't require it. 83 | */ 84 | uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) 85 | { 86 | return 0xFF; 87 | } 88 | 89 | /** 90 | * Abort on any error. 91 | */ 92 | void gb_error(struct gb_s *gb, const enum gb_error_e gb_err, const uint16_t val) 93 | { 94 | abort(); 95 | } 96 | 97 | void gb_serial_tx(struct gb_s *gb, const uint8_t tx) 98 | { 99 | struct priv *p = gb->direct.priv; 100 | char c = tx; 101 | 102 | /* Do not save any more characters if buffer does not have room for 103 | * nul character. */ 104 | if(p->count == (1023 - 1)) 105 | return; 106 | 107 | /* Filter newlines to make test output cleaner. */ 108 | if(tx < 32) 109 | c = ' '; 110 | 111 | printf("%c", c); 112 | p->str[p->count++] = c; 113 | } 114 | 115 | static void acid_lcd_draw_line(struct gb_s *gb, const uint8_t *pixels, 116 | const uint_fast8_t line) 117 | { 118 | struct acid_priv *p = gb->direct.priv; 119 | memcpy(p->fb[line], pixels, LCD_WIDTH); 120 | } 121 | 122 | 123 | void test_cpu_inst(void) 124 | { 125 | struct gb_s gb; 126 | const unsigned short pc_end = 0x06F1; /* Test ends when PC is this value. */ 127 | struct priv p = { .count = 0 }; 128 | enum gb_init_error_e gb_err; 129 | 130 | /* Run ROM test. */ 131 | gb_err = gb_init(&gb, &gb_rom_read_cpu_instrs, &gb_cart_ram_read, 132 | &gb_cart_ram_write, &gb_error, &p); 133 | lok(gb_err == GB_INIT_NO_ERROR); 134 | if(gb_err != GB_INIT_NO_ERROR) 135 | return; 136 | 137 | gb_init_serial(&gb, &gb_serial_tx, NULL); 138 | 139 | printf("Serial: "); 140 | 141 | /* Step CPU until test is complete. */ 142 | while(gb.cpu_reg.pc.reg != pc_end) 143 | __gb_step_cpu(&gb); 144 | 145 | p.str[p.count++] = '\0'; 146 | 147 | /* Check test results. */ 148 | lok(strstr(p.str, "Passed all tests") != NULL); 149 | 150 | return; 151 | } 152 | 153 | void test_instr_timing(void) 154 | { 155 | struct gb_s gb; 156 | const unsigned short pc_end = 0xC8B0; /* Test ends when PC is this value. */ 157 | struct priv p = { .count = 0 }; 158 | enum gb_init_error_e gb_err; 159 | 160 | /* Run ROM test. */ 161 | gb_err = gb_init(&gb, &gb_rom_read_instr_timing, &gb_cart_ram_read, 162 | &gb_cart_ram_write, &gb_error, &p); 163 | lok(gb_err == GB_INIT_NO_ERROR); 164 | if(gb_err != GB_INIT_NO_ERROR) 165 | return; 166 | 167 | gb_init_serial(&gb, &gb_serial_tx, NULL); 168 | 169 | printf("Serial: "); 170 | 171 | /* Step CPU until test is complete. */ 172 | while(gb.cpu_reg.pc.reg != pc_end) 173 | __gb_step_cpu(&gb); 174 | 175 | p.str[p.count++] = '\0'; 176 | 177 | /* Check test results. */ 178 | lok(strstr(p.str, "Passed") != NULL); 179 | 180 | return; 181 | } 182 | 183 | void test_dmg_acid2(void) 184 | { 185 | struct gb_s gb; 186 | struct acid_priv p = {0}; 187 | enum gb_init_error_e gb_err; 188 | 189 | gb_err = gb_init(&gb, &gb_rom_read_acid, &gb_cart_ram_read, 190 | &gb_cart_ram_write, &gb_error, &p); 191 | lok(gb_err == GB_INIT_NO_ERROR); 192 | if(gb_err != GB_INIT_NO_ERROR) 193 | return; 194 | 195 | gb_init_lcd(&gb, acid_lcd_draw_line); 196 | 197 | for(unsigned int i = 0; i < 100; i++) 198 | gb_run_frame(&gb); 199 | 200 | { 201 | uint32_t hash = fnv1a_hash(&p.fb[0][0], 202 | LCD_WIDTH * LCD_HEIGHT); 203 | if(hash != DMG_ACID2_HASH) 204 | printf("dmg-acid2 LCD hash: 0x%08X\n", hash); 205 | lok(hash == DMG_ACID2_HASH); 206 | } 207 | } 208 | 209 | int main(void) 210 | { 211 | lrun("cpu_inst blarrg tests ", test_cpu_inst); 212 | lrun("instr_timing blarrg tests", test_instr_timing); 213 | lrun("dmg-acid2 lcd test ", test_dmg_acid2); 214 | return lfails != 0; 215 | } 216 | -------------------------------------------------------------------------------- /test/test_external_rom.c: -------------------------------------------------------------------------------- 1 | #define ENABLE_SOUND 0 2 | #define ENABLE_LCD 0 3 | #include "../peanut_gb.h" 4 | 5 | #include 6 | #include 7 | 8 | struct priv 9 | { 10 | uint8_t *rom; 11 | size_t rom_sz; 12 | }; 13 | 14 | /** 15 | * Return byte from ROM. 16 | */ 17 | uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) 18 | { 19 | struct priv *p = gb->direct.priv; 20 | assert(addr < p->rom_sz); 21 | return p->rom[addr]; 22 | } 23 | 24 | /** 25 | * Ignore cart RAM writes, since the test doesn't require it. 26 | */ 27 | void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, const uint8_t val) 28 | { 29 | return; 30 | } 31 | 32 | /** 33 | * Ignore cart RAM reads, since the test doesn't require it. 34 | */ 35 | uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) 36 | { 37 | return 0xFF; 38 | } 39 | 40 | /** 41 | * Abort on any error. 42 | */ 43 | void gb_error(struct gb_s *gb, const enum gb_error_e gb_err, const uint16_t val) 44 | { 45 | abort(); 46 | } 47 | 48 | void gb_serial_tx(struct gb_s *gb, const uint8_t tx) 49 | { 50 | char c = tx; 51 | 52 | /* Filter newlines to make test output cleaner. */ 53 | //if(tx < 32) 54 | // c = ' '; 55 | 56 | putchar(c); 57 | } 58 | 59 | /** 60 | * Returns a pointer to the allocated space containing the ROM. Must be freed. 61 | */ 62 | static uint8_t *read_rom_to_ram(const char *file_name, size_t *sz) 63 | { 64 | FILE *rom_file = fopen(file_name, "rb"); 65 | size_t rom_size; 66 | uint8_t *rom = NULL; 67 | 68 | if(rom_file == NULL) 69 | return NULL; 70 | 71 | fseek(rom_file, 0, SEEK_END); 72 | rom_size = ftell(rom_file); 73 | rewind(rom_file); 74 | rom = malloc(rom_size); 75 | 76 | if(fread(rom, sizeof(uint8_t), rom_size, rom_file) != rom_size) 77 | { 78 | free(rom); 79 | fclose(rom_file); 80 | return NULL; 81 | } 82 | 83 | fclose(rom_file); 84 | *sz = rom_size; 85 | return rom; 86 | } 87 | 88 | int main(int argc, char *argv[]) 89 | { 90 | struct gb_s gb; 91 | struct priv p; 92 | int ret = EXIT_FAILURE; 93 | char *rom_file_name; 94 | unsigned long frames; 95 | 96 | if(argc != 3) 97 | { 98 | printf("Usage: %s ROM FRAMES\n", argv[0]); 99 | goto err; 100 | } 101 | 102 | rom_file_name = argv[1]; 103 | frames = strtoul(argv[2], NULL, 10); 104 | 105 | /* Copy input ROM file to allocated memory. */ 106 | if((p.rom = read_rom_to_ram(rom_file_name, &p.rom_sz)) == NULL) 107 | { 108 | perror("ROM read failed"); 109 | goto err; 110 | } 111 | 112 | /* Initialise context. */ 113 | ret = gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, 114 | &gb_cart_ram_write, &gb_error, &p); 115 | 116 | if(ret != GB_INIT_NO_ERROR) 117 | { 118 | fprintf(stderr, "Peanut-GB failed to initialise: %d\n", 119 | ret); 120 | free(p.rom); 121 | goto err; 122 | } 123 | 124 | gb_init_serial(&gb, gb_serial_tx, NULL); 125 | #if ENABLE_LCD 126 | gb_init_lcd(&gb, &lcd_draw_line); 127 | // gb.direct.interlace = true; 128 | #endif 129 | 130 | do 131 | { 132 | /* Execute CPU cycles until the screen has to be 133 | * redrawn. */ 134 | gb_run_frame(&gb); 135 | } 136 | while(frames--); 137 | 138 | free(p.rom); 139 | putchar('\n'); 140 | 141 | ret = EXIT_SUCCESS; 142 | err: 143 | return ret; 144 | } 145 | -------------------------------------------------------------------------------- /version.all: -------------------------------------------------------------------------------- 1 | #if 0 2 | SET(PEANUTGB_VERSION_MAJOR 1) 3 | SET(PEANUTGB_VERSION_MINOR 3) 4 | SET(PEANUTGB_VERSION_PATCH 0) 5 | #endif 6 | 7 | #pragma once 8 | #define PEANUTGB_VERSION_MAJOR 1 9 | #define PEANUTGB_VERSION_MINOR 3 10 | #define PEANUTGB_VERSION_PATCH 0 11 | --------------------------------------------------------------------------------