diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6c3ac7..4296f58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,7 @@ on: - '.github/workflows/**' branches: - master + - sdl workflow_dispatch: jobs: @@ -100,7 +101,7 @@ jobs: run: | mkdir -p build cd build - cmake .. -DCMAKE_BUILD_TYPE=Release + cmake .. -DCMAKE_BUILD_TYPE=Release -D CMAKE_SYSTEM_VERSION=10.0.26100.0 - name: Build CMake run: | diff --git a/.gitmodules b/.gitmodules index 0dbc2bf..669f44f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,12 @@ -[submodule "dependencies/sfml"] - path = dependencies/sfml - url = https://github.com/SFML/SFML.git -[submodule "dependencies/libconfig"] - path = dependencies/libconfig - url = https://github.com/hyperrealm/libconfig.git +[submodule "dependencies/sdl"] + path = dependencies/sdl + url = git@github.com:libsdl-org/SDL.git [submodule "dependencies/nativefiledialog"] path = dependencies/nativefiledialog url = git@github.com:btzy/nativefiledialog-extended.git +[submodule "dependencies/libconfig"] + path = dependencies/libconfig + url = https://github.com/hyperrealm/libconfig.git +[submodule "dependencies/sdl_ttf"] + path = dependencies/sdl_ttf + url = git@github.com:libsdl-org/SDL_ttf.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 074456b..75a5031 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,19 +5,11 @@ VERSION 0.1) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +option(SDL_VENDORED "Use vendored libraries" ON) add_executable(8ChocChip) -#option(BUILD_SHARED_LIBS "Build shared libraries" OFF) - -#file(GLOB ASSETS "${CMAKE_SOURCE_DIR}/assets/*") -# -#foreach(ASSET ${ASSETS}) -# configure_file(${ASSET} ${CMAKE_BINARY_DIR}/assets COPYONLY) -#endforeach() - add_subdirectory(dependencies) add_subdirectory(src) @@ -27,33 +19,13 @@ else() set(libname "config") endif() -target_link_libraries(8ChocChip PRIVATE sfml-graphics sfml-audio sfml-window sfml-system nfd ${libname}++) +target_link_libraries(8ChocChip PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf nfd ${libname}++) -# Set the RPATH of the executable to include the directory where SFML libraries are located set_target_properties(8ChocChip PROPERTIES INSTALL_RPATH "$ORIGIN" BUILD_RPATH "$ORIGIN" ) -# Copy DLLs needed for runtime on Windows -if(WIN32) - add_custom_command( - TARGET 8ChocChip - COMMENT "Copy OpenAL DLL" - PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${SFML_SOURCE_DIR}/extlibs/bin/$,x64,x86>/openal32.dll $ - VERBATIM) - - if (BUILD_SHARED_LIBS) - add_custom_command(TARGET 8ChocChip POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - $ - $ - $) - endif() -endif() - # Copy nfd and config++ libraries to the executable directory if (UNIX AND NOT APPLE) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD diff --git a/README.md b/README.md index cadf6c9..b66d6a8 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # 8ChocChip -8ChocChip is an emulator for the [Chip8](https://en.wikipedia.org/wiki/CHIP-8) software that I am working on to learn the basics around emulation. -I decided to program this in C++ because I wanted to do more in it and improve my skills. +8ChocChip is an emulator for the Chip8 software that I am working on to learn the basics around emulation. -The graphics and only library used in this is SFML which handles the window, inputs and sounds. +This uses SDL to handle graphics, input, audio and some other small things. libconfig is used to manage config and save data files, and NativeFileDialog-extended is used for basic popup windows like the file/directory selector. ## TODO There are a couple of things left to do until I would say it works well enough -- [x] ~~Fix some of the instructions that result in most programs failing except for `BLINKY`~~ Should be done but the test suite has issues I can not figure out how to fix. But is better than before and should work with most stuff +- [x] Proper flag compatibility unlike many out there - [ ] Cleaner/more optimised code - [x] Windows Support - [ ] MacOS Support (Builds, but unable to test) -- [ ] Switch to SDL from SFML +- [x] Switch to SDL from SFML ## Usage @@ -37,17 +36,17 @@ Some parts are just setting up ssh or installing dependencies which you may have ### Requirements -To build this it requires C++, CMake and whatever SFML and SFML Audio requires on your platform. Check build file above. +To build this it requires C++, CMake and whatever SDL/dependencies need. Check build file above. # Credits -Thanks to these two blogs that helped me through creating this emulator -[How to Create Your Very Own Chip-8 Emulator](https://www.freecodecamp.org/news/creating-your-very-own-chip-8-emulator/) -[Guide to making a CHIP-8 emulator ](https://tobiasvl.github.io/blog/write-a-chip-8-emulator/) +Thanks to these two blogs that helped me through creating this emulator. These are actually based on a "broken" emulator guide so not everything will work. But it is good as a base and learning how to do your own research into the functionality of the emulator. +- [How to Create Your Very Own Chip-8 Emulator](https://www.freecodecamp.org/news/creating-your-very-own-chip-8-emulator/) +- [Guide to making a CHIP-8 emulator ](https://tobiasvl.github.io/blog/write-a-chip-8-emulator/) ## Libraries Currently, three libraries are being used -- [SFML](https://github.com/SFML/SFML) - UI, graphics, input and sounds +- [SDL](https://github.com/libsdl-org/SDL) - UI, graphics, input and sounds - [NativeFileDialog-extended](https://github.com/btzy/nativefiledialog-extended) - Handles file dialogs for selecting files/directories. Fork of [nativefiledialog](https://github.com/mlabbe/nativefiledialog) which I used a fork of that added only CMake support, this new one adds that plus new fixes/features - [libconfig](https://github.com/hyperrealm/libconfig) - Library to manage save data (like directories that hold ROMs) and possible future config files diff --git a/assets/icon.bmp b/assets/icon.bmp new file mode 100644 index 0000000..c9a47e2 Binary files /dev/null and b/assets/icon.bmp differ diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 71e20ef..1ad37b3 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -1,8 +1,13 @@ -if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/sfml/CMakeLists.txt") - message("${CMAKE_CURRENT_SOURCE_DIR}/sfml/CMakeLists.txt") - message(FATAL_ERROR "Please initialize submodules using:\n git submodule update --init --recursive") +add_subdirectory(nativefiledialog) + +set(BUILD_EXAMPLES OFF CACHE BOOL "Disable examples for libconfig" FORCE) +set(BUILD_TESTS ON CACHE BOOL "Disable tests for libconfig" FORCE) +add_subdirectory(libconfig EXCLUDE_FROM_ALL) + +if(SDL_VENDORED) + add_subdirectory(sdl EXCLUDE_FROM_ALL) +else() + find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3-shared) endif() -add_subdirectory(sfml) -add_subdirectory(nativefiledialog) -add_subdirectory(libconfig) +add_subdirectory(sdl_ttf) \ No newline at end of file diff --git a/dependencies/libconfig b/dependencies/libconfig index f9404f6..690342b 160000 --- a/dependencies/libconfig +++ b/dependencies/libconfig @@ -1 +1 @@ -Subproject commit f9404f60a435aa06321f4ccd8357364dcb216d46 +Subproject commit 690342b9cbc8b39787a1501bd890d63ca63a003c diff --git a/dependencies/sdl b/dependencies/sdl new file mode 160000 index 0000000..78cc5c1 --- /dev/null +++ b/dependencies/sdl @@ -0,0 +1 @@ +Subproject commit 78cc5c173404488d80751af226d1eaf67033bcc4 diff --git a/dependencies/sdl_ttf b/dependencies/sdl_ttf new file mode 160000 index 0000000..a90b693 --- /dev/null +++ b/dependencies/sdl_ttf @@ -0,0 +1 @@ +Subproject commit a90b6938ddf2104f53d682d9c8388c6c173e98f0 diff --git a/dependencies/sfml b/dependencies/sfml deleted file mode 160000 index 69ea0cd..0000000 --- a/dependencies/sfml +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 69ea0cd863aed1d4092b970b676924a716ff718b diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 10959a7..92bd106 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,5 @@ -add_subdirectory(sfml) +add_subdirectory(sdl) add_subdirectory(util) add_subdirectory(emulator) -target_sources(8ChocChip PUBLIC main.cpp) - +target_sources(8ChocChip PUBLIC main.cpp Timer.cpp Timer.h) \ No newline at end of file diff --git a/src/Timer.cpp b/src/Timer.cpp new file mode 100644 index 0000000..e039ed5 --- /dev/null +++ b/src/Timer.cpp @@ -0,0 +1,67 @@ +#include "Timer.h" + +#include + +Timer::Timer() { + this->startTicks = 0; + this->pausedTicks = 0; + + this->paused = false; + this->started = false; +} + +void Timer::start() { + this->started = true; + this->paused = false; + + this->startTicks = SDL_GetTicks(); + this->pausedTicks = 0; +} + +void Timer::stop() { + this->started = false; + this->paused = false; + this->startTicks = 0; + this->pausedTicks = 0; +} + +void Timer::pause() { + if(this->started && !this->paused) { + this->paused = true; + + this->pausedTicks = SDL_GetTicks() - this->startTicks; + this->startTicks = 0; + } +} + +void Timer::unpause() { + if(this->started && this->paused) { + this->paused = false; + + this->startTicks = SDL_GetTicks() - this->pausedTicks; + + this->pausedTicks = 0; + } +} + +uint64_t Timer::getTicks() const { + uint64_t time = 0; + + if(this->started) { + if(this->paused) { + time = this->pausedTicks; + } else { + time = SDL_GetTicks() - this->startTicks; + } + } + + return time; +} + +bool Timer::isStarted() const { + return this->started; +} + +bool Timer::isPaused() const { + return this->paused && this->started; +} \ No newline at end of file diff --git a/src/Timer.h b/src/Timer.h new file mode 100644 index 0000000..d0d723c --- /dev/null +++ b/src/Timer.h @@ -0,0 +1,28 @@ +#ifndef TIMER_H +#define TIMER_H + +#include + +class Timer { +public: + Timer(); + + void start(); + void stop(); + void pause(); + void unpause(); + + uint64_t getTicks() const; + + bool isStarted() const; + bool isPaused() const; + +private: + uint64_t startTicks; + uint64_t pausedTicks; + + bool paused; + bool started; +}; + +#endif \ No newline at end of file diff --git a/src/emulator/Cpu.cpp b/src/emulator/Cpu.cpp index bac905e..5582198 100644 --- a/src/emulator/Cpu.cpp +++ b/src/emulator/Cpu.cpp @@ -205,19 +205,21 @@ void Cpu::runInstruction(const uint16_t opcode) { break; } case 0xD000: { - const uint16_t width = 8; const uint16_t height = (opcode & 0xF); + const uint8_t uX = this->registers[x]; + const uint8_t uY = this->registers[y]; this->registers[0xF] = 0; for (uint16_t row = 0; row < height; row++) { + constexpr uint16_t width = 8; uint8_t sprite = this->memory[this->address + row]; for (uint16_t col = 0; col < width; col++) { // If the bit (sprite) is not 0, render/erase the pixel if ((sprite & 0x80) > 0) { // If setPixel returns 1, which means a pixel was erased, set VF to 1 - if (this->renderer->setPixel(this->registers[x] + col, this->registers[y] + row)) { + if (this->renderer->setPixel(uX + col, uY + row)) { this->registers[0xF] = 1; } } @@ -232,12 +234,12 @@ void Cpu::runInstruction(const uint16_t opcode) { case 0xE000: switch (opcode & 0xFF) { case 0x9E: - if (this->keyboard->isKeyPressed(this->registers[x])) { + if (this->keyboard->isKeyPressed(this->registers[x] & 0xF)) { this->pc += 2; } break; case 0xA1: - if (!this->keyboard->isKeyPressed(this->registers[x])) { + if (!this->keyboard->isKeyPressed(this->registers[x] & 0xF)) { this->pc += 2; } break; @@ -269,7 +271,7 @@ void Cpu::runInstruction(const uint16_t opcode) { this->address += this->registers[x]; break; case 0x29: - this->address = 0x50 + this->registers[x] * 5; + this->address = 0x50 + (this->registers[x] & 0xF) * 5; break; case 0x33: { uint8_t value = this->registers[x]; diff --git a/src/emulator/Cpu.h b/src/emulator/Cpu.h index fd63def..090d336 100644 --- a/src/emulator/Cpu.h +++ b/src/emulator/Cpu.h @@ -40,4 +40,4 @@ class Cpu { Speaker* speaker; }; -#endif //CPU_H +#endif \ No newline at end of file diff --git a/src/emulator/Keyboard.h b/src/emulator/Keyboard.h index afcd7df..ef05f87 100644 --- a/src/emulator/Keyboard.h +++ b/src/emulator/Keyboard.h @@ -10,22 +10,22 @@ class Keyboard { std::unordered_map keysPressed; std::function onNextKeyPress; std::unordered_map KEYMAP = { - {27, 0x1}, // 1 - {28, 0x2}, // 2 - {29, 0x3}, // 3 - {30, 0xc}, // 4 - {16, 0x4}, // Q - {22, 0x5}, // W - {4, 0x6}, // E - {17, 0xD}, // R - {0, 0x7}, // A - {18, 0x8}, // S - {3, 0x9}, // D - {5, 0xE}, // F - {25, 0xA}, // Z - {23, 0x0}, // X - {2, 0xB}, // C - {21, 0xF} // V + {30, 0x1}, // 1 + {31, 0x2}, // 2 + {32, 0x3}, // 3 + {33, 0xc}, // 4 + {20, 0x4}, // Q + {26, 0x5}, // W + {8, 0x6}, // E + {21, 0xD}, // R + {4, 0x7}, // A + {22, 0x8}, // S + {7, 0x9}, // D + {9, 0xE}, // F + {29, 0xA}, // Z + {27, 0x0}, // X + {6, 0xB}, // C + {25, 0xF} // V }; void setOnNextKeyPress(std::function callback); @@ -34,6 +34,4 @@ class Keyboard { bool isKeyPressed(int keyCode); }; - - -#endif //KEYBOARD_H +#endif \ No newline at end of file diff --git a/src/emulator/Renderer.cpp b/src/emulator/Renderer.cpp index a6b2b77..28445f5 100644 --- a/src/emulator/Renderer.cpp +++ b/src/emulator/Renderer.cpp @@ -4,12 +4,12 @@ Renderer::Renderer() { this->display.resize(this->columns * this->rows); } -void Renderer::render() { +void Renderer::render(SDL_Renderer* renderer) { // Render the display for (uint16_t y = 0; y < this->rows; ++y) { for (uint16_t x = 0; x < this->columns; ++x) { if (this->display[y * this->columns + x]) { - drawPixel(x, y); + drawPixel(renderer, x, y); } } } @@ -20,7 +20,7 @@ bool Renderer::setPixel(uint8_t x, uint16_t y) { x %= this->columns; y %= this->rows; - uint16_t pixelLoc = x + (y * this->columns); + const uint16_t pixelLoc = x + (y * this->columns); this->display[pixelLoc] = !this->display[pixelLoc]; return !this->display[pixelLoc]; diff --git a/src/emulator/Renderer.h b/src/emulator/Renderer.h index 97b7c6f..f5d868b 100644 --- a/src/emulator/Renderer.h +++ b/src/emulator/Renderer.h @@ -1,8 +1,9 @@ #ifndef RENDERER_H #define RENDERER_H -#include +#include #include +#include class Renderer { public: @@ -12,12 +13,12 @@ class Renderer { void clear(); - void render(); + void render(SDL_Renderer* renderer); uint16_t getColumns() const; uint16_t getRows() const; - virtual void drawPixel(uint16_t x, uint16_t y) = 0; + virtual void drawPixel(SDL_Renderer* renderer, uint16_t x, uint16_t y) = 0; private: const uint16_t columns = 64; const uint16_t rows = 32; @@ -25,4 +26,4 @@ class Renderer { std::vector display; }; -#endif //RENDERER_H +#endif \ No newline at end of file diff --git a/src/emulator/Speaker.h b/src/emulator/Speaker.h index 7a95a8c..4ebcdef 100644 --- a/src/emulator/Speaker.h +++ b/src/emulator/Speaker.h @@ -3,9 +3,8 @@ class Speaker { public: - virtual ~Speaker() = default; virtual void play() = 0; virtual void stop() = 0; }; -#endif //SPEAKER_H +#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 1a672ab..85ef00d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,18 @@ -#include - #include +#include #include -#include -#include #include -#include -#include -#include "sfml/Emulator.h" +#include +#include +#include + +#include "Timer.h" +#include "sdl/Emulator.h" +#include "sdl/MainMenu.h" #include "util/MiscUtil.h" -#include "sfml/MainMenu.h" -#include "libconfig.hh" int main(int argc, char **argv) { - std::string rom; - for (int i = 0; i < argc; i++) { std::string_view arg = argv[i]; if (arg.rfind("--") != 0) continue; // TODO: Account for --longform or -sf (short form) commands. just needs a better command handler @@ -23,7 +20,57 @@ int main(int argc, char **argv) { std::string command = MiscUtil::toLowerCase(std::string(arg)); if (command == "--rom") { if (i + 1 < argc) { - return Emulator().launch(argv[++i]); + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { + std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl; + return 1; + } + + std::string rom = argv[++i]; + Emulator emulator(rom); + emulator.init(); + + bool debug = false; + bool quit = false; + SDL_Event event; + + Timer fpsTimer; + Timer capTimer; + + int countedFrames = 0; + fpsTimer.start(); + + while (!quit) { + capTimer.start(); + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_QUIT) { + quit = true; + } + + emulator.handleEvent(event); + } + + emulator.update(); + emulator.render(); + + float avgFPS = countedFrames / (fpsTimer.getTicks() / 1000.f); + if (avgFPS > 2000000) { + avgFPS = 0; + } + + if (debug) { + std::cout << "FPS: " << avgFPS << std::endl; + } + + ++countedFrames; + int frameTicks = capTimer.getTicks(); + if (frameTicks < 1000 / 60) { + SDL_Delay(1000 / 60 - frameTicks); + } + } + + SDL_Quit(); + + return 0; } std::cerr << "Please include the path to the file" << std::endl; return 0; @@ -47,8 +94,6 @@ int main(int argc, char **argv) { std::string configFilePath = (std::filesystem::path(home) / ".8chocchip.cfg").string(); - std::vector> windows; - std::vector romDirectories; std::unordered_map> romFiles; @@ -100,7 +145,88 @@ int main(int argc, char **argv) { } } - MainMenu menu(romFiles, romDirectories, windows, configFilePath); + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { + std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl; + return 1; + } + + if (!TTF_Init()) { + SDL_Log("Couldn't initialize TTF: %s\n", SDL_GetError()); + return 1; + } + + TTF_Font *font = TTF_OpenFont("assets/font.ttf", 22); + if (!font) { + SDL_Log("Failed to load font: %s\n", SDL_GetError()); + return 1; + } + + std::vector> windows; + windows.emplace_back(std::make_unique(font, configFilePath, romFiles, romDirectories, windows))->init(); + + bool debug = false; + bool quit = false; + SDL_Event event; + + Timer fpsTimer; + Timer capTimer; + + int countedFrames = 0; + fpsTimer.start(); + + while (!quit) { + capTimer.start(); + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_QUIT) { + quit = true; + } + + for (const auto& window : windows) { + window->handleEvent(event); + } + } + + // Do not change, this makes multiple windows not crash + for (int i = 0; i < windows.size(); ++i) { + if (windows[i]->isDestroyed()) { + windows.erase(windows.begin() + i); + continue; + } + windows[i]->update(); + windows[i]->render(); + } + + bool allWindowsClosed = true; + for (const auto& window : windows) { + if(window->isShown()){ + allWindowsClosed = false; + break; + } + } + + if (allWindowsClosed || windows.empty()) { + quit = true; + } + + float avgFPS = countedFrames / (fpsTimer.getTicks() / 1000.f); + if (avgFPS > 2000000) { + avgFPS = 0; + } + + if (debug) { + std::cout << "FPS: " << avgFPS << std::endl; + } + + ++countedFrames; + int frameTicks = capTimer.getTicks(); + if (frameTicks < 1000 / 60) { + SDL_Delay(1000 / 60 - frameTicks); + } + } + + TTF_CloseFont(font); + TTF_Quit(); + SDL_Quit(); return 0; -} +} \ No newline at end of file diff --git a/src/sfml/CMakeLists.txt b/src/sdl/CMakeLists.txt similarity index 56% rename from src/sfml/CMakeLists.txt rename to src/sdl/CMakeLists.txt index 6e1d4d1..a4b1f3e 100644 --- a/src/sfml/CMakeLists.txt +++ b/src/sdl/CMakeLists.txt @@ -1,10 +1,13 @@ target_sources(8ChocChip PRIVATE - MainMenu.cpp - MainMenu.h Emulator.cpp Emulator.h + MainMenu.cpp + MainMenu.h InputHandler.cpp - InputHandler.h) + InputHandler.h + Window.cpp + Window.h +) -add_subdirectory(ui) -add_subdirectory(emulator) \ No newline at end of file +add_subdirectory(emulator) +add_subdirectory(ui) \ No newline at end of file diff --git a/src/sdl/Emulator.cpp b/src/sdl/Emulator.cpp new file mode 100644 index 0000000..3144c31 --- /dev/null +++ b/src/sdl/Emulator.cpp @@ -0,0 +1,56 @@ +#include "Emulator.h" + +#include + +Emulator::Emulator(const std::string& rom) : cpu(&renderWrapper, &keyboard, &speaker), rom(rom) {} + +void Emulator::init() { + Window::init(); + + if (this->rom.empty()) { + std::cerr << "No ROM file has been specified :(" << std::endl; + return; + } + + std::ifstream file(this->rom, std::ios::binary | std::ios::ate); + if (!file.good()) { + std::cerr << "Can not find file " << this->rom << std::endl; + return; + } + + this->cpu.loadSpritesIntoMemory(); + this->cpu.loadProgramIntoMemory(&file); +} + +bool Emulator::handleEvent(SDL_Event& event) { + if (!Window::handleEvent(event)) { + return false; + } + + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + this->keyboard.handleKeyDown(event.key.scancode); + break; + case SDL_EVENT_KEY_UP: + this->keyboard.handleKeyUp(event.key.scancode); + break; + } + + return true; +} + +void Emulator::update() { + this->cpu.cycle(); +} + +void Emulator::render() { + SDL_SetRenderDrawColor(this->renderer, 0, 0, 0, 0); + SDL_RenderClear(this->renderer); + + this->renderWrapper.render(this->renderer); + SDL_RenderPresent(this->renderer); +} + +void Emulator::resize(SDL_Event &event) { + +} diff --git a/src/sdl/Emulator.h b/src/sdl/Emulator.h new file mode 100644 index 0000000..76950f6 --- /dev/null +++ b/src/sdl/Emulator.h @@ -0,0 +1,29 @@ +#ifndef INC_8CHOCCHIP_SDL_H +#define INC_8CHOCCHIP_SDL_H + +#include "Window.h" +#include "../emulator/Cpu.h" +#include "emulator/SdlKeyboard.h" +#include "emulator/SdlRenderer.h" +#include "emulator/SdlSpeaker.h" + +class Emulator : public Window { +private: + SdlRenderer renderWrapper; + SdlSpeaker speaker; + SdlKeyboard keyboard; + Cpu cpu; + + const std::string& rom; +public: + Emulator(const std::string& rom); + + void init() override; + bool handleEvent(SDL_Event& event) override; + void update() override; + void render() override; + void resize(SDL_Event &event) override; +}; + + +#endif \ No newline at end of file diff --git a/src/sfml/InputHandler.cpp b/src/sdl/InputHandler.cpp similarity index 64% rename from src/sfml/InputHandler.cpp rename to src/sdl/InputHandler.cpp index 17254a5..0b47b0b 100644 --- a/src/sfml/InputHandler.cpp +++ b/src/sdl/InputHandler.cpp @@ -3,15 +3,14 @@ #include #include -void InputHandler::addKey(sf::Keyboard::Key key) { +void InputHandler::addKey(SDL_Keycode key) { removeKey(key); // Do this because when a button is held down it adds many into the list and breaks functionality\ // If the key is already in the list this will remove it, if not it wont do anything this->keys.emplace_back(key); } -void InputHandler::removeKey(sf::Keyboard::Key key) { - auto it = std::find(this->keys.begin(), this->keys.end(), key); - if (it != this->keys.end()) { +void InputHandler::removeKey(const SDL_Keycode key) { + if (const auto it = std::find(this->keys.begin(), this->keys.end(), key); it != this->keys.end()) { this->keys.erase(it); } } @@ -23,22 +22,21 @@ void InputHandler::updateLastKeys() { } } -bool InputHandler::isJustPressed(sf::Keyboard::Key key) { +bool InputHandler::isJustPressed(const SDL_Keycode key) { return std::count(this->keys.begin(), this->keys.end(), key) > 0 && std::count(this->lastKeys.begin(), this->lastKeys.end(), key) == 0; } -bool InputHandler::isPressed(sf::Keyboard::Key key) { +bool InputHandler::isPressed(const SDL_Keycode key) { return std::count(this->keys.begin(), this->keys.end(), key); } -void InputHandler::addButton(sf::Mouse::Button button) { +void InputHandler::addButton(SDL_Keycode button) { removeButton(button); this->mouse.emplace_back(button); } -void InputHandler::removeButton(sf::Mouse::Button button) { - auto it = std::find(this->mouse.begin(), this->mouse.end(), button); - if (it != this->mouse.end()) { +void InputHandler::removeButton(const SDL_Keycode button) { + if (const auto it = std::find(this->mouse.begin(), this->mouse.end(), button); it != this->mouse.end()) { this->mouse.erase(it); } } @@ -50,10 +48,10 @@ void InputHandler::updateLastMouse() { } } -bool InputHandler::isJustClicked(sf::Mouse::Button button) { +bool InputHandler::isJustClicked(const SDL_Keycode button) { return std::count(this->mouse.begin(), this->mouse.end(), button) && !std::count(this->lastMouse.begin(), this->lastMouse.end(), button); } -bool InputHandler::isClicked(sf::Mouse::Button button) { +bool InputHandler::isClicked(const SDL_Keycode button) { return std::count(this->mouse.begin(), this->mouse.end(), button); } \ No newline at end of file diff --git a/src/sdl/InputHandler.h b/src/sdl/InputHandler.h new file mode 100644 index 0000000..16210b7 --- /dev/null +++ b/src/sdl/InputHandler.h @@ -0,0 +1,30 @@ +#ifndef INC_8CHOCCHIP_INPUTHANDLER_H +#define INC_8CHOCCHIP_INPUTHANDLER_H + +#include +#include + +class InputHandler { +private: + std::vector keys; + std::vector mouse; + + std::vector lastKeys; + std::vector lastMouse; +public: + InputHandler() = default; + + void addKey(SDL_Keycode key); + void removeKey(SDL_Keycode key); + void updateLastKeys(); + bool isJustPressed(SDL_Keycode key); + bool isPressed(SDL_Keycode key); + + void addButton(SDL_Keycode button); + void removeButton(SDL_Keycode button); + void updateLastMouse(); + bool isJustClicked(SDL_Keycode button); + bool isClicked(SDL_Keycode button); +}; + +#endif \ No newline at end of file diff --git a/src/sdl/MainMenu.cpp b/src/sdl/MainMenu.cpp new file mode 100644 index 0000000..3bc70e9 --- /dev/null +++ b/src/sdl/MainMenu.cpp @@ -0,0 +1,177 @@ +#include "MainMenu.h" + +#include +#include +#include + +#include +#include +#include + +#include "../util/MiscUtil.h" +#include "Emulator.h" + +#define WIDTH (64 * 15) +#define HEIGHT (32 * 15) + +MainMenu::MainMenu(TTF_Font* font, std::string configFilePath, std::unordered_map> &romFiles, + std::vector &romDirectories, std::vector> &windows) : + configFilePath(configFilePath), romDirectories(romDirectories), romFiles(romFiles), windows(windows), font(font) {} + +void MainMenu::init() { + Window::init(); + + this->textEngine = TTF_CreateRendererTextEngine(this->renderer); + + for (auto& thing: this->romFiles) { + for (std::string& file: thing.second) { + + TTF_Text* text = TTF_CreateText(this->textEngine, this->font, MiscUtil::getFileFromPath(file).c_str(), 0); + if (!text) { + SDL_Log("Failed to create text: %s\n", SDL_GetError()); + return; + } + + this->roms.emplace(file, TextButton(0, 25.0f * this->roms.size(), WIDTH, 25, text)); + } + } + + TTF_Text* text = TTF_CreateText(this->textEngine, this->font, "Select ROM", 0); + if (!text) { + SDL_Log("Failed to create text: %s\n", SDL_GetError()); + return; + } + this->chooseFolder = std::make_unique(0, 400, WIDTH, 80, text); +} + +bool MainMenu::handleEvent(SDL_Event &event) { + if (!Window::handleEvent(event)) { + return false; + } + + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + this->inputHandler.addKey(event.key.scancode); + break; + case SDL_EVENT_KEY_UP: + this->inputHandler.removeKey(event.key.scancode); + break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + this->inputHandler.addButton(event.button.button); + break; + case SDL_EVENT_MOUSE_BUTTON_UP: + this->inputHandler.removeButton(event.button.button); + break; + } + + return true; +} + +void MainMenu::update() { + if (!this->mouseFocus) return; + + SDL_FPoint point{}; + SDL_GetMouseState(&point.x, &point.y); + + if (this->inputHandler.isJustPressed(SDL_SCANCODE_F3)) { + this->debug = !this->debug; + } + + for (auto &romButton: this->roms) { + romButton.second.update(this->inputHandler, point); + + if (!romButton.second.isJustClicked()) { + continue; + } + + this->windows.emplace_back(std::make_unique(romButton.first))->init(); + } + this->chooseFolder->update(this->inputHandler, point); + + if (this->chooseFolder->isJustClicked()) { + + if (nfdresult_t result = PickFolder(this->outPath); result == NFD_OKAY) { + libconfig::Config config; + config.readFile(this->configFilePath); + + libconfig::Setting &settings = config.getRoot(); + + if (!settings.exists("directories")) { + settings.add("directories", libconfig::Setting::TypeArray); + } + + libconfig::Setting &directories = settings["directories"]; + directories.add(libconfig::Setting::TypeString) = this->outPath.get(); + + this->romDirectories.emplace_back(this->outPath.get()); + + for (const auto &file: std::filesystem::directory_iterator(this->outPath.get())) { + if (file.is_directory()) { + continue; // TODO: Make sure its a file that can be emulated, at least basic checks so it isn't like + // a word doc + } + + printf("Processing file - : %s\n", file.path().c_str()); + + // Check if the rom directory doesn't exist in romFiles, then add it + if (this->romFiles.find(&this->romDirectories.back()) == this->romFiles.end()) { + this->romFiles.emplace(&this->romDirectories.back(), std::vector()); + } + + // Add the file path to the romFiles entry + this->romFiles.find(&this->romDirectories.back())->second.emplace_back(file.path().string()); + + TTF_Text* text = TTF_CreateText(this->textEngine, this->font, file.path().filename().string().c_str(), 0); + if (!text) { + SDL_Log("Failed to create text: %s\n", SDL_GetError()); + return; + } + + this->roms.emplace(file.path().string(), TextButton(0, 25.0f * this->roms.size(), WIDTH, 25, text)); + } + config.writeFile(this->configFilePath); + } + } + + this->inputHandler.updateLastKeys(); + this->inputHandler.updateLastMouse(); +} + +void MainMenu::render() { + SDL_SetRenderDrawColor(this->renderer, 0, 0, 0, 255); + SDL_RenderClear(this->renderer); + for (auto &romButton: this->roms) { + romButton.second.draw(this->renderer); + } + this->chooseFolder->draw(this->renderer); + + SDL_RenderPresent(this->renderer); +} + +void MainMenu::resize(SDL_Event& event) { + SDL_Point size; + SDL_GetWindowSize(this->window, &size.x, &size.y); + + SDL_FPoint point{}; + SDL_GetMouseState(&point.x, &point.y); + + for (auto& romButton : this->roms) { + romButton.second.updateSize(this->originalSize, size); + romButton.second.update(this->inputHandler, point); + } + + this->chooseFolder->updateSize(this->originalSize,size); + this->chooseFolder->update(this->inputHandler, point); +} + +void MainMenu::close() { + for (const auto& window : this->windows) { + window->close(); + } + TTF_DestroyRendererTextEngine(this->textEngine); + Window::close(); +} + +void MainMenu::refreshRoms() { + +} diff --git a/src/sdl/MainMenu.h b/src/sdl/MainMenu.h new file mode 100644 index 0000000..2f5b1cd --- /dev/null +++ b/src/sdl/MainMenu.h @@ -0,0 +1,47 @@ +#ifndef MAINMENU_H +#define MAINMENU_H + +#include +#include +#include + +#include +#include + +#include "InputHandler.h" +#include "Window.h" +#include "ui/TextButton.h" + +class MainMenu : public Window { +private: + std::string configFilePath; + std::vector& romDirectories; + std::unordered_map>& romFiles; + std::vector>& windows; + + NFD::Guard nfdGuard; + NFD::UniquePath outPath; + + std::unordered_map roms; + std::unique_ptr chooseFolder; + + InputHandler inputHandler{}; + + TTF_TextEngine* textEngine; + TTF_Font* font = nullptr; +public: + MainMenu(TTF_Font* font, std::string configFilePath, std::unordered_map>& romFiles, std::vector& romDirectories, + std::vector>& windows); + + void init() override; + bool handleEvent(SDL_Event& event) override; + void update() override; + void render() override; + void resize(SDL_Event& event) override; + void close() override; + + void refreshRoms(); +}; + +#endif \ No newline at end of file diff --git a/src/sdl/Window.cpp b/src/sdl/Window.cpp new file mode 100644 index 0000000..b8b458b --- /dev/null +++ b/src/sdl/Window.cpp @@ -0,0 +1,102 @@ +#include "Window.h" + +#include + +Window::Window() { +} + +Window::~Window() { + +} + +void Window::init() { + if (!SDL_CreateWindowAndRenderer("8ChocChip - CHIP-8 Emulator", 64 * 15, 32 * 15, SDL_WINDOW_RESIZABLE, &this->window, &this->renderer)) { + std::cerr << "Couldn't create window and renderer. SDL_Error: " << SDL_GetError() << std::endl; + return; + } + + // Set window icon + SDL_Surface* icon = SDL_LoadBMP("assets/icon.bmp"); + if (!icon) { + std::cerr << "Icon could not be loaded! SDL_Error: " << SDL_GetError() << std::endl; + return; + } + SDL_SetWindowIcon(this->window, icon); + SDL_DestroySurface(icon); + + this->windowId = SDL_GetWindowID(this->window); + this->shown = true; + + SDL_GetWindowSize(this->window, &this->originalSize.x, &this->originalSize.y); +} + +bool Window::handleEvent(SDL_Event &event) { + if (event.window.windowID == this->windowId) { + switch (event.type) { + case SDL_EVENT_WINDOW_SHOWN: + this->shown = true; + break; + case SDL_EVENT_WINDOW_HIDDEN: + this->shown = false; + break; + case SDL_EVENT_WINDOW_RESIZED: + resize(event); + break; + case SDL_EVENT_WINDOW_EXPOSED: + SDL_RenderPresent(this->renderer); + break; + case SDL_EVENT_WINDOW_MOUSE_ENTER: + this->mouseFocus = true; + break; + case SDL_EVENT_WINDOW_MOUSE_LEAVE: + this->mouseFocus = false; + break; + case SDL_EVENT_WINDOW_FOCUS_GAINED: + this->keyboardFocus = true; + break; + case SDL_EVENT_WINDOW_FOCUS_LOST: + this->keyboardFocus = false; + break; + case SDL_EVENT_WINDOW_MINIMIZED: + this->minimised = true; + break; + case SDL_EVENT_WINDOW_MAXIMIZED: + this->minimised = false; + break; + case SDL_EVENT_WINDOW_RESTORED: + this->minimised = false; + break; + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + SDL_HideWindow(this->window); + close(); + break; + } + + return true; + } + + return false; +} + + +void Window::update() { +} + +void Window::resize(SDL_Event &event) { + +} + +void Window::close() { + SDL_DestroyRenderer(this->renderer); + SDL_DestroyWindow(this->window); + this->destroyed = true; +} + +bool Window::isShown() const { + return true; +} + +bool Window::isDestroyed() const { + return this->destroyed; +} + diff --git a/src/sdl/Window.h b/src/sdl/Window.h new file mode 100644 index 0000000..68986fc --- /dev/null +++ b/src/sdl/Window.h @@ -0,0 +1,39 @@ +#ifndef WINDOW_H +#define WINDOW_H + +#include + +class Window { +protected: + SDL_Window* window = nullptr; + SDL_Renderer* renderer = nullptr; + unsigned int windowId = -1; + SDL_Point originalSize; + + int height = 720; + int width = 480; + + bool mouseFocus = false; + bool keyboardFocus = false; + bool fullscreen = false; + bool minimised = false; + bool shown = false; + bool destroyed = false; + + bool debug = false; +public: + Window(); + virtual ~Window(); + + virtual void init(); + virtual bool handleEvent(SDL_Event& event); + virtual void update(); + virtual void render() = 0; + virtual void resize(SDL_Event& event); + virtual void close(); + + bool isShown() const; + bool isDestroyed() const; +}; + +#endif \ No newline at end of file diff --git a/src/sdl/emulator/CMakeLists.txt b/src/sdl/emulator/CMakeLists.txt new file mode 100644 index 0000000..161b3f7 --- /dev/null +++ b/src/sdl/emulator/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources(8ChocChip PRIVATE + SdlSpeaker.cpp + SdlSpeaker.h + SdlRenderer.cpp + SdlRenderer.h + SdlKeyboard.cpp + SdlKeyboard.h) \ No newline at end of file diff --git a/src/sdl/emulator/SdlKeyboard.cpp b/src/sdl/emulator/SdlKeyboard.cpp new file mode 100644 index 0000000..045bcef --- /dev/null +++ b/src/sdl/emulator/SdlKeyboard.cpp @@ -0,0 +1,19 @@ +#include "SdlKeyboard.h" + +void SdlKeyboard::handleKeyDown(const uint8_t keyCode) { + if (const auto keyMapIter = this->KEYMAP.find(keyCode); keyMapIter != this->KEYMAP.end()) { + const uint8_t key = keyMapIter->second; + this->keysPressed[key] = true; + if (this->onNextKeyPress) { + this->onNextKeyPress(key); + this->onNextKeyPress = nullptr; + } + } +} + +void SdlKeyboard::handleKeyUp(uint8_t keyCode) { + if (const auto keyMapIter = this->KEYMAP.find(keyCode); keyMapIter != this->KEYMAP.end()) { + const uint8_t key = keyMapIter->second; + this->keysPressed[key] = false; + } +} diff --git a/src/sdl/emulator/SdlKeyboard.h b/src/sdl/emulator/SdlKeyboard.h new file mode 100644 index 0000000..ca3491d --- /dev/null +++ b/src/sdl/emulator/SdlKeyboard.h @@ -0,0 +1,12 @@ +#ifndef INC_8CHOCCHIP_SDLKEYBOARD_H +#define INC_8CHOCCHIP_SDLKEYBOARD_H + +#include "../../emulator/Keyboard.h" + +class SdlKeyboard : public Keyboard { +public: + void handleKeyDown(uint8_t keyCode); + void handleKeyUp(uint8_t keyCode); +}; + +#endif \ No newline at end of file diff --git a/src/sdl/emulator/SdlRenderer.cpp b/src/sdl/emulator/SdlRenderer.cpp new file mode 100644 index 0000000..fd7c6e3 --- /dev/null +++ b/src/sdl/emulator/SdlRenderer.cpp @@ -0,0 +1,11 @@ +#include "SdlRenderer.h" + +void SdlRenderer::drawPixel(SDL_Renderer* renderer, const uint16_t x, const uint16_t y) { + const SDL_FRect rect = {x * this->scale, y * this->scale, this->scale, this->scale}; + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); // Set color to white + SDL_RenderFillRect(renderer, &rect); +} + +float SdlRenderer::getScale() const { + return this->scale; +} \ No newline at end of file diff --git a/src/sdl/emulator/SdlRenderer.h b/src/sdl/emulator/SdlRenderer.h new file mode 100644 index 0000000..408cddc --- /dev/null +++ b/src/sdl/emulator/SdlRenderer.h @@ -0,0 +1,15 @@ +#ifndef INC_8CHOCCHIP_SDLRENDERER_H +#define INC_8CHOCCHIP_SDLRENDERER_H + +#include "../../emulator/Renderer.h" +#include + +class SdlRenderer : public Renderer { +private: + const float scale = 15; +public: + void drawPixel(SDL_Renderer* renderer, uint16_t x, uint16_t y) override; + float getScale() const; +}; + +#endif \ No newline at end of file diff --git a/src/sdl/emulator/SdlSpeaker.cpp b/src/sdl/emulator/SdlSpeaker.cpp new file mode 100644 index 0000000..a068e79 --- /dev/null +++ b/src/sdl/emulator/SdlSpeaker.cpp @@ -0,0 +1,54 @@ +#include "SdlSpeaker.h" + +#include +#include +#include +#include + +#include + +SdlSpeaker::SdlSpeaker() : amplitude(28000), sampleRate(44100), frequency(220), duration(500), sampleCount((sampleRate * duration) / 1000), beepData(sampleCount) { + SDL_AudioSpec desiredSpec; + SDL_zero(desiredSpec); + desiredSpec.freq = this->sampleRate; + desiredSpec.format = SDL_AUDIO_S16; + desiredSpec.channels = 1; + + this->audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desiredSpec, nullptr, nullptr); + if (this->audioStream == nullptr) { + std::cerr << "Failed to open audio device stream: " << SDL_GetError() << std::endl; + return; + } + + generateSample(); +} + +SdlSpeaker::~SdlSpeaker() { + if (this->audioStream != nullptr) { + SDL_DestroyAudioStream(this->audioStream); + } +} + +void SdlSpeaker::play() { + if (this->audioStream != nullptr) { + if (SDL_GetAudioStreamAvailable(this->audioStream) == 0) { + generateSample(); + } + SDL_ResumeAudioStreamDevice(this->audioStream); + } +} + +void SdlSpeaker::stop() { + if (this->audioStream != nullptr) { + SDL_PauseAudioStreamDevice(this->audioStream); + } +} + +void SdlSpeaker::generateSample() { + for (int i = 0; i < this->sampleCount; ++i) { + this->beepData[i] = static_cast(this->amplitude * std::sin(2.0 * std::acos(-1) * this->frequency * i / this->sampleRate)); + } + if (SDL_PutAudioStreamData(this->audioStream, this->beepData.data(), this->beepData.size() * sizeof(int16_t)) < 0) { + std::cerr << "Failed to put data into audio stream: " << SDL_GetError() << std::endl; + } +} \ No newline at end of file diff --git a/src/sdl/emulator/SdlSpeaker.h b/src/sdl/emulator/SdlSpeaker.h new file mode 100644 index 0000000..49de784 --- /dev/null +++ b/src/sdl/emulator/SdlSpeaker.h @@ -0,0 +1,30 @@ +#ifndef INC_8CHOCCHIP_SDLSPEAKER_H +#define INC_8CHOCCHIP_SDLSPEAKER_H + +#include + +#include + +#include "../../emulator/Speaker.h" + +class SdlSpeaker : public Speaker { +private: + int amplitude; + int sampleRate; + int frequency; + int duration; + int sampleCount; + + SDL_AudioSpec audioSpec; + SDL_AudioStream* audioStream; + std::vector beepData; +public: + SdlSpeaker(); + ~SdlSpeaker(); + + void play() override; + void stop() override; + void generateSample(); +}; + +#endif \ No newline at end of file diff --git a/src/sfml/ui/CMakeLists.txt b/src/sdl/ui/CMakeLists.txt similarity index 71% rename from src/sfml/ui/CMakeLists.txt rename to src/sdl/ui/CMakeLists.txt index c7c212a..79ff477 100644 --- a/src/sfml/ui/CMakeLists.txt +++ b/src/sdl/ui/CMakeLists.txt @@ -1,3 +1,4 @@ target_sources(8ChocChip PRIVATE TextButton.cpp - TextButton.h) \ No newline at end of file + TextButton.h +) \ No newline at end of file diff --git a/src/sdl/ui/TextButton.cpp b/src/sdl/ui/TextButton.cpp new file mode 100644 index 0000000..aa72757 --- /dev/null +++ b/src/sdl/ui/TextButton.cpp @@ -0,0 +1,73 @@ +#include "TextButton.h" + +#include + +TextButton::TextButton(const float x, const float y, const float width, const float height, TTF_Text* text) : + text(text), idleColor(SDL_Color{192, 192, 192, 255}), + hoverColor(SDL_Color{128, 128, 128, 255}), activeColor(SDL_Color{64, 64, 64, 255}), + deleteColor(SDL_Color{255, 0, 0, 255}) { + this->button = SDL_FRect{x, y, width, height}; + + this->currentColor = this->idleColor; + + TTF_SetTextColor(text, 0, 0, 0, 255); + SDL_Point textSize{}; + TTF_GetTextSize(text, &textSize.x, &textSize.y); + this->textPos.x = x + width / 2 - textSize.x / 2; + this->textPos.y = y + height / 2 - textSize.y / 2 + 4; + + this->originalPosition = {this->button.x, this->button.y}; + this->originalSize = {this->button.w, this->button.h}; + + this->isPressed = false; + this->isHovered = false; +} + +void TextButton::updateSize(const SDL_Point& originalSize, const SDL_Point& updatedSize) { + this->button.x = this->originalPosition.x / originalSize.x * updatedSize.x; + this->button.y = this->originalPosition.y / originalSize.y * updatedSize.y; + this->button.w = this->originalSize.x / originalSize.x * updatedSize.x; + this->button.h = this->originalSize.y / originalSize.y * updatedSize.y; + + SDL_Point textSize{}; + TTF_GetTextSize(this->text, &textSize.x, &textSize.y); + this->textPos.x = this->button.x + this->button.w / 2 - textSize.x / 2; + this->textPos.y = this->button.y + this->button.h / 2 - textSize.y / 2 + 4; +} + +void TextButton::update(InputHandler& inputHandler, const SDL_FPoint & pos) { + this->lastPressed = this->isPressed; + this->isHovered = SDL_PointInRectFloat(&pos, &this->button); + + if (this->isHovered && inputHandler.isJustClicked(SDL_BUTTON_LEFT)) { + this->isPressed = true; + updateColour(this->activeColor); + } else if (this->isHovered && inputHandler.isPressed(SDL_SCANCODE_LSHIFT)) { + updateColour(this->deleteColor); + } else if (this->isHovered) { + updateColour(this->hoverColor); + } else { + this->isPressed = false; + updateColour(this->idleColor); + } +} + +void TextButton::updateColour(const SDL_Color & color) { + this->currentColor = color; +} + +void TextButton::draw(SDL_Renderer* window) const { + SDL_SetRenderDrawColor(window, this->currentColor.r, this->currentColor.g, this->currentColor.b, this->currentColor.a); + SDL_RenderFillRect(window, &this->button); + if (!TTF_DrawRendererText(this->text, this->textPos.x, this->textPos.y)) { + SDL_Log("Text rendering failed: %s\n", SDL_GetError()); + } +} + +bool TextButton::isClicked() const { + return this->isPressed; +} + +bool TextButton::isJustClicked() const { + return !this->lastPressed && this->isPressed; +} \ No newline at end of file diff --git a/src/sdl/ui/TextButton.h b/src/sdl/ui/TextButton.h new file mode 100644 index 0000000..10cb930 --- /dev/null +++ b/src/sdl/ui/TextButton.h @@ -0,0 +1,41 @@ +#ifndef TEXTBUTTON_H +#define TEXTBUTTON_H + +#include "../InputHandler.h" + +#include +#include + +class TextButton { +private: + SDL_FRect button; + SDL_FPoint originalSize; + SDL_FPoint originalPosition; + TTF_Text* text; + SDL_FPoint textPos; + SDL_Color idleColor; + SDL_Color hoverColor; + SDL_Color activeColor; + SDL_Color deleteColor; + SDL_Color currentColor; + bool isPressed; + bool lastPressed{}; + bool isHovered; + +public: + TextButton(float x, float y, float width, float height, TTF_Text* text); + + void updateSize(const SDL_Point & originalSize, const SDL_Point & updatedSize); + + void update(InputHandler& inputHandler, const SDL_FPoint & pos); + + void draw(SDL_Renderer* window) const; + + bool isClicked() const; + + bool isJustClicked() const; + + void updateColour(const SDL_Color & color); +}; + +#endif \ No newline at end of file diff --git a/src/sfml/Emulator.cpp b/src/sfml/Emulator.cpp deleted file mode 100644 index 834bd84..0000000 --- a/src/sfml/Emulator.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "Emulator.h" - -#include -#include - -#include "../emulator/Cpu.h" -#include "../emulator/Keyboard.h" -#include "emulator/SfmlRenderer.h" -#include "emulator/SfmlSpeaker.h" - -int Emulator::launch(const std::string &rom) { - if (rom.empty()) { - std::cerr << "No ROM file has been specified :(" << std::endl; - return 1; - } - - std::ifstream file(rom, std::ios::binary | std::ios::ate); - if (!file.good()) { - std::cerr << "Can not find file " << rom << std::endl; - return 1; - } - - sf::RenderWindow window(sf::VideoMode(64 * 15, 32 * 15), "8ChocChip - CHIP-8 Emulator", sf::Style::Titlebar | sf::Style::Close); - sf::Image icon; - icon.loadFromFile("../../assets/icon.png"); - window.setIcon(64, 64, icon.getPixelsPtr()); - window.setFramerateLimit(60); - - SfmlRenderer renderer(&window); - - SfmlSpeaker speaker; - Keyboard keyboard; - Cpu cpu(&renderer, &keyboard, &speaker); - - cpu.loadSpritesIntoMemory(); - - cpu.loadProgramIntoMemory(&file); - - bool focus; - - while (window.isOpen()) { - sf::Event event{}; - while (window.pollEvent(event)) { - if (event.type == sf::Event::Closed) { - window.close(); - } else if (event.type == sf::Event::LostFocus) { - focus = false; - } else if (event.type == sf::Event::GainedFocus) { - focus = true; - } - - if (!focus) continue; - - // TODO: You can hold down the button and it will run at the next possible point - // fix this but make it a little easier to time because otherwise you need to time it perfectly which doesnt feel good - // ^ if the InputHandler was to be used anyway - // Handle keyboard inputs - if (event.type == sf::Event::KeyPressed) { - keyboard.handleKeyDown(event.key.code); - } else if (event.type == sf::Event::KeyReleased) { - keyboard.handleKeyUp(event.key.code); - } - } - - // Run a cycle of the emulator - cpu.cycle(); - - // Clear the window - window.clear(sf::Color::Black); - - // Render the window - renderer.render(); - window.display(); - } - - return 0; -} diff --git a/src/sfml/Emulator.h b/src/sfml/Emulator.h deleted file mode 100644 index 46ed1cc..0000000 --- a/src/sfml/Emulator.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef INC_8CHOCCHIP_EMULATOR_H -#define INC_8CHOCCHIP_EMULATOR_H - -#include - -class Emulator { -private: -public: - int launch(const std::string& rom); -}; - - -#endif // INC_8CHOCCHIP_EMULATOR_H diff --git a/src/sfml/InputHandler.h b/src/sfml/InputHandler.h deleted file mode 100644 index eb01dfc..0000000 --- a/src/sfml/InputHandler.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef INC_8CHOCCHIP_INPUTHANDLER_H -#define INC_8CHOCCHIP_INPUTHANDLER_H - -#include - -#include "SFML/Window/Keyboard.hpp" -#include "SFML/Window/Mouse.hpp" - -class InputHandler { -private: - std::vector keys; - std::vector mouse; - - std::vector lastKeys; - std::vector lastMouse; -public: - InputHandler() = default; - - void addKey(sf::Keyboard::Key key); - void removeKey(sf::Keyboard::Key key); - void updateLastKeys(); - bool isJustPressed(sf::Keyboard::Key key); - bool isPressed(sf::Keyboard::Key key); - - void addButton(sf::Mouse::Button button); - void removeButton(sf::Mouse::Button button); - void updateLastMouse(); - bool isJustClicked(sf::Mouse::Button button); - bool isClicked(sf::Mouse::Button button); -}; - - -#endif // INC_8CHOCCHIP_INPUTHANDLER_H diff --git a/src/sfml/MainMenu.cpp b/src/sfml/MainMenu.cpp deleted file mode 100644 index eb13f24..0000000 --- a/src/sfml/MainMenu.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#include "MainMenu.h" - -#include -#include - -#include "libconfig.h++" -#include "nfd.hpp" - -#include "Emulator.h" -#include "ui/TextButton.h" -#include "../util/MiscUtil.h" - -MainMenu::MainMenu(std::unordered_map>& romFiles, - std::vector& romDirectories, std::vector>& windows, - std::string configFilePath) : - window(sf::VideoMode(640, 480), "8ChocChip - Chip8 Emulator"), windows(windows), - romDirectories(romDirectories), romFiles(romFiles), inputHandler() { - - NFD::Guard nfdGuard; - - // auto-freeing memory - NFD::UniquePath outPath; - - sf::Vector2u originalWindowSize = this->window.getSize(); - sf::Image icon; - icon.loadFromFile("assets/icon.png"); - this->window.setIcon(64, 64, icon.getPixelsPtr()); -// this->window.setVerticalSyncEnabled(true); - - sf::Font font; - font.loadFromFile("assets/font.ttf"); - - std::unordered_map roms; - - for (auto& thing : romFiles) { - for (std::string& file : thing.second) { - - TextButton romButton(0, 25.0f * roms.size(), this->window.getSize().x, 25, MiscUtil::getFileFromPath(file), &font); - - roms.emplace(file, romButton); - } - } - - TextButton button(0, 400, 640, 80, "Select ROM", &font); - - bool focus; - bool debug = false; - - auto start = std::chrono::high_resolution_clock::now(); - auto end = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_seconds{}; - - // Variables to count frames - int frames = 0; - double fps; - - while (this->window.isOpen()) { - sf::Event event{}; - sf::Vector2i pos = sf::Mouse::getPosition(this->window); - - this->inputHandler.updateLastKeys(); - this->inputHandler.updateLastMouse(); - - while (this->window.pollEvent(event)) { - if (event.type == sf::Event::Closed) { - this->window.close(); - } else if (event.type == sf::Event::LostFocus) { - focus = false; - } else if (event.type == sf::Event::GainedFocus) { - focus = true; - } else if (event.type == sf::Event::Resized) { - sf::FloatRect visibleArea(0, 0, event.size.width, event.size.height); - this->window.setView(sf::View(visibleArea)); - - for (auto& romButton : roms) { - romButton.second.updateSize(originalWindowSize, this->window.getSize()); - romButton.second.update(&this->inputHandler, pos); - } - - button.updateSize(originalWindowSize, this->window.getSize()); - button.update(&this->inputHandler, pos); - } else if (event.type == sf::Event::KeyPressed) { - this->inputHandler.addKey(event.key.code); - } else if (event.type == sf::Event::KeyReleased) { - this->inputHandler.removeKey(event.key.code); - } else if (event.type == sf::Event::MouseButtonPressed) { - this->inputHandler.addButton(event.mouseButton.button); - } else if (event.type == sf::Event::MouseButtonReleased) { - this->inputHandler.removeButton(event.mouseButton.button); - } - } - - if (focus) { - if (this->inputHandler.isJustPressed(sf::Keyboard::F3)) { - debug = !debug; - } - - for (auto& romButton : roms) { - romButton.second.update(&this->inputHandler, pos); - - if (!romButton.second.isJustClicked()) continue; - - Emulator emulator; - std::thread newWindow(&Emulator::launch, &emulator, romButton.first); - newWindow.detach(); - windows.emplace_back(&newWindow); - } - button.update(&this->inputHandler, pos); - - if (button.isJustClicked()) { - - if (nfdresult_t result = PickFolder(outPath); result == NFD_OKAY) { - libconfig::Config config; - config.readFile(configFilePath); - - libconfig::Setting& settings = config.getRoot(); - - if (!settings.exists("directories")) { - settings.add("directories", libconfig::Setting::TypeArray); - } - - libconfig::Setting& directories = settings["directories"]; - directories.add(libconfig::Setting::TypeString) = outPath.get(); - - romDirectories.emplace_back(outPath.get()); - - for (const auto& file : std::filesystem::directory_iterator(outPath.get())) { - if (file.is_directory()) continue; // TODO: Make sure its a file that can be emulated, at least basic checks so it isn't like a word doc - - printf("Processing file - : %s\n", file.path().c_str()); - - // Check if the rom directory doesn't exist in romFiles, then add it - if (romFiles.find(&romDirectories.back()) == romFiles.end()) { - romFiles.emplace(&romDirectories.back(), std::vector()); - } - - // Add the file path to the romFiles entry - romFiles.find(&romDirectories.back())->second.emplace_back(file.path().string()); - - TextButton romButton(0, 25.0f * roms.size(), this->window.getSize().x, 25, file.path().filename().string(), &font); - roms.emplace(file.path().string(), romButton); - } - config.writeFile(configFilePath); - } - } - } - - this->window.clear(sf::Color::White); - for (auto& romButton : roms) { - romButton.second.draw(this->window); - } - button.draw(this->window); - - this->window.display(); - - frames++; - - end = std::chrono::high_resolution_clock::now(); - elapsed_seconds = end - start; - - // If elapsed time is greater than or equal to 1 second - if (debug && elapsed_seconds.count() >= 1.0) { - // Calculate FPS - fps = frames / elapsed_seconds.count(); - - // Output FPS - std::cout << "FPS: " << fps << std::endl; - - // Reset frame count and start time - frames = 0; - start = std::chrono::high_resolution_clock::now(); - } - } -} diff --git a/src/sfml/MainMenu.h b/src/sfml/MainMenu.h deleted file mode 100644 index 9d5ec51..0000000 --- a/src/sfml/MainMenu.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef INC_8CHOCCHIP_MAINMENU_H -#define INC_8CHOCCHIP_MAINMENU_H - -#include -#include - -#include "SFML/Graphics.hpp" -#include "InputHandler.h" - -class MainMenu { -private: - sf::RenderWindow window; - std::vector>& windows; - - std::vector& romDirectories; - std::unordered_map>& romFiles; - - InputHandler inputHandler; -public: - MainMenu(std::unordered_map>& romFiles, - std::vector& romDirectories, std::vector>& windows, - std::string configFilePath); -}; - - -#endif // INC_8CHOCCHIP_MAINMENU_H diff --git a/src/sfml/emulator/CMakeLists.txt b/src/sfml/emulator/CMakeLists.txt deleted file mode 100644 index 9c17dbd..0000000 --- a/src/sfml/emulator/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -target_sources(8ChocChip PRIVATE - SfmlSpeaker.cpp - SfmlSpeaker.h - SfmlRenderer.cpp - SfmlRenderer.h) \ No newline at end of file diff --git a/src/sfml/emulator/SfmlRenderer.cpp b/src/sfml/emulator/SfmlRenderer.cpp deleted file mode 100644 index cfbeacc..0000000 --- a/src/sfml/emulator/SfmlRenderer.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "SfmlRenderer.h" - -SfmlRenderer::SfmlRenderer(sf::RenderWindow* window) : window(window) { - this->scale = 15; // Scale up because 64 x 32 would be tiny on our screens now -} - -void SfmlRenderer::drawPixel(const uint16_t x, const uint16_t y) { - sf::RectangleShape rectangle(sf::Vector2f(getScale(), getScale())); - rectangle.setPosition(x * getScale(), y * getScale()); - rectangle.setFillColor(sf::Color::White); - - this->window->draw(rectangle); -} - -uint8_t SfmlRenderer::getScale() const { - return this->scale; -} \ No newline at end of file diff --git a/src/sfml/emulator/SfmlRenderer.h b/src/sfml/emulator/SfmlRenderer.h deleted file mode 100644 index 0019848..0000000 --- a/src/sfml/emulator/SfmlRenderer.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef INC_8CHOCCHIP_SFMLRENDERER_H -#define INC_8CHOCCHIP_SFMLRENDERER_H - -#include "SFML/Graphics.hpp" - -#include "../../emulator/Renderer.h" - -class SfmlRenderer : public Renderer { -private: - sf::RenderWindow* window; - - uint8_t scale; -public: - SfmlRenderer(sf::RenderWindow* window); - - void drawPixel(uint16_t x, uint16_t y) override; - uint8_t getScale() const; -}; - -#endif // INC_8CHOCCHIP_SFMLRENDERER_H diff --git a/src/sfml/emulator/SfmlSpeaker.cpp b/src/sfml/emulator/SfmlSpeaker.cpp deleted file mode 100644 index 280b5bd..0000000 --- a/src/sfml/emulator/SfmlSpeaker.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "SfmlSpeaker.h" - -#include - -SfmlSpeaker::SfmlSpeaker() { - - this->sound.setVolume(100.f); - this->sound.setLoop(true); - - constexpr unsigned int sampleRate = 44100; - sf::Int16 samples[sampleRate]; - for (unsigned int i = 0; i < sampleRate; ++i) { - constexpr unsigned int frequency = 440; - samples[i] = static_cast(32767 * std::sin(2 * 3.14159265 * frequency * i / sampleRate)); // Mmmm yum pi - } - this->soundBuffer.loadFromSamples(samples, sampleRate, 1, sampleRate); - - this->sound.setBuffer(this->soundBuffer); -} - -void SfmlSpeaker::play() { - this->sound.play(); -} - -void SfmlSpeaker::stop() { - this->sound.stop(); -} - diff --git a/src/sfml/emulator/SfmlSpeaker.h b/src/sfml/emulator/SfmlSpeaker.h deleted file mode 100644 index 83888d8..0000000 --- a/src/sfml/emulator/SfmlSpeaker.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef INC_8CHOCCHIP_SFMLSPEAKER_H -#define INC_8CHOCCHIP_SFMLSPEAKER_H - -#include "SFML/Audio.hpp" - -#include "../../emulator/Speaker.h" - -class SfmlSpeaker : public Speaker { -private: - sf::SoundBuffer soundBuffer; - sf::Sound sound; -public: - SfmlSpeaker(); - - void play() override; - void stop() override; -}; - -#endif // INC_8CHOCCHIP_SFMLSPEAKER_H diff --git a/src/sfml/ui/TextButton.cpp b/src/sfml/ui/TextButton.cpp deleted file mode 100644 index 7c56856..0000000 --- a/src/sfml/ui/TextButton.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "TextButton.h" - -TextButton::TextButton(float x, float y, float width, float height, const std::string& buttonText, sf::Font* font) { - this->button.setSize(sf::Vector2f(width, height)); - this->button.setPosition(sf::Vector2f(x, y)); - - this->originalPosition = this->button.getPosition(); - this->originalSize = this->button.getSize(); - - this->text.setFont(*font); - this->text.setString(buttonText); - this->text.setCharacterSize(20); - this->text.setFillColor(sf::Color::Black); - this->text.setPosition(x + width / 2 - this->text.getGlobalBounds().width / 2, y + height / 2 - this->text.getGlobalBounds().height / 2); - - this->idleColor = sf::Color(192, 192, 192); - this->hoverColor = sf::Color(128, 128, 128); - this->activeColor = sf::Color(64, 64, 64); - - this->button.setFillColor(this->idleColor); - - this->isPressed = false; - this->isHovered = false; -} - -void TextButton::updateSize(const sf::Vector2u originalSize, const sf::Vector2u updatedSize) { - this->button.setSize(sf::Vector2f(this->originalSize.x / originalSize.x * updatedSize.x, this->originalSize.y / originalSize.y * updatedSize.y)); - this->button.setPosition(this->originalPosition.x / originalSize.x * updatedSize.x, this->originalPosition.y / originalSize.y * updatedSize.y); - - this->text.setPosition(this->button.getPosition().x + this->button.getSize().x / 2 - this->text.getGlobalBounds().width / 2, this->button.getPosition().y + this->button.getSize().y / 2 - this->text.getGlobalBounds().height / 2); -} - -void TextButton::update(InputHandler* inputHandler, sf::Vector2i pos) { - this->lastPressed = this->isPressed; - this->isHovered = this->button.getGlobalBounds().contains(pos.x, pos.y); - - if (this->isHovered && inputHandler->isJustClicked(sf::Mouse::Left)) { - this->isPressed = true; - updateColour(this->activeColor); - } else if (this->isHovered && inputHandler->isPressed(sf::Keyboard::Key::LShift)) { - updateColour( sf::Color(255, 0, 0)); - } else if (this->isHovered) { - updateColour(this->hoverColor); - } else { - this->isPressed = false; - updateColour(this->idleColor); - } -} - -void TextButton::updateColour(const sf::Color color) { - if (this->color == color) { - return; - } - - this->button.setFillColor(color); -} - -void TextButton::draw(sf::RenderWindow &window) const { - window.draw(this->button); - window.draw(this->text); -} - -bool TextButton::isClicked() const { - return this->isPressed; -} - -bool TextButton::isJustClicked() const { - return !this->lastPressed && this->isPressed; -} \ No newline at end of file diff --git a/src/sfml/ui/TextButton.h b/src/sfml/ui/TextButton.h deleted file mode 100644 index 7e3491a..0000000 --- a/src/sfml/ui/TextButton.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef TEXTBUTTON_H -#define TEXTBUTTON_H - -#include "SFML/Graphics.hpp" - -#include "../InputHandler.h" - -class TextButton { -private: - sf::RectangleShape button; - sf::Vector2f originalSize; - sf::Vector2f originalPosition; - sf::Text text; - sf::Font* font{}; - sf::Color color; - sf::Color idleColor; - sf::Color hoverColor; - sf::Color activeColor; - bool isPressed; - bool lastPressed{}; - bool isHovered; - -public: - TextButton(float x, float y, float width, float height, const std::string& buttonText, sf::Font *font); - - void updateSize(sf::Vector2u originalSize, sf::Vector2u updatedSize); - - void update(InputHandler* inputHandler, sf::Vector2i pos); - - void draw(sf::RenderWindow& window) const; - - bool isClicked() const; - - bool isJustClicked() const; - - void updateColour(sf::Color color); -}; - - - -#endif //TEXTBUTTON_H diff --git a/src/util/MiscUtil.h b/src/util/MiscUtil.h index b2591a8..d67726e 100644 --- a/src/util/MiscUtil.h +++ b/src/util/MiscUtil.h @@ -11,4 +11,4 @@ class MiscUtil { static std::string getFileFromPath(std::string path); }; -#endif //MISCUTIL_H +#endif \ No newline at end of file