From 7b7fc0b17e98abd69bfcf182b74f81144bf5818a Mon Sep 17 00:00:00 2001 From: thomz12 Date: Sat, 19 Sep 2026 10:39:03 +0200 Subject: [PATCH] setup project and first naive look at memory --- .clang-format | 1 + CMakeLists.txt | 12 +++++ CMakePresets.json | 24 ++++++++++ juiceboy/CMakeLists.txt | 23 +++++++++ juiceboy/include/juiceboy/cartridge.hpp | 18 +++++++ .../juiceboy/cartridges/rom_cartridge.hpp | 23 +++++++++ juiceboy/include/juiceboy/juiceboy.hpp | 12 +++++ juiceboy/include/juiceboy/memory.hpp | 28 +++++++++++ juiceboy/include/juiceboy/memory_region.hpp | 43 +++++++++++++++++ juiceboy/include/juiceboy/processor.hpp | 12 +++++ juiceboy/include/juiceboy/registers.hpp | 47 ++++++++++++++++++ juiceboy/src/memory.cpp | 48 +++++++++++++++++++ juiceboy/src/processor.cpp | 4 ++ juiceboy/src/registers.cpp | 5 ++ juiceboy/test/CMakeLists.txt | 8 ++++ juiceboy/test/memory_region_test.cpp | 44 +++++++++++++++++ vcpkg.json | 7 +++ 17 files changed, 359 insertions(+) create mode 100644 .clang-format create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 juiceboy/CMakeLists.txt create mode 100644 juiceboy/include/juiceboy/cartridge.hpp create mode 100644 juiceboy/include/juiceboy/cartridges/rom_cartridge.hpp create mode 100644 juiceboy/include/juiceboy/juiceboy.hpp create mode 100644 juiceboy/include/juiceboy/memory.hpp create mode 100644 juiceboy/include/juiceboy/memory_region.hpp create mode 100644 juiceboy/include/juiceboy/processor.hpp create mode 100644 juiceboy/include/juiceboy/registers.hpp create mode 100644 juiceboy/src/memory.cpp create mode 100644 juiceboy/src/processor.cpp create mode 100644 juiceboy/src/registers.cpp create mode 100644 juiceboy/test/CMakeLists.txt create mode 100644 juiceboy/test/memory_region_test.cpp create mode 100644 vcpkg.json diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..468ceea --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: LLVM \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3a8ba54 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 4.0) + +project(juiceboy + VERSION 0.0.1 + LANGUAGES CXX +) + +include(CTest) +find_package(GTest CONFIG REQUIRED) +include(GoogleTest) + +add_subdirectory(juiceboy) \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..eb36757 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,24 @@ +{ + "version": 4, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "build/${presetName}", + "generator": "Ninja", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_STANDARD_REQUIRED": "YES", + "CMAKE_CXX_EXTENSIONS": "NO", + "CMAKE_CXX_VISIBILITY_PRESET": "hidden" + } + }, + { + "name": "debug", + "displayName": "Debug", + "inherits": "base", + "generator": "Ninja" + } + ] +} \ No newline at end of file diff --git a/juiceboy/CMakeLists.txt b/juiceboy/CMakeLists.txt new file mode 100644 index 0000000..cd220fb --- /dev/null +++ b/juiceboy/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library(juiceboy STATIC) + +target_sources(juiceboy + PRIVATE + "src/memory.cpp" + "src/processor.cpp" + "src/registers.cpp" + PUBLIC + FILE_SET HEADERS + BASE_DIRS "include" + FILES + "include/juiceboy/cartridge.hpp" + "include/juiceboy/cartridges/rom_cartridge.hpp" + "include/juiceboy/juiceboy.hpp" + "include/juiceboy/memory.hpp" + "include/juiceboy/memory_region.hpp" + "include/juiceboy/processor.hpp" + "include/juiceboy/registers.hpp" +) + +if (BUILD_TESTING) + add_subdirectory(test) +endif () \ No newline at end of file diff --git a/juiceboy/include/juiceboy/cartridge.hpp b/juiceboy/include/juiceboy/cartridge.hpp new file mode 100644 index 0000000..e2b6cfb --- /dev/null +++ b/juiceboy/include/juiceboy/cartridge.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace juiceboy { + class Cartridge { + public: + virtual ~Cartridge() = default; + + virtual std::uint8_t ReadByte(std::uint16_t address) = 0; + + virtual std::uint16_t ReadWord(std::uint16_t address) = 0; + + virtual void WriteByte(std::uint16_t address, std::uint16_t value) = 0; + + virtual void WriteWord(std::uint16_t address, std::uint16_t value) = 0; + }; +} diff --git a/juiceboy/include/juiceboy/cartridges/rom_cartridge.hpp b/juiceboy/include/juiceboy/cartridges/rom_cartridge.hpp new file mode 100644 index 0000000..5925bfd --- /dev/null +++ b/juiceboy/include/juiceboy/cartridges/rom_cartridge.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "juiceboy/cartridge.hpp" + +#include + +namespace juiceboy { + class ROMCartridge : public Cartridge { + public: + explicit ROMCartridge(std::array data); + + std::uint8_t ReadByte(std::uint16_t address) override; + + std::uint16_t ReadWord(std::uint16_t address) override; + + void WriteByte(std::uint16_t address, std::uint16_t value) override; + + void WriteWord(std::uint16_t address, std::uint16_t value) override; + + private: + std::array m_data; + }; +} diff --git a/juiceboy/include/juiceboy/juiceboy.hpp b/juiceboy/include/juiceboy/juiceboy.hpp new file mode 100644 index 0000000..de723f4 --- /dev/null +++ b/juiceboy/include/juiceboy/juiceboy.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "memory.hpp" +#include "processor.hpp" + +namespace juiceboy { + class Juiceboy { + private: + Memory m_memory; + Processor m_processor; + }; +} diff --git a/juiceboy/include/juiceboy/memory.hpp b/juiceboy/include/juiceboy/memory.hpp new file mode 100644 index 0000000..7f9f028 --- /dev/null +++ b/juiceboy/include/juiceboy/memory.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "cartridge.hpp" +#include "memory_region.hpp" + +#include +#include + +namespace juiceboy { + class Memory { + public: + explicit Memory(std::shared_ptr cartridge); + + [[nodiscard]] std::uint8_t ReadByte(std::uint16_t address); + + void WriteByte(std::uint16_t address, std::uint8_t value); + + private: + std::shared_ptr m_cartridge; + + MemoryRegion<0x8000, 0x2000> m_vram{}; + MemoryRegion<0xC000, 0x2000> m_wram{}; + MemoryRegion<0xFE00, 0xA0> m_oam{}; + MemoryRegion<0xFF00, 0x80> m_io{}; + MemoryRegion<0xFF80, 0x7F> m_hram{}; + MemoryRegion<0xFFFF, 0x1> m_interruptEnable{}; + }; +} diff --git a/juiceboy/include/juiceboy/memory_region.hpp b/juiceboy/include/juiceboy/memory_region.hpp new file mode 100644 index 0000000..6006f76 --- /dev/null +++ b/juiceboy/include/juiceboy/memory_region.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +namespace juiceboy { + template + class MemoryRegion { + public: + using ArrayType = std::array; + + constexpr MemoryRegion() : m_data{0} { + } + + explicit constexpr MemoryRegion(ArrayType data) : m_data{std::move(data)} { + } + + [[nodiscard]] static constexpr bool ContainsAddress(const std::uint16_t address) { + return (address >= Start) && (address < Start + Size); + } + + [[nodiscard]] constexpr std::uint8_t ReadByte(const std::uint16_t address) const { + if (!ContainsAddress(address)) { + throw std::invalid_argument{"Attempted to read outside of memory region!"}; + } + return m_data[GetLocalAddress(address)]; + } + + constexpr void WriteByte(const std::uint16_t address, std::uint8_t value) { + if (!ContainsAddress(address)) { + throw std::invalid_argument{"Attempted to write outside of memory region!"}; + } + m_data[GetLocalAddress(address)] = value; + } + + private: + [[nodiscard]] static constexpr std::uint16_t GetLocalAddress(const std::uint16_t address) { + return address - Start; + } + + ArrayType m_data; + }; +} diff --git a/juiceboy/include/juiceboy/processor.hpp b/juiceboy/include/juiceboy/processor.hpp new file mode 100644 index 0000000..532a68a --- /dev/null +++ b/juiceboy/include/juiceboy/processor.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "registers.hpp" + +namespace juiceboy { + class Processor { + public: + + private: + Registers m_registers; + }; +} // juiceboy diff --git a/juiceboy/include/juiceboy/registers.hpp b/juiceboy/include/juiceboy/registers.hpp new file mode 100644 index 0000000..fb1c3fd --- /dev/null +++ b/juiceboy/include/juiceboy/registers.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +namespace juiceboy { + class Registers { + public: + /// @brief Flags in the flags (af) register. + enum class Flags { + Zero, + Subtract, + HalfCarry, + Carry + }; + + /// @brief Get the current program counter/pointer. + /// @return The current value of the program counter/pointer. + [[nodiscard]] std::uint16_t GetProgramCounter() const; + + /// @brief Get the current stack pointer. + /// @return The current value of the stack pointer. + [[nodiscard]] std::uint16_t GetStackPointer() const; + + /// @brief Get the current value of the given flag. + /// @return True if the given flag bit is set, false otherwise. + [[nodiscard]] bool GetFlag(Flags flag) const; + + private: + /// @brief Accumulator & Flags. + std::uint16_t m_af{0}; + + /// @brief BC Registers. + std::uint16_t m_bc{0}; + + /// @brief DE Registers. + std::uint16_t m_de{0}; + + /// @brief HL Registers. + std::uint16_t m_hl{0}; + + /// @brief Program counter/pointer. + std::uint16_t m_pc{0}; + + /// @brief Stack pointer. + std::uint16_t m_sp{0}; + }; +} diff --git a/juiceboy/src/memory.cpp b/juiceboy/src/memory.cpp new file mode 100644 index 0000000..7440aa6 --- /dev/null +++ b/juiceboy/src/memory.cpp @@ -0,0 +1,48 @@ +#include "juiceboy/memory.hpp" + +#include "juiceboy/cartridge.hpp" +#include "juiceboy/memory_region.hpp" + +#include + +namespace juiceboy { + Memory::Memory(std::shared_ptr cartridge) : m_cartridge(std::move(cartridge)) { + } + + std::uint8_t Memory::ReadByte(const std::uint16_t address) { + // Read from cartridge ROM or External RAM. + if ((address < 0x8000) || (address >= 0xA000 && address < 0xC000)) { + return m_cartridge->ReadByte(address); + } + + if (decltype(m_vram)::ContainsAddress(address)) { + return m_vram.ReadByte(address); + } + + if (decltype(m_wram)::ContainsAddress(address)) { + return m_wram.ReadByte(address); + } + + if (decltype(m_oam)::ContainsAddress(address)) { + return m_oam.ReadByte(address); + } + + if (decltype(m_hram)::ContainsAddress(address)) { + return m_io.ReadByte(address); + } + + if (decltype(m_hram)::ContainsAddress(address)) { + return m_io.ReadByte(address); + } + + if (decltype(m_interruptEnable)::ContainsAddress(address)) { + return m_interruptEnable.ReadByte(address); + } + + // For now, return 0 when no address was in range. + return 0x00; + } + + void Memory::WriteByte(std::uint16_t address, std::uint8_t value) { + } +} diff --git a/juiceboy/src/processor.cpp b/juiceboy/src/processor.cpp new file mode 100644 index 0000000..6a7c277 --- /dev/null +++ b/juiceboy/src/processor.cpp @@ -0,0 +1,4 @@ +#include "juiceboy/processor.hpp" + +namespace juiceboy { +} // juiceboy diff --git a/juiceboy/src/registers.cpp b/juiceboy/src/registers.cpp new file mode 100644 index 0000000..c0a2f2c --- /dev/null +++ b/juiceboy/src/registers.cpp @@ -0,0 +1,5 @@ +#include "juiceboy/registers.hpp" + +namespace juiceboy { + +} \ No newline at end of file diff --git a/juiceboy/test/CMakeLists.txt b/juiceboy/test/CMakeLists.txt new file mode 100644 index 0000000..26c39c3 --- /dev/null +++ b/juiceboy/test/CMakeLists.txt @@ -0,0 +1,8 @@ +add_executable(juiceboy_test) + +target_sources(juiceboy_test + PRIVATE + "memory_region_test.cpp" +) + +target_link_libraries(juiceboy_test PRIVATE juiceboy GTest::gtest_main) \ No newline at end of file diff --git a/juiceboy/test/memory_region_test.cpp b/juiceboy/test/memory_region_test.cpp new file mode 100644 index 0000000..0681238 --- /dev/null +++ b/juiceboy/test/memory_region_test.cpp @@ -0,0 +1,44 @@ +#include "juiceboy/memory_region.hpp" + +#include + +namespace juiceboy::test { + TEST(MemoryRegion, DefaultConstructorZeroesEverythingOut) { + constexpr MemoryRegion<0x00, 0x02> region; + + EXPECT_EQ(region.ReadByte(0x00), 0); + EXPECT_EQ(region.ReadByte(0x01), 0); + } + + TEST(MemoryRegion, ConstructorSetsGivenValues) { + constexpr MemoryRegion<0x00, 0x02> region{{0xFF, 0xFF}}; + + EXPECT_EQ(region.ReadByte(0x00), 0xFF); + EXPECT_EQ(region.ReadByte(0x01), 0xFF); + } + + TEST(MemoryRegion, StartAddressTranslatesAddressCorrectly) { + constexpr MemoryRegion<0x2, 0x02> region{{0xFF, 0xFF}}; + + EXPECT_EQ(region.ReadByte(0x02), 0xFF); + EXPECT_EQ(region.ReadByte(0x03), 0xFF); + } + + TEST(MemoryRegion, WriteByteUpdatesValue) { + MemoryRegion<0x0, 0x2> region; + + region.WriteByte(0x0, 0x1); + + EXPECT_EQ(region.ReadByte(0x0), 0x1); + } + + TEST(MemoryRegion, ReadByteOutOfRangeThrows) { + constexpr MemoryRegion<0x0, 0x02> region; + EXPECT_THROW((void)region.ReadByte(0xFF), std::invalid_argument); + } + + TEST(MemoryRegion, WriteByteOutOfRangeThrows) { + MemoryRegion<0x0, 0x02> region; + EXPECT_THROW(region.WriteByte(0xFF, 0x0), std::invalid_argument); + } +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..2945402 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,7 @@ +{ + "name": "juiceboy", + "version-string": "0.0.1", + "dependencies": [ + "gtest" + ] +} \ No newline at end of file