diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 794eaac2..a8abcc29 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,10 +9,10 @@ jobs: run: shell: msys2 {0} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: msys2/setup-msys2@v2 with: - install: make mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-SDL2 mingw-w64-x86_64-glew mingw-w64-x86_64-qt5-static + install: make mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-python-jinja mingw-w64-x86_64-python-lxml mingw-w64-x86_64-SDL2 mingw-w64-x86_64-qt5-static - name: Build NanoBoyAdvance run: | cmake \ @@ -20,9 +20,9 @@ jobs: -G"Unix Makefiles" \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_CXX_FLAGS="-s" \ + -DPython_EXECUTABLE="$(which python3)" \ -DPLATFORM_QT_STATIC=ON \ -DUSE_STATIC_SDL=ON \ - -DGLEW_USE_STATIC_LIBS=ON \ -DRELEASE_BUILD=ON \ -DQT5_STATIC_DIR="/c/tools/msys64/mingw64/qt5-static" cd build @@ -32,7 +32,7 @@ jobs: mkdir upload cp -r build/bin/qt/{NanoBoyAdvance.exe,config.toml} upload - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: NanoBoyAdvance-win64 path: upload @@ -41,11 +41,11 @@ jobs: build-linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup dependencies run: | sudo apt-get update -qq - sudo apt-get install -y libsdl2-dev libglew-dev qtbase5-dev + sudo apt-get install -y python3-jinja2 python3-lxml libsdl2-dev qtbase5-dev - name: Build NanoBoyAdvance run: | cmake -Bbuild -DCMAKE_BUILD_TYPE=Release -DRELEASE_BUILD=ON @@ -56,7 +56,7 @@ jobs: mkdir upload cp -r build/bin/qt/{NanoBoyAdvance,config.toml} upload - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: NanoBoyAdvance-linux path: upload @@ -65,20 +65,71 @@ jobs: build-macOS: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' - name: Setup dependencies env: HOMEBREW_NO_ANALYTICS: 1 - run: brew install sdl2 glew qt@5 + run: | + brew install sdl2 qt@5 + python3 -m pip install Jinja2 - name: Build NanoBoyAdvance run: | cmake -Bbuild \ -DCMAKE_CXX_FLAGS="-s" \ -DUSE_STATIC_SDL=ON \ - -DGLEW_USE_STATIC_LIBS=ON \ -DCMAKE_BUILD_TYPE=Release \ +<<<<<<< HEAD -DRELEASE_BUILD=ON \ -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5);$(brew --prefix glew)" \ +======= + -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" \ +>>>>>>> f53171877606cd6916c5b13c022b06bde3e93aa5 + -DMACOS_BUILD_APP_BUNDLE=ON \ + -DMACOS_BUNDLE_QT=ON + cd build + make -j$(getconf _NPROCESSORS_ONLN) + - name: Create disk image + run: | + mkdir dmg + cp -a build/bin/qt/NanoBoyAdvance.app dmg/NanoBoyAdvance.app + codesign -s - --deep -f dmg/NanoBoyAdvance.app + ln -s /Applications dmg/Applications + hdiutil create \ + -fs HFS+ \ + -volname NanoBoyAdvance \ + -srcfolder dmg \ + -ov -format UDBZ \ + NanoBoyAdvance.dmg + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: NanoBoyAdvance-${{ runner.os }}-x64 + path: NanoBoyAdvance.dmg + if-no-files-found: error + + build-macOS-arm: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Setup dependencies + env: + HOMEBREW_NO_ANALYTICS: 1 + run: | + brew install sdl2 qt@5 + python3 -m pip install Jinja2 + - name: Build NanoBoyAdvance + run: | + cmake -Bbuild \ + -DCMAKE_CXX_FLAGS="-s" \ + -DUSE_STATIC_SDL=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" \ -DMACOS_BUILD_APP_BUNDLE=ON \ -DMACOS_BUNDLE_QT=ON cd build @@ -96,8 +147,8 @@ jobs: -ov -format UDBZ \ NanoBoyAdvance.dmg - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: NanoBoyAdvance-${{ runner.os }} + name: NanoBoyAdvance-${{ runner.os }}-arm64 path: NanoBoyAdvance.dmg if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e26c039..98f4154f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.11...3.28) -project(NanoBoyAdvance LANGUAGES CXX) +project(NanoBoyAdvance LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/README.md b/README.md index 7892bf8e..e9d47f42 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ NanoBoyAdvance is a cycle-accurate Game Boy Advance emulator.
It aims to be as accurate as possible, while also offering enhancements such as improved audio quality.
-![screenshot1](docs/screenshot.png) - ## Features - Very high compatibility and accuracy (see [Accuracy](#accuracy)) - HQ audio mixer (for games which use Nintendo's MusicPlayer2000 sound engine) diff --git a/docs/ACCURACY.md b/docs/ACCURACY.md index 6a89566e..0f948ef5 100644 --- a/docs/ACCURACY.md +++ b/docs/ACCURACY.md @@ -3,11 +3,11 @@ Comparison of NBA and other emulators on the [mGBA test suite](https://github.com/mgba-emu/suite) by endrift: -Testname | Test Count | NanoBoyAdvance 1.8 | mGBA 0.10.3 | VBA-M 2.1.9 | Ares v135 | SkyEmu V3 | +Testname | Test Count | NanoBoyAdvance 1.8 | mGBA 0.10.3 | VBA-M 2.1.9 | Ares v137 | SkyEmu V3 | --------------|------------|--------------------|-------------|----------------|-----------|------------| Memory | 1552 | 1552 | 1552 | 1426 | 1552 | 1552 | IO read | 130 | 130 | 120 | 100 | 130 | 130 | -Timing | 2020 | 2020 | 1768 | 1024 | 1608 | 2020 | +Timing | 2020 | 2020 | 1768 | 1024 | 1890 | 2020 | Timer | 936 | 936 | 744 | 440 | 465 | 706 | Timer IRQ | 90 | 90 | 70 | 8 | 0 | 90 | Shifter | 140 | 140 | 140 | 132 | 140 | 140 | @@ -15,7 +15,7 @@ Carry | 93 | 93 | 93 | 93 | Multiply Long | 72 | 52 | 52 | 52 | 52 | 52 | BIOS math | 615 | 615 | 615 | 615 | 615 | 615 | DMA tests | 1256 | 1256 | 1232 | 1068 | 1212 | 1256 | -Misc Edge Case| 10 | 9 | 4 | 8 | 1 | 4 | +Misc Edge Case| 10 | 10 | 4 | 8 | 1 | 4 | Layer Toggle | 1 | pass | fail | pass | fail | pass | OAM Update | 1 | pass | fail | fail | fail | pass | diff --git a/docs/COMPILING.md b/docs/COMPILING.md index 36462cc5..2706d9b9 100644 --- a/docs/COMPILING.md +++ b/docs/COMPILING.md @@ -4,10 +4,10 @@ NanoBoyAdvance can be compiled on Windows, Linux, and macOS. - Clang or GCC with C++17 support - CMake 3.11 or higher +- Python modules Jinja and (optionally) lxml - OpenGL (usually provided by the operating system) -- SDL2 library -- GLEW library -- Qt5 library +- SDL 2 library +- Qt 5 library ### Source Code @@ -28,13 +28,13 @@ Here is a list of commands for popular distributions and macOS: ##### Arch Linux ```bash -pacman -S cmake sdl2 glew qt5-base +pacman -S cmake python-jinja python-lxml sdl2 qt5-base ``` ##### Ubuntu or other Debian-derived distribution ```bash -apt install cmake libsdl2-dev libglew-dev qtbase5-dev libqt5opengl5-dev +apt install cmake python3-jinja2 python3-lxml libsdl2-dev qtbase5-dev libqt5opengl5-dev ``` ##### macOS @@ -42,14 +42,15 @@ apt install cmake libsdl2-dev libglew-dev qtbase5-dev libqt5opengl5-dev Get [Brew](https://brew.sh/) and run: ``` bash -brew install cmake sdl2 glew qt@5 +brew install cmake python@3 sdl2 qt@5 +python3 -m pip install Jinja2 ``` ##### FreeBSD ```bash su -pkg install cmake git sdl2 glew qt5 qt5-opengl +pkg install cmake git py39-Jinja2 py39-lxml sdl2 qt5 qt5-opengl ``` #### 2. Setup CMake build directory @@ -67,7 +68,7 @@ NOTE: the location and name of the `build` directory is arbitrary. ``` cd /somewhere/on/your/system/NanoBoyAdvance -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5);$(brew --prefix glew)" +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" ``` NOTE: the location and name of the `build` directory is arbitrary. @@ -94,7 +95,7 @@ This guide uses [MSYS2](https://www.msys2.org/) to install Mingw-w64 and other d In your MSYS2 command line, run: ```bash -pacman -S make mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-SDL2 mingw-w64-x86_64-glew mingw-w64-x86_64-qt5-static +pacman -S make mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-python-jinja mingw-w64-x86_64-python-lxml mingw-w64-x86_64-SDL2 mingw-w64-x86_64-qt5-static ``` #### 2. Setup CMake build directory diff --git a/docs/screenshot.png b/docs/screenshot.png deleted file mode 100644 index eabe412b..00000000 Binary files a/docs/screenshot.png and /dev/null differ diff --git a/src/nba/include/nba/log.hpp b/src/nba/include/nba/log.hpp index e2beff45..505d2d92 100644 --- a/src/nba/include/nba/log.hpp +++ b/src/nba/include/nba/log.hpp @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -38,16 +39,35 @@ namespace detail { template inline void Log(std::string_view format, Args&&... args) { if constexpr((detail::kLogMask & level) != 0) { + fmt::text_style style = {}; char const* prefix = "[?]"; - if constexpr(level == Trace) prefix = "\e[36m[T]"; - if constexpr(level == Debug) prefix = "\e[34m[D]"; - if constexpr(level == Info) prefix = "\e[37m[I]"; - if constexpr(level == Warn) prefix = "\e[33m[W]"; - if constexpr(level == Error) prefix = "\e[35m[E]"; - if constexpr(level == Fatal) prefix = "\e[31m[F]"; + if constexpr(level == Trace) { + style = fmt::fg(fmt::terminal_color::cyan); + prefix = "[T]"; + } + if constexpr(level == Debug) { + style = fmt::fg(fmt::terminal_color::blue); + prefix = "[D]"; + } + if constexpr(level == Info) { + prefix = "[I]"; + } + if constexpr(level == Warn) { + style = fmt::fg(fmt::terminal_color::yellow); + prefix = "[W]"; + } + if constexpr(level == Error) { + style = fmt::fg(fmt::terminal_color::magenta); + prefix = "[E]"; + } + if constexpr(level == Fatal) { + style = fmt::fg(fmt::terminal_color::red); + prefix = "[F]"; + } - fmt::print("{} {}\n", prefix, fmt::format(format, std::forward(args)...)); + const auto& style_ref = style; + fmt::print(style_ref, "{} {}\n", prefix, fmt::format(format, std::forward(args)...)); } } diff --git a/src/nba/src/bus/bus.cpp b/src/nba/src/bus/bus.cpp index 5747ec22..db0ea00c 100644 --- a/src/nba/src/bus/bus.cpp +++ b/src/nba/src/bus/bus.cpp @@ -398,7 +398,7 @@ auto Bus::GetHostAddress(u32 address, size_t size) -> u8* { } } - Assert(false, "Bus: cannot get host address for 0x{:08X} ({} bytes)", address, size); + return nullptr; } } // namespace nba::core diff --git a/src/nba/src/core.cpp b/src/nba/src/core.cpp index 8b9245fe..73f3b336 100644 --- a/src/nba/src/core.cpp +++ b/src/nba/src/core.cpp @@ -83,12 +83,12 @@ void Core::Run(int cycles) { while(scheduler.GetTimestampNow() < limit) { if(bus.hw.haltcnt == HaltControl::Run) { if(cpu.state.r15 == hle_audio_hook) { - // @todo: cache the SoundInfo pointer once we have it? - apu.GetMP2K().SoundMainRAM( - *bus.GetHostAddress( - *bus.GetHostAddress(0x03007FF0) - ) - ); + const u32 sound_info_addr = *bus.GetHostAddress(0x03007FF0); + const auto sound_info = bus.GetHostAddress(sound_info_addr); + + if(sound_info != nullptr) { + apu.GetMP2K().SoundMainRAM(*sound_info); + } } cpu.Run(); diff --git a/src/nba/src/hw/apu/apu.cpp b/src/nba/src/hw/apu/apu.cpp index 70331feb..075a42e0 100644 --- a/src/nba/src/hw/apu/apu.cpp +++ b/src/nba/src/hw/apu/apu.cpp @@ -146,7 +146,7 @@ void APU::StepMixer() { if(psg.enable[channel][2]) psg_sample += mmio.psg3.GetSample(); if(psg.enable[channel][3]) psg_sample += mmio.psg4.GetSample(); - sample[channel] += psg_sample * psg_volume * psg.master[channel] / (28.0 * 0x200); + sample[channel] += psg_sample * psg_volume * (psg.master[channel] + 1) / (32.0 * 0x200); /* TODO: we assume that MP2K sends right channel to FIFO A and left channel to FIFO B, * but we haven't verified that this is actually correct. @@ -183,7 +183,7 @@ void APU::StepMixer() { if(psg.enable[channel][2]) psg_sample += mmio.psg3.GetSample(); if(psg.enable[channel][3]) psg_sample += mmio.psg4.GetSample(); - sample[channel] += psg_sample * psg_volume * psg.master[channel] / 28; + sample[channel] += psg_sample * psg_volume * (psg.master[channel] + 1) >> 5; for(int fifo = 0; fifo < 2; fifo++) { if(dma[fifo].enable[channel]) { diff --git a/src/nba/src/hw/apu/hle/mp2k.cpp b/src/nba/src/hw/apu/hle/mp2k.cpp index c160f6de..6c694e2f 100644 --- a/src/nba/src/hw/apu/hle/mp2k.cpp +++ b/src/nba/src/hw/apu/hle/mp2k.cpp @@ -70,8 +70,14 @@ void MP2K::SoundMainRAM(SoundInfo const& sound_info) { } hq_envelope_volume[0] = U8ToFloat(channel.envelope_attack); + const Sampler::WaveInfo* const wave_info = bus.GetHostAddress(channel.wave_address); + if(wave_info == nullptr) { + Log("MP2K: channel[{}] wave address is invalid: 0x{:08X}", channel.wave_address); + channel.status = 0; // Disable channel, there is no good way to deal with this. + continue; + } sampler = {}; - sampler.wave_info = *bus.GetHostAddress(channel.wave_address); + sampler.wave_info = *wave_info; if(sampler.wave_info.status & 0xC000) { channel.status |= CHANNEL_LOOP; } @@ -205,9 +211,13 @@ void MP2K::RenderFrame() { wave_size *= 33; wave_size = (wave_size + 63) / 64; } - sampler.wave_data = bus.GetHostAddress( - channel.wave_address + sizeof(Sampler::WaveInfo), wave_size - ); + const u32 wave_data_begin = channel.wave_address + sizeof(Sampler::WaveInfo); + sampler.wave_data = bus.GetHostAddress(wave_data_begin, wave_size); + if(sampler.wave_data == nullptr) { + Log("MP2K: channel[{}] sample data has bad memory range 0x{:08X} - 0x{:08X}.", i, wave_data_begin, wave_data_begin + wave_size); + channel.status = 0; // Disable channel, there is no good way to deal with this. + continue; + } sampler.compressed = compressed; } diff --git a/src/nba/src/hw/rom/gpio/serialization.cpp b/src/nba/src/hw/rom/gpio/serialization.cpp index 2eabb67e..526b1648 100644 --- a/src/nba/src/hw/rom/gpio/serialization.cpp +++ b/src/nba/src/hw/rom/gpio/serialization.cpp @@ -22,6 +22,7 @@ void GPIO::LoadState(SaveState const& state) { for(auto& device : devices) { device->LoadState(state); + device->SetPortDirections(wr_mask); } } diff --git a/src/platform/core/CMakeLists.txt b/src/platform/core/CMakeLists.txt index 3a211153..2bc6d8c3 100644 --- a/src/platform/core/CMakeLists.txt +++ b/src/platform/core/CMakeLists.txt @@ -15,12 +15,21 @@ else() endif() find_package(OpenGL REQUIRED) -find_package(GLEW REQUIRED) + +include(FetchContent) +FetchContent_Declare(glad + GIT_REPOSITORY https://github.com/Dav1dde/glad.git + GIT_TAG adc3d7a1d704e099581ca25bc5bbdf728c2db67b # v2.0.5-2-gadc3d7a + SOURCE_SUBDIR cmake +) +FetchContent_MakeAvailable(glad) +glad_add_library(glad_gl_core_33 STATIC + LANGUAGE c REPRODUCIBLE API gl:core=3.3 EXTENSIONS NONE +) if(USE_SYSTEM_TOML11) find_package(toml11 3.7 REQUIRED) else() - include(FetchContent) FetchContent_Declare(toml11 GIT_REPOSITORY https://github.com/ToruNiina/toml11.git GIT_TAG dcfe39a783a94e8d52c885e5883a6fbb21529019 # v3.7.1 @@ -37,7 +46,6 @@ else() set(USE_SYSTEM_ZLIB OFF CACHE BOOL "" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) - include(FetchContent) FetchContent_Declare(unarr GIT_REPOSITORY https://github.com/selmf/unarr.git GIT_TAG b211040df83dee513362cdeb9bd87afa26fd5e38 # v1.1.1 @@ -104,5 +112,5 @@ endif() target_link_libraries(platform-core PRIVATE unarr::unarr - PUBLIC nba toml11::toml11 OpenGL::GL GLEW::GLEW + PUBLIC nba toml11::toml11 OpenGL::GL glad_gl_core_33 ) diff --git a/src/platform/core/include/platform/device/ogl_video_device.hpp b/src/platform/core/include/platform/device/ogl_video_device.hpp index 1b7f3d1b..ebcf1602 100644 --- a/src/platform/core/include/platform/device/ogl_video_device.hpp +++ b/src/platform/core/include/platform/device/ogl_video_device.hpp @@ -8,8 +8,8 @@ #pragma once #include +#include #include -#include #include #include #include diff --git a/src/platform/core/src/device/ogl_video_device.cpp b/src/platform/core/src/device/ogl_video_device.cpp index 3fb65e63..a4a68711 100644 --- a/src/platform/core/src/device/ogl_video_device.cpp +++ b/src/platform/core/src/device/ogl_video_device.cpp @@ -47,8 +47,6 @@ OGLVideoDevice::~OGLVideoDevice() { } void OGLVideoDevice::Initialize() { - glewInit(); - // Create a fullscreen quad to render to the viewport. glGenVertexArrays(1, &quad_vao); glGenBuffers(1, &quad_vbo); @@ -64,8 +62,7 @@ void OGLVideoDevice::Initialize() { glGenFramebuffers(1, &fbo); glGenTextures(textures.size(), textures.data()); - glClearColor(0, 0, 0, 1); - glClear(GL_COLOR_BUFFER_BIT); + glClearColor(0.0, 0.0, 0.0, 1.0); ReloadConfig(); } @@ -354,14 +351,16 @@ void OGLVideoDevice::Draw(u32* buffer) { copy_area_y = view_y; copy_area_width = view_width; copy_area_height = view_height; + glBindFramebuffer(GL_READ_FRAMEBUFFER, default_fbo); + glReadBuffer(GL_BACK); } else { glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[output_index], 0); + glReadBuffer(GL_COLOR_ATTACHMENT0); } glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textures[history_index]); - glReadBuffer(GL_COLOR_ATTACHMENT0); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, copy_area_x, copy_area_y, copy_area_width, copy_area_height); } } diff --git a/src/platform/core/src/game_db.cpp b/src/platform/core/src/game_db.cpp index ec65bd49..3132cad0 100644 --- a/src/platform/core/src/game_db.cpp +++ b/src/platform/core/src/game_db.cpp @@ -99,9 +99,9 @@ const std::map g_game_db { { "KHPJ", { Config::BackupType::EEPROM_64/*_SENSOR*/, GPIODeviceType::None, false } }, /* Koro Koro Puzzle - Happy Panechu! (Japan) */ { "KYGJ", { Config::BackupType::EEPROM_64/*_SENSOR*/, GPIODeviceType::None, false } }, /* Yoshi no Banyuuinryoku (Japan) */ { "PSAJ", { Config::BackupType::FLASH_128, GPIODeviceType::None, false } }, /* Card e-Reader+ (Japan) */ - { "U3IJ", { Config::BackupType::Detect, GPIODeviceType::RTC, false } }, /* Bokura no Taiyou - Taiyou Action RPG (Japan) */ - { "U32J", { Config::BackupType::Detect, GPIODeviceType::RTC, false } }, /* Zoku Bokura no Taiyou - Taiyou Shounen Django (Japan) */ - { "U33J", { Config::BackupType::Detect, GPIODeviceType::RTC, false } }, /* Shin Bokura no Taiyou - Gyakushuu no Sabata (Japan) */ + { "U3IJ", { Config::BackupType::Detect, GPIODeviceType::RTC | GPIODeviceType::SolarSensor, false } }, /* Bokura no Taiyou - Taiyou Action RPG (Japan) */ + { "U32J", { Config::BackupType::Detect, GPIODeviceType::RTC | GPIODeviceType::SolarSensor, false } }, /* Zoku Bokura no Taiyou - Taiyou Shounen Django (Japan) */ + { "U33J", { Config::BackupType::Detect, GPIODeviceType::RTC | GPIODeviceType::SolarSensor, false } }, /* Shin Bokura no Taiyou - Gyakushuu no Sabata (Japan) */ { "AXPF", { Config::BackupType::FLASH_128, GPIODeviceType::RTC, false } }, /* Pokemon - Version Saphir (France) */ { "AXVF", { Config::BackupType::FLASH_128, GPIODeviceType::RTC, false } }, /* Pokemon - Version Rubis (France) */ { "BPEF", { Config::BackupType::FLASH_128, GPIODeviceType::RTC, false } }, /* Pokemon - Version Emeraude (France) */ diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 056419e4..a28a2833 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -47,8 +47,8 @@ option(USE_QT6 "Use Qt 6" OFF) option(PORTABLE_MODE "Portable Mode" ON) if(USE_QT6) - find_package(Qt6 COMPONENTS Core Gui Widgets OpenGL OpenGLWidgets REQUIRED) - set(QT_DEPS Qt6::Core Qt6::Gui Qt6::Widgets Qt6::OpenGL Qt6::OpenGLWidgets) + find_package(Qt6 COMPONENTS Core Gui Widgets OpenGL REQUIRED) + set(QT_DEPS Qt6::Core Qt6::Gui Qt6::Widgets Qt6::OpenGL) else() find_package(Qt5 COMPONENTS Core Gui Widgets OpenGL REQUIRED) set(QT_DEPS Qt5::Core Qt5::Gui Qt5::Widgets Qt5::OpenGL) diff --git a/src/platform/qt/rc/config.toml b/src/platform/qt/rc/config.toml index 76f3b897..89b8e66f 100644 --- a/src/platform/qt/rc/config.toml +++ b/src/platform/qt/rc/config.toml @@ -57,3 +57,5 @@ show_fps = false lock_aspect_ratio = true # Snap screen size to integer multiples of the original screen width and height use_integer_scaling = false +# Pause emulator when the main window becomes inactive +pause_emulator_when_inactive = true diff --git a/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.desktop b/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.desktop index 66bb8084..03c12966 100644 --- a/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.desktop +++ b/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.desktop @@ -1,12 +1,12 @@ -# Created with jdDesktopEntryEdit 1.0 [Desktop Entry] Type=Application Name=NanoBoyAdvance -Comment=A cycle-accurate Nintendo Game Boy Advance emulator +GenericName=Game Boy Advance Emulator +Comment=Cycle-accurate Nintendo Game Boy Advance emulator Icon=io.nanoboyadvance.NanoBoyAdvance TryExec=NanoBoyAdvance Exec=NanoBoyAdvance %f -MimeType=application/x-gameboy-advance-rom;pplication/x-agb-rom;application/x-gba-rom; +MimeType=application/x-gameboy-advance-rom;application/x-agb-rom;application/x-gba-rom; Categories=Game;Emulator; Keywords=Emulator;Nintendo;GameBoy;Game Boy Advance;GBA;GB; SingleMainWindow=true diff --git a/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.metainfo.xml b/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.metainfo.xml index 968fa467..5deb98f6 100644 --- a/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.metainfo.xml +++ b/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.metainfo.xml @@ -1,13 +1,12 @@ - io.nanoboyadvance.NanoBoyAdvance NanoBoyAdvance - A cycle-accurate Nintendo Game Boy Advance emulator + Cycle-accurate Nintendo Game Boy Advance emulator fleroviux io.nanoboyadvance.NanoBoyAdvance.desktop CC0-1.0 - GPL-3.0 + GPL-3.0+

