├── src ├── platform │ ├── qt │ │ ├── rc │ │ │ ├── app.ico │ │ │ ├── NanoBoyAdvance.icns │ │ │ ├── io.nanoboyadvance.NanoBoyAdvance.png │ │ │ ├── io.nanoboyadvance.NanoBoyAdvance.desktop │ │ │ ├── app.manifest │ │ │ ├── app.rc │ │ │ ├── Info.plist.in │ │ │ └── config.toml │ │ ├── version.in.hpp │ │ ├── src │ │ │ ├── widget │ │ │ │ ├── debugger │ │ │ │ │ ├── ppu │ │ │ │ │ │ ├── palette_viewer_window.hpp │ │ │ │ │ │ ├── tile_viewer_window.hpp │ │ │ │ │ │ ├── sprite_viewer_window.hpp │ │ │ │ │ │ ├── background_viewer_window.hpp │ │ │ │ │ │ ├── tile_viewer_window.cpp │ │ │ │ │ │ ├── sprite_viewer_window.cpp │ │ │ │ │ │ ├── palette_viewer_window.cpp │ │ │ │ │ │ ├── palette_viewer.hpp │ │ │ │ │ │ ├── background_viewer_window.cpp │ │ │ │ │ │ ├── color_grid.hpp │ │ │ │ │ │ ├── tile_viewer.hpp │ │ │ │ │ │ ├── sprite_viewer.hpp │ │ │ │ │ │ ├── color_grid.cpp │ │ │ │ │ │ ├── background_viewer.hpp │ │ │ │ │ │ └── palette_viewer.cpp │ │ │ │ │ └── utility.hpp │ │ │ │ ├── screen.hpp │ │ │ │ ├── controller_manager.hpp │ │ │ │ └── input_window.hpp │ │ │ ├── config.cpp │ │ │ └── main.cpp │ │ └── version.cmake │ └── core │ │ ├── src │ │ ├── device │ │ │ ├── shader │ │ │ │ ├── output.glsl.hpp │ │ │ │ ├── lcd_ghosting.glsl.hpp │ │ │ │ ├── color_higan.glsl.hpp │ │ │ │ ├── common.glsl.hpp │ │ │ │ ├── lcd1x.glsl.hpp │ │ │ │ ├── sharp_bilinear.glsl.hpp │ │ │ │ └── color_agb.glsl.hpp │ │ │ └── sdl_audio_device.cpp │ │ ├── writer │ │ │ └── save_state.cpp │ │ ├── loader │ │ │ ├── bios.cpp │ │ │ └── save_state.cpp │ │ └── frame_limiter.cpp │ │ ├── include │ │ └── platform │ │ │ ├── writer │ │ │ └── save_state.hpp │ │ │ ├── loader │ │ │ ├── bios.hpp │ │ │ ├── save_state.hpp │ │ │ └── rom.hpp │ │ │ ├── game_db.hpp │ │ │ ├── frame_limiter.hpp │ │ │ ├── device │ │ │ ├── sdl_audio_device.hpp │ │ │ └── ogl_video_device.hpp │ │ │ ├── config.hpp │ │ │ └── emulator_thread.hpp │ │ └── CMakeLists.txt └── nba │ ├── include │ └── nba │ │ ├── device │ │ ├── video_device.hpp │ │ └── audio_device.hpp │ │ ├── common │ │ ├── scope_exit.hpp │ │ ├── dsp │ │ │ ├── stream.hpp │ │ │ ├── resampler │ │ │ │ ├── nearest.hpp │ │ │ │ ├── cosine.hpp │ │ │ │ ├── cubic.hpp │ │ │ │ └── sinc.hpp │ │ │ ├── resampler.hpp │ │ │ ├── ring_buffer.hpp │ │ │ └── stereo.hpp │ │ ├── punning.hpp │ │ ├── crc32.hpp │ │ ├── compiler.hpp │ │ └── meta.hpp │ │ ├── rom │ │ ├── backup │ │ │ ├── backup.hpp │ │ │ ├── sram.hpp │ │ │ ├── flash.hpp │ │ │ ├── eeprom.hpp │ │ │ └── backup_file.hpp │ │ ├── gpio │ │ │ ├── solar_sensor.hpp │ │ │ ├── device.hpp │ │ │ ├── gpio.hpp │ │ │ └── rtc.hpp │ │ └── header.hpp │ │ ├── integer.hpp │ │ ├── config.hpp │ │ ├── core.hpp │ │ └── log.hpp │ ├── src │ ├── hw │ │ ├── keypad │ │ │ ├── serialization.cpp │ │ │ ├── keypad.hpp │ │ │ └── keypad.cpp │ │ ├── rom │ │ │ ├── backup │ │ │ │ ├── sram.cpp │ │ │ │ ├── serialization.cpp │ │ │ │ └── flash.cpp │ │ │ └── gpio │ │ │ │ ├── solar_sensor.cpp │ │ │ │ ├── gpio.cpp │ │ │ │ └── serialization.cpp │ │ ├── apu │ │ │ ├── channel │ │ │ │ ├── length_counter.hpp │ │ │ │ ├── noise_channel.hpp │ │ │ │ ├── quad_channel.hpp │ │ │ │ ├── wave_channel.hpp │ │ │ │ ├── envelope.hpp │ │ │ │ ├── base_channel.hpp │ │ │ │ ├── fifo.hpp │ │ │ │ ├── sweep.hpp │ │ │ │ ├── quad_channel.cpp │ │ │ │ └── wave_channel.cpp │ │ │ ├── callback.cpp │ │ │ ├── registers.hpp │ │ │ ├── apu.hpp │ │ │ └── hle │ │ │ │ └── mp2k.hpp │ │ ├── irq │ │ │ ├── serialization.cpp │ │ │ └── irq.hpp │ │ ├── ppu │ │ │ ├── window.cpp │ │ │ └── registers.hpp │ │ ├── timer │ │ │ ├── serialization.cpp │ │ │ └── timer.hpp │ │ └── dma │ │ │ ├── serialization.cpp │ │ │ └── dma.hpp │ ├── serialization.cpp │ ├── arm │ │ ├── handlers │ │ │ └── memory.inl │ │ ├── serialization.cpp │ │ ├── state.hpp │ │ └── tablegen │ │ │ └── tablegen.cpp │ ├── core.hpp │ └── bus │ │ └── serialization.cpp │ └── CMakeLists.txt ├── .gitignore ├── CMakeLists.txt ├── .editorconfig ├── docs └── COMPILING.md ├── .github └── workflows │ └── build.yml └── README.md /src/platform/qt/rc/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nba-emu/NanoBoyAdvance/HEAD/src/platform/qt/rc/app.ico -------------------------------------------------------------------------------- /src/platform/qt/rc/NanoBoyAdvance.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nba-emu/NanoBoyAdvance/HEAD/src/platform/qt/rc/NanoBoyAdvance.icns -------------------------------------------------------------------------------- /src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nba-emu/NanoBoyAdvance/HEAD/src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build-* 3 | 4 | # Visual Studio 5 | out 6 | .vs 7 | CMakeSettings.json 8 | 9 | # JetBrains CLion 10 | .idea 11 | cmake-build-* 12 | 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /src/platform/qt/version.in.hpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #define VERSION_MAJOR (${VERSION_MAJOR}) 5 | #define VERSION_MINOR (${VERSION_MINOR}) 6 | #define VERSION_PATCH (${VERSION_PATCH}) 7 | #define VERSION_GIT_BRANCH "${VERSION_GIT_BRANCH}" 8 | #define VERSION_GIT_HASH "${VERSION_GIT_HASH}" 9 | #cmakedefine RELEASE_BUILD -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11...3.28) 2 | project(NanoBoyAdvance LANGUAGES C CXX) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | option(PLATFORM_QT "Build Qt frontend." ON) 8 | 9 | add_subdirectory(src/nba) 10 | add_subdirectory(src/platform/core) 11 | 12 | if (PLATFORM_QT) 13 | add_subdirectory(src/platform/qt ${CMAKE_CURRENT_BINARY_DIR}/bin/qt/) 14 | endif() 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{cpp, hpp}] 11 | charset = utf-8 12 | indent_style = space 13 | indent_size = 2 14 | max_line_length = 100 15 | 16 | [{CMakeLists.txt, *.cmake}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /src/nba/include/nba/device/video_device.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | struct VideoDevice { 15 | virtual ~VideoDevice() = default; 16 | 17 | virtual void Draw(u32* buffer) = 0; 18 | }; 19 | 20 | struct NullVideoDevice : VideoDevice { 21 | void Draw(u32* buffer) final { } 22 | }; 23 | 24 | } // namespace nba 25 | -------------------------------------------------------------------------------- /src/platform/qt/rc/io.nanoboyadvance.NanoBoyAdvance.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=NanoBoyAdvance 4 | GenericName=Game Boy Advance Emulator 5 | Comment=Cycle-accurate Nintendo Game Boy Advance emulator 6 | Icon=io.nanoboyadvance.NanoBoyAdvance 7 | TryExec=NanoBoyAdvance 8 | Exec=NanoBoyAdvance %f 9 | MimeType=application/x-gameboy-advance-rom;application/x-agb-rom;application/x-gba-rom; 10 | Categories=Game;Emulator; 11 | Keywords=Emulator;Nintendo;GameBoy;Game Boy Advance;GBA;GB; 12 | SingleMainWindow=true 13 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/scope_exit.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | namespace nba { 11 | 12 | template 13 | struct ScopeExit { 14 | ScopeExit(Functor&& fn) : fn{fn} {} 15 | 16 | ScopeExit(ScopeExit&& other) = delete; 17 | ScopeExit(ScopeExit const& other) = delete; 18 | 19 | ~ScopeExit() { fn.operator()(); } 20 | 21 | Functor fn; 22 | }; 23 | 24 | } // namespace nba -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/palette_viewer_window.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include "palette_viewer.hpp" 14 | 15 | class PaletteViewerWindow : public QDialog { 16 | public: 17 | PaletteViewerWindow(nba::CoreBase* core, QWidget* parent = nullptr); 18 | 19 | public slots: 20 | void Update(); 21 | 22 | private: 23 | PaletteViewer* m_palette_viewer; 24 | 25 | Q_OBJECT 26 | }; 27 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/tile_viewer_window.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "tile_viewer.hpp" 15 | 16 | class TileViewerWindow : public QDialog { 17 | public: 18 | TileViewerWindow(nba::CoreBase* core, QWidget* parent = nullptr); 19 | 20 | public slots: 21 | void Update(); 22 | 23 | private: 24 | TileViewer* m_tile_viewer; 25 | 26 | Q_OBJECT 27 | }; 28 | -------------------------------------------------------------------------------- /src/platform/core/src/device/shader/output.glsl.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "device/shader/common.glsl.hpp" 11 | 12 | constexpr auto output_vert = common_flip_vert; 13 | 14 | constexpr auto output_frag = R"( 15 | #version 330 core 16 | 17 | layout(location = 0) out vec4 frag_color; 18 | 19 | in vec2 v_uv; 20 | 21 | uniform sampler2D u_input_map; 22 | 23 | void main() { 24 | frag_color = texture(u_input_map, v_uv); 25 | } 26 | )"; -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/sprite_viewer_window.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "sprite_viewer.hpp" 15 | 16 | class SpriteViewerWindow : public QDialog { 17 | public: 18 | SpriteViewerWindow(nba::CoreBase* core, QWidget* parent = nullptr); 19 | 20 | public slots: 21 | void Update(); 22 | 23 | private: 24 | SpriteViewer* m_sprite_viewer; 25 | 26 | Q_OBJECT 27 | }; 28 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/dsp/stream.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | namespace nba { 11 | 12 | template 13 | struct ReadStream { 14 | virtual ~ReadStream() = default; 15 | 16 | virtual auto Read() -> T = 0; 17 | }; 18 | 19 | template 20 | struct WriteStream { 21 | virtual ~WriteStream() = default; 22 | 23 | virtual void Write(T const& value) = 0; 24 | }; 25 | 26 | template 27 | struct Stream : ReadStream, WriteStream { }; 28 | 29 | } // namespace nba 30 | -------------------------------------------------------------------------------- /src/nba/include/nba/rom/backup/backup.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace nba { 14 | 15 | struct Backup { 16 | virtual ~Backup() = default; 17 | 18 | virtual void Reset() = 0; 19 | virtual auto Read (u32 address) -> u8 = 0; 20 | virtual void Write(u32 address, u8 value) = 0; 21 | 22 | virtual void LoadState(SaveState const& state) = 0; 23 | virtual void CopyState(SaveState& state) = 0; 24 | }; 25 | 26 | } // namespace nba 27 | -------------------------------------------------------------------------------- /src/platform/core/include/platform/writer/save_state.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace fs = std::filesystem; 15 | 16 | namespace nba { 17 | 18 | struct SaveStateWriter { 19 | enum class Result { 20 | CannotOpenFile, 21 | CannotWrite, 22 | Success 23 | }; 24 | 25 | static auto Write( 26 | std::unique_ptr& core, 27 | fs::path const& path 28 | ) -> Result; 29 | }; 30 | 31 | } // namespace nba 32 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/punning.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace nba { 14 | 15 | template 16 | auto read(void const* data, uint offset) -> T { 17 | T value; 18 | memcpy(&value, (u8*)data + offset, sizeof(T)); 19 | return value; 20 | } 21 | 22 | template 23 | void write(void* data, uint offset, T value) { 24 | memcpy((u8*)data + offset, &value, sizeof(T)); 25 | } 26 | 27 | } // namespace nba 28 | -------------------------------------------------------------------------------- /src/nba/include/nba/integer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #ifdef NBA_NO_EXPORT_INT_TYPES 11 | namespace nba { 12 | #endif 13 | 14 | #include 15 | 16 | using u8 = std::uint8_t; 17 | using s8 = std::int8_t; 18 | using u16 = std::uint16_t; 19 | using s16 = std::int16_t; 20 | using u32 = std::uint32_t; 21 | using s32 = std::int32_t; 22 | using u64 = std::uint64_t; 23 | using s64 = std::int64_t; 24 | using uint = unsigned int; 25 | 26 | using u8bool = u8; 27 | 28 | #ifdef NBA_NO_EXPORT_INT_TYPES 29 | } // namespace nba 30 | #endif 31 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/background_viewer_window.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "background_viewer.hpp" 15 | 16 | class BackgroundViewerWindow : public QDialog { 17 | public: 18 | BackgroundViewerWindow(nba::CoreBase* core, QWidget* parent = nullptr); 19 | 20 | public slots: 21 | void Update(); 22 | 23 | private: 24 | QTabWidget* m_tab_widget; 25 | BackgroundViewer* m_bg_viewers[4]; 26 | 27 | Q_OBJECT 28 | }; 29 | -------------------------------------------------------------------------------- /src/platform/core/include/platform/loader/bios.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace fs = std::filesystem; 15 | 16 | namespace nba { 17 | 18 | struct BIOSLoader { 19 | enum class Result { 20 | CannotFindFile, 21 | CannotOpenFile, 22 | BadImage, 23 | Success 24 | }; 25 | 26 | static auto Load( 27 | std::unique_ptr& core, 28 | fs::path const& path 29 | ) -> Result; 30 | }; 31 | 32 | } // namespace nba 33 | -------------------------------------------------------------------------------- /src/nba/src/hw/keypad/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "hw/keypad/keypad.hpp" 9 | 10 | namespace nba::core { 11 | 12 | void KeyPad::LoadState(SaveState const& state) { 13 | u16 keycnt = state.keycnt; 14 | 15 | control.mask = keycnt & 0x3FF; 16 | control.interrupt = keycnt & 0x4000; 17 | control.mode = (KeyControl::Mode)(keycnt >> 15); 18 | } 19 | 20 | void KeyPad::CopyState(SaveState& state) { 21 | state.keycnt = control.mask | 22 | (control.interrupt ? 0x4000 : 0) | 23 | ((int)control.mode << 15); 24 | } 25 | 26 | } // namespace nba::core -------------------------------------------------------------------------------- /src/platform/qt/rc/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | NanoBoyAvance Game Boy Advance emulator. 10 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/nba/src/hw/rom/backup/sram.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | namespace nba { 11 | 12 | SRAM::SRAM(fs::path const& save_path) 13 | : save_path(save_path) { 14 | Reset(); 15 | } 16 | 17 | void SRAM::Reset() { 18 | int bytes = 32768; 19 | file = BackupFile::OpenOrCreate(save_path, { 32768 }, bytes); 20 | } 21 | 22 | auto SRAM::Read(u32 address) -> u8 { 23 | return file->Read(address & 0x7FFF); 24 | } 25 | 26 | void SRAM::Write(u32 address, u8 value) { 27 | file->Write(address & 0x7FFF, value); 28 | } 29 | 30 | } // namespace nba 31 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/crc32.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | namespace nba { 11 | 12 | inline u32 crc32(u8 const* data, int length) { 13 | u32 crc32 = 0xFFFFFFFF; 14 | 15 | while(length-- != 0) { 16 | u8 byte = *data++; 17 | 18 | // TODO: speed this up using a lookup table. 19 | for(int i = 0; i < 8; i++) { 20 | if((crc32 ^ byte) & 1) { 21 | crc32 = (crc32 >> 1) ^ 0xEDB88320; 22 | } else { 23 | crc32 >>= 1; 24 | } 25 | byte >>= 1; 26 | } 27 | } 28 | 29 | return ~crc32; 30 | } 31 | 32 | } // namespace nba 33 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/compiler.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #if defined(__clang) || defined(__GNUC__) 11 | #define likely(x) __builtin_expect((x),1) 12 | #define unlikely(x) __builtin_expect((x),0) 13 | 14 | #define ALWAYS_INLINE inline __attribute__((always_inline)) 15 | #else 16 | #define likely(x) (x) 17 | #define unlikely(x) (x) 18 | 19 | #define ALWAYS_INLINE inline 20 | #endif 21 | 22 | #if defined(__clang) || defined(__GNUC__) 23 | #define unreachable() __builtin_unreachable() 24 | #elif defined(_MSC_VER) 25 | #define unreachable() __assume(0) 26 | #else 27 | #define unreachable() 28 | #endif -------------------------------------------------------------------------------- /src/nba/include/nba/rom/backup/sram.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace nba { 15 | 16 | struct SRAM : Backup { 17 | SRAM(fs::path const& save_path); 18 | 19 | void Reset() final; 20 | auto Read (u32 address) -> u8 final; 21 | void Write(u32 address, u8 value) final; 22 | 23 | void LoadState(SaveState const& state) final; 24 | void CopyState(SaveState& state) final; 25 | 26 | private: 27 | fs::path save_path; 28 | std::unique_ptr file; 29 | }; 30 | 31 | } // namespace nba 32 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/meta.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | namespace detail { 15 | 16 | template 17 | static constexpr void static_for_impl(Func&& f, std::integer_sequence) { 18 | (f(std::integral_constant{ }), ...); 19 | } 20 | 21 | } // namespace nba::detail 22 | 23 | template 24 | static constexpr void static_for(Func&& f) { 25 | detail::static_for_impl(std::forward(f), std::make_integer_sequence{ }); 26 | } 27 | 28 | } // namespace nba 29 | -------------------------------------------------------------------------------- /src/platform/core/src/device/shader/lcd_ghosting.glsl.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "device/shader/common.glsl.hpp" 11 | 12 | constexpr auto lcd_ghosting_vert = common_vert; 13 | 14 | constexpr auto lcd_ghosting_frag = R"( 15 | #version 330 core 16 | 17 | layout(location = 0) out vec4 frag_color; 18 | 19 | in vec2 v_uv; 20 | 21 | uniform sampler2D u_input_map; 22 | uniform sampler2D u_history_map; 23 | 24 | void main() { 25 | vec4 color_a = texture(u_input_map, v_uv); 26 | vec4 color_b = texture(u_history_map, v_uv); 27 | frag_color = mix(color_a, color_b, 0.5); 28 | } 29 | )"; -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/tile_viewer_window.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | #include "tile_viewer_window.hpp" 11 | 12 | TileViewerWindow::TileViewerWindow(nba::CoreBase* core, QWidget* parent) : QDialog(parent) { 13 | QVBoxLayout* vbox = new QVBoxLayout{}; 14 | 15 | m_tile_viewer = new TileViewer{core, nullptr}; 16 | vbox->addWidget(m_tile_viewer); 17 | setLayout(vbox); 18 | 19 | setWindowTitle(tr("Tile Viewer")); 20 | setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); 21 | } 22 | 23 | void TileViewerWindow::Update() { 24 | if(isVisible()) { 25 | m_tile_viewer->Update(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/platform/core/include/platform/loader/save_state.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace fs = std::filesystem; 15 | 16 | namespace nba { 17 | 18 | struct SaveStateLoader { 19 | enum class Result { 20 | CannotFindFile, 21 | CannotOpenFile, 22 | BadImage, 23 | UnsupportedVersion, 24 | Success 25 | }; 26 | 27 | static auto Load( 28 | std::unique_ptr& core, 29 | fs::path const& path 30 | ) -> Result; 31 | 32 | private: 33 | static auto Validate(SaveState const& save_state) -> Result; 34 | }; 35 | 36 | } // namespace nba 37 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/sprite_viewer_window.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | #include "sprite_viewer_window.hpp" 11 | 12 | SpriteViewerWindow::SpriteViewerWindow(nba::CoreBase* core, QWidget* parent) : QDialog(parent) { 13 | QVBoxLayout* vbox = new QVBoxLayout{}; 14 | 15 | m_sprite_viewer = new SpriteViewer{core, nullptr}; 16 | vbox->addWidget(m_sprite_viewer); 17 | setLayout(vbox); 18 | 19 | setWindowTitle(tr("Sprite Viewer")); 20 | setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); 21 | } 22 | 23 | void SpriteViewerWindow::Update() { 24 | if(isVisible()) { 25 | m_sprite_viewer->Update(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/nba/include/nba/rom/gpio/solar_sensor.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | struct SolarSensor final : GPIODevice { 15 | SolarSensor(); 16 | 17 | void Reset() override; 18 | auto Read() -> int override; 19 | void Write(int value) override; 20 | void SetLightLevel(u8 level); 21 | 22 | void LoadState(SaveState const& state) override; 23 | void CopyState(SaveState& state) override; 24 | 25 | private: 26 | enum Pin { 27 | CLK = 0, 28 | RST = 1, 29 | nCS = 2, 30 | FLG = 3 31 | }; 32 | 33 | bool old_clk; 34 | u8 counter; 35 | u8 current_level; 36 | }; 37 | 38 | } // namespace nba 39 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/length_counter.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | namespace nba::core { 11 | 12 | class LengthCounter { 13 | public: 14 | LengthCounter(int default_length) : default_length(default_length) { 15 | Reset(); 16 | } 17 | 18 | void Reset() { 19 | enabled = false; 20 | length = 0; 21 | } 22 | 23 | void Restart() { 24 | if(length == 0) { 25 | length = default_length; 26 | } 27 | } 28 | 29 | bool Tick() { 30 | if(enabled) { 31 | return --length > 0; 32 | } 33 | return true; 34 | } 35 | 36 | int length; 37 | bool enabled; 38 | 39 | private: 40 | int default_length; 41 | }; 42 | 43 | } // namespace nba::core 44 | -------------------------------------------------------------------------------- /src/platform/core/src/device/shader/color_higan.glsl.hpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "device/shader/common.glsl.hpp" 5 | 6 | constexpr auto color_higan_vert = common_vert; 7 | 8 | constexpr auto color_higan_frag = R"( 9 | #version 330 core 10 | 11 | layout(location = 0) out vec4 frag_color; 12 | 13 | in vec2 v_uv; 14 | 15 | uniform sampler2D u_input_map; 16 | 17 | void main() { 18 | vec4 color = texture(u_input_map, v_uv); 19 | 20 | color.rgb = pow(color.rgb, vec3(4.0)); 21 | 22 | frag_color = vec4( 23 | pow( 24 | vec3( 25 | 1.000 * color.r + 0.196 * color.g, 26 | 0.039 * color.r + 0.901 * color.g + 0.117 * color.b, 27 | 0.196 * color.r + 0.039 * color.g + 0.862 * color.b 28 | ), 29 | vec3(1.0 / 2.2) 30 | ), 1.0); 31 | } 32 | )"; -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/palette_viewer_window.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | #include "palette_viewer_window.hpp" 12 | 13 | PaletteViewerWindow::PaletteViewerWindow(nba::CoreBase* core, QWidget* parent) : QDialog(parent) { 14 | const auto vbox = new QVBoxLayout{}; 15 | 16 | m_palette_viewer = new PaletteViewer{core}; 17 | vbox->addWidget(m_palette_viewer); 18 | vbox->setSizeConstraint(QLayout::SetFixedSize); 19 | 20 | setLayout(vbox); 21 | setWindowTitle(tr("Palette Viewer")); 22 | setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); 23 | } 24 | 25 | void PaletteViewerWindow::Update() { 26 | m_palette_viewer->Update(); 27 | } 28 | -------------------------------------------------------------------------------- /src/platform/core/src/writer/save_state.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | namespace nba { 12 | 13 | auto SaveStateWriter::Write( 14 | std::unique_ptr& core, 15 | fs::path const& path 16 | ) -> Result { 17 | std::ofstream file_stream{path.c_str(), std::ios::binary}; 18 | 19 | if(!file_stream.good()) { 20 | return Result::CannotOpenFile; 21 | } 22 | 23 | SaveState save_state; 24 | core->CopyState(save_state); 25 | 26 | file_stream.write((const char*)&save_state, sizeof(SaveState)); 27 | 28 | if(!file_stream.good()) { 29 | return Result::CannotWrite; 30 | } 31 | 32 | return Result::Success; 33 | } 34 | 35 | } // namespace nba -------------------------------------------------------------------------------- /src/platform/core/src/device/shader/common.glsl.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | constexpr auto common_vert = R"( 11 | #version 330 core 12 | 13 | layout(location = 0) in vec2 position; 14 | layout(location = 1) in vec2 uv; 15 | 16 | out vec2 v_uv; 17 | 18 | void main() { 19 | v_uv = uv; 20 | gl_Position = vec4(position, 0.0, 1.0); 21 | } 22 | )"; 23 | 24 | constexpr auto common_flip_vert = R"( 25 | #version 330 core 26 | 27 | layout(location = 0) in vec2 position; 28 | layout(location = 1) in vec2 uv; 29 | 30 | out vec2 v_uv; 31 | 32 | void main() { 33 | v_uv = vec2(uv.x, 1.0 - uv.y); 34 | gl_Position = vec4(position, 0.0, 1.0); 35 | } 36 | )"; 37 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/dsp/resampler/nearest.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | template 15 | struct NearestResampler : Resampler { 16 | NearestResampler(std::shared_ptr> output) 17 | : Resampler(output) { 18 | } 19 | 20 | void Write(T const& input) final { 21 | while(resample_phase < 1.0) { 22 | this->output->Write(input); 23 | resample_phase += this->resample_phase_shift; 24 | } 25 | 26 | resample_phase = resample_phase - 1.0; 27 | } 28 | 29 | private: 30 | float resample_phase = 0; 31 | }; 32 | 33 | template 34 | using NearestStereoResampler = NearestResampler>; 35 | 36 | } // namespace nba 37 | -------------------------------------------------------------------------------- /src/platform/qt/version.cmake: -------------------------------------------------------------------------------- 1 | 2 | find_package(Git) 3 | 4 | set(VERSION_MAJOR 1) 5 | set(VERSION_MINOR 8) 6 | set(VERSION_PATCH 2) 7 | 8 | option(RELEASE_BUILD "Build a release version" OFF) 9 | 10 | if(GIT_FOUND) 11 | # https://stackoverflow.com/questions/1435953/how-can-i-pass-git-sha1-to-compiler-as-definition-using-cmake 12 | execute_process( 13 | COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD 14 | OUTPUT_VARIABLE VERSION_GIT_BRANCH) 15 | 16 | execute_process( 17 | COMMAND ${GIT_EXECUTABLE} describe --match=NeVeRmAtCh --always --dirty 18 | OUTPUT_VARIABLE VERSION_GIT_HASH) 19 | 20 | string(STRIP "${VERSION_GIT_BRANCH}" VERSION_GIT_BRANCH) 21 | string(STRIP "${VERSION_GIT_HASH}" VERSION_GIT_HASH) 22 | endif() 23 | 24 | configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.in.hpp" "${CMAKE_CURRENT_BINARY_DIR}/version.hpp") 25 | configure_file(${CMAKE_CURRENT_SOURCE_DIR}/rc/app.rc ${CMAKE_CURRENT_BINARY_DIR}/app.rc) -------------------------------------------------------------------------------- /src/platform/core/include/platform/game_db.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace nba { 14 | 15 | enum class GPIODeviceType { 16 | None = 0, 17 | RTC = 1, 18 | SolarSensor = 2 19 | }; 20 | 21 | struct GameInfo { 22 | Config::BackupType backup_type = Config::BackupType::Detect; 23 | GPIODeviceType gpio = GPIODeviceType::None; 24 | bool mirror = false; 25 | }; 26 | 27 | extern const std::map g_game_db; 28 | 29 | constexpr GPIODeviceType operator|(GPIODeviceType lhs, GPIODeviceType rhs) { 30 | return (GPIODeviceType)((int)lhs | (int)rhs); 31 | } 32 | 33 | constexpr int operator&(GPIODeviceType lhs, GPIODeviceType rhs) { 34 | return (int)lhs & (int)rhs; 35 | } 36 | 37 | } // namespace nba 38 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/utility.hpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | constexpr u32 Rgb565ToArgb8888(u16 color_rgb565) { 10 | const uint r = (color_rgb565 >> 0) & 31; 11 | const uint g = ((color_rgb565 >> 4) & 62) | (color_rgb565 >> 15); 12 | const uint b = (color_rgb565 >> 10) & 31; 13 | 14 | return 0xFF000000u | 15 | (r << 3 | r >> 2) << 16 | 16 | (g << 2 | g >> 4) << 8 | 17 | (b << 3 | b >> 2); 18 | } 19 | 20 | inline QLabel* CreateMonospaceLabel() { 21 | QLabel* label = new QLabel{"-"}; 22 | label->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 23 | label->setTextInteractionFlags(Qt::TextSelectableByMouse); 24 | return label; 25 | } 26 | 27 | inline QCheckBox* CreateReadOnlyCheckBox() { 28 | QCheckBox* check_box = new QCheckBox{}; 29 | check_box->setEnabled(false); 30 | return check_box; 31 | } -------------------------------------------------------------------------------- /src/nba/include/nba/common/dsp/resampler.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #ifndef M_PI 16 | #define M_PI (3.141592653589793238463) 17 | #endif 18 | 19 | namespace nba { 20 | 21 | template 22 | struct Resampler : WriteStream { 23 | Resampler(std::shared_ptr> output) : output(output) {} 24 | 25 | virtual void SetSampleRates(float samplerate_in, float samplerate_out) { 26 | resample_phase_shift = samplerate_in / samplerate_out; 27 | } 28 | 29 | protected: 30 | std::shared_ptr> output; 31 | 32 | float resample_phase_shift = 1; 33 | }; 34 | 35 | template 36 | using StereoResampler = Resampler>; 37 | 38 | } // namespace nba 39 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/palette_viewer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "color_grid.hpp" 15 | 16 | class PaletteViewer : public QWidget { 17 | public: 18 | PaletteViewer(nba::CoreBase* core, QWidget* parent = nullptr); 19 | 20 | void Update(); 21 | 22 | private: 23 | QLayout* CreatePaletteGrids(); 24 | QLayout* CreateColorInfoArea(); 25 | 26 | void ShowColorInfo(int color_index); 27 | 28 | u16* m_pram; 29 | ColorGrid* m_palette_color_grids[2]; 30 | QLabel* m_label_color_address; 31 | QLabel* m_label_color_r_component; 32 | QLabel* m_label_color_g_component; 33 | QLabel* m_label_color_b_component; 34 | QLabel* m_label_color_value; 35 | QWidget* m_box_color_preview; 36 | 37 | Q_OBJECT 38 | }; 39 | -------------------------------------------------------------------------------- /src/nba/include/nba/device/audio_device.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | struct AudioDevice { 15 | virtual ~AudioDevice() = default; 16 | 17 | typedef void (*Callback)(void* userdata, s16* stream, int byte_len); 18 | 19 | virtual auto GetSampleRate() -> int = 0; 20 | virtual auto GetBlockSize() -> int = 0; 21 | virtual bool Open(void* userdata, Callback callback) = 0; 22 | virtual void SetPause(bool value) = 0; 23 | virtual void Close() = 0; 24 | }; 25 | 26 | struct NullAudioDevice : AudioDevice { 27 | auto GetSampleRate() -> int final { return 32768; } 28 | auto GetBlockSize() -> int final { return 4096; } 29 | bool Open(void* userdata, Callback callback) final { return true; } 30 | void SetPause(bool value) final { } 31 | void Close() { } 32 | }; 33 | 34 | } // namespace nba 35 | -------------------------------------------------------------------------------- /src/nba/src/hw/irq/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "arm/arm7tdmi.hpp" 9 | #include "irq.hpp" 10 | 11 | namespace nba::core { 12 | 13 | void IRQ::LoadState(SaveState const& state) { 14 | pending_ime = state.irq.pending_ime; 15 | pending_ie = state.irq.pending_ie; 16 | pending_if = state.irq.pending_if; 17 | 18 | reg_ime = state.irq.reg_ime; 19 | reg_ie = state.irq.reg_ie; 20 | reg_if = state.irq.reg_if; 21 | 22 | irq_line = reg_ime && (reg_ie & reg_if) != 0; 23 | 24 | irq_available = state.irq.irq_available; 25 | } 26 | 27 | void IRQ::CopyState(SaveState& state) { 28 | state.irq.pending_ime = pending_ime; 29 | state.irq.pending_ie = pending_ie; 30 | state.irq.pending_if = pending_if; 31 | 32 | state.irq.reg_ime = reg_ime; 33 | state.irq.reg_ie = reg_ie; 34 | state.irq.reg_if = reg_if; 35 | 36 | state.irq.irq_available = irq_available; 37 | } 38 | 39 | } // namespace nba::core 40 | -------------------------------------------------------------------------------- /src/nba/include/nba/rom/gpio/device.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace nba { 14 | 15 | struct GPIODevice { 16 | enum class PortDirection { 17 | In = 0, // GPIO -> GBA 18 | Out = 1 // GPIO <- GBA 19 | }; 20 | 21 | virtual ~GPIODevice() = default; 22 | 23 | virtual void Reset() = 0; 24 | virtual auto Read() -> int = 0; 25 | virtual void Write(int value) = 0; 26 | 27 | virtual void LoadState(SaveState const& state) {} 28 | virtual void CopyState(SaveState& state) {} 29 | 30 | void SetPortDirections(int port_directions) { 31 | this->port_directions = port_directions; 32 | } 33 | 34 | auto GetPortDirection(int pin) const -> PortDirection { 35 | return static_cast((port_directions >> pin) & 1); 36 | } 37 | 38 | private: 39 | int port_directions = 0; 40 | }; 41 | 42 | } // namespace nba 43 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/background_viewer_window.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | #include "background_viewer_window.hpp" 11 | 12 | BackgroundViewerWindow::BackgroundViewerWindow(nba::CoreBase* core, QWidget* parent) : QDialog(parent) { 13 | m_tab_widget = new QTabWidget{}; 14 | 15 | for(int id = 0; id < 4; id++) { 16 | const auto bg_viewer = new BackgroundViewer{core}; 17 | 18 | bg_viewer->SetBackgroundID(id); 19 | m_tab_widget->addTab(bg_viewer, QStringLiteral("BG%1").arg(id)); 20 | m_bg_viewers[id] = bg_viewer; 21 | } 22 | 23 | setLayout(new QVBoxLayout{}); 24 | layout()->addWidget(m_tab_widget); 25 | 26 | setWindowTitle(tr("Background Viewer")); 27 | setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); 28 | } 29 | 30 | void BackgroundViewerWindow::Update() { 31 | if(isVisible()) { 32 | m_bg_viewers[m_tab_widget->currentIndex()]->Update(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/nba/src/hw/rom/gpio/solar_sensor.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | namespace nba { 12 | 13 | SolarSensor::SolarSensor() { 14 | Reset(); 15 | } 16 | 17 | void SolarSensor::Reset() { 18 | old_clk = false; 19 | counter = 0; 20 | SetLightLevel(0x60); 21 | } 22 | 23 | auto SolarSensor::Read() -> int { 24 | return (counter > current_level) ? (1 << Pin::FLG) : 0; 25 | } 26 | 27 | void SolarSensor::Write(int value) { 28 | bool clk = (value & (1 << Pin::CLK)) && GetPortDirection(Pin::CLK) == PortDirection::Out; 29 | bool rst = (value & (1 << Pin::RST)) && GetPortDirection(Pin::RST) == PortDirection::Out; 30 | 31 | if(rst) { 32 | counter = 0; 33 | } else if(old_clk && !clk) { 34 | counter++; 35 | } 36 | 37 | old_clk = clk; 38 | } 39 | 40 | void SolarSensor::SetLightLevel(u8 level) { 41 | current_level = 255 - level; 42 | } 43 | 44 | } // namespace nba 45 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/color_grid.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | class ColorGrid : public QWidget { 16 | public: 17 | ColorGrid(int rows, int columns, QWidget* parent = nullptr); 18 | ~ColorGrid() override; 19 | 20 | void Draw(u16* buffer_rgb565, int stride); 21 | void SetHighlightedPosition(int x, int y); 22 | void ClearHighlight(); 23 | u32 GetColorAt(int x, int y); 24 | 25 | signals: 26 | void selected(int x, int y); 27 | 28 | protected: 29 | void paintEvent(QPaintEvent* event) override; 30 | void mousePressEvent(QMouseEvent* event) override; 31 | 32 | private: 33 | static constexpr int k_box_size = 12; 34 | 35 | int m_rows; 36 | int m_columns; 37 | int m_highlighted_x = -1; 38 | int m_highlighted_y = -1; 39 | 40 | u32* m_buffer_argb8888; 41 | 42 | Q_OBJECT 43 | }; -------------------------------------------------------------------------------- /src/nba/include/nba/rom/header.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | #pragma pack(push, 1) 15 | 16 | // For details see: http://problemkaputt.de/gbatek.htm#gbacartridgeheader 17 | struct Header { 18 | // First instruction 19 | u32 entrypoint; 20 | 21 | // Compressed Bitmap (Nintendo Logo) 22 | u8 nintendo_logo[156]; 23 | 24 | // Game Title, Code and Maker Code 25 | struct { 26 | char title[12]; 27 | char code[4]; 28 | char maker[2]; 29 | } game; 30 | 31 | u8 fixed_96h; 32 | u8 unit_code; 33 | u8 device_type; 34 | u8 reserved[7]; 35 | u8 version; 36 | u8 checksum; 37 | u8 reserved2[2]; 38 | 39 | // Multiboot Header 40 | struct { 41 | u32 ram_entrypoint; 42 | u8 boot_mode; 43 | u8 slave_id; 44 | u8 unused[26]; 45 | u32 joy_entrypoint; 46 | } mb; 47 | }; 48 | 49 | #pragma pack(pop) 50 | 51 | } // namespace nba 52 | -------------------------------------------------------------------------------- /src/nba/src/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "core.hpp" 9 | 10 | namespace nba::core { 11 | 12 | void Core::LoadState(SaveState const& state) { 13 | scheduler.Reset(); 14 | scheduler.SetTimestampNow(state.timestamp); 15 | 16 | scheduler.LoadState(state); 17 | cpu.LoadState(state); 18 | bus.LoadState(state); 19 | irq.LoadState(state); 20 | ppu.LoadState(state); 21 | apu.LoadState(state); 22 | timer.LoadState(state); 23 | dma.LoadState(state); 24 | keypad.LoadState(state); 25 | } 26 | 27 | void Core::CopyState(SaveState& state) { 28 | state.magic = SaveState::kMagicNumber; 29 | state.version = SaveState::kCurrentVersion; 30 | state.timestamp = scheduler.GetTimestampNow(); 31 | 32 | scheduler.CopyState(state); 33 | cpu.CopyState(state); 34 | bus.CopyState(state); 35 | irq.CopyState(state); 36 | ppu.CopyState(state); 37 | apu.CopyState(state); 38 | timer.CopyState(state); 39 | dma.CopyState(state); 40 | keypad.CopyState(state); 41 | } 42 | 43 | } // namespace nba::core -------------------------------------------------------------------------------- /src/platform/core/include/platform/frame_limiter.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace nba { 15 | 16 | struct FrameLimiter { 17 | FrameLimiter(float fps = 60.0) { 18 | Reset(fps); 19 | } 20 | 21 | void Reset(); 22 | void Reset(float fps); 23 | auto GetFastForward() const -> bool; 24 | void SetFastForward(bool value); 25 | 26 | void Run( 27 | std::function frame_advance, 28 | std::function update_fps 29 | ); 30 | 31 | private: 32 | static constexpr int kMillisecondsPerSecond = 1000; 33 | static constexpr int kMicrosecondsPerSecond = 1000000; 34 | 35 | int frame_count = 0; 36 | int frame_duration; 37 | float frames_per_second; 38 | bool fast_forward = false; 39 | 40 | std::chrono::time_point timestamp_target; 41 | std::chrono::time_point timestamp_fps_update; 42 | }; 43 | 44 | } // namespace nba 45 | -------------------------------------------------------------------------------- /src/platform/core/include/platform/device/sdl_audio_device.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace nba { 15 | 16 | struct SDL2_AudioDevice : AudioDevice { 17 | void SetSampleRate(int sample_rate); 18 | void SetBlockSize(int buffer_size); 19 | void SetPassthrough(SDL_AudioCallback passthrough); 20 | void InvokeCallback(s16* stream, int byte_len); 21 | 22 | auto GetSampleRate() -> int final; 23 | auto GetBlockSize() -> int final; 24 | bool Open(void* userdata, Callback callback) final; 25 | void SetPause(bool value) final; 26 | void Close() final; 27 | 28 | private: 29 | Callback callback; 30 | void* callback_userdata; 31 | SDL_AudioCallback passthrough = nullptr; 32 | SDL_AudioDeviceID device; 33 | SDL_AudioSpec have; 34 | int want_sample_rate = 48000; 35 | int want_block_size = 2048; 36 | bool opened = false; 37 | bool paused = false; 38 | }; 39 | 40 | } // namespace nba 41 | -------------------------------------------------------------------------------- /src/platform/core/src/loader/bios.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace nba { 14 | 15 | static constexpr size_t kBIOSSize = 0x4000; 16 | 17 | auto BIOSLoader::Load( 18 | std::unique_ptr& core, 19 | fs::path const& path 20 | ) -> Result { 21 | if(!fs::exists(path)) { 22 | return Result::CannotFindFile; 23 | } 24 | 25 | if(fs::is_directory(path)) { 26 | return Result::CannotOpenFile; 27 | } 28 | 29 | auto size = fs::file_size(path); 30 | if(size != kBIOSSize) { 31 | return Result::BadImage; 32 | } 33 | 34 | auto file_stream = std::ifstream{path.c_str(), std::ios::binary}; 35 | if(!file_stream.good()) { 36 | return Result::CannotOpenFile; 37 | } 38 | 39 | std::vector file_data; 40 | file_data.resize(size); 41 | file_stream.read((char*)file_data.data(), size); 42 | file_stream.close(); 43 | 44 | core->Attach(file_data); 45 | return Result::Success; 46 | } 47 | 48 | } // namespace nba 49 | -------------------------------------------------------------------------------- /src/platform/qt/rc/app.rc: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | IDI_ICON1 ICON DISCARDABLE "${CMAKE_CURRENT_SOURCE_DIR}/rc/app.ico" 5 | 6 | VS_VERSION_INFO VERSIONINFO 7 | FILEVERSION ${VERSION_MAJOR},${VERSION_MINOR},${VERSION_PATCH} 8 | PRODUCTVERSION ${VERSION_MAJOR},${VERSION_MINOR},${VERSION_PATCH} 9 | BEGIN 10 | BLOCK "StringFileInfo" 11 | BEGIN 12 | BLOCK "040904E4" 13 | BEGIN 14 | VALUE "CompanyName", "fleroviux" 15 | VALUE "FileDescription", "NanoBoyAdvance" 16 | VALUE "FileVersion", "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}" 17 | VALUE "InternalName", "NanoBoyAdvance.exe" 18 | VALUE "LegalCopyright", "(c) 2015 - 2025 fleroviux" 19 | VALUE "OriginalFilename", "NanoBoyAdvance.exe" 20 | VALUE "ProductName", "NanoBoyAdvance" 21 | VALUE "ProductVersion", "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}" 22 | END 23 | END 24 | 25 | BLOCK "VarFileInfo" 26 | BEGIN 27 | VALUE "Translation", 0x409, 1252 28 | END 29 | END 30 | 31 | CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/rc/app.manifest" -------------------------------------------------------------------------------- /src/platform/core/include/platform/config.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace nba { 15 | 16 | struct PlatformConfig : Config { 17 | std::string bios_path = "bios.bin"; 18 | std::string save_folder = ""; 19 | 20 | struct Cartridge { 21 | BackupType backup_type = BackupType::Detect; 22 | bool force_rtc = true; 23 | bool force_solar_sensor = false; 24 | u8 solar_sensor_level = 23; 25 | } cartridge; 26 | 27 | struct Video { 28 | enum class Filter { 29 | Nearest, 30 | Linear, 31 | Sharp, 32 | xBRZ, 33 | Lcd1x 34 | } filter = Filter::Linear; 35 | 36 | enum class Color { 37 | No, 38 | higan, 39 | AGB 40 | } color = Color::AGB; 41 | 42 | bool lcd_ghosting = true; 43 | } video; 44 | 45 | void Load(std::string const& path); 46 | void Save(std::string const& path); 47 | 48 | protected: 49 | virtual void LoadCustomData(toml::value const& data) {} 50 | virtual void SaveCustomData(toml::value& data) {} 51 | }; 52 | 53 | } // namespace nba 54 | -------------------------------------------------------------------------------- /src/nba/src/hw/keypad/keypad.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "hw/irq/irq.hpp" 16 | 17 | namespace nba::core { 18 | 19 | struct KeyPad { 20 | KeyPad(Scheduler& scheduler, IRQ& irq); 21 | 22 | void Reset(); 23 | void SetKeyStatus(Key key, bool pressed); 24 | 25 | struct KeyInput { 26 | u16 value = 0x3FF; 27 | 28 | KeyPad* keypad; 29 | 30 | auto ReadByte(uint offset) -> u8; 31 | } input; 32 | 33 | struct KeyControl { 34 | u16 mask; 35 | bool interrupt; 36 | 37 | enum class Mode { 38 | LogicalOR = 0, 39 | LogicalAND = 1 40 | } mode = Mode::LogicalOR; 41 | 42 | KeyPad* keypad; 43 | 44 | auto ReadByte(uint offset) -> u8; 45 | void WriteByte(uint offset, u8 value); 46 | void WriteHalf(u16 value); 47 | } control; 48 | 49 | void LoadState(SaveState const& state); 50 | void CopyState(SaveState& state); 51 | 52 | private: 53 | void UpdateIRQ(); 54 | 55 | Scheduler& scheduler; 56 | IRQ& irq; 57 | }; 58 | 59 | } // namespace nba::core 60 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/noise_channel.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include "hw/apu/channel/base_channel.hpp" 14 | 15 | namespace nba::core { 16 | 17 | struct BIAS; 18 | 19 | class NoiseChannel : public BaseChannel { 20 | public: 21 | NoiseChannel(Scheduler& scheduler, BIAS& bias); 22 | 23 | void Reset(); 24 | auto GetSample() -> s8 override { return sample; } 25 | void Generate(); 26 | auto Read (int offset) -> u8; 27 | void Write(int offset, u8 value); 28 | 29 | void LoadState(SaveState::APU::IO::NoiseChannel const& state); 30 | void CopyState(SaveState::APU::IO::NoiseChannel& state); 31 | 32 | private: 33 | static constexpr int GetSynthesisInterval(int ratio, int shift) { 34 | int interval = 64 << shift; 35 | 36 | if(ratio == 0) { 37 | interval /= 2; 38 | } else { 39 | interval *= ratio; 40 | } 41 | 42 | return interval; 43 | } 44 | 45 | u16 lfsr; 46 | s8 sample = 0; 47 | 48 | Scheduler& scheduler; 49 | Scheduler::Event* event; 50 | 51 | int frequency_shift; 52 | int frequency_ratio; 53 | int width; 54 | bool dac_enable; 55 | 56 | BIAS& bias; 57 | int skip_count; 58 | }; 59 | 60 | } // namespace nba::core 61 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/quad_channel.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include "hw/apu/channel/base_channel.hpp" 14 | 15 | namespace nba::core { 16 | 17 | class QuadChannel final : public BaseChannel { 18 | public: 19 | QuadChannel(Scheduler& scheduler, Scheduler::EventClass event_class); 20 | 21 | void Reset(); 22 | auto GetSample() -> s8 override { return sample; } 23 | void Generate(); 24 | auto Read (int offset) -> u8; 25 | void Write(int offset, u8 value); 26 | 27 | void LoadState(SaveState::APU::IO::QuadChannel const& state); 28 | void CopyState(SaveState::APU::IO::QuadChannel& state); 29 | 30 | private: 31 | static constexpr int GetSynthesisIntervalFromFrequency(int frequency) { 32 | // 128 cycles equals 131072 Hz, the highest possible frequency. 33 | // We are dividing by eight, because the waveform can change at 34 | // eight evenly spaced points inside a single cycle, depending on the wave duty. 35 | return 128 * (2048 - frequency) / 8; 36 | } 37 | 38 | Scheduler& scheduler; 39 | Scheduler::EventClass event_class; 40 | Scheduler::Event* event; 41 | 42 | s8 sample = 0; 43 | int phase; 44 | int wave_duty; 45 | bool dac_enable; 46 | }; 47 | 48 | } // namespace nba::core 49 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/dsp/resampler/cosine.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | template 15 | struct CosineResampler : Resampler { 16 | CosineResampler(std::shared_ptr> output) 17 | : Resampler(output) { 18 | for(int i = 0; i < kLUTsize; i++) { 19 | lut[i] = (std::cos(M_PI * i / (float)(kLUTsize - 1)) + 1.0) * 0.5; 20 | } 21 | } 22 | 23 | void Write(T const& input) final { 24 | while(resample_phase < 1.0) { 25 | const float index = resample_phase * (float)(kLUTsize - 1); 26 | const float a0 = lut[(int)index]; 27 | const float a1 = lut[(int)index + 1]; 28 | const float a = a0 + (a1 - a0) * (index - int(index)); 29 | 30 | this->output->Write(previous * a + input * (1.0 - a)); 31 | 32 | resample_phase += this->resample_phase_shift; 33 | } 34 | 35 | resample_phase = resample_phase - 1.0; 36 | 37 | previous = input; 38 | } 39 | 40 | private: 41 | static constexpr int kLUTsize = 512; 42 | 43 | T previous = {}; 44 | float resample_phase = 0; 45 | float lut[kLUTsize]; 46 | }; 47 | 48 | template 49 | using CosineStereoResampler = CosineResampler>; 50 | 51 | } // namespace nba 52 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/screen.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "config.hpp" 15 | 16 | struct Screen : QWidget, nba::VideoDevice { 17 | explicit Screen( 18 | QWidget* parent, 19 | std::shared_ptr config 20 | ); 21 | 22 | bool Initialize(); 23 | void Draw(u32* buffer) final; 24 | void ReloadConfig(); 25 | QPaintEngine* paintEngine() const override { return nullptr; }; // Silence Qt. 26 | 27 | signals: 28 | void RequestDraw(u32* buffer); 29 | 30 | private slots: 31 | void OnRequestDraw(u32* buffer); 32 | 33 | protected: 34 | void paintEvent(QPaintEvent* event) override; 35 | void resizeEvent(QResizeEvent* event) override; 36 | 37 | private: 38 | static constexpr int kGBANativeWidth = 240; 39 | static constexpr int kGBANativeHeight = 160; 40 | static constexpr float kGBANativeAR = static_cast(kGBANativeWidth) / static_cast(kGBANativeHeight); 41 | 42 | void Render(); 43 | void UpdateViewport(); 44 | 45 | u32* buffer = nullptr; 46 | QOpenGLContext* context = nullptr; 47 | nba::OGLVideoDevice ogl_video_device; 48 | std::shared_ptr config; 49 | 50 | Q_OBJECT 51 | }; 52 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/dsp/resampler/cubic.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | template 15 | struct CubicResampler : Resampler { 16 | CubicResampler(std::shared_ptr> output) 17 | : Resampler(output) { 18 | } 19 | 20 | void Write(T const& input) final { 21 | while(resample_phase < 1.0) { 22 | // http://paulbourke.net/miscellaneous/interpolation/ 23 | T a0, a1, a2, a3; 24 | float mu, mu2; 25 | 26 | mu = resample_phase; 27 | mu2 = mu * mu; 28 | a0 = input - previous[0] - previous[2] + previous[1]; 29 | a1 = previous[2] - previous[1] - a0; 30 | a2 = previous[0] - previous[2]; 31 | a3 = previous[1]; 32 | 33 | this->output->Write(a0*mu*mu2 + a1*mu2 + a2*mu + a3); 34 | 35 | resample_phase += this->resample_phase_shift; 36 | } 37 | 38 | resample_phase = resample_phase - 1.0; 39 | 40 | previous[2] = previous[1]; 41 | previous[1] = previous[0]; 42 | previous[0] = input; 43 | } 44 | 45 | private: 46 | T previous[3] = {{},{},{}}; 47 | float resample_phase = 0; 48 | }; 49 | 50 | template 51 | using CubicStereoResampler = CubicResampler>; 52 | 53 | } // namespace nba 54 | -------------------------------------------------------------------------------- /src/nba/include/nba/rom/backup/flash.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace nba { 15 | 16 | struct FLASH : Backup { 17 | enum Size { 18 | SIZE_64K = 0, 19 | SIZE_128K = 1 20 | }; 21 | 22 | FLASH(fs::path const& save_path, Size size_hint); 23 | 24 | void Reset() final; 25 | auto Read (u32 address) -> u8 final; 26 | void Write(u32 address, u8 value) final; 27 | 28 | void LoadState(SaveState const& state) final; 29 | void CopyState(SaveState& state) final; 30 | 31 | private: 32 | 33 | enum Command { 34 | READ_CHIP_ID = 0x90, 35 | FINISH_CHIP_ID = 0xF0, 36 | ERASE = 0x80, 37 | ERASE_CHIP = 0x10, 38 | ERASE_SECTOR = 0x30, 39 | WRITE_BYTE = 0xA0, 40 | SELECT_BANK = 0xB0 41 | }; 42 | 43 | void HandleCommand(u32 address, u8 value); 44 | void HandleExtended(u32 address, u8 value); 45 | 46 | auto Physical(int index) -> int { return current_bank * 65536 + index; } 47 | 48 | Size size; 49 | fs::path save_path; 50 | std::unique_ptr file; 51 | 52 | int current_bank; 53 | int phase; 54 | bool enable_chip_id; 55 | bool enable_erase; 56 | bool enable_write; 57 | bool enable_select; 58 | }; 59 | 60 | } // namespace nba 61 | -------------------------------------------------------------------------------- /src/nba/include/nba/rom/gpio/gpio.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace nba { 20 | 21 | struct GPIO { 22 | GPIO() { 23 | Reset(); 24 | } 25 | 26 | void Reset(); 27 | void Attach(std::shared_ptr device); 28 | 29 | template 30 | auto Get() -> T* { 31 | auto match = device_map.find(std::type_index{typeid(T)}); 32 | 33 | if(match != device_map.end()) { 34 | return (T*)match->second; 35 | } 36 | return nullptr; 37 | } 38 | 39 | bool IsReadable() const { 40 | return allow_reads; 41 | } 42 | 43 | auto Read (u32 address) -> u8; 44 | void Write(u32 address, u8 value); 45 | 46 | void LoadState(SaveState const& state); 47 | void CopyState(SaveState& state); 48 | 49 | private: 50 | enum class Register { 51 | Data = 0xC4, 52 | Direction = 0xC6, 53 | Control = 0xC8 54 | }; 55 | 56 | bool allow_reads; 57 | u8 rd_mask; 58 | u8 wr_mask; 59 | u8 port_data; 60 | 61 | std::vector> devices; 62 | 63 | std::unordered_map device_map; 64 | }; 65 | 66 | } // namespace nba 67 | -------------------------------------------------------------------------------- /src/nba/src/hw/irq/irq.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace nba::core { 15 | 16 | namespace arm { 17 | struct ARM7TDMI; 18 | }; 19 | 20 | struct IRQ { 21 | enum class Source { 22 | VBlank, 23 | HBlank, 24 | VCount, 25 | Timer, 26 | Serial, 27 | DMA, 28 | Keypad, 29 | ROM 30 | }; 31 | 32 | IRQ(arm::ARM7TDMI& cpu, Scheduler& scheduler); 33 | 34 | void Reset(); 35 | auto ReadByte(int offset) const -> u8; 36 | auto ReadHalf(int offset) const -> u16; 37 | void WriteByte(int offset, u8 value); 38 | void WriteHalf(int offset, u16 value); 39 | void Raise(IRQ::Source source, int channel = 0); 40 | 41 | bool ShouldUnhaltCPU() const { 42 | return irq_available; 43 | } 44 | 45 | void LoadState(SaveState const& state); 46 | void CopyState(SaveState& state); 47 | 48 | private: 49 | enum Registers { 50 | REG_IE = 0, 51 | REG_IF = 2, 52 | REG_IME = 4 53 | }; 54 | 55 | void OnWriteIO(); 56 | void UpdateIEAndIF(u64 irq_available); 57 | void UpdateIRQLine(u64 irq_line); 58 | 59 | int pending_ime; 60 | u16 pending_ie; 61 | u16 pending_if; 62 | 63 | int reg_ime; 64 | u16 reg_ie; 65 | u16 reg_if; 66 | 67 | arm::ARM7TDMI& cpu; 68 | Scheduler& scheduler; 69 | bool irq_line; 70 | bool irq_available; 71 | }; 72 | 73 | } // namespace nba::core 74 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/dsp/ring_buffer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace nba { 15 | 16 | template 17 | struct RingBuffer : Stream { 18 | RingBuffer(int length, bool blocking = false) 19 | : length(length) 20 | , blocking(blocking) { 21 | data = std::make_unique(length); 22 | Reset(); 23 | } 24 | 25 | auto Available() -> int { return count; } 26 | 27 | void Reset() { 28 | rd_ptr = 0; 29 | wr_ptr = 0; 30 | count = 0; 31 | for(int i = 0; i < length; i++) { 32 | data[i] = {}; 33 | } 34 | } 35 | 36 | auto Peek(int offset) -> T const { 37 | return data[(rd_ptr + offset) % length]; 38 | } 39 | 40 | auto Read() -> T { 41 | T value = data[rd_ptr]; 42 | if(count > 0) { 43 | rd_ptr = (rd_ptr + 1) % length; 44 | count--; 45 | } 46 | return value; 47 | } 48 | 49 | void Write(T const& value) { 50 | if(blocking && count == length) { 51 | return; 52 | } 53 | data[wr_ptr] = value; 54 | wr_ptr = (wr_ptr + 1) % length; 55 | count++; 56 | } 57 | 58 | private: 59 | std::unique_ptr data; 60 | 61 | int rd_ptr; 62 | int wr_ptr; 63 | int length; 64 | int count; 65 | bool blocking; 66 | }; 67 | 68 | template 69 | using StereoRingBuffer = RingBuffer>; 70 | 71 | } // namespace nba 72 | -------------------------------------------------------------------------------- /src/nba/include/nba/rom/backup/eeprom.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace nba { 16 | 17 | struct EEPROM : Backup { 18 | enum Size { 19 | SIZE_4K = 0, 20 | SIZE_64K = 1, 21 | DETECT = 2 22 | }; 23 | 24 | EEPROM(fs::path const& save_path, Size size_hint, core::Scheduler& scheduler); 25 | 26 | void Reset() final; 27 | auto Read (u32 address) -> u8 final; 28 | void Write(u32 address, u8 value) final; 29 | 30 | void LoadState(SaveState const& state) final; 31 | void CopyState(SaveState& state) final; 32 | 33 | void SetSizeHint(Size size); 34 | 35 | private: 36 | enum State { 37 | STATE_ACCEPT_COMMAND = 1 << 0, 38 | STATE_READ_MODE = 1 << 1, 39 | STATE_WRITE_MODE = 1 << 2, 40 | STATE_GET_ADDRESS = 1 << 3, 41 | STATE_READING = 1 << 4, 42 | STATE_DUMMY_NIBBLE = 1 << 5, 43 | STATE_WRITING = 1 << 6, 44 | STATE_EAT_DUMMY = 1 << 7, 45 | STATE_BUSY = 1 << 8 46 | }; 47 | 48 | void ResetSerialBuffer(); 49 | 50 | void OnReadyAfterWrite(); 51 | 52 | int size; 53 | fs::path save_path; 54 | std::unique_ptr file; 55 | 56 | core::Scheduler& scheduler; 57 | 58 | int state; 59 | int address; 60 | u64 serial_buffer; 61 | int transmitted_bits; 62 | 63 | bool detect_size; 64 | }; 65 | 66 | } // namespace nba 67 | -------------------------------------------------------------------------------- /src/nba/src/hw/ppu/window.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "ppu.hpp" 9 | 10 | namespace nba::core { 11 | 12 | void PPU::InitWindow() { 13 | const int vcount = mmio.vcount; 14 | 15 | for(int i = 0; i < 2; i++) { 16 | const auto& winv = mmio.winv[i]; 17 | 18 | if(vcount == winv.min) { 19 | window.v_flag[i] = true; 20 | } 21 | 22 | if(vcount == winv.max) { 23 | window.v_flag[i] = false; 24 | } 25 | } 26 | 27 | window.timestamp_last_sync = scheduler.GetTimestampNow(); 28 | window.cycle = 0; 29 | } 30 | 31 | void PPU::DrawWindow() { 32 | const u64 timestamp_now = scheduler.GetTimestampNow(); 33 | 34 | const int cycles = (int)(timestamp_now - window.timestamp_last_sync); 35 | 36 | if(cycles == 0 || window.cycle >= 1024U) { 37 | return; 38 | } 39 | 40 | for(int i = 0; i < cycles; i++) { 41 | if((window.cycle & 3U) == 0U) { 42 | const uint x = window.cycle >> 2; 43 | 44 | for(int i = 0; i < 2; i++) { 45 | const auto& winh = mmio.winh[i]; 46 | 47 | if(x == winh.min) { 48 | window.h_flag[i] = true; 49 | } 50 | 51 | if(x == winh.max) { 52 | window.h_flag[i] = false; 53 | } 54 | 55 | if(x < 240) { 56 | window.buffer[x][i] = window.h_flag[i] && window.v_flag[i]; 57 | } 58 | } 59 | } 60 | 61 | if(++window.cycle == 1024U) { 62 | break; 63 | } 64 | } 65 | 66 | window.timestamp_last_sync = timestamp_now; 67 | } 68 | 69 | } // namespace nba::core 70 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/wave_channel.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include "hw/apu/channel/base_channel.hpp" 14 | 15 | namespace nba::core { 16 | 17 | class WaveChannel : public BaseChannel { 18 | public: 19 | enum class ResetWaveRAM { 20 | No, 21 | Yes 22 | }; 23 | 24 | WaveChannel(Scheduler& scheduler); 25 | 26 | void Reset(ResetWaveRAM reset_wave_ram); 27 | bool IsEnabled() override { return playing && BaseChannel::IsEnabled(); } 28 | auto GetSample() -> s8 override { return sample; } 29 | void Generate(); 30 | auto Read (int offset) -> u8; 31 | void Write(int offset, u8 value); 32 | 33 | void LoadState(SaveState::APU::IO::WaveChannel const& state); 34 | void CopyState(SaveState::APU::IO::WaveChannel& state); 35 | 36 | auto ReadSample(int offset) -> u8 { 37 | return wave_ram[wave_bank ^ 1][offset]; 38 | } 39 | 40 | void WriteSample(int offset, u8 value) { 41 | wave_ram[wave_bank ^ 1][offset] = value; 42 | } 43 | 44 | private: 45 | constexpr int GetSynthesisIntervalFromFrequency(int frequency) { 46 | // 8 cycles equals 2097152 Hz, the highest possible sample rate. 47 | return 8 * (2048 - frequency); 48 | } 49 | 50 | Scheduler& scheduler; 51 | Scheduler::Event* event = nullptr; 52 | 53 | s8 sample = 0; 54 | bool playing; 55 | bool force_volume; 56 | int volume; 57 | int frequency; 58 | int dimension; 59 | int wave_bank; 60 | u8 wave_ram[2][16]; 61 | int phase; 62 | }; 63 | 64 | } // namespace nba::core 65 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/controller_manager.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "config.hpp" 19 | #include "widget/input_window.hpp" 20 | 21 | struct MainWindow; 22 | 23 | struct ControllerManager : QWidget { 24 | ControllerManager( 25 | MainWindow* main_window, 26 | std::shared_ptr config 27 | ) : main_window(main_window) 28 | , config(config) { 29 | } 30 | 31 | ~ControllerManager(); 32 | 33 | void Initialize(); 34 | 35 | signals: 36 | void OnJoystickListChanged(); 37 | void OnJoystickButtonReleased(int button); 38 | void OnJoystickAxisMoved(int axis, bool negative); 39 | void OnJoystickHatMoved(int hat, int direction); 40 | 41 | private slots: 42 | void UpdateJoystickList(); 43 | void BindCurrentKeyToJoystickButton(int button); 44 | void BindCurrentKeyToJoystickAxis(int axis, bool negative); 45 | void BindCurrentKeyToJoystickHat(int hat, int direction); 46 | 47 | private: 48 | void Open(std::string const& guid); 49 | void Close(); 50 | void ProcessEvents(); 51 | void UpdateKeyState(); 52 | 53 | MainWindow* main_window; 54 | std::shared_ptr config; 55 | std::thread thread; 56 | QTimer* timer = nullptr; 57 | std::atomic_bool quitting = false; 58 | SDL_Joystick* joystick = nullptr; 59 | SDL_JoystickID instance_id; 60 | std::mutex lock; 61 | bool fast_forward_button_old = false; 62 | 63 | Q_OBJECT 64 | }; -------------------------------------------------------------------------------- /src/platform/qt/rc/Info.plist.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${MACOSX_BUNDLE_EXECUTABLE_NAME} 9 | CFBundleGetInfoString 10 | ${MACOSX_BUNDLE_INFO_STRING} 11 | CFBundleIconFile 12 | ${MACOSX_BUNDLE_ICON_FILE} 13 | CFBundleIdentifier 14 | ${MACOSX_BUNDLE_GUI_IDENTIFIER} 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleLongVersionString 18 | ${MACOSX_BUNDLE_LONG_VERSION_STRING} 19 | CFBundleName 20 | ${MACOSX_BUNDLE_BUNDLE_NAME} 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | ${MACOSX_BUNDLE_SHORT_VERSION_STRING} 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | ${MACOSX_BUNDLE_BUNDLE_VERSION} 29 | CSResourcesFileMapped 30 | 31 | NSHumanReadableCopyright 32 | ${MACOSX_BUNDLE_COPYRIGHT} 33 | CFBundleDocumentTypes 34 | 35 | 36 | CFBundleTypeExtensions 37 | 38 | gba 39 | 40 | CFBundleTypeName 41 | Game Boy Advance ROM Image 42 | CFBundleTypeRole 43 | Viewer 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/tile_viewer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "widget/debugger/utility.hpp" 18 | #include "color_grid.hpp" 19 | 20 | class TileViewer : public QWidget { 21 | public: 22 | TileViewer(nba::CoreBase* core, QWidget* parent = nullptr); 23 | ~TileViewer(); 24 | 25 | bool eventFilter(QObject* object, QEvent* event) override; 26 | 27 | void Update(); 28 | 29 | private: 30 | QWidget* CreateMagnificationInput(); 31 | QWidget* CreatePaletteInput(); 32 | QWidget* CreateTileBaseGroupBox(); 33 | QWidget* CreateTileInfoGroupBox(); 34 | 35 | void PresentTileMap(); 36 | void DrawTileDetail(int tile_x, int tile_y); 37 | void ClearTileSelection(); 38 | void UpdateImpl(); 39 | 40 | u16* m_image_rgb565; 41 | QImage m_image_rgb32{256, 256, QImage::Format_RGB32}; 42 | QSpinBox* m_spin_magnification; 43 | QSpinBox* m_spin_palette_index; 44 | QCheckBox* m_check_eight_bpp; 45 | QLabel* m_label_tile_number; 46 | QLabel* m_label_tile_address; 47 | ColorGrid* m_tile_color_grid; 48 | QLabel* m_label_color_r_component; 49 | QLabel* m_label_color_g_component; 50 | QLabel* m_label_color_b_component; 51 | QWidget* m_canvas; 52 | 53 | u32 m_tile_base = 0; 54 | bool m_display_selected_tile = false; 55 | int m_selected_tile_x; 56 | int m_selected_tile_y; 57 | 58 | u8* m_vram; 59 | u16* m_pram; 60 | 61 | Q_OBJECT 62 | }; 63 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/callback.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | #include "hw/apu/apu.hpp" 12 | 13 | namespace nba::core { 14 | 15 | void AudioCallback(APU* apu, s16* stream, int byte_len) { 16 | std::lock_guard guard(apu->buffer_mutex); 17 | 18 | // Do not try to access the buffer if it wasn't setup yet. 19 | if(!apu->buffer) { 20 | return; 21 | } 22 | 23 | int samples = byte_len/sizeof(s16)/2; 24 | int available = apu->buffer->Available(); 25 | 26 | static constexpr float kMaxAmplitude = 0.999; 27 | 28 | const float volume = (float)std::clamp(apu->config->audio.volume, 0, 100) / 100.0f; 29 | 30 | if(available >= samples) { 31 | for(int x = 0; x < samples; x++) { 32 | auto sample = apu->buffer->Read() * volume; 33 | sample[0] = std::clamp(sample[0], -kMaxAmplitude, kMaxAmplitude); 34 | sample[1] = std::clamp(sample[1], -kMaxAmplitude, kMaxAmplitude); 35 | sample *= 32767.0; 36 | 37 | stream[x*2+0] = (s16)std::round(sample.left); 38 | stream[x*2+1] = (s16)std::round(sample.right); 39 | } 40 | } else { 41 | int y = 0; 42 | 43 | for(int x = 0; x < samples; x++) { 44 | auto sample = apu->buffer->Peek(y) * volume; 45 | sample[0] = std::clamp(sample[0], -kMaxAmplitude, kMaxAmplitude); 46 | sample[1] = std::clamp(sample[1], -kMaxAmplitude, kMaxAmplitude); 47 | sample *= 32767.0; 48 | 49 | if(++y >= available) y = 0; 50 | 51 | stream[x*2+0] = (s16)std::round(sample.left); 52 | stream[x*2+1] = (s16)std::round(sample.right); 53 | } 54 | } 55 | } 56 | 57 | } // namespace nba::core 58 | -------------------------------------------------------------------------------- /src/platform/core/src/device/shader/lcd1x.glsl.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * lcd1x - Simple LCD 'scanline' shader, based on lcd3x 3 | * 4 | * Original code by Gigaherz, released into the public domain 5 | * 6 | * Ported to NanoBoyAdvance by Destiny 7 | * 8 | * This program is free software; you can redistribute it and/or modify it 9 | * under the terms of the GNU General Public License as published by the Free 10 | * Software Foundation; either version 2 of the License, or (at your option) 11 | * any later version. 12 | * 13 | * lcd1x differs from lcd3x in the following manner: 14 | * 15 | * > Omits LCD-style colour separation 16 | * 17 | * > Has 'correctly' aligned scanlines 18 | */ 19 | 20 | #pragma once 21 | 22 | constexpr auto lcd1x_vert = R"( 23 | #version 330 core 24 | 25 | layout(location = 0) in vec2 position; 26 | layout(location = 1) in vec2 uv; 27 | 28 | out vec2 v_uv; 29 | 30 | void main() { 31 | v_uv = vec2(uv.x, 1.0 - uv.y) * 1.0001; 32 | gl_Position = vec4(position, 0.0, 1.0); 33 | } 34 | )"; 35 | 36 | constexpr auto lcd1x_frag = R"( 37 | #version 330 core 38 | 39 | #define BRIGHTEN_SCANLINES 16.0 40 | #define BRIGHTEN_LCD 24.0 41 | #define PI 3.141592653589793 42 | #define INPUT_SIZE vec2(240,160) 43 | 44 | 45 | layout(location = 0) out vec4 frag_color; 46 | 47 | in vec2 v_uv; 48 | 49 | uniform sampler2D u_input_map; 50 | 51 | void main() { 52 | vec2 angle = 2.0 * PI * ((v_uv.xy * INPUT_SIZE.xy) - 0.25); 53 | float yfactor = (BRIGHTEN_SCANLINES + sin(angle.y)) / (BRIGHTEN_SCANLINES + 1.0); 54 | float xfactor = (BRIGHTEN_LCD + sin(angle.x)) / (BRIGHTEN_LCD + 1.0); 55 | vec3 colour = texture(u_input_map, v_uv).rgb; 56 | colour.rgb = yfactor * xfactor * colour.rgb; 57 | 58 | frag_color = vec4(colour.rgb, 1.0); 59 | } 60 | )"; 61 | -------------------------------------------------------------------------------- /src/nba/include/nba/config.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace nba { 17 | 18 | struct Config { 19 | bool skip_bios = false; 20 | 21 | enum class BackupType { 22 | Detect, 23 | None, 24 | SRAM, 25 | FLASH_64, 26 | FLASH_128, 27 | EEPROM_4, 28 | EEPROM_64, 29 | EEPROM_DETECT // for internal use 30 | }; 31 | 32 | struct Audio { 33 | enum class Interpolation { 34 | Cosine, 35 | Cubic, 36 | Sinc_32, 37 | Sinc_64, 38 | Sinc_128, 39 | Sinc_256 40 | } interpolation = Interpolation::Cubic; 41 | 42 | int volume = 100; // between 0 and 100 43 | bool mp2k_hle_enable = false; 44 | bool mp2k_hle_cubic = true; 45 | bool mp2k_hle_force_reverb = true; 46 | } audio; 47 | 48 | std::shared_ptr audio_dev = std::make_shared(); 49 | std::shared_ptr video_dev = std::make_shared(); 50 | }; 51 | 52 | } // namespace nba 53 | 54 | namespace std { 55 | 56 | using BackupType = nba::Config::BackupType; 57 | 58 | inline auto to_string(BackupType value) -> std::string { 59 | switch(value) { 60 | case BackupType::Detect: return "Detect"; 61 | case BackupType::None: return "None"; 62 | case BackupType::SRAM: return "SRAM"; 63 | case BackupType::FLASH_64: return "FLASH_64"; 64 | case BackupType::FLASH_128: return "FLASH_128"; 65 | case BackupType::EEPROM_4: return "EEPROM_4"; 66 | default: return "EEPROM_64"; 67 | } 68 | } 69 | 70 | } // namespace std 71 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/envelope.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | namespace nba::core { 11 | 12 | /** 13 | * TODO: 14 | * - Figure out how the envelope timer behaves when the envelope speed (divider) changes mid-note. 15 | * - Is the new value loaded into the counter right away or on the next reload? 16 | * - Is the timer ticked when the envelope is deactivated (divider == 0)? 17 | * - If so, what value is loaded into the timer on channel restart? 18 | */ 19 | 20 | class Envelope { 21 | public: 22 | void Reset() { 23 | direction = Direction::Decrement; 24 | initial_volume = 0; 25 | divider = 0; 26 | Restart(); 27 | } 28 | 29 | void Restart() { 30 | step = divider; 31 | current_volume = initial_volume; 32 | active = enabled; 33 | } 34 | 35 | void Tick() { 36 | if(step == 1) { 37 | step = divider; 38 | 39 | if(active && divider != 0) { 40 | if(direction == Direction::Increment) { 41 | if(current_volume != 15) { 42 | current_volume++; 43 | } else { 44 | active = false; 45 | } 46 | } else { 47 | if(current_volume != 0) { 48 | current_volume--; 49 | } else { 50 | active = false; 51 | } 52 | } 53 | } 54 | } else { 55 | step = (step - 1) & 7; 56 | } 57 | } 58 | 59 | bool active = false; 60 | bool enabled = false; 61 | 62 | enum Direction { 63 | Increment = 1, 64 | Decrement = 0 65 | } direction; 66 | 67 | int initial_volume; 68 | int current_volume; 69 | int divider; 70 | 71 | private: 72 | friend class BaseChannel; 73 | 74 | int step; 75 | }; 76 | 77 | } // namespace nba::core 78 | -------------------------------------------------------------------------------- /src/platform/core/include/platform/loader/rom.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace fs = std::filesystem; 16 | 17 | namespace nba { 18 | 19 | struct ROMLoader { 20 | enum class Result { 21 | CannotFindFile, 22 | CannotOpenFile, 23 | BadImage, 24 | Success 25 | }; 26 | 27 | static auto Load( 28 | std::unique_ptr& core, 29 | fs::path const& path, 30 | Config::BackupType backup_type = Config::BackupType::Detect, 31 | GPIODeviceType force_gpio = GPIODeviceType::None 32 | ) -> Result; 33 | 34 | static auto Load( 35 | std::unique_ptr& core, 36 | fs::path const& rom_path, 37 | fs::path const& save_path, 38 | Config::BackupType backup_type = Config::BackupType::Detect, 39 | GPIODeviceType force_gpio = GPIODeviceType::None 40 | ) -> Result; 41 | 42 | private: 43 | static auto ReadFile(fs::path const& path, std::vector& file_data) -> Result; 44 | static auto ReadFileFromArchive(fs::path const& path, std::vector& file_data) -> Result; 45 | 46 | static auto GetGameInfo( 47 | std::vector& file_data 48 | ) -> GameInfo; 49 | 50 | static auto GetBackupType( 51 | std::vector& file_data 52 | ) -> Config::BackupType; 53 | 54 | static auto CreateBackup( 55 | std::unique_ptr& core, 56 | fs::path const& save_path, 57 | Config::BackupType backup_type 58 | ) -> std::unique_ptr; 59 | 60 | static auto RoundSizeToPowerOfTwo(size_t size) -> size_t; 61 | }; 62 | 63 | } // namespace nba 64 | -------------------------------------------------------------------------------- /src/nba/src/arm/handlers/memory.inl: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | u32 ReadByte(u32 address, int access) { 9 | return bus.ReadByte(address, access); 10 | } 11 | 12 | u32 ReadHalf(u32 address, int access) { 13 | return bus.ReadHalf(address, access); 14 | } 15 | 16 | u32 ReadWord(u32 address, int access) { 17 | return bus.ReadWord(address, access); 18 | } 19 | 20 | u32 ReadByteSigned(u32 address, int access) { 21 | u32 value = bus.ReadByte(address, access); 22 | 23 | if (value & 0x80) { 24 | value |= 0xFFFFFF00; 25 | } 26 | 27 | return value; 28 | } 29 | 30 | u32 ReadHalfRotate(u32 address, int access) { 31 | u32 value = bus.ReadHalf(address, access); 32 | 33 | if (address & 1) { 34 | value = (value >> 8) | (value << 24); 35 | } 36 | 37 | return value; 38 | } 39 | 40 | u32 ReadHalfSigned(u32 address, int access) { 41 | u32 value; 42 | 43 | if (address & 1) { 44 | value = bus.ReadByte(address, access); 45 | if (value & 0x80) { 46 | value |= 0xFFFFFF00; 47 | } 48 | } else { 49 | value = bus.ReadHalf(address, access); 50 | if (value & 0x8000) { 51 | value |= 0xFFFF0000; 52 | } 53 | } 54 | 55 | return value; 56 | } 57 | 58 | u32 ReadWordRotate(u32 address, int access) { 59 | auto value = bus.ReadWord(address, access); 60 | auto shift = (address & 3) * 8; 61 | 62 | return (value >> shift) | (value << (32 - shift)); 63 | } 64 | 65 | void WriteByte(u32 address, u8 value, int access) { 66 | bus.WriteByte(address, value, access); 67 | } 68 | 69 | void WriteHalf(u32 address, u16 value, int access) { 70 | bus.WriteHalf(address, value, access); 71 | } 72 | 73 | void WriteWord(u32 address, u32 value, int access) { 74 | bus.WriteWord(address, value, access); 75 | } 76 | -------------------------------------------------------------------------------- /src/nba/src/hw/timer/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "hw/timer/timer.hpp" 9 | 10 | namespace nba::core { 11 | 12 | // TODO: deduplicate this: 13 | static constexpr int g_ticks_shift[4] = { 0, 6, 8, 10 }; 14 | static constexpr int g_ticks_mask[4] = { 0, 0x3F, 0xFF, 0x3FF }; 15 | 16 | void Timer::LoadState(SaveState const& state) { 17 | for(int i = 0; i < 4; i++) { 18 | u16 reload = state.timer[i].reload; 19 | u16 control = state.timer[i].control; 20 | 21 | channels[i].reload = reload; 22 | channels[i].counter = state.timer[i].counter; 23 | 24 | channels[i].control.frequency = control & 3; 25 | channels[i].control.cascade = control & 4; 26 | channels[i].control.interrupt = control & 64; 27 | channels[i].control.enable = control & 128; 28 | 29 | channels[i].shift = g_ticks_shift[channels[i].control.frequency]; 30 | channels[i].mask = g_ticks_mask[channels[i].control.frequency]; 31 | 32 | channels[i].running = false; 33 | channels[i].event_overflow = scheduler.GetEventByUID(state.timer[i].event_uid); 34 | 35 | channels[i].pending.reload = state.timer[i].pending.reload; 36 | channels[i].pending.control = state.timer[i].pending.control; 37 | } 38 | } 39 | 40 | void Timer::CopyState(SaveState& state) { 41 | for(int i = 0; i < 4; i++) { 42 | state.timer[i].counter = ReadCounter(channels[i]); 43 | state.timer[i].reload = channels[i].reload; 44 | state.timer[i].control = ReadControl(channels[i]); 45 | state.timer[i].pending.reload = channels[i].pending.reload; 46 | state.timer[i].pending.control = channels[i].pending.control; 47 | state.timer[i].event_uid = GetEventUID(channels[i].event_overflow); 48 | } 49 | } 50 | 51 | } // namespace nba::core -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/base_channel.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | #include "hw/apu/channel/length_counter.hpp" 13 | #include "hw/apu/channel/envelope.hpp" 14 | #include "hw/apu/channel/sweep.hpp" 15 | 16 | namespace nba::core { 17 | 18 | class BaseChannel { 19 | public: 20 | static constexpr int s_cycles_per_step = 16777216 / 512; 21 | 22 | BaseChannel( 23 | bool enable_envelope, 24 | bool enable_sweep, 25 | int default_length = 64 26 | ) : length(default_length) { 27 | envelope.enabled = enable_envelope; 28 | sweep.enabled = enable_sweep; 29 | Reset(); 30 | } 31 | 32 | virtual bool IsEnabled() { return enabled; } 33 | virtual auto GetSample() -> s8 = 0; 34 | 35 | void Reset() { 36 | length.Reset(); 37 | envelope.Reset(); 38 | sweep.Reset(); 39 | enabled = false; 40 | step = 0; 41 | } 42 | 43 | void Tick() { 44 | // http://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware#Frame_Sequencer 45 | if((step & 1) == 0) enabled &= length.Tick(); 46 | if((step & 3) == 2) enabled &= sweep.Tick(); 47 | if(step == 7) envelope.Tick(); 48 | 49 | step++; 50 | step &= 7; 51 | } 52 | 53 | void LoadState(SaveState::APU::IO::PSG const& state); 54 | void CopyState(SaveState::APU::IO::PSG& state); 55 | 56 | protected: 57 | void Restart() { 58 | length.Restart(); 59 | sweep.Restart(); 60 | envelope.Restart(); 61 | enabled = true; 62 | step = 0; 63 | } 64 | 65 | void Disable() { 66 | enabled = false; 67 | } 68 | 69 | LengthCounter length; 70 | Envelope envelope; 71 | Sweep sweep; 72 | 73 | private: 74 | bool enabled; 75 | int step; 76 | }; 77 | 78 | } // namespace nba::core 79 | -------------------------------------------------------------------------------- /src/platform/core/src/frame_limiter.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | namespace nba { 11 | 12 | void FrameLimiter::Reset() { 13 | Reset(frames_per_second); 14 | } 15 | 16 | void FrameLimiter::Reset(float fps) { 17 | frame_count = 0; 18 | frame_duration = int(kMicrosecondsPerSecond / fps); 19 | frames_per_second = fps; 20 | fast_forward = false; 21 | timestamp_target = std::chrono::steady_clock::now(); 22 | timestamp_fps_update = std::chrono::steady_clock::now(); 23 | } 24 | 25 | auto FrameLimiter::GetFastForward() const -> bool { 26 | return fast_forward; 27 | } 28 | 29 | void FrameLimiter::SetFastForward(bool value) { 30 | if(fast_forward != value) { 31 | fast_forward = value; 32 | if(!fast_forward) { 33 | timestamp_target = std::chrono::steady_clock::now(); 34 | } 35 | } 36 | } 37 | 38 | void FrameLimiter::Run( 39 | std::function frame_advance, 40 | std::function update_fps 41 | ) { 42 | if(!fast_forward) { 43 | timestamp_target += std::chrono::microseconds(frame_duration); 44 | } 45 | 46 | frame_advance(); 47 | frame_count++; 48 | 49 | auto now = std::chrono::steady_clock::now(); 50 | auto fps_update_delta = std::chrono::duration_cast( 51 | now - timestamp_fps_update).count(); 52 | 53 | if(fps_update_delta >= kMillisecondsPerSecond) { 54 | update_fps(frame_count * float(kMillisecondsPerSecond) / fps_update_delta); 55 | frame_count = 0; 56 | timestamp_fps_update = std::chrono::steady_clock::now(); 57 | } 58 | 59 | if(!fast_forward) { 60 | std::this_thread::sleep_until(timestamp_target); 61 | } 62 | } 63 | 64 | } // namespace nba 65 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/registers.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | #include "hw/apu/channel/quad_channel.hpp" 13 | #include "hw/apu/channel/wave_channel.hpp" 14 | #include "hw/apu/channel/noise_channel.hpp" 15 | #include "hw/apu/channel/fifo.hpp" 16 | 17 | namespace nba::core { 18 | 19 | enum Side { 20 | SIDE_LEFT = 0, 21 | SIDE_RIGHT = 1 22 | }; 23 | 24 | enum DMANumber { 25 | DMA_A = 0, 26 | DMA_B = 1 27 | }; 28 | 29 | struct SoundControl { 30 | SoundControl( 31 | FIFO* fifos, 32 | QuadChannel& psg1, 33 | QuadChannel& psg2, 34 | WaveChannel& psg3, 35 | NoiseChannel& psg4) 36 | : fifos(fifos) 37 | , psg1(psg1) 38 | , psg2(psg2) 39 | , psg3(psg3) 40 | , psg4(psg4) { 41 | } 42 | 43 | bool master_enable; 44 | 45 | struct PSG { 46 | int volume; 47 | int master[2]; 48 | bool enable[2][4]; 49 | } psg; 50 | 51 | struct DMA { 52 | int volume; 53 | bool enable[2]; 54 | int timer_id; 55 | } dma[2]; 56 | 57 | void Reset(); 58 | auto Read(int address) -> u8; 59 | void Write(int address, u8 value); 60 | 61 | auto ReadWord() -> u32; 62 | void WriteWord(u32 value); 63 | 64 | private: 65 | FIFO* fifos; 66 | 67 | QuadChannel& psg1; 68 | QuadChannel& psg2; 69 | WaveChannel& psg3; 70 | NoiseChannel& psg4; 71 | }; 72 | 73 | struct BIAS { 74 | int level; 75 | int resolution; 76 | 77 | void Reset(); 78 | auto Read(int address) -> u8; 79 | void Write(int address, u8 value); 80 | 81 | auto ReadHalf() -> u16; 82 | void WriteHalf(u16 value); 83 | 84 | auto GetSampleInterval() -> int { return 512 >> resolution; } 85 | auto GetSampleRate() -> int { return 32768 << resolution; } 86 | }; 87 | 88 | } // namespace nba::core 89 | -------------------------------------------------------------------------------- /src/nba/src/arm/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "arm/arm7tdmi.hpp" 9 | 10 | namespace nba::core::arm { 11 | 12 | void ARM7TDMI::LoadState(SaveState const& save_state) { 13 | for(int i = 0; i < 16; i++) { 14 | state.reg[i] = save_state.arm.regs.gpr[i]; 15 | } 16 | 17 | for(int i = 0; i < BANK_COUNT; i++) { 18 | for(int j = 0; j < 7; j++) { 19 | state.bank[i][j] = save_state.arm.regs.bank[i][j]; 20 | } 21 | state.spsr[i].v = save_state.arm.regs.spsr[i]; 22 | } 23 | 24 | state.cpsr.v = save_state.arm.regs.cpsr; 25 | 26 | auto bank = GetRegisterBankByMode(state.cpsr.f.mode); 27 | if(bank != BANK_NONE) { 28 | p_spsr = &state.spsr[bank]; 29 | } else { 30 | p_spsr = &state.cpsr; 31 | } 32 | 33 | pipe.access = save_state.arm.pipe.access; 34 | 35 | for(int i = 0; i < 2; i++) { 36 | pipe.opcode[i] = save_state.arm.pipe.opcode[i]; 37 | } 38 | 39 | irq_line = save_state.arm.irq_line; 40 | 41 | // TODO: save and restore these variables: 42 | ldm_usermode_conflict = false; 43 | cpu_mode_is_invalid = false; 44 | latch_irq_disable = state.cpsr.f.mask_irq; 45 | } 46 | 47 | void ARM7TDMI::CopyState(SaveState& save_state) { 48 | for(int i = 0; i < 16; i++) { 49 | save_state.arm.regs.gpr[i] = state.reg[i]; 50 | } 51 | 52 | for(int i = 0; i < BANK_COUNT; i++) { 53 | for(int j = 0; j < 7; j++) { 54 | save_state.arm.regs.bank[i][j] = state.bank[i][j]; 55 | } 56 | save_state.arm.regs.spsr[i] = state.spsr[i].v; 57 | } 58 | 59 | save_state.arm.regs.cpsr = state.cpsr.v; 60 | 61 | save_state.arm.pipe.access = pipe.access; 62 | 63 | for(int i = 0; i < 2; i++) { 64 | save_state.arm.pipe.opcode[i] = pipe.opcode[i]; 65 | } 66 | 67 | save_state.arm.irq_line = irq_line; 68 | } 69 | 70 | } // namespace nba::core::arm 71 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/sprite_viewer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | class SpriteViewer : public QWidget { 21 | public: 22 | SpriteViewer(nba::CoreBase* core, QWidget* parent = nullptr); 23 | 24 | void Update(); 25 | bool eventFilter(QObject* object, QEvent* event) override; 26 | 27 | private: 28 | QWidget* CreateSpriteIndexInput(); 29 | QWidget* CreateMagnificationInput(); 30 | QGroupBox* CreateSpriteAttributesGroupBox(); 31 | QLayout* CreateInfoLayout(); 32 | QLayout* CreateCanvasLayout(); 33 | 34 | QImage m_image_rgb32{64, 64, QImage::Format_RGB32}; 35 | QSpinBox* m_spin_sprite_index; 36 | QSpinBox* m_spin_magnification; 37 | QWidget* m_canvas; 38 | 39 | QCheckBox* m_check_sprite_enabled; 40 | QLabel* m_label_sprite_position; 41 | QLabel* m_label_sprite_size; 42 | QLabel* m_label_sprite_tile_number; 43 | QLabel* m_label_sprite_palette; 44 | QCheckBox* m_check_sprite_8bpp; 45 | QCheckBox* m_check_sprite_vflip; 46 | QCheckBox* m_check_sprite_hflip; 47 | QLabel* m_label_sprite_mode; 48 | QCheckBox* m_check_sprite_affine; 49 | QLabel* m_label_sprite_transform; 50 | QCheckBox* m_check_sprite_double_size; 51 | QCheckBox* m_check_sprite_mosaic; 52 | QLabel* m_label_sprite_render_cycles; 53 | 54 | int m_sprite_width = 0; 55 | int m_sprite_height = 0; 56 | int m_magnified_sprite_width = 0; 57 | int m_magnified_sprite_height = 0; 58 | 59 | nba::CoreBase* m_core; 60 | u16* m_pram; 61 | u8* m_vram; 62 | u8* m_oam; 63 | 64 | Q_OBJECT 65 | }; 66 | -------------------------------------------------------------------------------- /src/nba/src/core.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | #include "arm/arm7tdmi.hpp" 12 | #include "bus/bus.hpp" 13 | #include "hw/apu/apu.hpp" 14 | #include "hw/ppu/ppu.hpp" 15 | #include "hw/dma/dma.hpp" 16 | #include "hw/irq/irq.hpp" 17 | #include "hw/keypad/keypad.hpp" 18 | #include "hw/timer/timer.hpp" 19 | 20 | namespace nba::core { 21 | 22 | struct Core final : CoreBase { 23 | Core(std::shared_ptr config); 24 | 25 | void Reset() override; 26 | 27 | void Attach(std::vector const& bios) override; 28 | void Attach(ROM&& rom) override; 29 | auto CreateRTC() -> std::unique_ptr override; 30 | auto CreateSolarSensor() -> std::unique_ptr override; 31 | void LoadState(SaveState const& state) override; 32 | void CopyState(SaveState& state) override; 33 | void SetKeyStatus(Key key, bool pressed) override; 34 | void Run(int cycles) override; 35 | 36 | auto GetROM() -> ROM& override; 37 | auto GetPRAM() -> u8* override; 38 | auto GetVRAM() -> u8* override; 39 | auto GetOAM() -> u8* override; 40 | auto PeekByteIO(u32 address) -> u8 override; 41 | auto PeekHalfIO(u32 address) -> u16 override; 42 | auto PeekWordIO(u32 address) -> u32 override; 43 | auto GetBGHOFS(int id) -> u16 override; 44 | auto GetBGVOFS(int id) -> u16 override; 45 | 46 | Scheduler& GetScheduler() override; 47 | 48 | private: 49 | void SkipBootScreen(); 50 | auto SearchSoundMainRAM() -> u32; 51 | 52 | u32 hle_audio_hook; 53 | std::shared_ptr config; 54 | 55 | Scheduler scheduler; 56 | 57 | arm::ARM7TDMI cpu; 58 | IRQ irq; 59 | DMA dma; 60 | APU apu; 61 | PPU ppu; 62 | Timer timer; 63 | KeyPad keypad; 64 | Bus bus; 65 | }; 66 | 67 | } // namespace nba::core 68 | -------------------------------------------------------------------------------- /src/nba/src/hw/rom/gpio/gpio.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | namespace nba { 11 | 12 | void GPIO::Reset() { 13 | allow_reads = false; 14 | port_data = 0; 15 | rd_mask = 0b1111; 16 | wr_mask = 0b0000; 17 | 18 | for(auto& device : devices) { 19 | device->Reset(); 20 | device->SetPortDirections(0); 21 | } 22 | } 23 | 24 | void GPIO::Attach(std::shared_ptr device) { 25 | devices.push_back(device); 26 | device_map[std::type_index{typeid(*device)}] = device.get(); 27 | } 28 | 29 | auto GPIO::Read(u32 address) -> u8 { 30 | if(!allow_reads) { 31 | return 0; 32 | } 33 | 34 | switch(static_cast(address)) { 35 | case Register::Data: { 36 | u8 value = 0; 37 | 38 | for(auto& device : devices) { 39 | value |= device->Read(); 40 | } 41 | 42 | port_data &= wr_mask; 43 | port_data |= rd_mask & value; 44 | return value; 45 | } 46 | case Register::Direction: { 47 | return rd_mask; 48 | } 49 | case Register::Control: { 50 | return allow_reads ? 1 : 0; 51 | } 52 | } 53 | 54 | return 0; 55 | } 56 | 57 | void GPIO::Write(u32 address, u8 value) { 58 | switch(static_cast(address)) { 59 | case Register::Data: { 60 | port_data &= rd_mask; 61 | port_data |= wr_mask & value; 62 | 63 | for(auto& device : devices) { 64 | device->Write(port_data); 65 | } 66 | break; 67 | } 68 | case Register::Direction: { 69 | value &= 15; 70 | 71 | rd_mask = ~value & 15; 72 | wr_mask = value; 73 | 74 | for(auto& device : devices) { 75 | device->SetPortDirections(value); 76 | } 77 | break; 78 | } 79 | case Register::Control: { 80 | allow_reads = value & 1; 81 | break; 82 | } 83 | } 84 | } 85 | 86 | } // namespace nba 87 | -------------------------------------------------------------------------------- /src/platform/qt/rc/config.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | bios_path = "bios.bin" 3 | bios_skip = false 4 | save_folder = "" 5 | 6 | [cartridge] 7 | # Possible values: detect, none, sram, flash64, flash128, eeprom512, eeprom8192 8 | save_type = "detect" 9 | # Force-enable RTC emulation, otherwise rely on game database. 10 | force_rtc = true 11 | # Force-enable Solar Sensor emulation, otherwise rely on game database. 12 | force_solar_sensor = false 13 | # Solar Sensor light intensity (0 = lowest intensity, 255 = highest intensity) 14 | solar_sensor_level = 23 15 | 16 | [video] 17 | filter = "linear" 18 | color_correction = "agb" 19 | lcd_ghosting = true 20 | 21 | [audio] 22 | # Possible values: cosine, cubic, sinc64, sinc128, sinc256 23 | resampler = "cubic" 24 | # Reimplementation of the popular MP2K/M4A audio mixer with higher quality. 25 | # This is experimental and may still have issues. 26 | mp2k_hle_enable = false 27 | # Use cubic interpolation in the MP2K reimplementation. 28 | mp2k_hle_cubic = true 29 | # Force-enable the reverb effect 30 | mp2k_hle_force_reverb = true 31 | 32 | [input] 33 | hold_fast_forward = true 34 | fast_forward = [32, -1, -1, -1, 0] 35 | controller_guid = "" 36 | [input.gba] 37 | a = [65,-1,-1,-1,0] 38 | b = [83,-1,-1,-1,0] 39 | select = [16777219,-1,-1,-1,0] 40 | start = [16777220,-1,-1,-1,0] 41 | right = [16777236,-1,-1,-1,0] 42 | left = [16777234,-1,-1,-1,0] 43 | up = [16777235,-1,-1,-1,0] 44 | down = [16777237,-1,-1,-1,0] 45 | l = [68,-1,-1,-1,0] 46 | r = [70,-1,-1,-1,0] 47 | 48 | [window] 49 | fullscreen = false 50 | fullscreen_show_menu = false 51 | scale = 2 52 | # Set to zero to unlock the scale 53 | maximum_scale = 0 54 | # Show the current frames per second in the window title 55 | show_fps = false 56 | # Lock the screen to a 3:2 aspect ratio 57 | lock_aspect_ratio = true 58 | # Snap screen size to integer multiples of the original screen width and height 59 | use_integer_scaling = false 60 | # Pause emulator when the main window becomes inactive 61 | pause_emulator_when_inactive = true 62 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/fifo.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace nba::core { 14 | 15 | struct FIFO { 16 | FIFO() { Reset(); } 17 | 18 | void Reset() { 19 | rd_ptr = 0; 20 | wr_ptr = 0; 21 | count = 0; 22 | 23 | for(u32& word : data) word = 0U; 24 | } 25 | 26 | int Count() const { return count; } 27 | 28 | void WriteByte(uint offset, u8 value) { 29 | const auto shift = offset * 8; 30 | 31 | WriteWord((data[wr_ptr] & ~(0x00FF << shift)) | (value << shift)); 32 | } 33 | 34 | void WriteHalf(uint offset, u8 value) { 35 | const auto shift = offset * 8; 36 | 37 | WriteWord((data[wr_ptr] & ~(0xFFFF << shift)) | (value << shift)); 38 | } 39 | 40 | void WriteWord(u32 value) { 41 | if(count < s_fifo_len) { 42 | data[wr_ptr] = value; 43 | if(++wr_ptr == s_fifo_len) wr_ptr = 0; 44 | count++; 45 | } else { 46 | Reset(); 47 | } 48 | } 49 | 50 | auto ReadWord() -> u32 { 51 | u32 value = data[rd_ptr]; 52 | 53 | if(count > 0) { 54 | if(++rd_ptr == s_fifo_len) rd_ptr = 0; 55 | count--; 56 | } 57 | 58 | return value; 59 | } 60 | 61 | void LoadState(SaveState::APU::FIFO const& state) { 62 | rd_ptr = 0; 63 | wr_ptr = state.count % s_fifo_len; 64 | count = state.count; 65 | 66 | for(int i = 0; i < s_fifo_len; i++) { 67 | data[i] = state.data[i]; 68 | } 69 | } 70 | 71 | void CopyState(SaveState::APU::FIFO& state) { 72 | state.count = count; 73 | 74 | for(int i = 0; i < s_fifo_len; i++) { 75 | state.data[i] = data[(rd_ptr + i) % s_fifo_len]; 76 | } 77 | } 78 | 79 | private: 80 | static constexpr int s_fifo_len = 7; 81 | 82 | u32 data[s_fifo_len]; 83 | u32 pending; 84 | 85 | int rd_ptr; 86 | int wr_ptr; 87 | int count; 88 | }; 89 | 90 | } // namespace nba::core 91 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/sweep.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | namespace nba::core { 11 | 12 | /** 13 | * TODO: figure out specifics of how the sweep timer works when the sweep speed (divider) is set to zero 14 | * or changed in the middle of a note. 15 | */ 16 | 17 | class Sweep { 18 | public: 19 | void Reset() { 20 | direction = Direction::Increment; 21 | initial_freq = 0; 22 | divider = 0; 23 | shift = 0; 24 | Restart(); 25 | } 26 | 27 | void Restart() { 28 | /* TODO: If the sweep shift is non-zero, frequency calculation and the 29 | * overflow check are performed immediately. 30 | */ 31 | if(enabled) { 32 | current_freq = initial_freq; 33 | shadow_freq = initial_freq; 34 | step = divider; 35 | active = shift != 0 || divider != 0; 36 | } 37 | } 38 | 39 | bool Tick() { 40 | if(active && --step == 0) { 41 | int new_freq; 42 | int offset = shadow_freq >> shift; 43 | 44 | /* TODO: then frequency calculation and overflow check are run AGAIN immediately 45 | * using this new value, but this second new frequency is not written back. 46 | */ 47 | step = divider; 48 | 49 | if(direction == Direction::Increment) { 50 | new_freq = shadow_freq + offset; 51 | } else { 52 | new_freq = shadow_freq - offset; 53 | } 54 | 55 | if(new_freq >= 2048) { 56 | return false; 57 | } else if(shift != 0) { 58 | shadow_freq = new_freq; 59 | current_freq = new_freq; 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | bool active = false; 67 | bool enabled = false; 68 | 69 | enum Direction { 70 | Increment = 0, 71 | Decrement = 1 72 | } direction; 73 | 74 | int initial_freq; 75 | int current_freq; 76 | int shadow_freq; 77 | int divider; 78 | int shift; 79 | 80 | private: 81 | friend class BaseChannel; 82 | 83 | int step; 84 | }; 85 | 86 | } // namespace nba::core 87 | -------------------------------------------------------------------------------- /src/platform/core/include/platform/device/ogl_video_device.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace nba { 19 | 20 | struct OGLVideoDevice : VideoDevice { 21 | OGLVideoDevice(std::shared_ptr config); 22 | ~OGLVideoDevice() override; 23 | 24 | void Initialize(); 25 | void SetViewport(int x, int y, int width, int height); 26 | void SetDefaultFBO(GLuint fbo); 27 | void Draw(u32* buffer) override; 28 | void ReloadConfig(); 29 | 30 | private: 31 | void UpdateTextures(); 32 | void CreateShaderPrograms(); 33 | void UpdateShaderUniforms(); 34 | void ReleaseShaderPrograms(); 35 | 36 | auto CompileShader( 37 | GLenum type, 38 | char const* source 39 | ) -> std::pair; 40 | 41 | auto CompileProgram( 42 | char const* vertex_src, 43 | char const* fragment_src 44 | ) -> std::pair; 45 | 46 | static constexpr size_t input_index = 0; 47 | static constexpr size_t output_index = 1; 48 | static constexpr size_t history_index = 2; 49 | static constexpr size_t xbrz_output_index = 3; 50 | 51 | struct ShaderPass { 52 | GLuint program = 0; 53 | struct { 54 | std::vector inputs = {input_index}; 55 | GLuint output = output_index; 56 | } textures = {}; 57 | }; 58 | 59 | int view_x = 0; 60 | int view_y = 0; 61 | int view_width = 1; 62 | int view_height = 1; 63 | GLuint default_fbo = 0; 64 | 65 | GLuint quad_vao; 66 | GLuint quad_vbo; 67 | GLuint fbo; 68 | std::array textures = {}; 69 | std::vector shader_passes = {}; 70 | GLint texture_filter = GL_NEAREST; 71 | 72 | std::shared_ptr config; 73 | }; 74 | 75 | } // namespace nba 76 | -------------------------------------------------------------------------------- /src/platform/core/src/device/shader/sharp_bilinear.glsl.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | Author: rsn8887 (based on TheMaister) 3 | License: Public domain 4 | 5 | This is an integer prescale filter that should be combined 6 | with a bilinear hardware filtering (GL_BILINEAR filter or some such) to achieve 7 | a smooth scaling result with minimum blur. This is good for pixelgraphics 8 | that are scaled by non-integer factors. 9 | 10 | The prescale factor and texel coordinates are precalculated 11 | in the vertex shader for speed. 12 | */ 13 | 14 | #pragma once 15 | 16 | constexpr auto sharp_bilinear_vert = R"( 17 | #version 330 core 18 | 19 | layout(location = 0) in vec2 position; 20 | layout(location = 1) in vec2 uv; 21 | 22 | out vec2 v_uv; 23 | out vec2 precalc_texel; 24 | out vec2 precalc_scale; 25 | 26 | uniform vec2 u_output_size; 27 | 28 | void main() { 29 | gl_Position = vec4(position, 0.0, 1.0); 30 | v_uv = vec2(uv.x, 1.0 - uv.y); 31 | 32 | const vec2 input_size = vec2(240, 160); 33 | precalc_scale = max(floor(u_output_size / input_size), vec2(1.0, 1.0)); 34 | precalc_texel = v_uv * input_size; 35 | } 36 | )"; 37 | 38 | constexpr auto sharp_bilinear_frag = R"( 39 | #version 330 core 40 | 41 | in vec2 v_uv; 42 | in vec2 precalc_texel; 43 | in vec2 precalc_scale; 44 | 45 | layout(location = 0) out vec4 frag_color; 46 | 47 | uniform sampler2D u_input_map; 48 | 49 | void main() { 50 | vec2 texel = precalc_texel; 51 | vec2 scale = precalc_scale; 52 | vec2 texel_floored = floor(texel); 53 | vec2 s = fract(texel); 54 | vec2 region_range = 0.5 - 0.5 / scale; 55 | 56 | // Figure out where in the texel to sample to get correct pre-scaled bilinear. 57 | // Uses the hardware bilinear interpolator to avoid having to sample 4 times manually. 58 | 59 | vec2 center_dist = s - 0.5; 60 | vec2 f = (center_dist - clamp(center_dist, -region_range, region_range)) * scale + 0.5; 61 | 62 | vec2 mod_texel = texel_floored + f; 63 | 64 | const vec2 input_size = vec2(240, 160); 65 | frag_color = vec4(texture(u_input_map, mod_texel / input_size).rgb, 1.0); 66 | } 67 | )"; 68 | -------------------------------------------------------------------------------- /src/nba/include/nba/core.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace nba { 21 | 22 | enum class Key : u8 { 23 | A = 0, 24 | B = 1, 25 | Select = 2, 26 | Start = 3, 27 | Right = 4, 28 | Left = 5, 29 | Up = 6, 30 | Down = 7, 31 | R = 8, 32 | L = 9, 33 | Count = 10 34 | }; 35 | 36 | struct CoreBase { 37 | static constexpr int kCyclesPerFrame = 280896; 38 | 39 | virtual ~CoreBase() = default; 40 | 41 | virtual void Reset() = 0; 42 | 43 | virtual void Attach(std::vector const& bios) = 0; 44 | virtual void Attach(ROM&& rom) = 0; 45 | virtual auto CreateRTC() -> std::unique_ptr = 0; 46 | virtual auto CreateSolarSensor() -> std::unique_ptr = 0; 47 | virtual void LoadState(SaveState const& state) = 0; 48 | virtual void CopyState(SaveState& state) = 0; 49 | virtual void SetKeyStatus(Key key, bool pressed) = 0; 50 | virtual void Run(int cycles) = 0; 51 | 52 | virtual auto GetROM() -> ROM& = 0; 53 | virtual auto GetPRAM() -> u8* = 0; 54 | virtual auto GetVRAM() -> u8* = 0; 55 | virtual auto GetOAM() -> u8* = 0; 56 | // @todo: come up with a solution for reading write-only registers. 57 | virtual auto PeekByteIO(u32 address) -> u8 = 0; 58 | virtual auto PeekHalfIO(u32 address) -> u16 = 0; 59 | virtual auto PeekWordIO(u32 address) -> u32 = 0; 60 | virtual auto GetBGHOFS(int id) -> u16 = 0; 61 | virtual auto GetBGVOFS(int id) -> u16 = 0; 62 | 63 | virtual core::Scheduler& GetScheduler() = 0; 64 | 65 | void RunForOneFrame() { 66 | Run(kCyclesPerFrame); 67 | } 68 | }; 69 | 70 | auto CreateCore( 71 | std::shared_ptr config 72 | ) -> std::unique_ptr; 73 | 74 | } // namespace nba -------------------------------------------------------------------------------- /src/platform/core/include/platform/emulator_thread.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace nba { 20 | 21 | struct EmulatorThread { 22 | EmulatorThread(); 23 | ~EmulatorThread(); 24 | 25 | bool IsRunning() const; 26 | bool IsPaused() const; 27 | void SetPause(bool value); 28 | bool GetFastForward() const; 29 | void SetFastForward(bool enabled); 30 | void SetFrameRateCallback(std::function callback); 31 | void SetPerFrameCallback(std::function callback); 32 | 33 | void Start(std::unique_ptr core); 34 | std::unique_ptr Stop(); 35 | 36 | void Reset(); 37 | void SetKeyStatus(Key key, bool pressed); 38 | 39 | private: 40 | enum class MessageType : u8 { 41 | Reset, 42 | SetKeyStatus 43 | }; 44 | 45 | struct Message { 46 | MessageType type; 47 | union { 48 | struct { 49 | Key key; 50 | u8bool pressed; 51 | } set_key_status; 52 | }; 53 | }; 54 | 55 | void PushMessage(const Message& message); 56 | void ProcessMessages(); 57 | void ProcessMessage(const Message& message); 58 | 59 | static constexpr int k_number_of_input_subframes = 4; 60 | static constexpr int k_cycles_per_second = 16777216; 61 | static constexpr int k_cycles_per_frame = 280896; 62 | static constexpr int k_cycles_per_subframe = k_cycles_per_frame / k_number_of_input_subframes; 63 | 64 | static_assert(k_cycles_per_frame % k_number_of_input_subframes == 0); 65 | 66 | std::queue msg_queue; 67 | std::mutex msg_queue_mutex; 68 | 69 | std::unique_ptr core; 70 | FrameLimiter frame_limiter; 71 | std::thread thread; 72 | std::atomic_bool running = false; 73 | bool paused = false; 74 | std::function frame_rate_cb = [](float) {}; 75 | std::function per_frame_cb = []() {}; 76 | }; 77 | 78 | } // namespace nba 79 | -------------------------------------------------------------------------------- /src/nba/include/nba/log.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace nba { 17 | 18 | enum Level { 19 | Trace = 1, 20 | Debug = 2, 21 | Info = 4, 22 | Warn = 8, 23 | Error = 16, 24 | Fatal = 32, 25 | 26 | All = Trace | Debug | Info | Warn | Error | Fatal 27 | }; 28 | 29 | namespace detail { 30 | 31 | #if defined(NDEBUG) 32 | static constexpr int kLogMask = Info | Warn | Error | Fatal; 33 | #else 34 | static constexpr int kLogMask = All; 35 | #endif 36 | 37 | } // namespace nba::detail 38 | 39 | template 40 | inline void Log(std::string_view format, Args&&... args) { 41 | if constexpr((detail::kLogMask & level) != 0) { 42 | fmt::text_style style = {}; 43 | char const* prefix = "[?]"; 44 | 45 | if constexpr(level == Trace) { 46 | style = fmt::fg(fmt::terminal_color::cyan); 47 | prefix = "[T]"; 48 | } 49 | if constexpr(level == Debug) { 50 | style = fmt::fg(fmt::terminal_color::blue); 51 | prefix = "[D]"; 52 | } 53 | if constexpr(level == Info) { 54 | prefix = "[I]"; 55 | } 56 | if constexpr(level == Warn) { 57 | style = fmt::fg(fmt::terminal_color::yellow); 58 | prefix = "[W]"; 59 | } 60 | if constexpr(level == Error) { 61 | style = fmt::fg(fmt::terminal_color::magenta); 62 | prefix = "[E]"; 63 | } 64 | if constexpr(level == Fatal) { 65 | style = fmt::fg(fmt::terminal_color::red); 66 | prefix = "[F]"; 67 | } 68 | 69 | const auto& style_ref = style; 70 | fmt::print(style_ref, "{} {}\n", prefix, fmt::format(format, std::forward(args)...)); 71 | } 72 | } 73 | 74 | template 75 | inline void Assert(bool condition, Args... args) { 76 | if(!condition) { 77 | Log(std::forward(args)...); 78 | std::exit(-1); 79 | } 80 | } 81 | 82 | } // namespace nba 83 | -------------------------------------------------------------------------------- /src/nba/src/hw/rom/backup/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace nba { 14 | 15 | void EEPROM::LoadState(SaveState const& state) { 16 | this->state = state.backup.eeprom.state; 17 | address = state.backup.eeprom.address; 18 | serial_buffer = state.backup.eeprom.serial_buffer; 19 | transmitted_bits = state.backup.eeprom.transmitted_bits; 20 | 21 | std::memcpy(file->Buffer(), state.backup.data, file->Size()); 22 | } 23 | 24 | void EEPROM::CopyState(SaveState& state) { 25 | state.backup.eeprom.state = this->state; 26 | state.backup.eeprom.address = address; 27 | state.backup.eeprom.serial_buffer = serial_buffer; 28 | state.backup.eeprom.transmitted_bits = transmitted_bits; 29 | 30 | std::memcpy(state.backup.data, file->Buffer(), file->Size()); 31 | } 32 | 33 | void FLASH::LoadState(SaveState const& state) { 34 | current_bank = state.backup.flash.current_bank; 35 | phase = state.backup.flash.phase; 36 | enable_chip_id = state.backup.flash.enable_chip_id; 37 | enable_erase = state.backup.flash.enable_erase; 38 | enable_write = state.backup.flash.enable_write; 39 | enable_select = state.backup.flash.enable_select; 40 | 41 | std::memcpy(file->Buffer(), state.backup.data, file->Size()); 42 | } 43 | 44 | void FLASH::CopyState(SaveState& state) { 45 | state.backup.flash.current_bank = current_bank; 46 | state.backup.flash.phase = phase; 47 | state.backup.flash.enable_chip_id = enable_chip_id; 48 | state.backup.flash.enable_erase = enable_erase; 49 | state.backup.flash.enable_write = enable_write; 50 | state.backup.flash.enable_select = enable_select; 51 | 52 | std::memcpy(state.backup.data, file->Buffer(), file->Size()); 53 | } 54 | 55 | void SRAM::LoadState(SaveState const& state) { 56 | std::memcpy(file->Buffer(), state.backup.data, file->Size()); 57 | } 58 | 59 | void SRAM::CopyState(SaveState& state) { 60 | std::memcpy(state.backup.data, file->Buffer(), file->Size()); 61 | } 62 | 63 | } // namespace nba -------------------------------------------------------------------------------- /src/platform/core/src/device/shader/color_agb.glsl.hpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "device/shader/common.glsl.hpp" 5 | 6 | constexpr auto color_agb_vert = common_vert; 7 | 8 | constexpr auto color_agb_frag = R"( 9 | #version 330 core 10 | 11 | /* 12 | Shader Modified: Pokefan531 13 | Color Mangler 14 | Author: hunterk 15 | License: Public domain 16 | */ 17 | 18 | // Shader that replicates the LCD dynamics from a Game Boy Advance 19 | 20 | #define darken_screen 1.0 21 | #define target_gamma 2.2 22 | #define display_gamma 2.2 23 | #define sat 1.0 24 | #define lum 0.94 25 | #define contrast 1.0 26 | #define blr 0.0 27 | #define blg 0.0 28 | #define blb 0.0 29 | #define r 0.82 30 | #define g 0.665 31 | #define b 0.73 32 | #define rg 0.125 33 | #define rb 0.195 34 | #define gr 0.24 35 | #define gb 0.075 36 | #define br -0.06 37 | #define bg 0.21 38 | #define overscan_percent_x 0.0 39 | #define overscan_percent_y 0.0 40 | 41 | layout(location = 0) out vec4 frag_color; 42 | 43 | in vec2 v_uv; 44 | 45 | uniform sampler2D u_input_map; 46 | 47 | void main() { 48 | vec4 screen = pow(texture(u_input_map, v_uv), vec4(target_gamma + darken_screen)).rgba; 49 | vec4 avglum = vec4(0.5); 50 | screen = mix(screen, avglum, (1.0 - contrast)); 51 | 52 | mat4 color = mat4( 53 | r, rg, rb, 0.0, //red channel 54 | gr, g, gb, 0.0, //green channel 55 | br, bg, b, 0.0, //blue channel 56 | blr, blg, blb, 0.0 //alpha channel; these numbers do nothing for our purposes. 57 | ); 58 | 59 | mat4 adjust = mat4( 60 | (1.0 - sat) * 0.3086 + sat, (1.0 - sat) * 0.3086, (1.0 - sat) * 0.3086, 1.0, 61 | (1.0 - sat) * 0.6094, (1.0 - sat) * 0.6094 + sat, (1.0 - sat) * 0.6094, 1.0, 62 | (1.0 - sat) * 0.0820, (1.0 - sat) * 0.0820, (1.0 - sat) * 0.0820 + sat, 1.0, 63 | 0.0, 0.0, 0.0, 1.0 64 | ); 65 | 66 | color *= adjust; 67 | screen = clamp(screen * lum, 0.0, 1.0); 68 | screen = color * screen; 69 | 70 | frag_color = pow(screen, vec4(1.0 / display_gamma)); 71 | frag_color.a = 1; 72 | } 73 | )"; 74 | -------------------------------------------------------------------------------- /src/nba/include/nba/rom/gpio/rtc.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba { 13 | 14 | namespace core { 15 | 16 | struct IRQ; 17 | 18 | } // namespace nba::core 19 | 20 | struct RTC final : GPIODevice { 21 | enum class Port { 22 | SCK, 23 | SIO, 24 | CS 25 | }; 26 | 27 | enum class State { 28 | Command, 29 | Sending, 30 | Receiving, 31 | Complete 32 | }; 33 | 34 | enum class Register { 35 | ForceReset = 0, 36 | DateTime = 2, 37 | ForceIRQ = 3, 38 | Control = 4, 39 | Time = 6, 40 | Free = 7 41 | }; 42 | 43 | RTC(core::IRQ& irq); 44 | 45 | void Reset() override; 46 | auto Read() -> int override; 47 | void Write(int value) override; 48 | 49 | void LoadState(SaveState const& state) override; 50 | void CopyState(SaveState& state) override; 51 | 52 | private: 53 | bool ReadSIO(); 54 | void ReceiveCommandSIO(); 55 | void ReceiveBufferSIO(); 56 | void TransmitBufferSIO(); 57 | void ReadRegister(); 58 | void WriteRegister(); 59 | 60 | static auto ConvertDecimalToBCD(u8 x) -> u8 { 61 | u8 y = 0; 62 | u8 e = 1; 63 | 64 | while(x > 0) { 65 | y += (x % 10) * e; 66 | e *= 16; 67 | x /= 10; 68 | } 69 | 70 | return y; 71 | } 72 | 73 | int current_bit; 74 | int current_byte; 75 | 76 | Register reg; 77 | u8 data; 78 | u8 buffer[7]; 79 | 80 | struct PortData { 81 | int sck; 82 | int sio; 83 | int cs; 84 | } port; 85 | 86 | State state; 87 | 88 | struct ControlRegister { 89 | bool unknown1 = false; 90 | bool per_minute_irq = false; 91 | bool unknown2 = false; 92 | bool mode_24h = false; 93 | bool poweroff = false; 94 | } control; 95 | 96 | core::IRQ& irq; 97 | 98 | static constexpr int s_argument_count[8] = { 99 | 0, // ForceReset 100 | 0, // Unused? 101 | 7, // DateTime 102 | 0, // ForceIRQ 103 | 1, // Control 104 | 0, // Unused? 105 | 3, // Time 106 | 0 // FreeRegister 107 | }; 108 | }; 109 | 110 | } // namespace nba 111 | -------------------------------------------------------------------------------- /src/nba/src/hw/timer/timer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "hw/apu/apu.hpp" 16 | #include "hw/irq/irq.hpp" 17 | 18 | namespace nba::core { 19 | 20 | struct Timer { 21 | Timer(Scheduler& scheduler, IRQ& irq, APU& apu); 22 | 23 | void Reset(); 24 | auto ReadByte(int chan_id, int offset) -> u8; 25 | auto ReadHalf(int chan_id, int offset) -> u16; 26 | auto ReadWord(int chan_id) -> u32; 27 | void WriteByte(int chan_id, int offset, u8 value); 28 | void WriteHalf(int chan_id, int offset, u16 value); 29 | void WriteWord(int chan_id, u32 value); 30 | 31 | void LoadState(SaveState const& state); 32 | void CopyState(SaveState& state); 33 | 34 | private: 35 | enum Registers { 36 | REG_TMXCNT_L = 0, 37 | REG_TMXCNT_H = 2 38 | }; 39 | 40 | struct Channel { 41 | int id; 42 | u16 reload = 0; 43 | u32 counter = 0; 44 | 45 | struct Pending { 46 | u16 reload = 0; 47 | u16 control = 0; 48 | } pending = {}; 49 | 50 | struct Control { 51 | int frequency = 0; 52 | bool cascade = false; 53 | bool interrupt = false; 54 | bool enable = false; 55 | } control = {}; 56 | 57 | bool running = false; 58 | int shift; 59 | int mask; 60 | int samplerate; 61 | u64 timestamp_started; 62 | Scheduler::Event* event_overflow = nullptr; 63 | } channels[4]; 64 | 65 | Scheduler& scheduler; 66 | IRQ& irq; 67 | APU& apu; 68 | 69 | auto ReadCounter(Channel const& channel) -> u16; 70 | void WriteReload(Channel& channel, u16 value); 71 | 72 | auto ReadControl(Channel const& channel) -> u16; 73 | void WriteControl(Channel& channel, u16 value); 74 | 75 | void OnReloadWritten(u64 chan_id); 76 | void OnControlWritten(u64 chan_id); 77 | 78 | auto GetCounterDeltaSinceLastUpdate(Channel const& channel) -> u32; 79 | void StartChannel(Channel& channel, int cycle_offset); 80 | void StopChannel(Channel& channel); 81 | void ReloadCascadeAndRequestIRQ(Channel& channel); 82 | void OnOverflow(u64 chan_id); 83 | }; 84 | 85 | } // namespace nba::core 86 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/input_window.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include "config.hpp" 26 | 27 | inline auto GetJoystickGUIDStringFromIndex(int device_index) -> std::string { 28 | auto guid = SDL_JoystickGetDeviceGUID(device_index); 29 | auto guid_string = std::string{}; 30 | 31 | guid_string.resize(sizeof(SDL_JoystickGUID) * 2); 32 | SDL_JoystickGetGUIDString(guid, guid_string.data(), guid_string.size() + 1); 33 | return guid_string; 34 | } 35 | 36 | struct InputWindow : QDialog { 37 | using Key = nba::Key; 38 | 39 | InputWindow( 40 | QApplication* app, 41 | QWidget* parent, 42 | std::shared_ptr config 43 | ); 44 | 45 | void BindCurrentKeyToJoystickButton(int button); 46 | void BindCurrentKeyToJoystickAxis(int axis, bool negative); 47 | void BindCurrentKeyToJoystickHat(int hat, int direction); 48 | 49 | void UpdateJoystickList(); 50 | 51 | std::atomic_bool has_game_controller_choice_changed = false; 52 | 53 | protected: 54 | bool eventFilter(QObject* obj, QEvent* event); 55 | 56 | private: 57 | auto CreateJoystickList() -> QLayout*; 58 | auto CreateKeyMapTable() -> QLayout*; 59 | 60 | void CreateKeyMapEntry( 61 | QGridLayout* layout, 62 | const char* label, 63 | QtConfig::Input::Map* mapping 64 | ); 65 | 66 | void RestoreActiveButtonLabel(); 67 | 68 | static auto GetKeyboardButtonName(int key) -> QString; 69 | static auto GetJoystickButtonName(QtConfig::Input::Map* mapping) -> QString; 70 | 71 | bool waiting_for_keyboard = false; 72 | bool waiting_for_joystick = false; 73 | QtConfig::Input::Map* active_mapping = nullptr; 74 | QPushButton* active_button = nullptr; 75 | QComboBox* joystick_combo_box; 76 | std::shared_ptr config; 77 | }; -------------------------------------------------------------------------------- /src/nba/src/hw/apu/apu.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "hw/apu/channel/quad_channel.hpp" 18 | #include "hw/apu/channel/wave_channel.hpp" 19 | #include "hw/apu/channel/noise_channel.hpp" 20 | #include "hw/apu/channel/fifo.hpp" 21 | #include "hw/apu/hle/mp2k.hpp" 22 | #include "hw/apu/registers.hpp" 23 | #include "hw/dma/dma.hpp" 24 | 25 | namespace nba::core { 26 | 27 | // See callback.cpp for implementation 28 | void AudioCallback(struct APU* apu, s16* stream, int byte_len); 29 | 30 | struct APU { 31 | APU( 32 | Scheduler& scheduler, 33 | DMA& dma, 34 | Bus& bus, 35 | std::shared_ptr 36 | ); 37 | 38 | ~APU(); 39 | 40 | void Reset(); 41 | auto GetMP2K() -> MP2K& { return mp2k; } 42 | void OnTimerOverflow(int timer_id, int times); 43 | 44 | void LoadState(SaveState const& state); 45 | void CopyState(SaveState& state); 46 | 47 | struct MMIO { 48 | MMIO(Scheduler& scheduler) 49 | : psg1(scheduler, Scheduler::EventClass::APU_PSG1_generate) 50 | , psg2(scheduler, Scheduler::EventClass::APU_PSG2_generate) 51 | , psg3(scheduler) 52 | , psg4(scheduler, bias) { 53 | } 54 | 55 | FIFO fifo[2]; 56 | 57 | QuadChannel psg1; 58 | QuadChannel psg2; 59 | WaveChannel psg3; 60 | NoiseChannel psg4; 61 | 62 | SoundControl soundcnt { fifo, psg1, psg2, psg3, psg4 }; 63 | BIAS bias; 64 | } mmio; 65 | 66 | struct Pipe { 67 | u32 word = 0; 68 | int size = 0; 69 | } fifo_pipe[2]; 70 | 71 | std::mutex buffer_mutex; 72 | std::shared_ptr> buffer; 73 | std::unique_ptr> resampler; 74 | 75 | private: 76 | friend void AudioCallback(APU* apu, s16* stream, int byte_len); 77 | 78 | void StepMixer(); 79 | void StepSequencer(); 80 | 81 | s8 latch[2]; 82 | 83 | Scheduler& scheduler; 84 | DMA& dma; 85 | MP2K mp2k; 86 | int mp2k_read_index; 87 | std::shared_ptr config; 88 | int resolution_old = 0; 89 | }; 90 | 91 | } // namespace nba::core 92 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/color_grid.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "widget/debugger/utility.hpp" 13 | #include "color_grid.hpp" 14 | 15 | ColorGrid::ColorGrid(int rows, int columns, QWidget* parent) 16 | : QWidget(parent) 17 | , m_rows(rows) 18 | , m_columns(columns) { 19 | m_buffer_argb8888 = new u32[rows * columns]; 20 | std::memset(m_buffer_argb8888, 0, rows * columns * sizeof(u32)); 21 | 22 | setFixedSize(columns * k_box_size, rows * k_box_size); 23 | } 24 | 25 | ColorGrid::~ColorGrid() { 26 | delete[] m_buffer_argb8888; 27 | } 28 | 29 | void ColorGrid::Draw(u16* buffer_rgb565, int stride) { 30 | int i = 0; 31 | 32 | for(int y = 0; y < m_rows; y++) { 33 | for(int x = 0; x < m_columns; x++) { 34 | m_buffer_argb8888[i++] = Rgb565ToArgb8888(buffer_rgb565[y * stride + x]); 35 | } 36 | } 37 | 38 | update(); 39 | } 40 | 41 | void ColorGrid::SetHighlightedPosition(int x, int y) { 42 | m_highlighted_x = std::min(m_columns, x); 43 | m_highlighted_y = std::min(m_rows, y); 44 | update(); 45 | } 46 | 47 | void ColorGrid::ClearHighlight() { 48 | SetHighlightedPosition(-1, -1); 49 | } 50 | 51 | u32 ColorGrid::GetColorAt(int x, int y) { 52 | return m_buffer_argb8888[y * m_columns + x]; 53 | } 54 | 55 | void ColorGrid::paintEvent(QPaintEvent* event) { 56 | QPainter painter{this}; 57 | 58 | painter.setPen(Qt::gray); 59 | 60 | int i = 0; 61 | 62 | for(int y = 0; y < m_rows; y++) { 63 | for(int x = 0; x < m_columns; x++) { 64 | painter.setBrush(QBrush{m_buffer_argb8888[i++]}); 65 | painter.drawRect(x * k_box_size, y * k_box_size, k_box_size, k_box_size); 66 | } 67 | } 68 | 69 | if(m_highlighted_x > -1 && m_highlighted_y > -1) { 70 | QPen red_highlight_pen{Qt::red}; 71 | red_highlight_pen.setWidth(2); 72 | 73 | painter.setPen(red_highlight_pen); 74 | painter.setBrush(Qt::NoBrush); 75 | painter.drawRect(m_highlighted_x * k_box_size, m_highlighted_y * k_box_size, k_box_size, k_box_size); 76 | } 77 | } 78 | 79 | void ColorGrid::mousePressEvent(QMouseEvent* event) { 80 | const int x = std::min((int)(event->x() / k_box_size), m_columns); 81 | const int y = std::min((int)(event->y() / k_box_size), m_rows); 82 | 83 | emit selected(x, y); 84 | } -------------------------------------------------------------------------------- /src/nba/include/nba/common/dsp/stereo.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace nba { 15 | 16 | template 17 | struct StereoSample { 18 | T left {}; 19 | T right {}; 20 | 21 | template 22 | operator StereoSample() { 23 | return { (U)left, (U)right }; 24 | } 25 | 26 | T& operator[](int index) { 27 | if(index == 0) return left; 28 | if(index == 1) return right; 29 | 30 | throw std::runtime_error("StereoSample: bad index for operator[]."); 31 | } 32 | 33 | StereoSample operator+(T scalar) const { 34 | return { left + scalar, right + scalar }; 35 | } 36 | 37 | StereoSample operator+(StereoSample const& other) const { 38 | return { left + other.left, right + other.right }; 39 | } 40 | 41 | StereoSample& operator+=(T scalar) { 42 | left += scalar; 43 | right += scalar; 44 | return *this; 45 | } 46 | 47 | StereoSample& operator+=(StereoSample const& other) { 48 | left += other.left; 49 | right += other.right; 50 | return *this; 51 | } 52 | 53 | StereoSample operator-(T scalar) const { 54 | return { left - scalar, right - scalar }; 55 | } 56 | 57 | StereoSample operator-(StereoSample const& other) const { 58 | return { left - other.left, right - other.right }; 59 | } 60 | 61 | StereoSample& operator-=(T scalar) { 62 | left -= scalar; 63 | right -= scalar; 64 | return *this; 65 | } 66 | 67 | StereoSample& operator-=(StereoSample const& other) { 68 | left -= other.left; 69 | right -= other.right; 70 | return *this; 71 | } 72 | 73 | StereoSample operator*(T scalar) const { 74 | return { left * scalar, right * scalar }; 75 | } 76 | 77 | StereoSample operator*(StereoSample const& other) const { 78 | return { left * other.left, right * other.right }; 79 | } 80 | 81 | StereoSample& operator*=(T scalar) { 82 | left *= scalar; 83 | right *= scalar; 84 | return *this; 85 | } 86 | 87 | StereoSample& operator*=(StereoSample const& other) { 88 | left *= other.left; 89 | right *= other.right; 90 | return *this; 91 | } 92 | }; 93 | 94 | } // namespace nba 95 | -------------------------------------------------------------------------------- /src/nba/src/hw/keypad/keypad.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | #include "hw/keypad/keypad.hpp" 11 | 12 | namespace nba::core { 13 | 14 | KeyPad::KeyPad(Scheduler& scheduler, IRQ& irq) 15 | : scheduler(scheduler) 16 | , irq(irq) { 17 | Reset(); 18 | } 19 | 20 | void KeyPad::Reset() { 21 | input = {}; 22 | input.keypad = this; 23 | control = {}; 24 | control.keypad = this; 25 | } 26 | 27 | void KeyPad::SetKeyStatus(Key key, bool pressed) { 28 | const u16 bit = 1 << (int)key; 29 | 30 | if(pressed) { 31 | input.value &= ~bit; 32 | } else { 33 | input.value |= bit; 34 | } 35 | 36 | UpdateIRQ(); 37 | } 38 | 39 | void KeyPad::UpdateIRQ() { 40 | if(control.interrupt) { 41 | auto not_input = ~input.value & 0x3FF; 42 | 43 | if(control.mode == KeyControl::Mode::LogicalAND) { 44 | if(control.mask == not_input) { 45 | irq.Raise(IRQ::Source::Keypad); 46 | } 47 | } else if((control.mask & not_input) != 0) { 48 | irq.Raise(IRQ::Source::Keypad); 49 | } 50 | } 51 | } 52 | 53 | auto KeyPad::KeyInput::ReadByte(uint offset) -> u8 { 54 | switch(offset) { 55 | case 0: 56 | return u8(value); 57 | case 1: 58 | return u8(value >> 8); 59 | } 60 | 61 | unreachable(); 62 | } 63 | 64 | auto KeyPad::KeyControl::ReadByte(uint offset) -> u8 { 65 | switch(offset) { 66 | case 0: { 67 | return u8(mask); 68 | } 69 | case 1: { 70 | return ((mask >> 8) & 3) | 71 | (interrupt ? 64 : 0) | 72 | (int(mode) << 7); 73 | } 74 | } 75 | 76 | unreachable(); 77 | } 78 | 79 | void KeyPad::KeyControl::WriteByte(uint offset, u8 value) { 80 | switch(offset) { 81 | case 0: { 82 | mask &= 0xFF00; 83 | mask |= value; 84 | break; 85 | } 86 | case 1: { 87 | mask &= 0x00FF; 88 | mask |= (value & 3) << 8; 89 | interrupt = value & 64; 90 | mode = Mode(value >> 7); 91 | break; 92 | } 93 | default: { 94 | unreachable(); 95 | } 96 | } 97 | 98 | keypad->UpdateIRQ(); 99 | } 100 | 101 | void KeyPad::KeyControl::WriteHalf(u16 value) { 102 | mask = value & 0x03FF; 103 | interrupt = value & 0x4000; 104 | mode = Mode(value >> 15); 105 | keypad->UpdateIRQ(); 106 | } 107 | 108 | } // namespace nba::core 109 | -------------------------------------------------------------------------------- /src/nba/src/hw/rom/gpio/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | namespace nba { 16 | 17 | void GPIO::LoadState(SaveState const& state) { 18 | allow_reads = state.gpio.allow_reads; 19 | rd_mask = state.gpio.rd_mask; 20 | wr_mask = (~state.gpio.rd_mask) & 15; 21 | port_data = state.gpio.port_data; 22 | 23 | for(auto& device : devices) { 24 | device->LoadState(state); 25 | device->SetPortDirections(wr_mask); 26 | } 27 | } 28 | 29 | void GPIO::CopyState(SaveState& state) { 30 | state.gpio.allow_reads = allow_reads; 31 | state.gpio.rd_mask = rd_mask; 32 | state.gpio.port_data = port_data; 33 | 34 | for(auto& device : devices) { 35 | device->CopyState(state); 36 | } 37 | } 38 | 39 | void RTC::LoadState(SaveState const& state) { 40 | current_bit = state.gpio.rtc.current_bit; 41 | current_byte = state.gpio.rtc.current_byte; 42 | reg = (Register)state.gpio.rtc.reg; 43 | data = state.gpio.rtc.data; 44 | this->state = (State)state.gpio.rtc.state; 45 | control.unknown1 = state.gpio.rtc.control.unknown1; 46 | control.per_minute_irq = state.gpio.rtc.control.per_minute_irq; 47 | control.unknown2 = state.gpio.rtc.control.unknown2; 48 | control.mode_24h = state.gpio.rtc.control.mode_24h; 49 | control.poweroff = state.gpio.rtc.control.poweroff; 50 | 51 | std::memcpy(buffer, state.gpio.rtc.buffer, sizeof(buffer)); 52 | } 53 | 54 | void RTC::CopyState(SaveState& state) { 55 | state.gpio.rtc.current_bit = current_bit; 56 | state.gpio.rtc.current_byte = current_byte; 57 | state.gpio.rtc.reg = (u8)reg; 58 | state.gpio.rtc.data = data; 59 | state.gpio.rtc.state = (u8)this->state; 60 | state.gpio.rtc.control.unknown1 = control.unknown1; 61 | state.gpio.rtc.control.per_minute_irq = control.per_minute_irq; 62 | state.gpio.rtc.control.unknown2 = control.unknown2; 63 | state.gpio.rtc.control.mode_24h = control.mode_24h; 64 | state.gpio.rtc.control.poweroff = control.poweroff; 65 | 66 | std::memcpy(state.gpio.rtc.buffer, buffer, sizeof(buffer)); 67 | } 68 | 69 | void SolarSensor::LoadState(SaveState const& state) { 70 | old_clk = state.gpio.solar_sensor.old_clk; 71 | counter = state.gpio.solar_sensor.counter; 72 | } 73 | 74 | void SolarSensor::CopyState(SaveState& state) { 75 | state.gpio.solar_sensor.old_clk = old_clk; 76 | state.gpio.solar_sensor.counter = counter; 77 | } 78 | 79 | } // namespace nba 80 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/background_viewer.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "color_grid.hpp" 17 | 18 | class BackgroundViewer : public QWidget { 19 | public: 20 | BackgroundViewer(nba::CoreBase* core, QWidget* parent = nullptr); 21 | ~BackgroundViewer() override; 22 | 23 | void SetBackgroundID(int id); 24 | void Update(); 25 | 26 | bool eventFilter(QObject* object, QEvent* event); 27 | 28 | private: 29 | QWidget* CreateBackgroundInfoGroupBox(); 30 | QWidget* CreateTileInfoGroupBox(); 31 | QLayout* CreateInfoPanel(); 32 | QWidget* CreateCanvasScrollArea(); 33 | 34 | void DrawBackgroundMode0(); 35 | void DrawBackgroundMode2(); 36 | void DrawBackgroundMode3(); 37 | void DrawBackgroundMode4(); 38 | void DrawBackgroundMode5(); 39 | 40 | void DrawTileDetail(int tile_x, int tile_y); 41 | void ClearTileSelection(); 42 | 43 | void PresentBackground(); 44 | 45 | int m_bg_id = 0; 46 | int m_bg_mode = 0; 47 | u16 m_bghofs = 0; 48 | u16 m_bgvofs = 0; 49 | 50 | QLabel* m_label_bg_mode; 51 | QLabel* m_label_bg_priority; 52 | QLabel* m_label_bg_size; 53 | QLabel* m_label_tile_base; 54 | QLabel* m_label_map_base; 55 | QCheckBox* m_check_8bpp; 56 | QCheckBox* m_check_bg_wraparound; 57 | QCheckBox* m_check_bg_mosaic; 58 | QLabel* m_label_bg_scroll; 59 | 60 | QLabel* m_label_tile_number; 61 | QLabel* m_label_tile_address; 62 | QLabel* m_label_tile_map_entry_address; 63 | QCheckBox* m_check_tile_flip_v; 64 | QCheckBox* m_check_tile_flip_h; 65 | QLabel* m_label_tile_palette; 66 | ColorGrid* m_tile_color_grid; 67 | QLabel* m_label_color_r_component; 68 | QLabel* m_label_color_g_component; 69 | QLabel* m_label_color_b_component; 70 | 71 | QCheckBox* m_check_display_screen_viewport; 72 | 73 | bool m_display_selected_tile = false; 74 | int m_selected_tile_x; 75 | int m_selected_tile_y; 76 | 77 | QWidget* m_canvas; 78 | QImage m_image_rgb32{1024, 1024, QImage::Format_RGB32}; 79 | u16* m_image_rgb565; 80 | 81 | struct TileMetaData { 82 | int tile_number; 83 | u32 tile_address; 84 | u32 map_entry_address; 85 | bool flip_v; 86 | bool flip_h; 87 | int palette; 88 | } m_tile_meta_data[128][128]; 89 | 90 | nba::CoreBase* m_core; 91 | u16* m_pram; 92 | u8* m_vram; 93 | 94 | Q_OBJECT 95 | }; -------------------------------------------------------------------------------- /src/nba/src/arm/state.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba::core::arm { 13 | 14 | enum Mode : unsigned int { 15 | MODE_USR = 0x10, 16 | MODE_FIQ = 0x11, 17 | MODE_IRQ = 0x12, 18 | MODE_SVC = 0x13, 19 | MODE_ABT = 0x17, 20 | MODE_UND = 0x1B, 21 | MODE_SYS = 0x1F 22 | }; 23 | 24 | enum Bank { 25 | BANK_NONE = 0, 26 | BANK_FIQ = 1, 27 | BANK_SVC = 2, 28 | BANK_ABT = 3, 29 | BANK_IRQ = 4, 30 | BANK_UND = 5, 31 | BANK_INVALID = 6, 32 | BANK_COUNT, 33 | }; 34 | 35 | enum Condition { 36 | COND_EQ = 0, 37 | COND_NE = 1, 38 | COND_CS = 2, 39 | COND_CC = 3, 40 | COND_MI = 4, 41 | COND_PL = 5, 42 | COND_VS = 6, 43 | COND_VC = 7, 44 | COND_HI = 8, 45 | COND_LS = 9, 46 | COND_GE = 10, 47 | COND_LT = 11, 48 | COND_GT = 12, 49 | COND_LE = 13, 50 | COND_AL = 14, 51 | COND_NV = 15 52 | }; 53 | 54 | enum BankedRegister { 55 | BANK_R8 = 0, 56 | BANK_R9 = 1, 57 | BANK_R10 = 2, 58 | BANK_R11 = 3, 59 | BANK_R12 = 4, 60 | BANK_R13 = 5, 61 | BANK_R14 = 6 62 | }; 63 | 64 | union StatusRegister { 65 | StatusRegister() {}; 66 | StatusRegister(u32 value) { v = value; } 67 | 68 | struct { 69 | Mode mode : 5; 70 | unsigned int thumb : 1; 71 | unsigned int mask_fiq : 1; 72 | unsigned int mask_irq : 1; 73 | unsigned int reserved : 19; 74 | unsigned int q : 1; 75 | unsigned int v : 1; 76 | unsigned int c : 1; 77 | unsigned int z : 1; 78 | unsigned int n : 1; 79 | } f; 80 | u32 v; 81 | }; 82 | 83 | struct RegisterFile { 84 | // General Purpose Registers 85 | union { 86 | struct { 87 | u32 r0; 88 | u32 r1; 89 | u32 r2; 90 | u32 r3; 91 | u32 r4; 92 | u32 r5; 93 | u32 r6; 94 | u32 r7; 95 | u32 r8; 96 | u32 r9; 97 | u32 r10; 98 | u32 r11; 99 | u32 r12; 100 | u32 r13; 101 | u32 r14; 102 | u32 r15; 103 | }; 104 | u32 reg[16]; 105 | }; 106 | 107 | // Banked Registers 108 | u32 bank[BANK_COUNT][7]; 109 | 110 | // Program Status Registers 111 | StatusRegister cpsr; 112 | StatusRegister spsr[BANK_COUNT]; 113 | 114 | RegisterFile() { Reset(); } 115 | 116 | void Reset() { 117 | for(int i = 0; i < 16; i++) reg[i] = 0; 118 | 119 | for(int i = 0; i < BANK_COUNT; i++) { 120 | for(int j = 0; j < 7; j++) 121 | bank[i][j] = 0; 122 | spsr[i].v = 0; 123 | } 124 | 125 | cpsr.v = MODE_SVC; 126 | cpsr.f.mask_irq = 1; 127 | cpsr.f.mask_fiq = 1; 128 | } 129 | }; 130 | 131 | } // namespace nba::core::arm 132 | -------------------------------------------------------------------------------- /src/platform/core/src/device/sdl_audio_device.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | namespace nba { 12 | 13 | void SDL2_AudioDevice::SetSampleRate(int sample_rate) { 14 | want_sample_rate = sample_rate; 15 | } 16 | 17 | void SDL2_AudioDevice::SetBlockSize(int block_size) { 18 | want_block_size = block_size; 19 | } 20 | 21 | void SDL2_AudioDevice::SetPassthrough(SDL_AudioCallback passthrough) { 22 | this->passthrough = passthrough; 23 | } 24 | 25 | void SDL2_AudioDevice::InvokeCallback(s16* stream, int byte_len) { 26 | if(callback) { 27 | callback(callback_userdata, stream, byte_len); 28 | } 29 | } 30 | 31 | auto SDL2_AudioDevice::GetSampleRate() -> int { 32 | return have.freq; 33 | } 34 | 35 | auto SDL2_AudioDevice::GetBlockSize() -> int { 36 | return have.samples; 37 | } 38 | 39 | bool SDL2_AudioDevice::Open(void* userdata, Callback callback) { 40 | auto want = SDL_AudioSpec{}; 41 | 42 | if(SDL_Init(SDL_INIT_AUDIO) < 0) { 43 | Log("Audio: SDL_Init(SDL_INIT_AUDIO) failed."); 44 | return false; 45 | } 46 | 47 | want.freq = want_sample_rate; 48 | want.samples = want_block_size; 49 | want.format = AUDIO_S16; 50 | want.channels = 2; 51 | 52 | if(passthrough != nullptr) { 53 | want.callback = passthrough; 54 | want.userdata = this; 55 | } else { 56 | want.callback = (SDL_AudioCallback)callback; 57 | want.userdata = userdata; 58 | } 59 | 60 | this->callback = callback; 61 | callback_userdata = userdata; 62 | 63 | device = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); 64 | 65 | if(device == 0) { 66 | Log("Audio: SDL_OpenAudioDevice: failed to open audio: %s\n", SDL_GetError()); 67 | return false; 68 | } 69 | 70 | opened = true; 71 | 72 | if(have.format != want.format) { 73 | Log("Audio: SDL_AudioDevice: S16 sample format unavailable."); 74 | return false; 75 | } 76 | 77 | if(have.channels != want.channels) { 78 | Log("Audio: SDL_AudioDevice: Stereo output unavailable."); 79 | return false; 80 | } 81 | 82 | if(!paused) { 83 | SDL_PauseAudioDevice(device, 0); 84 | } 85 | return true; 86 | } 87 | 88 | void SDL2_AudioDevice::SetPause(bool value) { 89 | if(opened) { 90 | SDL_PauseAudioDevice(device, value ? 1 : 0); 91 | } 92 | } 93 | 94 | void SDL2_AudioDevice::Close() { 95 | if(opened) { 96 | SDL_CloseAudioDevice(device); 97 | opened = false; 98 | } 99 | } 100 | 101 | } // namespace nba 102 | -------------------------------------------------------------------------------- /src/nba/include/nba/common/dsp/resampler/sinc.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace nba { 14 | 15 | template 16 | struct SincResampler : Resampler { 17 | static_assert((points % 4) == 0, "SincResampler: points must be divisible by four."); 18 | 19 | SincResampler(std::shared_ptr> output) 20 | : Resampler(output) { 21 | SetSampleRates(1, 1); 22 | 23 | for(int i = 0; i < points - 1; i++) { 24 | taps.Write({}); 25 | } 26 | } 27 | 28 | void SetSampleRates(float samplerate_in, float samplerate_out) final { 29 | Resampler::SetSampleRates(samplerate_in, samplerate_out); 30 | 31 | float kernelSum = 0.0; 32 | float cutoff = 0.9; 33 | 34 | if(this->resample_phase_shift > 1.0) { 35 | cutoff /= this->resample_phase_shift; 36 | } 37 | 38 | for(int n = 0; n < points; n++) { 39 | for(int m = 0; m < s_lut_resolution; m++) { 40 | double t = m/double(s_lut_resolution); 41 | double x1 = M_PI * (t - n + points/2) + 1e-6; 42 | double x2 = 2 * M_PI * (n + t)/points; 43 | double sinc = std::sin(cutoff * x1)/x1; 44 | double blackman = 0.42 - 0.49 * std::cos(x2) + 0.076 * std::cos(2 * x2); 45 | 46 | lut[n * s_lut_resolution + m] = sinc * blackman; 47 | kernelSum += sinc * blackman; 48 | } 49 | } 50 | 51 | kernelSum /= s_lut_resolution; 52 | 53 | for(int i = 0; i < points * s_lut_resolution; i++) { 54 | lut[i] /= kernelSum; 55 | } 56 | } 57 | 58 | void Write(T const& input) final { 59 | taps.Write(input); 60 | 61 | while(resample_phase < 1.0) { 62 | T sample = {}; 63 | 64 | int x = (int)(resample_phase * s_lut_resolution); 65 | 66 | for(int n = 0; n < points; n += 4) { 67 | sample += taps.Peek(n + 0) * lut[x + 0 * s_lut_resolution]; 68 | sample += taps.Peek(n + 1) * lut[x + 1 * s_lut_resolution]; 69 | sample += taps.Peek(n + 2) * lut[x + 2 * s_lut_resolution]; 70 | sample += taps.Peek(n + 3) * lut[x + 3 * s_lut_resolution]; 71 | 72 | x += 4 * s_lut_resolution; 73 | } 74 | 75 | this->output->Write(sample); 76 | 77 | resample_phase += this->resample_phase_shift; 78 | } 79 | 80 | taps.Read(); 81 | 82 | resample_phase = resample_phase - 1.0; 83 | } 84 | 85 | private: 86 | static constexpr int s_lut_resolution = 512; 87 | 88 | double lut[points * s_lut_resolution]; 89 | float resample_phase = 0; 90 | RingBuffer taps { points }; 91 | }; 92 | 93 | template 94 | using SincStereoResampler = SincResampler, points>; 95 | 96 | } // namespace nba 97 | -------------------------------------------------------------------------------- /src/nba/src/hw/ppu/registers.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba::core { 13 | 14 | class PPU; 15 | 16 | struct DisplayControl { 17 | u16 hword; 18 | 19 | int mode; 20 | int cgb_mode; 21 | int frame; 22 | int hblank_oam_access; 23 | int oam_mapping_1d; 24 | int forced_blank; 25 | int enable[8]; 26 | 27 | void Reset(); 28 | auto Read(int address) -> u8; 29 | void Write(int address, u8 value); 30 | 31 | auto ReadHalf() -> u16; 32 | void WriteHalf(u16 value); 33 | 34 | PPU* ppu = nullptr; 35 | }; 36 | 37 | struct DisplayStatus { 38 | int vblank_flag; 39 | int hblank_flag; 40 | int vcount_flag; 41 | int vblank_irq_enable; 42 | int hblank_irq_enable; 43 | int vcount_irq_enable; 44 | int vcount_setting; 45 | 46 | void Reset(); 47 | auto Read(int address) -> u8; 48 | void Write(int address, u8 value); 49 | 50 | auto ReadHalf() -> u16; 51 | void WriteHalf(u16 value); 52 | 53 | PPU* ppu = nullptr; 54 | }; 55 | 56 | struct BackgroundControl { 57 | int priority; 58 | int tile_block; 59 | int unused; 60 | int mosaic_enable; 61 | int full_palette; 62 | int map_block; 63 | int wraparound = false; 64 | int size; 65 | 66 | BackgroundControl(int id) : id(id) {} 67 | 68 | void Reset(); 69 | auto Read(int address) -> u8; 70 | void Write(int address, u8 value); 71 | 72 | auto ReadHalf() -> u16; 73 | void WriteHalf(u16 value); 74 | 75 | private: 76 | int id; 77 | }; 78 | 79 | struct ReferencePoint { 80 | s32 initial; 81 | s32 _current; 82 | bool written; 83 | 84 | void Reset(); 85 | void Write(int address, u8 value); 86 | }; 87 | 88 | struct BlendControl { 89 | enum Effect { 90 | SFX_NONE, 91 | SFX_BLEND, 92 | SFX_BRIGHTEN, 93 | SFX_DARKEN 94 | } sfx; 95 | 96 | int targets[2][6]; 97 | 98 | void Reset(); 99 | auto Read(int address) -> u8; 100 | void Write(int address, u8 value); 101 | 102 | auto ReadHalf() -> u16; 103 | void WriteHalf(u16 value); 104 | }; 105 | 106 | struct WindowRange { 107 | int min; 108 | int max; 109 | 110 | void Reset(); 111 | void Write(int address, u8 value); 112 | 113 | auto ReadHalf() -> u16; 114 | void WriteHalf(u16 value); 115 | }; 116 | 117 | struct WindowLayerSelect { 118 | int enable[2][6]; 119 | 120 | void Reset(); 121 | auto Read(int offset) -> u8; 122 | void Write(int offset, u8 value); 123 | 124 | auto ReadHalf() -> u16; 125 | void WriteHalf(u16 value); 126 | }; 127 | 128 | struct Mosaic { 129 | struct { 130 | int size_x; 131 | int size_y; 132 | int _counter_y; 133 | } bg, obj; 134 | 135 | void Reset(); 136 | void Write(int address, u8 value); 137 | }; 138 | 139 | } // namespace nba::core 140 | -------------------------------------------------------------------------------- /src/platform/core/src/loader/save_state.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace nba { 13 | 14 | auto SaveStateLoader::Load( 15 | std::unique_ptr& core, 16 | fs::path const& path 17 | ) -> Result { 18 | if(!fs::exists(path)) { 19 | return Result::CannotFindFile; 20 | } 21 | 22 | if(!fs::is_regular_file(path)) { 23 | return Result::CannotOpenFile; 24 | } 25 | 26 | auto file_size = fs::file_size(path); 27 | 28 | if(file_size != sizeof(SaveState)) { 29 | return Result::BadImage; 30 | } 31 | 32 | SaveState save_state; 33 | 34 | std::ifstream file_stream{path.c_str(), std::ios::binary}; 35 | 36 | if(!file_stream.good()) { 37 | return Result::CannotOpenFile; 38 | } 39 | 40 | file_stream.read((char*)&save_state, sizeof(SaveState)); 41 | 42 | auto validate_result = Validate(save_state); 43 | 44 | if(validate_result != Result::Success) { 45 | return validate_result; 46 | } 47 | 48 | core->LoadState(save_state); 49 | return Result::Success; 50 | } 51 | 52 | auto SaveStateLoader::Validate(SaveState const& save_state) -> Result { 53 | if(save_state.magic != SaveState::kMagicNumber) { 54 | return Result::BadImage; 55 | } 56 | 57 | if(save_state.version != SaveState::kCurrentVersion) { 58 | return Result::UnsupportedVersion; 59 | } 60 | 61 | bool bad_image = false; 62 | 63 | { 64 | auto& waitcnt = save_state.bus.io.waitcnt; 65 | bad_image |= waitcnt.sram > 3; 66 | bad_image |= waitcnt.ws0[0] > 3; 67 | bad_image |= waitcnt.ws0[1] > 1; 68 | bad_image |= waitcnt.ws1[0] > 3; 69 | bad_image |= waitcnt.ws1[1] > 1; 70 | bad_image |= waitcnt.ws2[0] > 3; 71 | bad_image |= waitcnt.ws2[1] > 1; 72 | bad_image |= waitcnt.phi > 3; 73 | } 74 | 75 | bad_image |= save_state.ppu.io.vcount > 227; 76 | 77 | { 78 | auto& apu = save_state.apu; 79 | 80 | for(int i = 0; i < 2; i++) { 81 | bad_image |= apu.io.quad[i].phase > 7; 82 | bad_image |= apu.io.quad[i].wave_duty > 3; 83 | bad_image |= apu.fifo[i].count > 7; 84 | } 85 | 86 | bad_image |= apu.io.wave.phase > 31; 87 | bad_image |= apu.io.wave.wave_bank > 1; 88 | bad_image |= apu.io.noise.width > 1; 89 | } 90 | 91 | { 92 | auto& dma = save_state.dma; 93 | bad_image |= dma.hblank_set > 0b1111; 94 | bad_image |= dma.vblank_set > 0b1111; 95 | bad_image |= dma.video_set > 0b1111; 96 | bad_image |= dma.runnable_set > 0b1111; 97 | } 98 | 99 | bad_image |= save_state.gpio.rtc.current_byte > 7; 100 | 101 | if(bad_image) { 102 | return Result::BadImage; 103 | } 104 | 105 | return Result::Success; 106 | } 107 | 108 | } // namespace nba 109 | -------------------------------------------------------------------------------- /src/nba/src/arm/tablegen/tablegen.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | #include "arm/arm7tdmi.hpp" 11 | 12 | namespace nba::core::arm { 13 | 14 | using Handler16 = ARM7TDMI::Handler16; 15 | using Handler32 = ARM7TDMI::Handler32; 16 | 17 | /** A helper class used to generate lookup tables for 18 | * the interpreter at compiletime. 19 | * The motivation is to separate the code used for generation from 20 | * the interpreter class and its header itself. 21 | */ 22 | struct TableGen { 23 | #ifdef __clang__ 24 | #pragma clang diagnostic push 25 | #pragma clang diagnostic ignored "-Weverything" 26 | #endif 27 | 28 | #include "gen_arm.hpp" 29 | #include "gen_thumb.hpp" 30 | 31 | #ifdef __clang__ 32 | #pragma clang diagnostic pop 33 | #endif 34 | 35 | static constexpr auto GenerateTableThumb() -> std::array { 36 | std::array lut{}; 37 | 38 | static_for([&](auto i) { 39 | lut[i] = GenerateHandlerThumb(); 40 | }); 41 | return lut; 42 | } 43 | 44 | static constexpr auto GenerateTableARM() -> std::array { 45 | std::array lut{}; 46 | 47 | static_for([&](auto i) { 48 | lut[i] = GenerateHandlerARM< 49 | ((i & 0xFF0) << 16) | 50 | ((i & 0xF) << 4)>(); 51 | }); 52 | return lut; 53 | } 54 | 55 | static constexpr auto GenerateConditionTable() -> std::array { 56 | std::array lut{}; 57 | 58 | for(int flag_set = 0; flag_set < 16; flag_set++) { 59 | bool n = flag_set & 8; 60 | bool z = flag_set & 4; 61 | bool c = flag_set & 2; 62 | bool v = flag_set & 1; 63 | 64 | lut[(COND_EQ << 4) | flag_set] = z; 65 | lut[(COND_NE << 4) | flag_set] = !z; 66 | lut[(COND_CS << 4) | flag_set] = c; 67 | lut[(COND_CC << 4) | flag_set] = !c; 68 | lut[(COND_MI << 4) | flag_set] = n; 69 | lut[(COND_PL << 4) | flag_set] = !n; 70 | lut[(COND_VS << 4) | flag_set] = v; 71 | lut[(COND_VC << 4) | flag_set] = !v; 72 | lut[(COND_HI << 4) | flag_set] = c && !z; 73 | lut[(COND_LS << 4) | flag_set] = !c || z; 74 | lut[(COND_GE << 4) | flag_set] = n == v; 75 | lut[(COND_LT << 4) | flag_set] = n != v; 76 | lut[(COND_GT << 4) | flag_set] = !(z || (n != v)); 77 | lut[(COND_LE << 4) | flag_set] = (z || (n != v)); 78 | lut[(COND_AL << 4) | flag_set] = true; 79 | lut[(COND_NV << 4) | flag_set] = false; 80 | } 81 | 82 | return lut; 83 | } 84 | }; 85 | 86 | std::array ARM7TDMI::s_opcode_lut_16 = TableGen::GenerateTableThumb(); 87 | std::array ARM7TDMI::s_opcode_lut_32 = TableGen::GenerateTableARM(); 88 | std::array ARM7TDMI::s_condition_lut = TableGen::GenerateConditionTable(); 89 | 90 | } // namespace nba::core::arm 91 | -------------------------------------------------------------------------------- /docs/COMPILING.md: -------------------------------------------------------------------------------- 1 | NanoBoyAdvance can be compiled on Windows, Linux, and macOS. 2 | 3 | ### Prerequisites 4 | 5 | - Clang or GCC with C++17 support 6 | - CMake 3.11 or higher 7 | - Python modules Jinja and (optionally) lxml 8 | - OpenGL (usually provided by the operating system) 9 | - SDL 2 library 10 | - Qt 5 library 11 | 12 | ### Source Code 13 | 14 | Clone the Git repository: 15 | 16 | ```bash 17 | git clone https://github.com/nba-emu/NanoBoyAdvance.git 18 | ``` 19 | 20 | ### Unix Build (Linux, macOS) 21 | 22 | #### 1. Install dependencies 23 | 24 | The way that you install the dependencies will vary depending on the distribution you use. 25 | Typically you'll have to invoke the install command of some package manager. 26 | Here is a list of commands for popular distributions and macOS: 27 | 28 | ##### Arch Linux 29 | 30 | ```bash 31 | pacman -S cmake python-jinja python-lxml sdl2 qt5-base 32 | ``` 33 | 34 | ##### Ubuntu or other Debian-derived distribution 35 | 36 | ```bash 37 | apt install cmake python3-jinja2 python3-lxml libsdl2-dev qtbase5-dev libqt5opengl5-dev 38 | ``` 39 | 40 | ##### macOS 41 | 42 | Get [Brew](https://brew.sh/) and run: 43 | 44 | ``` bash 45 | brew install cmake python@3 sdl2 qt@5 46 | python3 -m pip install Jinja2 47 | ``` 48 | 49 | ##### FreeBSD 50 | 51 | ```bash 52 | su 53 | pkg install cmake git py39-Jinja2 py39-lxml sdl2 qt5 qt5-opengl 54 | ``` 55 | 56 | #### 2. Setup CMake build directory 57 | 58 | ##### Linux and FreeBSD 59 | 60 | ``` 61 | cd /somewhere/on/your/system/NanoBoyAdvance 62 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release 63 | ``` 64 | 65 | NOTE: the location and name of the `build` directory is arbitrary. 66 | 67 | ##### macOS 68 | 69 | ``` 70 | cd /somewhere/on/your/system/NanoBoyAdvance 71 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" 72 | ``` 73 | 74 | NOTE: the location and name of the `build` directory is arbitrary. 75 | 76 | 77 | #### 3. Compile 78 | 79 | Run CMake build command: 80 | 81 | ``` 82 | cmake --build build 83 | ``` 84 | 85 | Append `-j $(nproc)` to use all processor cores for compilation. 86 | 87 | Binaries will be output to `build/bin/`. 88 | 89 | ### Windows Mingw-w64 (GCC) 90 | 91 | This guide uses [MSYS2](https://www.msys2.org/) to install Mingw-w64 and other dependencies. 92 | 93 | #### 1. Install dependencies 94 | 95 | In your MSYS2 command line, run: 96 | 97 | ```bash 98 | 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 99 | ``` 100 | 101 | #### 2. Setup CMake build directory 102 | 103 | ```bash 104 | cd path/to/NanoBoyAdvance 105 | cmake -S . -B build -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release 106 | ``` 107 | 108 | NOTE: the location and name of the `build` directory is arbitrary. 109 | 110 | #### 3. Compile 111 | 112 | Run CMake build command: 113 | 114 | ``` 115 | cmake --build build 116 | ``` 117 | 118 | Append `-j $(nproc)` to use all processor cores for compilation. 119 | 120 | Binaries will be output to `build/bin/`. 121 | -------------------------------------------------------------------------------- /src/nba/src/bus/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "bus/bus.hpp" 9 | 10 | namespace nba::core { 11 | 12 | void Bus::LoadState(SaveState const& state) { 13 | memory.wram = state.bus.memory.wram; 14 | memory.iram = state.bus.memory.iram; 15 | memory.latch.bios = state.bus.memory.latch.bios; 16 | memory.rom.LoadState(state); 17 | 18 | hw.waitcnt.sram = state.bus.io.waitcnt.sram; 19 | for(int i = 0; i < 2; i++) { 20 | hw.waitcnt.ws0[i] = state.bus.io.waitcnt.ws0[i]; 21 | hw.waitcnt.ws1[i] = state.bus.io.waitcnt.ws1[i]; 22 | hw.waitcnt.ws2[i] = state.bus.io.waitcnt.ws2[i]; 23 | } 24 | hw.waitcnt.phi = state.bus.io.waitcnt.phi; 25 | hw.waitcnt.prefetch = state.bus.io.waitcnt.prefetch; 26 | UpdateWaitStateTable(); 27 | 28 | hw.haltcnt = (Hardware::HaltControl)state.bus.io.haltcnt; 29 | hw.rcnt[0] = state.bus.io.rcnt[0]; 30 | hw.rcnt[1] = state.bus.io.rcnt[1]; 31 | hw.postflg = state.bus.io.postflg; 32 | hw.prefetch_buffer_was_disabled = state.bus.prefetch_buffer_was_disabled; 33 | 34 | prefetch.active = state.bus.prefetch.active; 35 | prefetch.head_address = state.bus.prefetch.head_address; 36 | prefetch.count = state.bus.prefetch.count; 37 | prefetch.countdown = state.bus.prefetch.countdown; 38 | prefetch.thumb = state.bus.prefetch.thumb; 39 | if(prefetch.thumb) { 40 | prefetch.opcode_width = sizeof(u16); 41 | prefetch.capacity = 8; 42 | prefetch.duty = wait16[int(Access::Sequential)][prefetch.last_address >> 24]; 43 | } else { 44 | prefetch.opcode_width = sizeof(u32); 45 | prefetch.capacity = 4; 46 | prefetch.duty = wait32[int(Access::Sequential)][prefetch.last_address >> 24]; 47 | } 48 | 49 | last_access = state.bus.last_access; 50 | 51 | parallel_internal_cpu_cycle_limit = state.bus.parallel_internal_cpu_cycle_limit; 52 | } 53 | 54 | void Bus::CopyState(SaveState& state) { 55 | state.bus.memory.wram = memory.wram; 56 | state.bus.memory.iram = memory.iram; 57 | state.bus.memory.latch.bios = memory.latch.bios; 58 | memory.rom.CopyState(state); 59 | 60 | state.bus.io.waitcnt.sram = hw.waitcnt.sram; 61 | for(int i = 0; i < 2; i++) { 62 | state.bus.io.waitcnt.ws0[i] = hw.waitcnt.ws0[i]; 63 | state.bus.io.waitcnt.ws1[i] = hw.waitcnt.ws1[i]; 64 | state.bus.io.waitcnt.ws2[i] = hw.waitcnt.ws2[i]; 65 | } 66 | state.bus.io.waitcnt.phi = hw.waitcnt.phi; 67 | state.bus.io.waitcnt.prefetch = hw.waitcnt.prefetch; 68 | 69 | state.bus.io.haltcnt = (int)hw.haltcnt; 70 | state.bus.io.rcnt[0] = hw.rcnt[0]; 71 | state.bus.io.rcnt[1] = hw.rcnt[1]; 72 | state.bus.io.postflg = hw.postflg; 73 | state.bus.prefetch_buffer_was_disabled = hw.prefetch_buffer_was_disabled; 74 | 75 | state.bus.prefetch.active = prefetch.active; 76 | state.bus.prefetch.head_address = prefetch.head_address; 77 | state.bus.prefetch.count = prefetch.count; 78 | state.bus.prefetch.countdown = prefetch.countdown; 79 | state.bus.prefetch.thumb = prefetch.thumb; 80 | 81 | state.bus.last_access = last_access; 82 | 83 | state.bus.parallel_internal_cpu_cycle_limit = parallel_internal_cpu_cycle_limit; 84 | } 85 | 86 | } // namespace nba::core -------------------------------------------------------------------------------- /src/nba/src/hw/dma/serialization.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "hw/dma/dma.hpp" 9 | 10 | namespace nba::core { 11 | 12 | void DMA::LoadState(SaveState const& state) { 13 | should_reenter_transfer_loop = false; 14 | 15 | hblank_set = state.dma.hblank_set; 16 | vblank_set = state.dma.vblank_set; 17 | video_set = state.dma.video_set; 18 | runnable_set = state.dma.runnable_set; 19 | latch = state.dma.latch; 20 | 21 | for(int i = 0; i < 4; i++) { 22 | auto& channel_src = state.dma.channels[i]; 23 | auto& channel_dst = channels[i]; 24 | 25 | u16 control = channel_src.control; 26 | 27 | channel_dst.dst_addr = channel_src.dst_address; 28 | channel_dst.src_addr = channel_src.src_address; 29 | channel_dst.length = channel_src.length; 30 | 31 | channel_dst.enable = control & 32768; 32 | channel_dst.repeat = control & 512; 33 | channel_dst.interrupt = control & 16384; 34 | channel_dst.gamepak = control & 2048; 35 | channel_dst.dst_cntl = (Channel::Control)((control >> 5) & 3); 36 | channel_dst.src_cntl = (Channel::Control)((control >> 7) & 3); 37 | channel_dst.time = (Channel::Timing)((control >> 12) & 3); 38 | channel_dst.size = (Channel::Size)((control >> 10) & 1); 39 | 40 | channel_dst.latch.dst_addr = channel_src.latch.dst_address; 41 | channel_dst.latch.src_addr = channel_src.latch.src_address; 42 | channel_dst.latch.length = channel_src.latch.length; 43 | channel_dst.latch.bus = channel_src.latch.bus; 44 | 45 | channel_dst.is_fifo_dma = channel_src.is_fifo_dma; 46 | 47 | channel_dst.event = scheduler.GetEventByUID(channel_src.event_uid); 48 | } 49 | 50 | SelectNextDMA(); 51 | } 52 | 53 | void DMA::CopyState(SaveState& state) { 54 | state.dma.hblank_set = (u8)hblank_set.to_ulong(); 55 | state.dma.vblank_set = (u8)vblank_set.to_ulong(); 56 | state.dma.video_set = (u8)video_set.to_ulong(); 57 | state.dma.runnable_set = (u8)runnable_set.to_ulong(); 58 | state.dma.latch = latch; 59 | 60 | for(int i = 0; i < 4; i++) { 61 | auto& channel_src = channels[i]; 62 | auto& channel_dst = state.dma.channels[i]; 63 | 64 | channel_dst.dst_address = channel_src.dst_addr; 65 | channel_dst.src_address = channel_src.src_addr; 66 | channel_dst.length = channel_src.length; 67 | 68 | channel_dst.control = ((int)channel_src.dst_cntl << 5) | 69 | ((int)channel_src.src_cntl << 7) | 70 | (channel_src.repeat ? 512 : 0) | 71 | ((int)channel_src.size << 10) | 72 | (channel_src.gamepak ? 2048 : 0) | 73 | ((int)channel_src.time << 12) | 74 | (channel_src.interrupt ? 16384 : 0) | 75 | (channel_src.enable ? 32768 : 0); 76 | 77 | channel_dst.latch.dst_address = channel_src.latch.dst_addr; 78 | channel_dst.latch.src_address = channel_src.latch.src_addr; 79 | channel_dst.latch.length = channel_src.latch.length; 80 | channel_dst.latch.bus = channel_src.latch.bus; 81 | 82 | channel_dst.is_fifo_dma = channel_src.is_fifo_dma; 83 | 84 | channel_dst.event_uid = GetEventUID(channel_src.event); 85 | } 86 | } 87 | 88 | } // namespace nba::core -------------------------------------------------------------------------------- /src/nba/src/hw/dma/dma.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "hw/irq/irq.hpp" 16 | 17 | namespace nba::core { 18 | 19 | struct Bus; 20 | 21 | struct DMA { 22 | DMA(Bus& bus, IRQ& irq, Scheduler& scheduler); 23 | 24 | enum class Occasion { 25 | HBlank, 26 | VBlank, 27 | Video, 28 | FIFO0, 29 | FIFO1 30 | }; 31 | 32 | void Reset(); 33 | void Request(Occasion occasion); 34 | void StopVideoTransferDMA(); 35 | bool HasVideoTransferDMA(); 36 | auto Run() -> int; 37 | auto Read (int chan_id, int offset) -> u8; 38 | void Write(int chan_id, int offset, u8 value); 39 | bool IsRunning() { return runnable_set.any(); } 40 | auto GetOpenBusValue() -> u32 { return latch; } 41 | 42 | void LoadState(SaveState const& state); 43 | void CopyState(SaveState& state); 44 | 45 | private: 46 | enum Registers { 47 | REG_DMAXSAD = 0, 48 | REG_DMAXDAD = 4, 49 | REG_DMAXCNT_L = 8, 50 | REG_DMAXCNT_H = 10 51 | }; 52 | 53 | struct Channel { 54 | int id; 55 | bool enable = false; 56 | bool repeat = false; 57 | bool interrupt = false; 58 | bool gamepak = false; 59 | 60 | u16 length = 0; 61 | u32 dst_addr = 0; 62 | u32 src_addr = 0; 63 | 64 | enum Control { 65 | Increment = 0, 66 | Decrement = 1, 67 | Fixed = 2, 68 | Reload = 3 69 | } dst_cntl = Increment, src_cntl = Increment; 70 | 71 | enum Timing { 72 | Immediate = 0, 73 | VBlank = 1, 74 | HBlank = 2, 75 | Special = 3 76 | } time = Immediate; 77 | 78 | enum Size { 79 | Half = 0, 80 | Word = 1 81 | } size = Half; 82 | 83 | struct Latch { 84 | u32 length; 85 | u32 dst_addr; 86 | u32 src_addr; 87 | 88 | /// Most recently read (half)word by this channel. 89 | u32 bus = 0; 90 | } latch = {}; 91 | 92 | bool is_fifo_dma = false; 93 | Scheduler::Event* event = nullptr; 94 | } channels[4]; 95 | 96 | constexpr int GetUnaliasedMemoryArea(int page) { 97 | if(page >= 0x09 && page <= 0x0D) { 98 | return 0x08; 99 | } 100 | 101 | if(page == 0x0F) { 102 | return 0x0E; 103 | } 104 | 105 | return page; 106 | } 107 | 108 | void ScheduleDMAs(unsigned int bitset); 109 | void OnActivated(u64 chan_id); 110 | 111 | void SelectNextDMA(); 112 | void OnChannelWritten(Channel& channel, bool enable_old); 113 | void AddChannelToDMASet(Channel& channel); 114 | void RemoveChannelFromDMASets(Channel& channel); 115 | void RunChannel(); 116 | 117 | Bus& bus; 118 | IRQ& irq; 119 | Scheduler& scheduler; 120 | 121 | int active_dma_id; 122 | bool should_reenter_transfer_loop; 123 | 124 | /// Set of currently enabled H-blank DMAs. 125 | std::bitset<4> hblank_set; 126 | 127 | /// Set of currently enabled V-blank DMAs. 128 | std::bitset<4> vblank_set; 129 | 130 | /// Set of currently enabled video transfer DMAs. 131 | std::bitset<4> video_set; 132 | 133 | /// Set of DMAs which are currently scheduled for execution. 134 | std::bitset<4> runnable_set; 135 | 136 | /// Most recent value transferred by any DMA channel. 137 | /// DMAs will read this when reading from unused memory or IO. 138 | u32 latch; 139 | }; 140 | 141 | } // namespace nba::core 142 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/hle/mp2k.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace nba::core { 13 | 14 | struct Bus; 15 | 16 | struct MP2K { 17 | static constexpr u8 kMaxSoundChannels = 12; 18 | 19 | enum SoundChannelStatus : u8 { 20 | CHANNEL_START = 0x80, 21 | CHANNEL_STOP = 0x40, 22 | CHANNEL_LOOP = 0x10, 23 | CHANNEL_ECHO = 0x04, 24 | 25 | CHANNEL_ENV_MASK = 0x03, 26 | CHANNEL_ENV_ATTACK = 0x03, 27 | CHANNEL_ENV_DECAY = 0x02, 28 | CHANNEL_ENV_SUSTAIN = 0x01, 29 | CHANNEL_ENV_RELEASE = 0x00, 30 | 31 | CHANNEL_ON = CHANNEL_START | CHANNEL_STOP | CHANNEL_ECHO | CHANNEL_ENV_MASK 32 | }; 33 | 34 | struct SoundChannel { 35 | u8 status; 36 | u8 type; 37 | u8 volume_r; 38 | u8 volume_l; 39 | u8 envelope_attack; 40 | u8 envelope_decay; 41 | u8 envelope_sustain; 42 | u8 envelope_release; 43 | u8 unknown0; 44 | u8 envelope_volume; 45 | u8 envelope_volume_r; 46 | u8 envelope_volume_l; 47 | u8 echo_volume; 48 | u8 echo_length; 49 | u8 unknown1[18]; 50 | u32 frequency; 51 | u32 wave_address; 52 | u32 unknown2[6]; 53 | }; 54 | 55 | struct SoundInfo { 56 | u32 magic; 57 | u8 pcm_dma_counter; 58 | u8 reverb; 59 | u8 max_channels; 60 | u8 master_volume; 61 | u8 unknown0[8]; 62 | s32 pcm_samples_per_vblank; 63 | s32 pcm_sample_rate; 64 | u32 unknown1[14]; 65 | SoundChannel channels[kMaxSoundChannels]; 66 | }; 67 | 68 | MP2K(Bus& bus) : bus(bus) { 69 | Reset(); 70 | } 71 | 72 | bool IsEngaged() const { 73 | return engaged; 74 | } 75 | 76 | bool& UseCubicFilter() { 77 | return use_cubic_filter; 78 | } 79 | 80 | bool& ForceReverb() { 81 | return force_reverb; 82 | } 83 | 84 | void Reset(); 85 | void SoundMainRAM(SoundInfo const& sound_info); 86 | void RenderFrame(); 87 | auto ReadSample() -> float*; 88 | 89 | private: 90 | static constexpr int k_sample_rate = 65536; 91 | static constexpr int k_samples_per_frame = k_sample_rate / 60 + 1; 92 | static constexpr int k_total_frame_count = 7; 93 | 94 | static constexpr float S8ToFloat(s8 value) { 95 | return value / 127.0; 96 | } 97 | 98 | static constexpr float U8ToFloat(u8 value) { 99 | return value / 256.0; 100 | } 101 | 102 | void RenderReverb(float* destination, u8 strength); 103 | 104 | struct Sampler { 105 | bool compressed = false; 106 | bool should_fetch_sample = true; 107 | u32 current_position = 0; 108 | float resample_phase = 0.0; 109 | float sample_history[4] {0}; 110 | 111 | struct WaveInfo { 112 | u16 type; 113 | u16 status; 114 | u32 frequency; 115 | u32 loop_position; 116 | u32 number_of_samples; 117 | } wave_info; 118 | 119 | u8* wave_data = nullptr; 120 | } samplers[kMaxSoundChannels]; 121 | 122 | struct Envelope { 123 | float volume = 0.0; 124 | float volume_l[2] {0.0, 0.0}; 125 | float volume_r[2] {0.0, 0.0}; 126 | } envelopes[kMaxSoundChannels]; 127 | 128 | bool engaged; 129 | bool use_cubic_filter = false; 130 | bool force_reverb = false; 131 | Bus& bus; 132 | SoundInfo sound_info; 133 | std::unique_ptr buffer; 134 | int current_frame; 135 | int buffer_read_index; 136 | }; 137 | 138 | } // namespace nba::core 139 | -------------------------------------------------------------------------------- /src/platform/qt/src/config.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "config.hpp" 9 | 10 | void QtConfig::LoadCustomData(toml::value const& data) { 11 | if(data.contains("input")) { 12 | using Map = Input::Map; 13 | 14 | auto input_ = data.at("input"); 15 | 16 | input.controller_guid = toml::find_or(input_, "controller_guid", ""); 17 | input.hold_fast_forward = toml::find_or(input_, "hold_fast_forward", true); 18 | 19 | const auto get_map = [&](toml::value const& value, std::string key) { 20 | return Map::FromArray(toml::find_or>(value, key, {0, -1, -1, -1, 0})); 21 | }; 22 | 23 | input.fast_forward = get_map(input_, "fast_forward"); 24 | 25 | if(input_.contains("gba")) { 26 | auto gba = input_.at("gba"); 27 | 28 | input.gba[0] = get_map(gba, "a"); 29 | input.gba[1] = get_map(gba, "b"); 30 | input.gba[2] = get_map(gba, "select"); 31 | input.gba[3] = get_map(gba, "start"); 32 | input.gba[4] = get_map(gba, "right"); 33 | input.gba[5] = get_map(gba, "left"); 34 | input.gba[6] = get_map(gba, "up"); 35 | input.gba[7] = get_map(gba, "down"); 36 | input.gba[8] = get_map(gba, "r"); 37 | input.gba[9] = get_map(gba, "l"); 38 | } 39 | } 40 | 41 | if(data.contains("window")) { 42 | auto window_ = data.at("window"); 43 | 44 | window.scale = toml::find_or(window_, "scale", 2); 45 | window.maximum_scale = toml::find_or(window_, "maximum_scale", 0); 46 | window.fullscreen = toml::find_or(window_, "fullscreen", false); 47 | window.fullscreen_show_menu = toml::find_or(window_, "fullscreen_show_menu", false); 48 | window.lock_aspect_ratio = toml::find_or(window_, "lock_aspect_ratio", true); 49 | window.use_integer_scaling = toml::find_or(window_, "use_integer_scaling", false); 50 | window.show_fps = toml::find_or(window_, "show_fps", false); 51 | window.pause_emulator_when_inactive = toml::find_or(window_, "pause_emulator_when_inactive", true); 52 | } 53 | 54 | recent_files = toml::find_or>(data, "recent_files", {}); 55 | } 56 | 57 | void QtConfig::SaveCustomData(toml::value& data) { 58 | data["input"]["controller_guid"] = input.controller_guid; 59 | data["input"]["fast_forward"] = input.fast_forward.Array(); 60 | data["input"]["hold_fast_forward"] = input.hold_fast_forward; 61 | 62 | data["input"]["gba"]["a"] = input.gba[0].Array(); 63 | data["input"]["gba"]["b"] = input.gba[1].Array(); 64 | data["input"]["gba"]["select"] = input.gba[2].Array(); 65 | data["input"]["gba"]["start"] = input.gba[3].Array(); 66 | data["input"]["gba"]["right"] = input.gba[4].Array(); 67 | data["input"]["gba"]["left"] = input.gba[5].Array(); 68 | data["input"]["gba"]["up"] = input.gba[6].Array(); 69 | data["input"]["gba"]["down"] = input.gba[7].Array(); 70 | data["input"]["gba"]["r"] = input.gba[8].Array(); 71 | data["input"]["gba"]["l"] = input.gba[9].Array(); 72 | 73 | data["window"]["scale"] = window.scale; 74 | data["window"]["maximum_scale"] = window.maximum_scale; 75 | data["window"]["fullscreen"] = window.fullscreen; 76 | data["window"]["fullscreen_show_menu"] = window.fullscreen_show_menu; 77 | data["window"]["lock_aspect_ratio"] = window.lock_aspect_ratio; 78 | data["window"]["use_integer_scaling"] = window.use_integer_scaling; 79 | data["window"]["show_fps"] = window.show_fps; 80 | data["window"]["pause_emulator_when_inactive"] = window.pause_emulator_when_inactive; 81 | 82 | data["recent_files"] = recent_files; 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build-win64: 7 | runs-on: windows-latest 8 | defaults: 9 | run: 10 | shell: msys2 {0} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: msys2/setup-msys2@v2 14 | with: 15 | 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 16 | - name: Build NanoBoyAdvance 17 | run: | 18 | cmake \ 19 | -Bbuild \ 20 | -G"Unix Makefiles" \ 21 | -DCMAKE_BUILD_TYPE=Release \ 22 | -DCMAKE_CXX_FLAGS="-s" \ 23 | -DPython_EXECUTABLE="$(which python3)" \ 24 | -DPLATFORM_QT_STATIC=ON \ 25 | -DUSE_STATIC_SDL=ON \ 26 | -DQT5_STATIC_DIR="/mingw64/qt5-static" 27 | cd build 28 | make -j$NUMBER_OF_PROCESSORS 29 | - name: Collect artifacts 30 | run: | 31 | mkdir upload 32 | cp -r build/bin/qt/{NanoBoyAdvance.exe,config.toml} upload 33 | - name: Upload artifacts 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: NanoBoyAdvance-win64 37 | path: upload 38 | if-no-files-found: error 39 | 40 | build-linux: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Setup dependencies 45 | run: | 46 | sudo apt-get update -qq 47 | sudo apt-get install -y python3-jinja2 python3-lxml libsdl2-dev qtbase5-dev 48 | - name: Build NanoBoyAdvance 49 | run: | 50 | cmake -Bbuild -DCMAKE_BUILD_TYPE=Release 51 | cd build 52 | make -j$(nproc) 53 | - name: Collect artifacts 54 | run: | 55 | mkdir upload 56 | cp -r build/bin/qt/{NanoBoyAdvance,config.toml} upload 57 | - name: Upload artifacts 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: NanoBoyAdvance-linux 61 | path: upload 62 | if-no-files-found: error 63 | 64 | build-macOS: 65 | strategy: 66 | matrix: 67 | os: [macos-13, macos-14] 68 | runs-on: ${{ matrix.os }} 69 | steps: 70 | - uses: actions/checkout@v4 71 | - uses: actions/setup-python@v5 72 | with: 73 | python-version: '3.x' 74 | - name: Setup dependencies 75 | env: 76 | HOMEBREW_NO_ANALYTICS: 1 77 | run: | 78 | brew install sdl2 qt@5 79 | python3 -m pip install Jinja2 80 | - name: Build NanoBoyAdvance 81 | run: | 82 | cmake -Bbuild \ 83 | -DCMAKE_CXX_FLAGS="-s" \ 84 | -DUSE_STATIC_SDL=ON \ 85 | -DCMAKE_BUILD_TYPE=Release \ 86 | -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)" \ 87 | -DMACOS_BUILD_APP_BUNDLE=ON \ 88 | -DMACOS_BUNDLE_QT=ON 89 | cd build 90 | make -j$(getconf _NPROCESSORS_ONLN) 91 | - name: Create disk image 92 | run: | 93 | mkdir dmg 94 | cp -a build/bin/qt/NanoBoyAdvance.app dmg/NanoBoyAdvance.app 95 | codesign -s - --deep -f dmg/NanoBoyAdvance.app 96 | ln -s /Applications dmg/Applications 97 | hdiutil create \ 98 | -fs HFS+ \ 99 | -volname NanoBoyAdvance \ 100 | -srcfolder dmg \ 101 | -ov -format UDBZ \ 102 | NanoBoyAdvance.dmg 103 | - name: Upload artifacts 104 | uses: actions/upload-artifact@v4 105 | with: 106 | name: NanoBoyAdvance-${{ runner.os }}-${{ runner.arch }} 107 | path: NanoBoyAdvance.dmg 108 | if-no-files-found: error 109 | -------------------------------------------------------------------------------- /src/nba/src/hw/rom/backup/flash.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | 10 | namespace nba { 11 | 12 | static constexpr int g_save_size[2] = { 65536, 131072 }; 13 | 14 | FLASH::FLASH(fs::path const& save_path, Size size_hint) 15 | : size(size_hint) 16 | , save_path(save_path) { 17 | Reset(); 18 | } 19 | 20 | void FLASH::Reset() { 21 | current_bank = 0; 22 | phase = 0; 23 | enable_chip_id = false; 24 | enable_erase = false; 25 | enable_write = false; 26 | enable_select = false; 27 | 28 | int bytes = g_save_size[size]; 29 | 30 | file = BackupFile::OpenOrCreate(save_path, { 65536, 131072 }, bytes); 31 | if(bytes == g_save_size[0]) { 32 | size = SIZE_64K; 33 | } else { 34 | size = SIZE_128K; 35 | } 36 | } 37 | 38 | auto FLASH::Read (u32 address) -> u8 { 39 | address &= 0xFFFF; 40 | 41 | // TODO: check if the Chip ID should be mirrored each 0x100 bytes. 42 | if(enable_chip_id && address < 2) { 43 | // Chip identifier for FLASH64: D4BF (SST 64K) 44 | // Chip identifier for FLASH128: 09C2 (Macronix 128K) 45 | if(size == SIZE_128K) { 46 | return (address == 0) ? 0xC2 : 0x09; 47 | } else { 48 | return (address == 0) ? 0xBF : 0xD4; 49 | } 50 | } 51 | 52 | return file->Read(Physical(address)); 53 | } 54 | 55 | void FLASH::Write(u32 address, u8 value) { 56 | /* TODO: figure out how malformed sequences behave. */ 57 | switch(phase) { 58 | case 0: { 59 | if(address == 0x0E005555 && value == 0xAA) { 60 | phase = 1; 61 | } 62 | break; 63 | } 64 | case 1: { 65 | if(address == 0x0E002AAA && value == 0x55) { 66 | phase = 2; 67 | } 68 | break; 69 | } 70 | case 2: { 71 | HandleCommand(address, value); 72 | break; 73 | } 74 | case 3: { 75 | HandleExtended(address, value); 76 | break; 77 | } 78 | } 79 | } 80 | 81 | void FLASH::HandleCommand(u32 address, u8 value) { 82 | if(address == 0x0E005555) { 83 | switch(static_cast(value)) { 84 | case READ_CHIP_ID: { 85 | enable_chip_id = true; 86 | phase = 0; 87 | break; 88 | } 89 | case FINISH_CHIP_ID: { 90 | enable_chip_id = false; 91 | phase = 0; 92 | break; 93 | } 94 | case ERASE: { 95 | enable_erase = true; 96 | phase = 0; 97 | break; 98 | } 99 | case ERASE_CHIP: { 100 | if(enable_erase) { 101 | file->MemorySet(0, g_save_size[size], 0xFF); 102 | enable_erase = false; 103 | } 104 | phase = 0; 105 | break; 106 | } 107 | case WRITE_BYTE: { 108 | enable_write = true; 109 | phase = 3; 110 | break; 111 | } 112 | case SELECT_BANK: { 113 | if(size == SIZE_128K) { 114 | enable_select = true; 115 | phase = 3; 116 | } else { 117 | phase = 0; 118 | } 119 | break; 120 | } 121 | } 122 | } else if(enable_erase && (address & ~0xF000) == 0x0E000000 && static_cast(value) == ERASE_SECTOR) { 123 | u32 base = address & 0xF000; 124 | 125 | file->MemorySet(Physical(base), 0x1000, 0xFF); 126 | enable_erase = false; 127 | phase = 0; 128 | } 129 | } 130 | 131 | void FLASH::HandleExtended(u32 address, u8 value) { 132 | if(enable_write) { 133 | file->Write(Physical(address & 0xFFFF), value); 134 | enable_write = false; 135 | } else if(enable_select && address == 0x0E000000) { 136 | current_bank = value & 1; 137 | enable_select = false; 138 | } 139 | 140 | phase = 0; 141 | } 142 | 143 | } // namespace nba 144 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/quad_channel.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | *<< 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "hw/apu/channel/quad_channel.hpp" 9 | 10 | namespace nba::core { 11 | 12 | QuadChannel::QuadChannel(Scheduler& scheduler, Scheduler::EventClass event_class) 13 | : BaseChannel(true, true) 14 | , scheduler(scheduler) 15 | , event_class(event_class) { 16 | scheduler.Register(event_class, this, &QuadChannel::Generate); 17 | 18 | Reset(); 19 | } 20 | 21 | void QuadChannel::Reset() { 22 | BaseChannel::Reset(); 23 | phase = 0; 24 | sample = 0; 25 | wave_duty = 0; 26 | dac_enable = false; 27 | event = nullptr; 28 | } 29 | 30 | void QuadChannel::Generate() { 31 | if(!IsEnabled()) { 32 | sample = 0; 33 | event = nullptr; 34 | return; 35 | } 36 | 37 | static constexpr int pattern[4][8] = { 38 | { +8, -8, -8, -8, -8, -8, -8, -8 }, 39 | { +8, +8, -8, -8, -8, -8, -8, -8 }, 40 | { +8, +8, +8, +8, -8, -8, -8, -8 }, 41 | { +8, +8, +8, +8, +8, +8, -8, -8 } 42 | }; 43 | 44 | if(dac_enable) { 45 | sample = s8(pattern[wave_duty][phase] * envelope.current_volume); 46 | } else { 47 | sample = 0; 48 | } 49 | phase = (phase + 1) % 8; 50 | 51 | event = scheduler.Add(GetSynthesisIntervalFromFrequency(sweep.current_freq), event_class); 52 | } 53 | 54 | auto QuadChannel::Read(int offset) -> u8 { 55 | switch(offset) { 56 | // Sweep Register 57 | case 0: { 58 | return sweep.shift | 59 | ((int)sweep.direction << 3) | 60 | (sweep.divider << 4); 61 | } 62 | case 1: return 0; 63 | 64 | // Wave Duty / Length / Envelope 65 | case 2: { 66 | return wave_duty << 6; 67 | } 68 | case 3: { 69 | return envelope.divider | 70 | ((int)envelope.direction << 3) | 71 | (envelope.initial_volume << 4); 72 | } 73 | 74 | // Frequency / Control 75 | case 4: return 0; 76 | case 5: { 77 | return length.enabled ? 0x40 : 0; 78 | } 79 | 80 | default: return 0; 81 | } 82 | } 83 | 84 | void QuadChannel::Write(int offset, u8 value) { 85 | switch(offset) { 86 | // Sweep Register 87 | case 0: { 88 | sweep.shift = value & 7; 89 | sweep.direction = Sweep::Direction((value >> 3) & 1); 90 | sweep.divider = (value >> 4) & 7; 91 | break; 92 | } 93 | case 1: break; 94 | 95 | // Wave Duty / Length / Envelope 96 | case 2: { 97 | length.length = 64 - (value & 63); 98 | wave_duty = (value >> 6) & 3; 99 | break; 100 | } 101 | case 3: { 102 | envelope.divider = value & 7; 103 | envelope.direction = Envelope::Direction((value >> 3) & 1); 104 | envelope.initial_volume = value >> 4; 105 | 106 | dac_enable = (value >> 3) != 0; 107 | if(!dac_enable) { 108 | Disable(); 109 | } 110 | break; 111 | } 112 | 113 | // Frequency / Control 114 | case 4: { 115 | sweep.initial_freq = (sweep.initial_freq & ~0xFF) | value; 116 | sweep.current_freq = sweep.initial_freq; 117 | break; 118 | } 119 | case 5: { 120 | sweep.initial_freq = (sweep.initial_freq & 0xFF) | (((int)value & 7) << 8); 121 | sweep.current_freq = sweep.initial_freq; 122 | length.enabled = value & 0x40; 123 | 124 | if(dac_enable && (value & 0x80)) { 125 | if(!IsEnabled()) { 126 | if(event) { 127 | scheduler.Cancel(event); 128 | } 129 | event = scheduler.Add(GetSynthesisIntervalFromFrequency(sweep.current_freq), event_class); 130 | } 131 | phase = 0; 132 | Restart(); 133 | } 134 | 135 | break; 136 | } 137 | } 138 | } 139 | 140 | } // namespace nba::core 141 | -------------------------------------------------------------------------------- /src/nba/include/nba/rom/backup/backup_file.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace fs = std::filesystem; 21 | 22 | namespace nba { 23 | 24 | struct BackupFile { 25 | static auto OpenOrCreate(fs::path const& save_path, 26 | std::vector const& valid_sizes, 27 | int& default_size) -> std::unique_ptr { 28 | bool create = true; 29 | auto flags = std::ios::binary | std::ios::in | std::ios::out; 30 | std::unique_ptr file { new BackupFile() }; 31 | 32 | // @todo: check file type and permissions? 33 | if(fs::is_regular_file(save_path)) { 34 | auto file_size = fs::file_size(save_path); 35 | 36 | // allow for some extra/unused data; required for mGBA save compatibility 37 | auto save_size = file_size & ~63u; 38 | 39 | auto begin = valid_sizes.begin(); 40 | auto end = valid_sizes.end(); 41 | 42 | if(std::find(begin, end, save_size) != end) { 43 | file->stream.open(save_path.c_str(), flags); 44 | if(file->stream.fail()) { 45 | throw std::runtime_error("BackupFile: unable to open file: " + save_path.string()); 46 | } 47 | default_size = save_size; 48 | file->save_size = save_size; 49 | file->memory.reset(new u8[file_size]); 50 | file->stream.read((char*)file->memory.get(), file_size); 51 | create = false; 52 | } 53 | } 54 | 55 | /* A new save file is created either when no file exists yet, 56 | * or when the existing file has an invalid size. 57 | */ 58 | if(create) { 59 | file->save_size = default_size; 60 | file->stream.open(save_path, flags | std::ios::trunc); 61 | if(file->stream.fail()) { 62 | throw std::runtime_error("BackupFile: unable to create file: " + save_path.string()); 63 | } 64 | file->memory.reset(new u8[default_size]); 65 | file->MemorySet(0, default_size, 0xFF); 66 | } 67 | 68 | return file; 69 | } 70 | 71 | auto Read(unsigned index) -> u8 { 72 | if(index >= save_size) { 73 | throw std::runtime_error("BackupFile: out-of-bounds index while reading."); 74 | } 75 | return memory[index]; 76 | } 77 | 78 | void Write(unsigned index, u8 value) { 79 | if(index >= save_size) { 80 | throw std::runtime_error("BackupFile: out-of-bounds index while writing."); 81 | } 82 | memory[index] = value; 83 | if(auto_update) { 84 | Update(index, 1); 85 | } 86 | } 87 | 88 | void MemorySet(unsigned index, size_t length, u8 value) { 89 | if((index + length) > save_size) { 90 | throw std::runtime_error("BackupFile: out-of-bounds index while setting memory."); 91 | } 92 | std::memset(&memory[index], value, length); 93 | if(auto_update) { 94 | Update(index, length); 95 | } 96 | } 97 | 98 | void Update(unsigned index, size_t length) { 99 | if((index + length) > save_size) { 100 | throw std::runtime_error("BackupFile: out-of-bounds index while updating file."); 101 | } 102 | stream.seekg(index); 103 | stream.write((char*)&memory[index], length); 104 | } 105 | 106 | auto Buffer() -> u8* { 107 | return memory.get(); 108 | } 109 | 110 | auto Size() -> size_t { 111 | return save_size; 112 | } 113 | 114 | bool auto_update = true; 115 | 116 | private: 117 | BackupFile() { } 118 | 119 | size_t save_size; 120 | std::fstream stream; 121 | std::unique_ptr memory; 122 | }; 123 | 124 | } // namespace nba 125 | -------------------------------------------------------------------------------- /src/platform/qt/src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "widget/main_window.hpp" 16 | 17 | #if defined(WIN32) 18 | #include 19 | #endif 20 | 21 | namespace fs = std::filesystem; 22 | 23 | // See: https://stackoverflow.com/a/37023032 24 | struct MenuStyle : QProxyStyle { 25 | int styleHint( 26 | StyleHint stylehint, 27 | const QStyleOption *opt, 28 | const QWidget *widget, 29 | QStyleHintReturn *returnData 30 | ) const { 31 | if(stylehint == QStyle::SH_MenuBar_AltKeyNavigation) 32 | return 0; 33 | 34 | return QProxyStyle::styleHint(stylehint, opt, widget, returnData); 35 | } 36 | }; 37 | 38 | auto create_window(QApplication& app, int argc, char** argv) -> std::unique_ptr { 39 | fs::path rom; 40 | 41 | if(argc >= 2) { 42 | rom = fs::path{argv[1]}; 43 | 44 | if(rom.is_relative()) { 45 | rom = fs::current_path() / rom; 46 | } 47 | 48 | fs::path binary_directory = fs::path{argv[0]}.remove_filename(); 49 | if(binary_directory.is_relative()) { 50 | binary_directory = fs::current_path() / binary_directory; 51 | } 52 | fs::current_path(binary_directory); 53 | } 54 | 55 | auto window = std::make_unique(&app); 56 | if(!window->Initialize()) { 57 | return nullptr; 58 | } 59 | 60 | if(!rom.empty()) { 61 | window->LoadROM(rom.u16string()); 62 | } 63 | 64 | window->show(); 65 | return window; 66 | } 67 | 68 | int main(int argc, char** argv) { 69 | // See: https://trac.wxwidgets.org/ticket/19023 70 | #if defined(__APPLE__) 71 | setenv("LC_NUMERIC", "C", 1); 72 | #endif 73 | 74 | #if defined(WIN32) 75 | constexpr auto terminal_output = "CONOUT$"; 76 | constexpr auto null_output = "NUL:"; 77 | 78 | // Check whether we are already attached to an output stream. 79 | const auto output_handle = GetStdHandle(STD_OUTPUT_HANDLE); 80 | if(!output_handle || (output_handle == INVALID_HANDLE_VALUE)) { 81 | // If started from terminal, attach and output logs to it. 82 | if(AttachConsole(ATTACH_PARENT_PROCESS)) { 83 | if(!freopen(terminal_output, "a", stdout)) { assert(false); } 84 | if(!freopen(terminal_output, "a", stderr)) { assert(false); } 85 | } else { 86 | // Otherwise, discard log output. 87 | if(!freopen(null_output, "w", stdout)) { assert(false); } 88 | if(!freopen(null_output, "w", stderr)) { assert(false); } 89 | } 90 | } 91 | #endif 92 | 93 | // On some systems (e.g. macOS) QSurfaceFormat::setDefaultFormat() must be called before constructing QApplication. 94 | auto format = QSurfaceFormat{}; 95 | format.setProfile(QSurfaceFormat::CoreProfile); 96 | format.setMajorVersion(3); 97 | format.setMinorVersion(3); 98 | format.setSwapInterval(0); 99 | QSurfaceFormat::setDefaultFormat(format); 100 | 101 | QApplication app{ argc, argv }; 102 | 103 | app.setStyle(new MenuStyle()); 104 | 105 | #if defined(WIN32) 106 | // See: https://doc.qt.io/qt-5/windows-issues.html#fullscreen-opengl-based-windows 107 | QWindowsWindowFunctions::setHasBorderInFullScreenDefault(true); 108 | #endif 109 | 110 | QCoreApplication::setOrganizationName("fleroviux"); 111 | QCoreApplication::setApplicationName("NanoBoyAdvance"); 112 | QGuiApplication::setDesktopFileName("io.nanoboyadvance.NanoBoyAdvance"); 113 | 114 | auto window = create_window(app, argc, argv); 115 | if(!window) { 116 | return EXIT_FAILURE; 117 | } 118 | 119 | return app.exec(); 120 | } 121 | -------------------------------------------------------------------------------- /src/nba/src/hw/apu/channel/wave_channel.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include "hw/apu/channel/wave_channel.hpp" 9 | 10 | namespace nba::core { 11 | 12 | WaveChannel::WaveChannel(Scheduler& scheduler) 13 | : BaseChannel(false, false, 256) 14 | , scheduler(scheduler) { 15 | scheduler.Register(Scheduler::EventClass::APU_PSG3_generate, this, &WaveChannel::Generate); 16 | 17 | Reset(WaveChannel::ResetWaveRAM::Yes); 18 | } 19 | 20 | void WaveChannel::Reset(ResetWaveRAM reset_wave_ram) { 21 | BaseChannel::Reset(); 22 | 23 | phase = 0; 24 | sample = 0; 25 | 26 | playing = false; 27 | force_volume = false; 28 | volume = 0; 29 | frequency = 0; 30 | dimension = 0; 31 | wave_bank = 0; 32 | 33 | if(reset_wave_ram == ResetWaveRAM::Yes) { 34 | for(int i = 0; i < 2; i++) { 35 | for(int j = 0; j < 16; j++) { 36 | wave_ram[i][j] = 0; 37 | } 38 | } 39 | } 40 | 41 | event = nullptr; 42 | } 43 | 44 | void WaveChannel::Generate() { 45 | if(!IsEnabled()) { 46 | sample = 0; 47 | if(BaseChannel::IsEnabled()) { 48 | event = scheduler.Add(GetSynthesisIntervalFromFrequency(frequency), Scheduler::EventClass::APU_PSG3_generate); 49 | } else { 50 | event = nullptr; 51 | } 52 | return; 53 | } 54 | 55 | auto byte = wave_ram[wave_bank][phase / 2]; 56 | 57 | if((phase % 2) == 0) { 58 | sample = byte >> 4; 59 | } else { 60 | sample = byte & 15; 61 | } 62 | 63 | constexpr int volume_table[4] = { 0, 4, 2, 1 }; 64 | 65 | sample = (sample - 8) * 4 * (force_volume ? 3 : volume_table[volume]); 66 | 67 | if(++phase == 32) { 68 | phase = 0; 69 | if(dimension) { 70 | wave_bank ^= 1; 71 | } 72 | } 73 | 74 | event = scheduler.Add(GetSynthesisIntervalFromFrequency(frequency), Scheduler::EventClass::APU_PSG3_generate); 75 | } 76 | 77 | auto WaveChannel::Read(int offset) -> u8 { 78 | switch(offset) { 79 | // Stop / Wave RAM select 80 | case 0: { 81 | return (dimension << 5) | 82 | (wave_bank << 6) | 83 | (playing ? 0x80 : 0); 84 | } 85 | case 1: return 0; 86 | 87 | // Length / Volume 88 | case 2: return 0; 89 | case 3: { 90 | return (volume << 5) | 91 | (force_volume ? 0x80 : 0); 92 | } 93 | 94 | // Frequency / Control 95 | case 4: return 0; 96 | case 5: { 97 | return length.enabled ? 0x40 : 0; 98 | } 99 | 100 | default: return 0; 101 | } 102 | } 103 | 104 | void WaveChannel::Write(int offset, u8 value) { 105 | switch(offset) { 106 | // Stop / Wave RAM select 107 | case 0: { 108 | dimension = (value >> 5) & 1; 109 | wave_bank = (value >> 6) & 1; 110 | playing = value & 0x80; 111 | break; 112 | } 113 | case 1: break; 114 | 115 | // Length / Volume 116 | case 2: { 117 | length.length = 256 - value; 118 | break; 119 | } 120 | case 3: { 121 | volume = (value >> 5) & 3; 122 | force_volume = value & 0x80; 123 | break; 124 | } 125 | 126 | // Frequency / Control 127 | case 4: { 128 | frequency = (frequency & ~0xFF) | value; 129 | break; 130 | } 131 | case 5: { 132 | frequency = (frequency & 0xFF) | ((value & 7) << 8); 133 | length.enabled = value & 0x40; 134 | 135 | if(playing && (value & 0x80)) { 136 | if(!BaseChannel::IsEnabled()) { 137 | if(event) { 138 | scheduler.Cancel(event); 139 | } 140 | event = scheduler.Add(GetSynthesisIntervalFromFrequency(frequency), Scheduler::EventClass::APU_PSG3_generate); 141 | } 142 | phase = 0; 143 | if(dimension) { 144 | wave_bank = 0; 145 | } 146 | Restart(); 147 | } 148 | break; 149 | } 150 | } 151 | } 152 | 153 | } // namespace nba::core 154 | -------------------------------------------------------------------------------- /src/platform/core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | option(USE_STATIC_SDL "Link against static version of SDL library." OFF) 2 | option(USE_SYSTEM_GLAD "Use system-provided glad library." OFF) 3 | option(USE_SYSTEM_TOML11 "Use system-provided toml11 library." OFF) 4 | option(USE_SYSTEM_UNARR "Use system-provided unarr library." OFF) 5 | 6 | if(USE_STATIC_SDL) 7 | find_package(SDL2 2.0.10 8 | REQUIRED COMPONENTS SDL2-static 9 | OPTIONAL_COMPONENTS SDL2main 10 | CONFIG) 11 | else() 12 | find_package(SDL2 2.0.10 13 | REQUIRED COMPONENTS SDL2 14 | OPTIONAL_COMPONENTS SDL2main 15 | CONFIG) 16 | endif() 17 | 18 | find_package(OpenGL REQUIRED) 19 | 20 | include(FetchContent) 21 | 22 | if(USE_SYSTEM_GLAD) 23 | find_package(Glad CONFIG REQUIRED) 24 | else() 25 | FetchContent_Declare(glad 26 | GIT_REPOSITORY https://github.com/Dav1dde/glad.git 27 | GIT_TAG 658f48e72aee3c6582e80b05ac0f8787a64fe6bb # v2.0.6 28 | SOURCE_SUBDIR cmake 29 | ) 30 | FetchContent_MakeAvailable(glad) 31 | endif() 32 | 33 | glad_add_library(glad_gl_core_33 STATIC 34 | LANGUAGE c REPRODUCIBLE API gl:core=3.3 EXTENSIONS NONE 35 | ) 36 | 37 | if(USE_SYSTEM_TOML11) 38 | find_package(toml11 4.0 REQUIRED) 39 | else() 40 | FetchContent_Declare(toml11 41 | GIT_REPOSITORY https://github.com/ToruNiina/toml11.git 42 | GIT_TAG be08ba2be2a964edcdb3d3e3ea8d100abc26f286 # v4.4.0 43 | ) 44 | set(toml11_INSTALL OFF CACHE BOOL "" FORCE) 45 | FetchContent_MakeAvailable(toml11) 46 | endif() 47 | 48 | if(USE_SYSTEM_UNARR) 49 | find_package(unarr REQUIRED) 50 | else() 51 | set(USE_SYSTEM_BZ2 OFF CACHE BOOL "" FORCE) 52 | set(USE_SYSTEM_LZMA OFF CACHE BOOL "" FORCE) 53 | set(USE_SYSTEM_ZLIB OFF CACHE BOOL "" FORCE) 54 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) 55 | 56 | FetchContent_Declare(unarr 57 | GIT_REPOSITORY https://github.com/nba-emu/unarr.git 58 | GIT_TAG 839066857b68a78213510f47ccbe72e2945b1b1d # v1.1.1 59 | ) 60 | FetchContent_GetProperties(unarr) 61 | if (NOT unarr_POPULATED) 62 | FetchContent_Populate(unarr) 63 | if (EXISTS ${unarr_SOURCE_DIR}/CMakeLists.txt) 64 | add_subdirectory(${unarr_SOURCE_DIR} ${unarr_BINARY_DIR} EXCLUDE_FROM_ALL) 65 | add_library(unarr::unarr ALIAS unarr) 66 | endif() 67 | endif() 68 | endif() 69 | 70 | set(SOURCES 71 | src/device/ogl_video_device.cpp 72 | src/device/sdl_audio_device.cpp 73 | src/loader/bios.cpp 74 | src/loader/rom.cpp 75 | src/loader/save_state.cpp 76 | src/writer/save_state.cpp 77 | src/config.cpp 78 | src/emulator_thread.cpp 79 | src/frame_limiter.cpp 80 | src/game_db.cpp 81 | ) 82 | 83 | set(HEADERS 84 | src/device/shader/color_higan.glsl.hpp 85 | src/device/shader/color_agb.glsl.hpp 86 | src/device/shader/common.glsl.hpp 87 | src/device/shader/lcd_ghosting.glsl.hpp 88 | src/device/shader/lcd1x.glsl.hpp 89 | src/device/shader/output.glsl.hpp 90 | src/device/shader/sharp_bilinear.glsl.hpp 91 | src/device/shader/xbrz.glsl.hpp 92 | ) 93 | 94 | set(HEADERS_PUBLIC 95 | include/platform/device/ogl_video_device.hpp 96 | include/platform/device/sdl_audio_device.hpp 97 | include/platform/loader/bios.hpp 98 | include/platform/loader/rom.hpp 99 | include/platform/loader/save_state.hpp 100 | include/platform/writer/save_state.hpp 101 | include/platform/config.hpp 102 | include/platform/emulator_thread.hpp 103 | include/platform/frame_limiter.hpp 104 | include/platform/game_db.hpp 105 | ) 106 | 107 | add_library(platform-core STATIC) 108 | target_sources(platform-core PRIVATE ${SOURCES} ${HEADERS} ${HEADERS_PUBLIC}) 109 | target_include_directories(platform-core PRIVATE src PUBLIC include) 110 | 111 | if(TARGET SDL2::SDL2main) 112 | target_link_libraries(platform-core PUBLIC SDL2::SDL2main) 113 | endif() 114 | 115 | if(USE_STATIC_SDL) 116 | target_link_libraries(platform-core PUBLIC SDL2::SDL2-static) 117 | else() 118 | target_link_libraries(platform-core PUBLIC SDL2::SDL2) 119 | endif() 120 | 121 | target_link_libraries(platform-core 122 | PRIVATE unarr::unarr 123 | PUBLIC nba toml11::toml11 OpenGL::GL glad_gl_core_33 124 | ) 125 | -------------------------------------------------------------------------------- /src/platform/qt/src/widget/debugger/ppu/palette_viewer.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2025 fleroviux 3 | * 4 | * Licensed under GPLv3 or any later version. 5 | * Refer to the included LICENSE file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "widget/debugger/utility.hpp" 16 | #include "palette_viewer.hpp" 17 | 18 | PaletteViewer::PaletteViewer(nba::CoreBase* core, QWidget* parent) : QWidget(parent) { 19 | QVBoxLayout* vbox = new QVBoxLayout{}; 20 | 21 | vbox->addWidget(new QLabel{tr("Select a color for detailed information:")}); 22 | vbox->addLayout(CreatePaletteGrids()); 23 | vbox->addLayout(CreateColorInfoArea()); 24 | setLayout(vbox); 25 | 26 | m_pram = (u16*)core->GetPRAM(); 27 | } 28 | 29 | QLayout* PaletteViewer::CreatePaletteGrids() { 30 | QHBoxLayout* hbox = new QHBoxLayout{}; 31 | 32 | for(int grid = 0; grid < 2; grid++) { 33 | QVBoxLayout* vbox = new QVBoxLayout{}; 34 | 35 | QGroupBox* group_box = new QGroupBox{}; 36 | group_box->setTitle(grid == 0 ? tr("Background") : tr("Sprite")); 37 | group_box->setLayout(vbox); 38 | 39 | m_palette_color_grids[grid] = new ColorGrid{16, 16}; 40 | 41 | connect(m_palette_color_grids[grid], &ColorGrid::selected, [=](int x, int y) { 42 | m_palette_color_grids[grid]->SetHighlightedPosition(x, y); 43 | m_palette_color_grids[grid ^ 1]->ClearHighlight(); 44 | ShowColorInfo(grid << 8 | y << 4 | x); 45 | }); 46 | 47 | vbox->addWidget(m_palette_color_grids[grid]); 48 | hbox->addWidget(group_box); 49 | } 50 | 51 | return hbox; 52 | } 53 | 54 | QLayout* PaletteViewer::CreateColorInfoArea() { 55 | QHBoxLayout* hbox = new QHBoxLayout{}; 56 | 57 | m_label_color_address = CreateMonospaceLabel(); 58 | m_label_color_r_component = CreateMonospaceLabel(); 59 | m_label_color_g_component = CreateMonospaceLabel(); 60 | m_label_color_b_component = CreateMonospaceLabel(); 61 | m_label_color_value = CreateMonospaceLabel(); 62 | 63 | QGridLayout* grid = new QGridLayout{}; 64 | grid->addWidget(new QLabel{tr("Address:")}, 0, 0); 65 | grid->addWidget(m_label_color_address, 0, 1); 66 | grid->addWidget(new QLabel{tr("R:")}, 1, 0); 67 | grid->addWidget(m_label_color_r_component, 1, 1); 68 | grid->addWidget(new QLabel{tr("G:")}, 2, 0); 69 | grid->addWidget(m_label_color_g_component, 2, 1); 70 | grid->addWidget(new QLabel{tr("B:")}, 3, 0); 71 | grid->addWidget(m_label_color_b_component, 3, 1); 72 | grid->addWidget(new QLabel{tr("Value:")}, 4, 0); 73 | grid->addWidget(m_label_color_value, 4, 1); 74 | grid->setColumnStretch(1, 1); 75 | 76 | m_box_color_preview = new QWidget{}; 77 | m_box_color_preview->setStyleSheet("background-color: black;"); 78 | m_box_color_preview->setFixedSize(64, 64); 79 | 80 | hbox->addLayout(grid); 81 | hbox->addWidget(m_box_color_preview); 82 | return hbox; 83 | } 84 | 85 | void PaletteViewer::Update() { 86 | if(!isVisible()) { 87 | return; 88 | } 89 | 90 | m_palette_color_grids[0]->Draw(&m_pram[0], 16); 91 | m_palette_color_grids[1]->Draw(&m_pram[256], 16); 92 | } 93 | 94 | void PaletteViewer::ShowColorInfo(int color_index) { 95 | const u32 address = 0x05000000 + (color_index << 1); 96 | const int x = color_index & 15; 97 | const int y = (color_index >> 4) & 15; 98 | const u16 color = m_pram[color_index]; 99 | 100 | const int r = (color << 1) & 62; 101 | const int g = ((color >> 4) & 62) | (color >> 15); 102 | const int b = (color >> 9) & 62; 103 | 104 | const u32 rgb = Rgb565ToArgb8888(color); 105 | 106 | m_label_color_address->setText(QString::fromStdString(fmt::format("0x{:08X} ({}, {})", address, x, y))); 107 | m_label_color_r_component->setText(QStringLiteral("%1").arg(r)); 108 | m_label_color_g_component->setText(QStringLiteral("%1").arg(g)); 109 | m_label_color_b_component->setText(QStringLiteral("%1").arg(b)); 110 | m_label_color_value->setText(QString::fromStdString(fmt::format("0x{:04X}", color))); 111 | 112 | m_box_color_preview->setStyleSheet(QString::fromStdString(fmt::format("background-color: #{:06X};", rgb))); 113 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

