From 79553ab1fa3d707612e80dd03bf95ca65fa0c411 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Tue, 4 Feb 2025 23:11:08 +0100 Subject: refactor file browser --- app/mac/impl/CMakeLists.txt | 13 +++ app/mac/impl/display-spritekit.hh | 24 +++++ app/mac/impl/display-spritekit.mm | 50 ++++++++++ app/mac/impl/file-browser-posix.cc | 129 +++++++++++++++++++++++++ app/mac/impl/file-browser-posix.hh | 30 ++++++ app/mac/impl/input-device-gc-gamepad.hh | 24 +++++ app/mac/impl/input-device-gc-gamepad.mm | 60 ++++++++++++ app/mac/impl/input-device-gc-keyboard.hh | 30 ++++++ app/mac/impl/input-device-gc-keyboard.mm | 119 +++++++++++++++++++++++ app/mac/impl/input-device-serial-controller.cc | 96 ++++++++++++++++++ app/mac/impl/input-device-serial-controller.hh | 29 ++++++ 11 files changed, 604 insertions(+) create mode 100644 app/mac/impl/CMakeLists.txt create mode 100644 app/mac/impl/display-spritekit.hh create mode 100644 app/mac/impl/display-spritekit.mm create mode 100644 app/mac/impl/file-browser-posix.cc create mode 100644 app/mac/impl/file-browser-posix.hh create mode 100644 app/mac/impl/input-device-gc-gamepad.hh create mode 100644 app/mac/impl/input-device-gc-gamepad.mm create mode 100644 app/mac/impl/input-device-gc-keyboard.hh create mode 100644 app/mac/impl/input-device-gc-keyboard.mm create mode 100644 app/mac/impl/input-device-serial-controller.cc create mode 100644 app/mac/impl/input-device-serial-controller.hh (limited to 'app/mac/impl') diff --git a/app/mac/impl/CMakeLists.txt b/app/mac/impl/CMakeLists.txt new file mode 100644 index 0000000..f8ff091 --- /dev/null +++ b/app/mac/impl/CMakeLists.txt @@ -0,0 +1,13 @@ +target_sources( + nes_app_mac + PRIVATE + display-spritekit.hh + display-spritekit.mm + input-device-gc-gamepad.hh + input-device-gc-gamepad.mm + input-device-gc-keyboard.hh + input-device-gc-keyboard.mm + input-device-serial-controller.hh + input-device-serial-controller.cc + file-browser-posix.hh + file-browser-posix.cc) diff --git a/app/mac/impl/display-spritekit.hh b/app/mac/impl/display-spritekit.hh new file mode 100644 index 0000000..64e3b03 --- /dev/null +++ b/app/mac/impl/display-spritekit.hh @@ -0,0 +1,24 @@ +#pragma once + +#import "nes/common/display.hh" + +#import + +namespace nes::app::mac +{ + /// Display implementation rendering contents into a SpriteKit node. + class display_spritekit final : public display + { + SKSpriteNode* node_; + NSMutableData* buffer_front_; + NSMutableData* buffer_back_; + u8* bytes_back_; + + public: + explicit display_spritekit(SKSpriteNode* node); + + virtual auto switch_buffers() -> void override; + virtual auto set(u32 x, u32 y, rgb value) -> void override; + virtual auto get(u32 x, u32 y) const -> rgb override; + }; +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/display-spritekit.mm b/app/mac/impl/display-spritekit.mm new file mode 100644 index 0000000..b6123d6 --- /dev/null +++ b/app/mac/impl/display-spritekit.mm @@ -0,0 +1,50 @@ +#import "impl/display-spritekit.hh" + +namespace nes::app::mac +{ + namespace + { + auto make_buffer() -> NSMutableData* + { + auto const length = NSUInteger{ display::width * display::height * 4 }; + return [NSMutableData dataWithLength:length]; + } + } // namespace + + display_spritekit::display_spritekit(SKSpriteNode* node) + : node_{ node } + , buffer_front_{ make_buffer() } + , buffer_back_{ make_buffer() } + , bytes_back_{ static_cast([buffer_back_ mutableBytes]) } + { + } + + auto display_spritekit::switch_buffers() -> void + { + std::swap(buffer_back_, buffer_front_); + bytes_back_ = static_cast([buffer_back_ mutableBytes]); + + auto texture = [SKTexture textureWithData:buffer_front_ size:CGSizeMake(width, height) flipped:YES]; + [texture setFilteringMode:SKTextureFilteringNearest]; + [node_ setTexture:texture]; + } + + auto display_spritekit::set(u32 const x, u32 const y, rgb const value) -> void + { + auto const offset = (y * width + x) * 4; + bytes_back_[offset + 0] = value.r; + bytes_back_[offset + 1] = value.g; + bytes_back_[offset + 2] = value.b; + bytes_back_[offset + 3] = 255; + } + + auto display_spritekit::get(u32 const x, u32 const y) const -> rgb + { + auto const offset = (y * width + x) * 4; + auto res = rgb{}; + res.r = bytes_back_[offset + 0]; + res.g = bytes_back_[offset + 1]; + res.b = bytes_back_[offset + 2]; + return res; + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/file-browser-posix.cc b/app/mac/impl/file-browser-posix.cc new file mode 100644 index 0000000..4c717fd --- /dev/null +++ b/app/mac/impl/file-browser-posix.cc @@ -0,0 +1,129 @@ +#include "impl/file-browser-posix.hh" +#include +#include +#include +#include + +namespace nes::app::mac +{ + file_browser_posix::file_browser_posix() + { + 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(item* out_item) -> bool + { + if (!it_) { return false; } + + while (true) + { + errno = 0; + auto const entry = readdir(it_); + if (!entry) + { + if (errno) { perror("file_browser_posix: readdir"); } + return false; + } + + auto const name = std::string_view{ entry->d_name }; + if (name.length() > max_name_length) { continue; } + if (name[0] == '.') { continue; } + + auto type = item_type::directory; + if (entry->d_type == DT_DIR) { type = item_type::directory; } + else if (entry->d_type == DT_REG) { type = item_type::file; } + else { continue; } + + it_pos_ += 1; + if (out_item) { *out_item = item{ 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(std::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(std::string_view const item, span 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() }; + 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(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(std::string_view const path) -> status + { + auto const path_str = std::string{ path }; + 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 \ No newline at end of file diff --git a/app/mac/impl/file-browser-posix.hh b/app/mac/impl/file-browser-posix.hh new file mode 100644 index 0000000..b760e53 --- /dev/null +++ b/app/mac/impl/file-browser-posix.hh @@ -0,0 +1,30 @@ +#pragma once + +#include "nes/app/file-browser.hh" +#include "nes/common/path-builder.hh" +#include + +namespace nes::app::mac +{ + /// 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(item* out_item) -> bool override; + auto navigate_up() -> status override; + auto navigate(std::string_view) -> status override; + auto load(std::string_view, span, u32* out_length) -> status override; + + private: + auto reopen_directory(std::string_view path) -> status; + }; +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-gc-gamepad.hh b/app/mac/impl/input-device-gc-gamepad.hh new file mode 100644 index 0000000..b4b4a4e --- /dev/null +++ b/app/mac/impl/input-device-gc-gamepad.hh @@ -0,0 +1,24 @@ +#pragma once + +#import "nes/app/input/input-device-controller.hh" + +#import + +namespace nes::app::mac +{ + /// Input device wrapping a GCExtendedGamepad. + class input_device_gc_gamepad final : public input_device_controller + { + GCExtendedGamepad* profile_; + + public: + explicit input_device_gc_gamepad(GCExtendedGamepad* profile); + + auto is_reliable() const -> bool override { return true; } + auto get_controller() const -> GCController* { return [profile_ controller]; } + auto get_name() const -> std::string_view override; + auto get_index() const -> index override; + auto set_index(index) -> void override; + auto read_buttons() -> sys::button_mask override; + }; +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-gc-gamepad.mm b/app/mac/impl/input-device-gc-gamepad.mm new file mode 100644 index 0000000..d2f84ec --- /dev/null +++ b/app/mac/impl/input-device-gc-gamepad.mm @@ -0,0 +1,60 @@ +#import "impl/input-device-gc-gamepad.hh" + +namespace nes::app::mac +{ + input_device_gc_gamepad::input_device_gc_gamepad(GCExtendedGamepad* profile) + : profile_{ profile } + { + } + + auto input_device_gc_gamepad::get_name() const -> std::string_view + { + return [[get_controller() vendorName] UTF8String]; + } + + auto input_device_gc_gamepad::get_index() const -> index + { + switch ([get_controller() playerIndex]) + { + case GCControllerPlayerIndex1: + return index::player_1; + case GCControllerPlayerIndex2: + return index::player_2; + case GCControllerPlayerIndex3: + case GCControllerPlayerIndex4: + case GCControllerPlayerIndexUnset: + break; + } + return index::unused; + } + + auto input_device_gc_gamepad::set_index(index const idx) -> void + { + switch (idx) + { + case index::unused: + [get_controller() setPlayerIndex:GCControllerPlayerIndexUnset]; + break; + case index::player_1: + [get_controller() setPlayerIndex:GCControllerPlayerIndex1]; + break; + case index::player_2: + [get_controller() setPlayerIndex:GCControllerPlayerIndex2]; + break; + } + } + + auto input_device_gc_gamepad::read_buttons() -> sys::button_mask + { + auto res = sys::button_mask{}; + if ([[[profile_ dpad] up] isPressed]) { res.add(sys::buttons::up); } + if ([[[profile_ dpad] down] isPressed]) { res.add(sys::buttons::down); } + if ([[[profile_ dpad] left] isPressed]) { res.add(sys::buttons::left); } + if ([[[profile_ dpad] right] isPressed]) { res.add(sys::buttons::right); } + if ([[profile_ buttonA] isPressed]) { res.add(sys::buttons::a); } + if ([[profile_ buttonB] isPressed]) { res.add(sys::buttons::b); } + if ([[profile_ leftTrigger] isPressed]) { res.add(sys::buttons::select); } + if ([[profile_ rightTrigger] isPressed]) { res.add(sys::buttons::start); } + return res; + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-gc-keyboard.hh b/app/mac/impl/input-device-gc-keyboard.hh new file mode 100644 index 0000000..c879dcc --- /dev/null +++ b/app/mac/impl/input-device-gc-keyboard.hh @@ -0,0 +1,30 @@ +#pragma once + +#import "nes/app/input/input-device-keyboard.hh" +#import "nes/app/input/input-buffer.hh" +#import + +#import + +namespace nes::app::mac +{ + /// Input device wrapping a GCKeyboard. + class input_device_gc_keyboard final : public input_device_keyboard + { + GCKeyboardInput* profile_{ nil }; + input_buffer buffer_; + + public: + explicit input_device_gc_keyboard(GCKeyboardInput* profile); + ~input_device_gc_keyboard() override; + + auto set_profile(GCKeyboardInput* profile) -> 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 handle_key_changed(GCKeyCode key_code, BOOL pressed) -> void; + auto convert_key_code(GCKeyCode key_code) -> std::optional; + }; +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-gc-keyboard.mm b/app/mac/impl/input-device-gc-keyboard.mm new file mode 100644 index 0000000..34ab98d --- /dev/null +++ b/app/mac/impl/input-device-gc-keyboard.mm @@ -0,0 +1,119 @@ +#import "impl/input-device-gc-keyboard.hh" + +namespace nes::app::mac +{ + input_device_gc_keyboard::input_device_gc_keyboard(GCKeyboardInput* profile) + { + set_profile(profile); + } + + input_device_gc_keyboard::~input_device_gc_keyboard() + { + set_profile(nil); + } + + auto input_device_gc_keyboard::set_profile(GCKeyboardInput* profile) -> void + { + [profile_ setKeyChangedHandler:nil]; + profile_ = profile; + [profile_ setKeyChangedHandler:^(GCKeyboardInput*, GCControllerButtonInput*, GCKeyCode key_code, BOOL pressed) { + handle_key_changed(key_code, pressed); + }]; + } + + auto input_device_gc_keyboard::handle_key_changed(GCKeyCode const key_code, BOOL const pressed) -> void + { + auto const key = convert_key_code(key_code); + if (key) + { + if (pressed) + { + buffer_.key_down(*key); + } + else + { + buffer_.key_up(*key); + } + } + } + + auto input_device_gc_keyboard::convert_key_code(GCKeyCode const key_code) -> std::optional + { + if (key_code == GCKeyCodeZero) { return key::digit_0; } + if (key_code == GCKeyCodeOne) { return key::digit_1; } + if (key_code == GCKeyCodeTwo) { return key::digit_2; } + if (key_code == GCKeyCodeThree) { return key::digit_3; } + if (key_code == GCKeyCodeFour) { return key::digit_4; } + if (key_code == GCKeyCodeFive) { return key::digit_5; } + if (key_code == GCKeyCodeSix) { return key::digit_6; } + if (key_code == GCKeyCodeSeven) { return key::digit_7; } + if (key_code == GCKeyCodeEight) { return key::digit_8; } + if (key_code == GCKeyCodeNine) { return key::digit_9; } + if (key_code == GCKeyCodeKeyA) { return key::letter_a; } + if (key_code == GCKeyCodeKeyB) { return key::letter_b; } + if (key_code == GCKeyCodeKeyC) { return key::letter_c; } + if (key_code == GCKeyCodeKeyD) { return key::letter_d; } + if (key_code == GCKeyCodeKeyE) { return key::letter_e; } + if (key_code == GCKeyCodeKeyF) { return key::letter_f; } + if (key_code == GCKeyCodeKeyG) { return key::letter_g; } + if (key_code == GCKeyCodeKeyH) { return key::letter_h; } + if (key_code == GCKeyCodeKeyI) { return key::letter_i; } + if (key_code == GCKeyCodeKeyJ) { return key::letter_j; } + if (key_code == GCKeyCodeKeyK) { return key::letter_k; } + if (key_code == GCKeyCodeKeyL) { return key::letter_l; } + if (key_code == GCKeyCodeKeyM) { return key::letter_m; } + if (key_code == GCKeyCodeKeyN) { return key::letter_n; } + if (key_code == GCKeyCodeKeyO) { return key::letter_o; } + if (key_code == GCKeyCodeKeyP) { return key::letter_p; } + if (key_code == GCKeyCodeKeyQ) { return key::letter_q; } + if (key_code == GCKeyCodeKeyR) { return key::letter_r; } + if (key_code == GCKeyCodeKeyS) { return key::letter_s; } + if (key_code == GCKeyCodeKeyT) { return key::letter_t; } + if (key_code == GCKeyCodeKeyU) { return key::letter_u; } + if (key_code == GCKeyCodeKeyV) { return key::letter_v; } + if (key_code == GCKeyCodeKeyW) { return key::letter_w; } + if (key_code == GCKeyCodeKeyX) { return key::letter_x; } + if (key_code == GCKeyCodeKeyY) { return key::letter_y; } + if (key_code == GCKeyCodeKeyZ) { return key::letter_z; } + if (key_code == GCKeyCodeEscape) { return key::escape; } + if (key_code == GCKeyCodeHyphen) { return key::dash; } + if (key_code == GCKeyCodeEqualSign) { return key::equal; } + if (key_code == GCKeyCodeDeleteOrBackspace) { return key::backspace; } + if (key_code == GCKeyCodeTab) { return key::tab; } + if (key_code == GCKeyCodeReturnOrEnter) { return key::enter; } + if (key_code == GCKeyCodeSemicolon) { return key::semicolon; } + if (key_code == GCKeyCodeQuote) { return key::apostrophe; } + if (key_code == GCKeyCodeBackslash) { return key::backslash; } + if (key_code == GCKeyCodeComma) { return key::comma; } + if (key_code == GCKeyCodePeriod) { return key::period; } + if (key_code == GCKeyCodeSlash) { return key::slash; } + if (key_code == GCKeyCodeSpacebar) { return key::space; } + if (key_code == GCKeyCodeCapsLock) { return key::caps_lock; } + if (key_code == GCKeyCodeOpenBracket) { return key::bracket_open; } + if (key_code == GCKeyCodeCloseBracket) { return key::bracket_close; } + if (key_code == GCKeyCodeLeftControl) { return key::control_left; } + if (key_code == GCKeyCodeRightControl) { return key::control_right; } + if (key_code == GCKeyCodeLeftShift) { return key::shift_left; } + if (key_code == GCKeyCodeRightShift) { return key::shift_right; } + if (key_code == GCKeyCodeLeftAlt) { return key::alt_left; } + if (key_code == GCKeyCodeRightAlt) { return key::alt_right; } + if (key_code == GCKeyCodeLeftArrow) { return key::arrow_left; } + if (key_code == GCKeyCodeRightArrow) { return key::arrow_right; } + if (key_code == GCKeyCodeUpArrow) { return key::arrow_up; } + if (key_code == GCKeyCodeDownArrow) { return key::arrow_down; } + if (key_code == GCKeyCodeF1) { return key::f1; } + if (key_code == GCKeyCodeF2) { return key::f2; } + if (key_code == GCKeyCodeF3) { return key::f3; } + if (key_code == GCKeyCodeF4) { return key::f4; } + if (key_code == GCKeyCodeF5) { return key::f5; } + if (key_code == GCKeyCodeF6) { return key::f6; } + if (key_code == GCKeyCodeF7) { return key::f7; } + if (key_code == GCKeyCodeF8) { return key::f8; } + if (key_code == GCKeyCodeF9) { return key::f9; } + if (key_code == GCKeyCodeF10) { return key::f10; } + if (key_code == GCKeyCodeF11) { return key::f11; } + if (key_code == GCKeyCodeF12) { return key::f12; } + + return std::nullopt; + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-serial-controller.cc b/app/mac/impl/input-device-serial-controller.cc new file mode 100644 index 0000000..7a493a7 --- /dev/null +++ b/app/mac/impl/input-device-serial-controller.cc @@ -0,0 +1,96 @@ +#include "impl/input-device-serial-controller.hh" +#include +#include +#include +#include +#include +#include + +namespace nes::app::mac +{ + input_device_serial_controller::input_device_serial_controller(char const* path) + : path_{ path } + { + fd_ = open(path, O_RDONLY | O_NOCTTY | O_NDELAY | O_NONBLOCK); + if (fd_ == -1) + { + perror("input_device_serial_controller: open"); + return; + } + + if (!set_up()) { close(); } + } + + input_device_serial_controller::~input_device_serial_controller() + { + close(); + } + + auto input_device_serial_controller::is_reliable() const -> bool + { + return fd_ != -1; + } + + auto input_device_serial_controller::read_buttons() -> sys::button_mask + { + if (fd_ == -1) { return sys::button_mask{}; } + + // Poll for any updates until we can't read anymore. + while (true) + { + auto buf = u8{}; + auto const res = read(fd_, &buf, 1); + if (res == -1) + { + if (errno == EAGAIN || errno == EWOULDBLOCK) { break; } + + perror("read from controller"); + close(); + break; + } + + current_ = sys::button_mask::from_raw_value(buf); + } + + return current_; + } + + + auto input_device_serial_controller::set_up() -> bool + { + if (ioctl(fd_, TIOCEXCL) == -1) + { + perror("input_device_serial_controller: ioctl"); + return false; + } + + auto attr = termios{}; + if (tcgetattr(fd_, &attr) == -1) + { + perror("input_device_serial_controller: tcgetattr"); + return false; + } + + cfsetispeed(&attr, B57600); + cfsetospeed(&attr, B57600); + attr.c_cflag = attr.c_cflag | CLOCAL | CREAD; + attr.c_cflag = (attr.c_cflag & static_cast(~CSIZE)) | CS8; + + if (tcsetattr(fd_, TCSANOW, &attr) == -1) + { + perror("input_device_serial_controller: tcsetattr"); + return false; + } + + return true; + } + + auto input_device_serial_controller::close() -> void + { + if (fd_ != -1) + { + ::close(fd_); + fd_ = -1; + } + } +} // namespace nes::app::mac \ No newline at end of file diff --git a/app/mac/impl/input-device-serial-controller.hh b/app/mac/impl/input-device-serial-controller.hh new file mode 100644 index 0000000..2022ed2 --- /dev/null +++ b/app/mac/impl/input-device-serial-controller.hh @@ -0,0 +1,29 @@ +#pragma once + +#include "nes/app/input/input-device-controller.hh" + +namespace nes::app::mac +{ + /// Input device for an NES controller plugged in via serial port. + class input_device_serial_controller final : public input_device_controller + { + char const* path_; + int fd_{ -1 }; + sys::button_mask current_{}; + index index_{ index::unused }; + + public: + explicit input_device_serial_controller(char const* path); + ~input_device_serial_controller() override; + + auto is_reliable() const -> bool override; + auto get_name() const -> std::string_view override { return path_; } + auto get_index() const -> index override { return index_; } + auto set_index(index const v) -> void override { index_ = v; } + auto read_buttons() -> sys::button_mask override; + + private: + auto set_up() -> bool; + auto close() -> void; + }; +} // namespace nes::app:mac \ No newline at end of file -- cgit v1.3.1