diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | app/CMakeLists.txt | 13 | ||||
| -rw-r--r-- | app/sdl/CMakeLists.txt | 18 | ||||
| -rw-r--r-- | app/sdl/impl/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | app/sdl/impl/display-sdl.cc | 16 | ||||
| -rw-r--r-- | app/sdl/impl/display-sdl.hh | 24 | ||||
| -rw-r--r-- | app/sdl/impl/file-browser-posix.cc | 139 | ||||
| -rw-r--r-- | app/sdl/impl/file-browser-posix.hh | 30 | ||||
| -rw-r--r-- | app/sdl/impl/input-device-keyboard-sdl.cc | 97 | ||||
| -rw-r--r-- | app/sdl/impl/input-device-keyboard-sdl.hh | 28 | ||||
| -rw-r--r-- | app/sdl/main.cc | 40 | ||||
| -rw-r--r-- | app/sdl/state.cc | 89 | ||||
| -rw-r--r-- | app/sdl/state.hh | 37 | ||||
| -rw-r--r-- | flake.nix | 2 | ||||
| -rw-r--r-- | lib/nes/app/application.cc | 3 |
15 files changed, 543 insertions, 3 deletions
@@ -2,3 +2,4 @@ cmake-build-*/ build/ .vscode/ .idea/ +.cache/ diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index f3e864f..b75615b 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,3 +1,14 @@ if(APPLE) + message(STATUS "Building macOS port.") add_subdirectory(mac) -endif()
\ No newline at end of file +else() + message(STATUS "macOS port disabled.") +endif() + +find_package(SDL3 CONFIG COMPONENTS SDL3-shared) +if(SDL3_FOUND) + message(STATUS "Building SDL3 port.") + add_subdirectory(sdl) +else() + message(STATUS "SDL3 port disabled.") +endif() diff --git a/app/sdl/CMakeLists.txt b/app/sdl/CMakeLists.txt new file mode 100644 index 0000000..c311928 --- /dev/null +++ b/app/sdl/CMakeLists.txt @@ -0,0 +1,18 @@ +add_executable(nes_app_sdl) + +target_include_directories(nes_app_sdl PRIVATE .) +target_link_libraries( + nes_app_sdl + PRIVATE + nes::options + nes::nes + SDL3::SDL3) + +target_sources( + nes_app_sdl + PRIVATE + main.cc + state.hh + state.cc) + +add_subdirectory(impl) diff --git a/app/sdl/impl/CMakeLists.txt b/app/sdl/impl/CMakeLists.txt new file mode 100644 index 0000000..6ba1824 --- /dev/null +++ b/app/sdl/impl/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources( + nes_app_sdl + PRIVATE + display-sdl.hh + display-sdl.cc + input-device-keyboard-sdl.hh + input-device-keyboard-sdl.cc + file-browser-posix.hh + file-browser-posix.cc) diff --git a/app/sdl/impl/display-sdl.cc b/app/sdl/impl/display-sdl.cc new file mode 100644 index 0000000..d8e22d1 --- /dev/null +++ b/app/sdl/impl/display-sdl.cc @@ -0,0 +1,16 @@ +#include "impl/display-sdl.hh" + +namespace nes::app::sdl +{ + auto display_sdl::set(u32 const x, u32 const y, rgb const color) -> void + { + buffer_back_[(y * width) + x] = (color.r << 24) | (color.g << 16) | (color.b << 8); + } + + auto display_sdl::switch_buffers() -> void + { + if (!texture_) { return; } + + SDL_UpdateTexture(texture_, nullptr, buffer_back_, width * 4); + } +} // namespace nes::app::sdl diff --git a/app/sdl/impl/display-sdl.hh b/app/sdl/impl/display-sdl.hh new file mode 100644 index 0000000..9310eca --- /dev/null +++ b/app/sdl/impl/display-sdl.hh @@ -0,0 +1,24 @@ +#pragma once + +#include "nes/common/display.hh" + +#include <SDL3/SDL.h> + +namespace nes::app::sdl +{ + /// Display implementation rendering contents into an SDL texture. + class display_sdl final : public display + { + SDL_Texture* texture_{ nullptr }; + u32 buffer_back_[width * height]{}; + + public: + explicit display_sdl() = default; + + auto get_texture() const -> SDL_Texture* { return texture_; } + auto set_texture(SDL_Texture* v) -> void { texture_ = v; } + + auto switch_buffers() -> void override; + auto set(u32 x, u32 y, rgb value) -> void override; + }; +} // namespace nes::app::sdl diff --git a/app/sdl/impl/file-browser-posix.cc b/app/sdl/impl/file-browser-posix.cc new file mode 100644 index 0000000..50de9e1 --- /dev/null +++ b/app/sdl/impl/file-browser-posix.cc @@ -0,0 +1,139 @@ +#include "impl/file-browser-posix.hh" +#include <string> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdlib.h> + +namespace nes::app::sdl +{ + file_browser_posix::file_browser_posix() + { + auto const initial_path = getenv("HOME"); + if (initial_path) + { + for (auto const component : path_view{ initial_path }) + { + if (path_.push(component) != status::success) { break; } + } + } + + reopen_directory(path_.get_path()); + } + + file_browser_posix::~file_browser_posix() + { + if (it_) { closedir(it_); } + } + + auto file_browser_posix::seek(u32 const pos) -> void + { + if (it_pos_ > pos) + { + reopen_directory(path_.get_path()); + } + + while (it_pos_ < pos) + { + if (!read_next(nullptr)) { break; } + } + } + + auto file_browser_posix::read_next(entry* out_entry) -> bool + { + if (!it_) { return false; } + + while (true) + { + errno = 0; + auto const ent = readdir(it_); + if (!ent) + { + if (errno) { perror("file_browser_posix: readdir"); } + return false; + } + + auto const name = string_view{ ent->d_name }; + if (name.get_length() > max_name_length) { continue; } + if (name[0] == '.') { continue; } + + auto type = entry_type{}; + if (ent->d_type == DT_DIR) { type = entry_type::directory; } + else if (ent->d_type == DT_REG) { type = entry_type::file; } + else { continue; } + + it_pos_ += 1; + if (out_entry) { *out_entry = entry{ type, name }; } + return true; + } + } + + auto file_browser_posix::navigate_up() -> status + { + auto new_path = path_; + if (auto const s = new_path.pop(); s != status::success) { return s; } + if (auto const s = reopen_directory(new_path.get_path()); s != status::success) { return s; } + path_ = new_path; + return status::success; + } + + auto file_browser_posix::navigate(string_view const item) -> status + { + auto new_path = path_; + if (auto const s = new_path.push(item); s != status::success) { return s; } + if (auto const s = reopen_directory(new_path.get_path()); s != status::success) { return s; } + path_ = new_path; + return status::success; + } + + auto file_browser_posix::load(string_view const item, span<u8> const buffer, u32* out_length) -> status + { + auto file_path = path_; + if (auto const s = file_path.push(item); s != status::success) { return s; } + + auto const file_path_str = std::string{ file_path.get_path().get_data(), file_path.get_path().get_length() }; + auto const fd = open(file_path_str.c_str(), O_RDONLY); + if (fd == -1) + { + perror("file_browser_posix: open"); + return status::error_system_error; + } + + auto const res = read(fd, buffer.get_data(), buffer.get_length()); + if (res < 0) + { + perror("file_browser_posix: read"); + close(fd); + return status::error_system_error; + } + if (out_length) { *out_length = static_cast<u32>(res); } + + auto remaining = char{}; + if (read(fd, &remaining, 1) != 0) + { + // File was too long. + close(fd); + return status::error_invalid_ines_data; + } + + close(fd); + + return status::success; + } + + auto file_browser_posix::reopen_directory(string_view const path) -> status + { + auto const path_str = std::string{ path.get_data(), path.get_length() }; + auto const new_it = opendir(path_str.c_str()); + if (!new_it) + { + perror("file_browser_posix: opendir"); + return status::error_system_error; + } + + if (it_) { closedir(it_); } + it_ = new_it; + it_pos_ = 0; + return status::success; + } +} // namespace nes::app::mac diff --git a/app/sdl/impl/file-browser-posix.hh b/app/sdl/impl/file-browser-posix.hh new file mode 100644 index 0000000..8d56575 --- /dev/null +++ b/app/sdl/impl/file-browser-posix.hh @@ -0,0 +1,30 @@ +#pragma once + +#include "nes/app/file-browser.hh" +#include "nes/common/containers/path-builder.hh" +#include <dirent.h> + +namespace nes::app::sdl +{ + /// File browser implementation using the POSIX directory API. + class file_browser_posix final : public file_browser + { + path_buffer<1024> path_; + DIR* it_{ nullptr }; + u32 it_pos_{ 0 }; + + public: + explicit file_browser_posix(); + ~file_browser_posix() override; + + auto get_path() const -> path_view override { return path_.get_components(); } + auto seek(u32) -> void override; + auto read_next(entry* out_entry) -> bool override; + auto navigate_up() -> status override; + auto navigate(string_view) -> status override; + auto load(string_view, span<u8>, u32* out_length) -> status override; + + private: + auto reopen_directory(string_view path) -> status; + }; +} // namespace nes::app::sdl diff --git a/app/sdl/impl/input-device-keyboard-sdl.cc b/app/sdl/impl/input-device-keyboard-sdl.cc new file mode 100644 index 0000000..43f17ac --- /dev/null +++ b/app/sdl/impl/input-device-keyboard-sdl.cc @@ -0,0 +1,97 @@ +#include "impl/input-device-keyboard-sdl.hh" +#include <SDL3/SDL_keycode.h> + +namespace nes::app::sdl +{ + auto input_device_keyboard_sdl::handle_key_down(SDL_Event* event) -> void + { + if (auto key = convert_key(event->key.scancode)) { buffer_.key_down(*key); } + } + + auto input_device_keyboard_sdl::handle_key_up(SDL_Event* event) -> void + { + if (auto key = convert_key(event->key.scancode)) { buffer_.key_up(*key); } + } + + auto input_device_keyboard_sdl::convert_key(SDL_Scancode code) const -> std::optional<key> + { + switch (static_cast<int>(code)) + { + case SDL_SCANCODE_0: return key::digit_0; + case SDL_SCANCODE_1: return key::digit_1; + case SDL_SCANCODE_2: return key::digit_2; + case SDL_SCANCODE_3: return key::digit_3; + case SDL_SCANCODE_4: return key::digit_4; + case SDL_SCANCODE_5: return key::digit_5; + case SDL_SCANCODE_6: return key::digit_6; + case SDL_SCANCODE_7: return key::digit_7; + case SDL_SCANCODE_8: return key::digit_8; + case SDL_SCANCODE_9: return key::digit_9; + case SDL_SCANCODE_A: return key::letter_a; + case SDL_SCANCODE_B: return key::letter_b; + case SDL_SCANCODE_C: return key::letter_c; + case SDL_SCANCODE_D: return key::letter_d; + case SDL_SCANCODE_E: return key::letter_e; + case SDL_SCANCODE_F: return key::letter_f; + case SDL_SCANCODE_G: return key::letter_g; + case SDL_SCANCODE_H: return key::letter_h; + case SDL_SCANCODE_I: return key::letter_i; + case SDL_SCANCODE_J: return key::letter_j; + case SDL_SCANCODE_K: return key::letter_k; + case SDL_SCANCODE_L: return key::letter_l; + case SDL_SCANCODE_M: return key::letter_m; + case SDL_SCANCODE_N: return key::letter_n; + case SDL_SCANCODE_O: return key::letter_o; + case SDL_SCANCODE_P: return key::letter_p; + case SDL_SCANCODE_Q: return key::letter_q; + case SDL_SCANCODE_R: return key::letter_r; + case SDL_SCANCODE_S: return key::letter_s; + case SDL_SCANCODE_T: return key::letter_t; + case SDL_SCANCODE_U: return key::letter_u; + case SDL_SCANCODE_V: return key::letter_v; + case SDL_SCANCODE_W: return key::letter_w; + case SDL_SCANCODE_X: return key::letter_x; + case SDL_SCANCODE_Y: return key::letter_y; + case SDL_SCANCODE_Z: return key::letter_z; + case SDL_SCANCODE_ESCAPE: return key::escape; + case SDL_SCANCODE_MINUS: return key::dash; + case SDL_SCANCODE_EQUALS: return key::equal; + case SDL_SCANCODE_BACKSPACE: return key::backspace; + case SDL_SCANCODE_TAB: return key::tab; + case SDL_SCANCODE_RETURN: return key::enter; + case SDL_SCANCODE_SEMICOLON: return key::semicolon; + case SDL_SCANCODE_APOSTROPHE: return key::apostrophe; + case SDL_SCANCODE_BACKSLASH: return key::backslash; + case SDL_SCANCODE_COMMA: return key::comma; + case SDL_SCANCODE_PERIOD: return key::period; + case SDL_SCANCODE_SLASH: return key::slash; + case SDL_SCANCODE_SPACE: return key::space; + case SDL_SCANCODE_CAPSLOCK: return key::caps_lock; + case SDL_SCANCODE_LEFTBRACKET: return key::bracket_open; + case SDL_SCANCODE_RIGHTBRACKET: return key::bracket_close; + case SDL_SCANCODE_LCTRL: return key::control_left; + case SDL_SCANCODE_RCTRL: return key::control_right; + case SDL_SCANCODE_LSHIFT: return key::shift_left; + case SDL_SCANCODE_RSHIFT: return key::shift_right; + case SDL_SCANCODE_LALT: return key::alt_left; + case SDL_SCANCODE_RALT: return key::alt_right; + case SDL_SCANCODE_LEFT: return key::arrow_left; + case SDL_SCANCODE_RIGHT: return key::arrow_right; + case SDL_SCANCODE_UP: return key::arrow_up; + case SDL_SCANCODE_DOWN: return key::arrow_down; + case SDL_SCANCODE_F1: return key::f1; + case SDL_SCANCODE_F2: return key::f2; + case SDL_SCANCODE_F3: return key::f3; + case SDL_SCANCODE_F4: return key::f4; + case SDL_SCANCODE_F5: return key::f5; + case SDL_SCANCODE_F6: return key::f6; + case SDL_SCANCODE_F7: return key::f7; + case SDL_SCANCODE_F8: return key::f8; + case SDL_SCANCODE_F9: return key::f9; + case SDL_SCANCODE_F10: return key::f10; + case SDL_SCANCODE_F11: return key::f11; + case SDL_SCANCODE_F12: return key::f12; + default: return std::nullopt; + } + } +} // namespace nes::app::sdl diff --git a/app/sdl/impl/input-device-keyboard-sdl.hh b/app/sdl/impl/input-device-keyboard-sdl.hh new file mode 100644 index 0000000..1c38065 --- /dev/null +++ b/app/sdl/impl/input-device-keyboard-sdl.hh @@ -0,0 +1,28 @@ +#pragma once + +#include "nes/app/input/input-device-keyboard.hh" +#include "nes/app/input/input-buffer.hh" +#include <optional> + +#include <SDL3/SDL.h> + +namespace nes::app::sdl +{ + /// Keyboard implementation using SDL. + class input_device_keyboard_sdl final : public input_device_keyboard + { + input_buffer buffer_; + + public: + explicit input_device_keyboard_sdl() = default; + + auto handle_key_down(SDL_Event*) -> void; + auto handle_key_up(SDL_Event*) -> void; + + auto read_key(key const key) -> bool override { return buffer_.read_key(key); } + auto poll_event() -> input_event override { return buffer_.poll_event(); } + + private: + auto convert_key(SDL_Scancode) const -> std::optional<key>; + }; +} // namespace nes::app::sdl diff --git a/app/sdl/main.cc b/app/sdl/main.cc new file mode 100644 index 0000000..f2c3621 --- /dev/null +++ b/app/sdl/main.cc @@ -0,0 +1,40 @@ +#include "state.hh" +#include <SDL3/SDL_init.h> + +#define SDL_MAIN_USE_CALLBACKS +#include <SDL3/SDL_main.h> + +auto SDL_AppInit(void** appstate, int argc, char** argv) -> SDL_AppResult +{ + (void)argc; + (void)argv; + + auto const state = new nes::app::sdl::state; + *appstate = state; + + return state->get_status() == nes::status::success ? SDL_APP_CONTINUE : SDL_APP_FAILURE; +} + +auto SDL_AppIterate(void* appstate) -> SDL_AppResult +{ + auto const state = static_cast<nes::app::sdl::state*>(appstate); + state->handle_iterate(); + return state->get_status() == nes::status::success ? SDL_APP_CONTINUE : SDL_APP_FAILURE; +} + +auto SDL_AppEvent(void* appstate, SDL_Event* event) -> SDL_AppResult +{ + if (event->type == SDL_EVENT_QUIT) + { + return SDL_APP_SUCCESS; + } + + auto const state = static_cast<nes::app::sdl::state*>(appstate); + state->handle_event(event); + return state->get_status() == nes::status::success ? SDL_APP_CONTINUE : SDL_APP_FAILURE; +} + +auto SDL_AppQuit(void* appstate, SDL_AppResult) -> void +{ + delete static_cast<nes::app::sdl::state*>(appstate); +} diff --git a/app/sdl/state.cc b/app/sdl/state.cc new file mode 100644 index 0000000..fd3bcc5 --- /dev/null +++ b/app/sdl/state.cc @@ -0,0 +1,89 @@ +#include "state.hh" +#include <SDL3/SDL_events.h> +#include <SDL3/SDL_video.h> +#include <iostream> + +namespace nes::app::sdl +{ + state::state() + : application_{ display_, keyboard_, file_browser_ } + { + SDL_SetAppMetadata("NES", "1.0", "com.github.hannesschulze.nes"); + + if (!SDL_Init(SDL_INIT_VIDEO)) + { + std::cerr << "Couldn't initialize SDL: " << SDL_GetError() << std::endl; + status_ = status::error_system_error; + return; + } + + if (!SDL_CreateWindowAndRenderer("NES", display::width * 3, display::height * 3, 0, &window_, &renderer_)) + { + std::cerr << "Couldn't create window/renderer: " << SDL_GetError() << std::endl; + status_ = status::error_system_error; + return; + } + + SDL_SetRenderVSync(renderer_, SDL_RENDERER_VSYNC_ADAPTIVE); + + if (!(texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBX8888, SDL_TEXTUREACCESS_STREAMING, display::width, display::height))) + { + std::cerr << "Couldn't create texture: " << SDL_GetError() << std::endl; + status_ = status::error_system_error; + return; + } + + if (!SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST)) + { + std::cerr << "Unable to set texture scale mode: " << SDL_GetError() << std::endl; + status_ = status::error_system_error; + return; + } + + display_.set_texture(texture_); + } + + state::~state() + { + if (texture_) { SDL_DestroyTexture(texture_); } + } + + auto state::handle_event(SDL_Event* event) -> void + { + switch (event->type) + { + case SDL_EVENT_KEY_DOWN: + keyboard_.handle_key_down(event); + break; + case SDL_EVENT_KEY_UP: + keyboard_.handle_key_up(event); + break; + default: + break; + } + } + + auto state::handle_iterate() -> void + { + auto const current = std::chrono::steady_clock::now(); + auto const previous = last_time_point_.value_or(current); + last_time_point_ = current; + + auto const elapsed_us = std::chrono::duration_cast<std::chrono::microseconds>(current - previous); + application_.frame(static_cast<u32>(elapsed_us.count())); + + if (!SDL_RenderTexture(renderer_, texture_, nullptr, nullptr)) + { + std::cerr << "Unable to render texture: " << SDL_GetError() << std::endl; + status_ = status::error_system_error; + return; + } + + if (!SDL_RenderPresent(renderer_)) + { + std::cerr << "Unable to present frame: " << SDL_GetError() << std::endl; + status_ = status::error_system_error; + return; + } + } +} // namespace nes::app::sdl diff --git a/app/sdl/state.hh b/app/sdl/state.hh new file mode 100644 index 0000000..f67a5f9 --- /dev/null +++ b/app/sdl/state.hh @@ -0,0 +1,37 @@ +#pragma once + +#include "nes/common/status.hh" +#include "nes/app/application.hh" +#include "impl/display-sdl.hh" +#include "impl/input-device-keyboard-sdl.hh" +#include "impl/file-browser-posix.hh" +#include <chrono> +#include <optional> + +#include <SDL3/SDL.h> + +namespace nes::app::sdl +{ + /// Manages the SDL application lifecycle. + class state + { + status status_{ status::success }; + SDL_Window* window_{ nullptr }; + SDL_Renderer* renderer_{ nullptr }; + SDL_Texture* texture_{ nullptr }; + std::optional<std::chrono::steady_clock::time_point> last_time_point_; + display_sdl display_; + input_device_keyboard_sdl keyboard_; + file_browser_posix file_browser_; + application application_; + + public: + explicit state(); + ~state(); + + auto get_status() const -> status { return status_; } + + auto handle_event(SDL_Event* event) -> void; + auto handle_iterate() -> void; + }; +} // namespace nes::app::sdl @@ -16,7 +16,7 @@ let pkgs = import nixpkgs { inherit system; }; shell = pkgs.mkShell { - packages = with pkgs; [ cmake gcc binutils gdb gnumake valgrind clang-tools ]; + packages = with pkgs; [ cmake sdl3 gcc binutils gdb gnumake valgrind clang-tools ]; hardeningDisable = [ "fortify" ]; }; in diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc index ca04999..09c9f34 100644 --- a/lib/nes/app/application.cc +++ b/lib/nes/app/application.cc @@ -12,7 +12,8 @@ namespace nes::app return buffer_front_; } - auto application::display_proxy::set(u32 const x, u32 const y, rgb const value) -> void { + auto application::display_proxy::set(u32 const x, u32 const y, rgb const value) -> void + { buffer_back_[y * display::width + x] = value; base.set(x, y, value); } |