NanoBoyAdvance

2 | 3 | [![license](https://img.shields.io/github/license/nba-emu/NanoBoyAdvance)](https://github.com/nba-emu/NanoBoyAdvance/blob/master/LICENSE) 4 | [![build](https://img.shields.io/github/actions/workflow/status/nba-emu/NanoBoyAdvance/build.yml?branch=master)](https://github.com/nba-emu/NanoBoyAdvance/actions/workflows/build.yml) 5 | [![discord](https://img.shields.io/discord/969218483873251338?logo=discord&label=discord)](https://discord.gg/4NnUjsf7Am) 6 | 7 | NanoBoyAdvance is a cycle-accurate Game Boy Advance emulator.
8 | It aims to be as accurate as possible, while also offering enhancements such as 9 | improved audio quality.
10 | 11 | ## Features 12 | - Very high compatibility and accuracy (see [Accuracy](#accuracy)) 13 | - HQ audio mixer (for games which use Nintendo's MusicPlayer2000 sound engine) 14 | - Post-processing options (color correction, xBRZ upscaling and LCD ghosting simulation) 15 | - Save State support (10x save slots available) 16 | - Game controller support (buttons and axises can be remapped) 17 | - Loading ROMs from archives (Zip, 7z, Tar and limited RAR[^1] support) 18 | - RTC emulation 19 | - Solar Sensor emulation (for example: for Boktai - The Sun is in Your Hand) 20 | - Debug tools: PPU palette, tile, background and sprite viewers 21 | 22 | [^1]: RAR 5.0 is currently not supported. 23 | 24 | ## Running 25 | 26 | Download a recent [development build](https://nightly.link/nba-emu/NanoBoyAdvance/workflows/build/master) or the last [stable release](https://github.com/nba-emu/NanoBoyAdvance/releases). 27 | 28 | Upon loading a ROM for the first time you will be prompted to assign the Game Boy Advance BIOS file. 29 | You can [dump](https://github.com/mgba-emu/bios-dump/tree/master/src) it from a real console (accurate) or use an [unofficial BIOS](https://github.com/Nebuleon/ReGBA/blob/master/bios/gba_bios.bin) (less accurate). 30 | 31 | ## Accuracy 32 | 33 | A lot of research and attention to detail has been put into developing this core and making it accurate. 34 | 35 | - Cycle-accurate emulation of most components, including: CPU, DMA, timers, PPU and Game Pak prefetch 36 | - Passes all AGS aging cartridge tests (NBA was the first public emulator to achieve this) 37 | - Passes most tests in the [mGBA test suite](https://github.com/mgba-emu/suite) 38 | - Passes [ARMWrestler](https://github.com/destoer/armwrestler-gba-fixed), [gba-suite](https://github.com/jsmolka/gba-tests) and [FuzzARM](https://github.com/DenSinH/FuzzARM) CPU tests 39 | - Very high compatibility, including games that require emulation of peculiar hardware edge-cases 40 | 41 | ## Compiling 42 | 43 | See [COMPILING.md](docs/COMPILING.md) in the `docs` folder. 44 | 45 | ## Acknowledgements 46 | 47 | - Martin Korth: for [GBATEK](http://problemkaputt.de/gbatek.htm), a good piece of hardware documentation. 48 | - [endrift](https://github.com/endrift): for hardware [research](http://mgba.io/tag/emulation/) and her excellent [test suite](https://github.com/mgba-emu/suite). 49 | - [destoer](https://github.com/destoer), [Noumi](https://github.com/noumidev), [Zayd](https://github.com/GhostRain0) and [Sky](https://github.com/skylersaleh): for hardware research and tests, countless discussions and being good friends. 50 | - Pokefan531 and hunterk: for the default GBA color correction algorithm 51 | - Talarubi and Near: for [higan's GBA color correction algorithm](https://near.sh/articles/video/color-emulation) 52 | - DeSmuME team and Hyllian: xBRZ upscaling code 53 | 54 | ## Sister Projects 55 | - [Panda3DS](https://github.com/wheremyfoodat/panda3DS): A new HLE Nintendo 3DS emulator 56 | - [Dust](https://github.com/Kelpsyberry/dust/): Nintendo DS emulator for desktop devices and the web 57 | - [Kaizen](https://github.com/SimoneN64/Kaizen): Experimental work-in-progress low-level N64 emulator 58 | - [SkyEmu](https://github.com/skylersaleh/SkyEmu/): A low-level GameBoy, GameBoy Color, GameBoy Advance and Nintendo DS emulator that is designed to be easy to use, cross platform and accurate 59 | 60 | ## Copyright 61 | 62 | NanoBoyAdvance is Copyright © 2015 - 2025 fleroviux.
63 | It is licensed under the terms of the GNU General Public License (GPL) 3.0 or any later version. See [LICENSE](LICENSE) for details. 64 | 65 | Game Boy Advance is a registered trademark of Nintendo Co., Ltd. 66 | -------------------------------------------------------------------------------- /src/nba/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | option(USE_SYSTEM_FMT "Use system-provided fmt library." OFF) 2 | 3 | if(USE_SYSTEM_FMT) 4 | find_package(fmt 8.0.1 REQUIRED) 5 | else() 6 | include(FetchContent) 7 | FetchContent_Declare(fmt 8 | GIT_REPOSITORY https://github.com/fmtlib/fmt.git 9 | GIT_TAG e69e5f977d458f2650bb346dadf2ad30c5320281 # 10.2.1 10 | ) 11 | set(FMT_INSTALL OFF CACHE BOOL "" FORCE) 12 | FetchContent_MakeAvailable(fmt) 13 | endif() 14 | 15 | set(SOURCES 16 | src/arm/tablegen/tablegen.cpp 17 | src/arm/serialization.cpp 18 | src/bus/bus.cpp 19 | src/bus/io.cpp 20 | src/bus/serialization.cpp 21 | src/bus/timing.cpp 22 | src/hw/apu/channel/noise_channel.cpp 23 | src/hw/apu/channel/quad_channel.cpp 24 | src/hw/apu/channel/wave_channel.cpp 25 | src/hw/apu/hle/mp2k.cpp 26 | src/hw/apu/apu.cpp 27 | src/hw/apu/callback.cpp 28 | src/hw/apu/registers.cpp 29 | src/hw/apu/serialization.cpp 30 | src/hw/ppu/background.cpp 31 | src/hw/ppu/merge.cpp 32 | src/hw/ppu/ppu.cpp 33 | src/hw/ppu/registers.cpp 34 | src/hw/ppu/serialization.cpp 35 | src/hw/ppu/sprite.cpp 36 | src/hw/ppu/window.cpp 37 | src/hw/rom/backup/eeprom.cpp 38 | src/hw/rom/backup/flash.cpp 39 | src/hw/rom/backup/serialization.cpp 40 | src/hw/rom/backup/sram.cpp 41 | src/hw/rom/gpio/gpio.cpp 42 | src/hw/rom/gpio/rtc.cpp 43 | src/hw/rom/gpio/serialization.cpp 44 | src/hw/rom/gpio/solar_sensor.cpp 45 | src/hw/dma/dma.cpp 46 | src/hw/dma/serialization.cpp 47 | src/hw/irq/irq.cpp 48 | src/hw/irq/serialization.cpp 49 | src/hw/keypad/keypad.cpp 50 | src/hw/keypad/serialization.cpp 51 | src/hw/timer/serialization.cpp 52 | src/hw/timer/timer.cpp 53 | src/core.cpp 54 | src/serialization.cpp 55 | ) 56 | 57 | set(HEADERS 58 | src/arm/handlers/arithmetic.inl 59 | src/arm/handlers/handler16.inl 60 | src/arm/handlers/handler32.inl 61 | src/arm/handlers/memory.inl 62 | src/arm/tablegen/gen_arm.hpp 63 | src/arm/tablegen/gen_thumb.hpp 64 | src/arm/arm7tdmi.hpp 65 | src/arm/state.hpp 66 | src/bus/bus.hpp 67 | src/bus/io.hpp 68 | src/hw/apu/channel/base_channel.hpp 69 | src/hw/apu/channel/envelope.hpp 70 | src/hw/apu/channel/fifo.hpp 71 | src/hw/apu/channel/length_counter.hpp 72 | src/hw/apu/channel/noise_channel.hpp 73 | src/hw/apu/channel/quad_channel.hpp 74 | src/hw/apu/channel/sweep.hpp 75 | src/hw/apu/channel/wave_channel.hpp 76 | src/hw/apu/hle/mp2k.hpp 77 | src/hw/apu/apu.hpp 78 | src/hw/apu/registers.hpp 79 | src/hw/ppu/background.inl 80 | src/hw/ppu/ppu.hpp 81 | src/hw/ppu/registers.hpp 82 | src/hw/dma/dma.hpp 83 | src/hw/irq/irq.hpp 84 | src/hw/keypad/keypad.hpp 85 | src/hw/timer/timer.hpp 86 | src/core.hpp 87 | ) 88 | 89 | set(HEADERS_PUBLIC 90 | include/nba/common/dsp/resampler/cosine.hpp 91 | include/nba/common/dsp/resampler/cubic.hpp 92 | include/nba/common/dsp/resampler/nearest.hpp 93 | include/nba/common/dsp/resampler/sinc.hpp 94 | include/nba/common/dsp/resampler.hpp 95 | include/nba/common/compiler.hpp 96 | include/nba/common/crc32.hpp 97 | include/nba/common/meta.hpp 98 | include/nba/common/punning.hpp 99 | include/nba/common/scope_exit.hpp 100 | include/nba/device/audio_device.hpp 101 | include/nba/device/video_device.hpp 102 | include/nba/rom/backup/backup.hpp 103 | include/nba/rom/backup/backup_file.hpp 104 | include/nba/rom/backup/eeprom.hpp 105 | include/nba/rom/backup/flash.hpp 106 | include/nba/rom/backup/sram.hpp 107 | include/nba/rom/gpio/gpio.hpp 108 | include/nba/rom/gpio/rtc.hpp 109 | include/nba/rom/gpio/solar_sensor.hpp 110 | include/nba/rom/header.hpp 111 | include/nba/rom/rom.hpp 112 | include/nba/config.hpp 113 | include/nba/core.hpp 114 | include/nba/integer.hpp 115 | include/nba/log.hpp 116 | include/nba/save_state.hpp 117 | include/nba/scheduler.hpp 118 | ) 119 | 120 | add_library(nba STATIC) 121 | target_sources(nba PRIVATE ${SOURCES} ${HEADERS} ${HEADERS_PUBLIC}) 122 | target_include_directories(nba PRIVATE src PUBLIC include) 123 | 124 | target_link_libraries(nba PUBLIC fmt::fmt) 125 | 126 | if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") 127 | target_compile_options(nba PRIVATE $<$:-fbracket-depth=4096>) 128 | endif() 129 | --------------------------------------------------------------------------------