summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--app/CMakeLists.txt13
-rw-r--r--app/sdl/CMakeLists.txt18
-rw-r--r--app/sdl/impl/CMakeLists.txt9
-rw-r--r--app/sdl/impl/display-sdl.cc16
-rw-r--r--app/sdl/impl/display-sdl.hh24
-rw-r--r--app/sdl/impl/file-browser-posix.cc139
-rw-r--r--app/sdl/impl/file-browser-posix.hh30
-rw-r--r--app/sdl/impl/input-device-keyboard-sdl.cc97
-rw-r--r--app/sdl/impl/input-device-keyboard-sdl.hh28
-rw-r--r--app/sdl/main.cc40
-rw-r--r--app/sdl/state.cc89
-rw-r--r--app/sdl/state.hh37
-rw-r--r--flake.nix2
-rw-r--r--lib/nes/app/application.cc3
15 files changed, 543 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index a13f433..bc0bc4f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/flake.nix b/flake.nix
index bf0f856..9a8509d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -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);
}