NanoBoyAdvance is a cycle-accurate Game Boy Advance emulator. It aims to be as accurate as possible, while also offering enhancements such as improved audio quality.

@@ -18,9 +17,10 @@ It aims to be as accurate as possible, while also offering enhancements such as
  • Post-processing options (color correction, xBRZ upscaling and LCD ghosting simulation)
  • Save State support (10x save slots available)
  • Game controller support (buttons and axises can be remapped)
  • -
  • Loading ROMs from archives (Zip, 7z, Tar and limited RAR1 support)
  • +
  • Loading ROMs from archives (Zip, 7z, Tar and limited RAR support)
  • RTC emulation
  • Solar Sensor emulation (for example: for Boktai - The Sun is in Your Hand)
  • +
  • Debug tools: PPU palette, tile, background and sprite viewers
  • Accuracy

    A lot of research and attention to detail has been put into developing this core and making it accurate.

    @@ -29,14 +29,64 @@ It aims to be as accurate as possible, while also offering enhancements such as
  • Passes all AGS aging cartridge tests (NBA was the first public emulator to achieve this)
  • Passes most tests in the mGBA test suite
  • Passes ARMWrestler, gba-suite and FuzzARM CPU tests
  • +
  • Very high compatibility, including games that require emulation of peculiar hardware edge-cases
  • - https://github.com/nba-emu/NanoBoyAdvance/raw/v1.7/docs/screenshot.png + https://raw.githubusercontent.com/nba-emu/NanoBoyAdvance/c8493948743f6e9e6f72abe057ba3ed5d03ad16a/docs/screenshot.png + + https://github.com/nba-emu/NanoBoyAdvance/releases/tag/v1.8.0 + +
      +
    • UI: implement "Sharp" video filter for better nearest interpolation at non-integer scales (thanks GranMinigun)
    • +
    • UI: implemented PPU viewers for palettes, tiles, backgrounds and sprites
    • +
    • UI: add an option to set the audio volume
    • +
    • UI: add an option to set the audio volume
    • +
    • Input: fix input dropping regression introduced in NBA 1.7.1
    • +
    • Input: move from SDL game controller to SDL joystick API (fixes broken mapping on some controllers)
    • +
    • APU: MP2K HLE: interpolate envelopes and use floating-point math
    • +
    • APU: MP2K HLE: implement a better reverb algorithm
    • +
    • APU: MP2K HLE: add an option to force-enable reverb
    • +
    • APU: remove "Zombie" mode emulation (does not appear to exist on GBA)
    • +
    • APU: fix mid-note envelope frequency changes
    • +
    • APU: FIFO writes should happen in place
    • +
    • APU: reset FIFOs on overflow
    • +
    • APU: add master-enable checks for 16-bit/32-bit IO writes
    • +
    • PPU: fix sprite pixels from previous scanline displayed in the last scanline
    • +
    • PPU: more accurate update of attribute buffer for OBJWIN sprites
    • +
    • PPU: more accurate emulation of horizontal and vertical sprite mosaic
    • +
    • PPU: more accurate handling of mid-frame vertical mosaic reconfiguration
    • +
    • PPU: fix out-of-bounds bitmap fetches in Mode 3 to 5
    • +
    • DMA: fix incorrect mapping of DMA channel to APU FIFO
    • +
    • Timer: somewhat handle timer overflow during one cycle enable delay (thanks alyosha)
    • +
    • Game Pak: simulate ~6 ms EEPROM device busy period after write (not yet accurate to cartridges that e.g. have been in the freezer for 30 minutes)
    • +
    • Game Pak: emulate the Mask ROM internal address register
    • +
    • Audio: fix clicking artifacts when using Sinc resampling
    • +
    • Audio: fix incorrect liner interpolation in Cosine resampling
    • +
    • PlatformCore: fixed hang when fast-forwarding while the emulator is paused
    • +
    • Misc: implemented support for unicode paths
    • +
    +
    +
    + + https://github.com/nba-emu/NanoBoyAdvance/releases/tag/v1.7.1 + +
      +
    • PPU: disallow out-of-bounds BG VRAM tile fetches and return open bus
    • +
    • Core: do not skip to the next event if the CPU woke up during a DMA
    • +
    • KeyPad: always request IRQs from the emulator (not the calling) thread
    • +
    • GameDB: fix entries for a bunch of Classic NES and Famicom Mini titles
    • +
    • IO: do not enter STOP mode when it is not implemented
    • +
    • mGBA log: clear the message buffer after printing the message
    • +
    • mGBA log: flush STDOUT after each message
    • +
    • Catch fmt::system_error when fmt::print() fails to write to STDOUT
    • +
    +
    +
    https://github.com/nba-emu/NanoBoyAdvance/releases/tag/v1.7 @@ -90,7 +140,7 @@ It aims to be as accurate as possible, while also offering enhancements such as
    https://github.com/nba-emu/NanoBoyAdvance - https://github.com/nba-emu/NanoBoyAdvance + https://github.com/nba-emu/NanoBoyAdvance/issues Game Emulator @@ -104,7 +154,7 @@ It aims to be as accurate as possible, while also offering enhancements such as NanoBoyAdvance application/x-gameboy-advance-rom - pplication/x-agb-rom + application/x-agb-rom application/x-gba-rom diff --git a/src/platform/qt/src/config.cpp b/src/platform/qt/src/config.cpp index b09e9b1c..7fbc712b 100644 --- a/src/platform/qt/src/config.cpp +++ b/src/platform/qt/src/config.cpp @@ -59,6 +59,7 @@ void QtConfig::LoadCustomData(toml::value const& data) { window.lock_aspect_ratio = toml::find_or(window_, "lock_aspect_ratio", true); window.use_integer_scaling = toml::find_or(window_, "use_integer_scaling", false); window.show_fps = toml::find_or(window_, "show_fps", false); + window.pause_emulator_when_inactive = toml::find_or(window_, "pause_emulator_when_inactive", true); } } @@ -90,6 +91,7 @@ void QtConfig::SaveCustomData( data["window"]["lock_aspect_ratio"] = window.lock_aspect_ratio; data["window"]["use_integer_scaling"] = window.use_integer_scaling; data["window"]["show_fps"] = window.show_fps; + data["window"]["pause_emulator_when_inactive"] = window.pause_emulator_when_inactive; data["recent_files"] = recent_files; } diff --git a/src/platform/qt/src/config.hpp b/src/platform/qt/src/config.hpp index 17dd7875..91af8291 100644 --- a/src/platform/qt/src/config.hpp +++ b/src/platform/qt/src/config.hpp @@ -77,6 +77,7 @@ struct QtConfig final : nba::PlatformConfig { bool lock_aspect_ratio = true; bool use_integer_scaling = false; bool show_fps = false; + bool pause_emulator_when_inactive = true; } window; std::vector recent_files; diff --git a/src/platform/qt/src/main.cpp b/src/platform/qt/src/main.cpp index d6d5149a..e85cc979 100644 --- a/src/platform/qt/src/main.cpp +++ b/src/platform/qt/src/main.cpp @@ -48,6 +48,9 @@ auto create_window(QApplication& app, int argc, char** argv) -> std::unique_ptr< } auto window = std::make_unique(&app); + if(!window->Initialize()) { + return nullptr; + } if(!rom.empty()) { window->LoadROM(rom.u16string()); @@ -63,6 +66,25 @@ int main(int argc, char** argv) { setenv("LC_NUMERIC", "C", 1); #endif +#if defined(WIN32) + constexpr auto terminal_output = "CONOUT$"; + constexpr auto null_output = "NUL:"; + + // Check whether we are already attached to an output stream. + const auto output_handle = GetStdHandle(STD_OUTPUT_HANDLE); + if(!output_handle || (output_handle == INVALID_HANDLE_VALUE)) { + // If started from terminal, attach and output logs to it. + if(AttachConsole(ATTACH_PARENT_PROCESS)) { + if(!freopen(terminal_output, "a", stdout)) { assert(false); } + if(!freopen(terminal_output, "a", stderr)) { assert(false); } + } else { + // Otherwise, discard log output. + if(!freopen(null_output, "w", stdout)) { assert(false); } + if(!freopen(null_output, "w", stderr)) { assert(false); } + } + } +#endif + // On some systems (e.g. macOS) QSurfaceFormat::setDefaultFormat() must be called before constructing QApplication. auto format = QSurfaceFormat{}; format.setProfile(QSurfaceFormat::CoreProfile); @@ -85,6 +107,9 @@ int main(int argc, char** argv) { QGuiApplication::setDesktopFileName("io.github.nba_emuNanoBoyAdvance"); auto window = create_window(app, argc, argv); + if(!window) { + return EXIT_FAILURE; + } return app.exec(); } diff --git a/src/platform/qt/src/widget/debugger/ppu/sprite_viewer.cpp b/src/platform/qt/src/widget/debugger/ppu/sprite_viewer.cpp index a329ea5d..032eeead 100644 --- a/src/platform/qt/src/widget/debugger/ppu/sprite_viewer.cpp +++ b/src/platform/qt/src/widget/debugger/ppu/sprite_viewer.cpp @@ -32,6 +32,7 @@ QWidget* SpriteViewer::CreateSpriteIndexInput() { m_spin_sprite_index = new QSpinBox{}; m_spin_sprite_index->setMinimum(0); m_spin_sprite_index->setMaximum(127); + connect(m_spin_sprite_index, QOverload::of(&QSpinBox::valueChanged), [this](int _) { Update(); }); return m_spin_sprite_index; } @@ -231,10 +232,6 @@ void SpriteViewer::Update() { m_check_sprite_affine->setChecked(affine); m_check_sprite_mosaic->setChecked(mosaic); - m_check_sprite_vflip->setEnabled(!affine); - m_check_sprite_hflip->setEnabled(!affine); - m_check_sprite_double_size->setEnabled(affine); - const int signed_x = x >= 240 ? ((int)x - 512) : (int)x; int render_cycles = 0; diff --git a/src/platform/qt/src/widget/main_window.cpp b/src/platform/qt/src/widget/main_window.cpp index ac5ef5b1..0688e3dc 100644 --- a/src/platform/qt/src/widget/main_window.cpp +++ b/src/platform/qt/src/widget/main_window.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "widget/main_window.hpp" @@ -40,11 +41,11 @@ MainWindow::MainWindow( setWindowTitle(base_window_title); setAcceptDrops(true); + config->Load(); + screen = std::make_shared(this, config); setCentralWidget(screen.get()); - config->Load(); - auto menu_bar = new QMenuBar(this); setMenuBar(menu_bar); @@ -90,6 +91,19 @@ MainWindow::~MainWindow() { delete controller_manager; } +bool MainWindow::Initialize() { + screen->windowHandle()->create(); + if(!screen->Initialize()) { + QMessageBox::critical(this, QApplication::instance()->applicationName(), + tr("Failed to initialize graphics subsystem.\n\n" + "Make sure that your hardware supports OpenGL 3.3 or later, " + "and you have the latest driver version installed.")); + return false; + } + + return true; +} + void MainWindow::CreateFileMenu() { auto file_menu = menuBar()->addMenu(tr("File")); @@ -366,6 +380,7 @@ void MainWindow::CreateWindowMenu(QMenu* parent) { menu->addSeparator(); CreateBooleanOption(menu, "Show FPS", &config->window.show_fps); + CreateBooleanOption(menu, "Pause emulator when inactive", &config->window.pause_emulator_when_inactive); } void MainWindow::CreateConfigMenu() { @@ -613,7 +628,7 @@ void MainWindow::PromptUserForReset() { } bool MainWindow::eventFilter(QObject* obj, QEvent* event) { - auto type = event->type(); + const auto type = event->type(); if(obj == this && (type == QEvent::KeyPress || type == QEvent::KeyRelease)) { auto key = dynamic_cast(event)->key(); @@ -633,7 +648,7 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event) { if(pressed && key == Qt::Key_Escape) { SetFullscreen(false); } - } else if(type == QEvent::FileOpen) { + } if(type == QEvent::FileOpen) { auto file = dynamic_cast(event)->file(); LoadROM(file.toStdU16String()); } else if(type == QEvent::Drop) { @@ -651,6 +666,12 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event) { return QObject::eventFilter(obj, event); } +void MainWindow::changeEvent(QEvent* event) { + if(event->type() == QEvent::ActivationChange) { + ApplyPauseState(); + } +} + void MainWindow::dragEnterEvent(QDragEnterEvent* event) { event->acceptProposedAction(); } @@ -679,20 +700,25 @@ void MainWindow::Reset() { } void MainWindow::SetPause(bool paused) { - if(!paused) { - screen->SetForceClear(false); - } + pause_action->setChecked(paused); + ApplyPauseState(); +} + +void MainWindow::ApplyPauseState() { + const bool window_inactive_and_should_pause = !isActiveWindow() && config->window.pause_emulator_when_inactive; + const bool paused = pause_action->isChecked() || window_inactive_and_should_pause; emu_thread->SetPause(paused); config->audio_dev->SetPause(paused); - pause_action->setChecked(paused); } void MainWindow::Stop() { if(emu_thread->IsRunning()) { core = emu_thread->Stop(); config->audio_dev->Close(); - screen->SetForceClear(true); + + // Clear the screen. + screen->Draw(nullptr); // Clear the list of save state slots: game_loaded = false; @@ -818,13 +844,6 @@ void MainWindow::LoadROM(std::u16string const& path) { core->Reset(); emu_thread->Start(std::move(core)); - /** - * If the the emulator is paused we force-clear the screen to avoid - * presenting the last frame from before pausing, since that could be confusing. - * In the other case we explicitly disable force-clear, because it is currently enabled (due to the call to Stop() at the top). - */ - screen->SetForceClear(pause_action->isChecked()); - UpdateSolarSensorLevel(); UpdateMenuBarVisibility(); } @@ -955,8 +974,8 @@ void MainWindow::SetFullscreen(bool value) { void MainWindow::UpdateSolarSensorLevel() { auto level = config->cartridge.solar_sensor_level; - if(core) { - auto solar_sensor = core->GetROM().GetGPIODevice(); + if(core_not_thread_safe) { + nba::SolarSensor* solar_sensor = core_not_thread_safe->GetROM().GetGPIODevice(); if(solar_sensor) { solar_sensor->SetLightLevel(level); diff --git a/src/platform/qt/src/widget/main_window.hpp b/src/platform/qt/src/widget/main_window.hpp index e8377346..8b406ab8 100644 --- a/src/platform/qt/src/widget/main_window.hpp +++ b/src/platform/qt/src/widget/main_window.hpp @@ -39,6 +39,7 @@ struct MainWindow : QMainWindow { ~MainWindow(); + bool Initialize(); void LoadROM(std::u16string const& path); signals: @@ -49,6 +50,7 @@ private slots: protected: bool eventFilter(QObject* obj, QEvent* event) override; + void changeEvent(QEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override; @@ -114,6 +116,7 @@ private slots: void Reset(); void SetPause(bool paused); + void ApplyPauseState(); void Stop(); void UpdateMenuBarVisibility(); void UpdateMainWindowActionList(); @@ -140,7 +143,7 @@ private slots: // The PPU debuggers do not access the core in a thread-safe way yet. // So until that is fixed we have to keep a raw pointer around... - nba::CoreBase* core_not_thread_safe; + nba::CoreBase* core_not_thread_safe{}; QAction* pause_action; InputWindow* input_window; diff --git a/src/platform/qt/src/widget/screen.cpp b/src/platform/qt/src/widget/screen.cpp index b8546474..8c5a8b45 100644 --- a/src/platform/qt/src/widget/screen.cpp +++ b/src/platform/qt/src/widget/screen.cpp @@ -5,29 +5,69 @@ * Refer to the included LICENSE file. */ -#include - #include "widget/screen.hpp" +#include +#include // Has to go after glad. +#include + Screen::Screen( QWidget* parent, std::shared_ptr config -) : QOpenGLWidget(parent) +) : QWidget(parent) , ogl_video_device(config) , config(config) { connect(this, &Screen::RequestDraw, this, &Screen::OnRequestDraw); + setAttribute(Qt::WA_NativeWindow); + setAttribute(Qt::WA_OpaquePaintEvent); + setAttribute(Qt::WA_PaintOnScreen); + windowHandle()->setSurfaceType(QWindow::OpenGLSurface); } -void Screen::Draw(u32* buffer) { - emit RequestDraw(buffer); +static auto get_proc_address(const char* proc_name) { + return QOpenGLContext::currentContext()->getProcAddress(proc_name); } -void Screen::SetForceClear(bool force_clear) { - this->force_clear = force_clear; - update(); +bool Screen::Initialize() { + context = new QOpenGLContext(); + if(!context->create()) { + delete context; + context = nullptr; + return false; + } + if(context->format().majorVersion() < 3 || + context->format().majorVersion() == 3 && context->format().minorVersion() < 3) { + delete context; + context = nullptr; + return false; + } + + if(!context->makeCurrent(this->windowHandle())) { + delete context; + context = nullptr; + return false; + } + + if(!gladLoadGL(get_proc_address)) { + delete context; + context = nullptr; + return false; + } + + ogl_video_device.Initialize(); + UpdateViewport(); + + context->doneCurrent(); + + return true; +} + +void Screen::Draw(u32* buffer) { + emit RequestDraw(buffer); } void Screen::ReloadConfig() { + context->makeCurrent(this->windowHandle()); ogl_video_device.ReloadConfig(); UpdateViewport(); } @@ -37,7 +77,36 @@ void Screen::OnRequestDraw(u32* buffer) { update(); } +void Screen::paintEvent([[maybe_unused]] QPaintEvent* event) { + Render(); +} + +void Screen::resizeEvent([[maybe_unused]] QResizeEvent* event) { + UpdateViewport(); +} + +void Screen::Render() { + if(!context) { + return; + } + + context->makeCurrent(this->windowHandle()); + glClear(GL_COLOR_BUFFER_BIT); + + if(buffer) { + ogl_video_device.SetDefaultFBO(context->defaultFramebufferObject()); + ogl_video_device.Draw(buffer); + } + + context->swapBuffers(this->windowHandle()); + context->doneCurrent(); +} + void Screen::UpdateViewport() { + if(!context) { + return; + } + auto dpr = devicePixelRatio(); int width = size().width() * dpr; int height = size().height() * dpr; @@ -90,24 +159,7 @@ void Screen::UpdateViewport() { viewport_x = (width - viewport_width ) / 2; viewport_y = (height - viewport_height) / 2; + context->makeCurrent(this->windowHandle()); ogl_video_device.SetViewport(viewport_x, viewport_y, viewport_width, viewport_height); -} - -void Screen::initializeGL() { - makeCurrent(); - ogl_video_device.Initialize(); -} - -void Screen::paintGL() { - if(force_clear) { - glClearColor(0.0, 0.0, 0.0, 0.0); - glClear(GL_COLOR_BUFFER_BIT); - } else if(buffer != nullptr) { - ogl_video_device.SetDefaultFBO(defaultFramebufferObject()); - ogl_video_device.Draw(buffer); - } -} - -void Screen::resizeGL(int width, int height) { - UpdateViewport(); + context->doneCurrent(); } diff --git a/src/platform/qt/src/widget/screen.hpp b/src/platform/qt/src/widget/screen.hpp index 02c50ac5..b69bfb74 100644 --- a/src/platform/qt/src/widget/screen.hpp +++ b/src/platform/qt/src/widget/screen.hpp @@ -8,19 +8,21 @@ #pragma once #include -#include +#include +#include #include "config.hpp" -struct Screen : QOpenGLWidget, nba::VideoDevice { - Screen( +struct Screen : QWidget, nba::VideoDevice { + explicit Screen( QWidget* parent, std::shared_ptr config ); + bool Initialize(); void Draw(u32* buffer) final; - void SetForceClear(bool force_clear); void ReloadConfig(); + QPaintEngine* paintEngine() const override { return nullptr; }; // Silence Qt. signals: void RequestDraw(u32* buffer); @@ -29,19 +31,19 @@ private slots: void OnRequestDraw(u32* buffer); protected: - void initializeGL() override; - void paintGL() override; - void resizeGL(int width, int height) override; + void paintEvent(QPaintEvent* event) override; + void resizeEvent(QResizeEvent* event) override; private: static constexpr int kGBANativeWidth = 240; static constexpr int kGBANativeHeight = 160; static constexpr float kGBANativeAR = static_cast(kGBANativeWidth) / static_cast(kGBANativeHeight); + void Render(); void UpdateViewport(); u32* buffer = nullptr; - bool force_clear = false; + QOpenGLContext* context = nullptr; nba::OGLVideoDevice ogl_video_device; std::shared_ptr config; diff --git a/src/platform/qt/version.cmake b/src/platform/qt/version.cmake index 71de716b..43dffb52 100644 --- a/src/platform/qt/version.cmake +++ b/src/platform/qt/version.cmake @@ -3,7 +3,7 @@ find_package(Git) set(VERSION_MAJOR 1) set(VERSION_MINOR 8) -set(VERSION_PATCH 0) +set(VERSION_PATCH 1) option(RELEASE_BUILD "Build a release version" OFF)