From 1d5f5482f91391a6dfee9ec0e7cb0a22ee5f0464 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Sat, 1 Feb 2025 00:38:55 +0100 Subject: implement screens --- lib/nes/app/CMakeLists.txt | 3 +- lib/nes/app/application.cc | 78 ++++++++++++++++++++++++++++++-------- lib/nes/app/application.hh | 36 ++++++++++++++---- lib/nes/app/graphics/image-tile.hh | 15 -------- lib/nes/app/graphics/renderer.cc | 2 +- lib/nes/app/ui/CMakeLists.txt | 7 ++++ lib/nes/app/ui/action.hh | 11 ++++++ lib/nes/app/ui/screen-title.cc | 30 +++++++++++++++ lib/nes/app/ui/screen-title.hh | 22 +++++++++++ lib/nes/app/ui/screen.hh | 31 +++++++++++++++ lib/nes/common/CMakeLists.txt | 1 + lib/nes/common/box.hh | 55 +++++++++++++++++++++++++++ lib/nes/common/list.hh | 2 +- 13 files changed, 252 insertions(+), 41 deletions(-) create mode 100644 lib/nes/app/ui/CMakeLists.txt create mode 100644 lib/nes/app/ui/action.hh create mode 100644 lib/nes/app/ui/screen-title.cc create mode 100644 lib/nes/app/ui/screen-title.hh create mode 100644 lib/nes/app/ui/screen.hh create mode 100644 lib/nes/common/box.hh (limited to 'lib/nes') diff --git a/lib/nes/app/CMakeLists.txt b/lib/nes/app/CMakeLists.txt index 98a0490..76ccb74 100644 --- a/lib/nes/app/CMakeLists.txt +++ b/lib/nes/app/CMakeLists.txt @@ -5,4 +5,5 @@ target_sources( application.cc) add_subdirectory(input) -add_subdirectory(graphics) \ No newline at end of file +add_subdirectory(graphics) +add_subdirectory(ui) \ No newline at end of file diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc index 4da3848..0c1a4cb 100644 --- a/lib/nes/app/application.cc +++ b/lib/nes/app/application.cc @@ -1,11 +1,10 @@ #include "nes/app/application.hh" #include "nes/app/input/input-device-keyboard.hh" #include "nes/app/input/input-device-controller.hh" +#include "nes/app/ui/screen.hh" #include "nes/app/graphics/renderer.hh" -#include "nes/app/graphics/color.hh" -#include "nes/app/graphics/mask-tile.hh" -#include "nes/app/graphics/image-tile.hh" -#include "nes/common/display.hh" +#include "ui/action.hh" + #include namespace nes::app @@ -15,29 +14,76 @@ namespace nes::app constexpr auto file = std::string_view{ "/Users/hannes/Documents/temp/supermario.nes" }; } // namespace + auto application::display_proxy::switch_buffers() -> void + { + if (screen) + { + auto r = renderer{ base }; + screen->render(r); + } + + base.switch_buffers(); + } + application::application(display& display, input_device_keyboard& keyboard) : display_{ display } , input_manager_{ keyboard } - , console_{ sys::cartridge::from_file(file), display_ } + , screen_title_{ input_manager_ } { - if (console_.get_status() != sys::status::success) - { - std::cerr << "Unable to load cartridge: " << to_string(console_.get_status()) << std::endl; - std::abort(); - } + display_.screen = &screen_title_; } auto application::frame(std::chrono::microseconds const elapsed_time) -> void { - console_.ref_controller_1().set_pressed(input_manager_.get_input_1().read_mask()); - console_.ref_controller_2().set_pressed(input_manager_.get_input_2().read_mask()); + if (display_.screen) + { + auto const a = display_.screen->process_events(); + handle_action(a); + } + + if (console_) + { + console_->ref_controller_1().set_pressed(input_manager_.get_input_1().read_mask()); + console_->ref_controller_2().set_pressed(input_manager_.get_input_2().read_mask()); - console_.step(elapsed_time); + // The scene is rendered by the console after the PPU requests a new frame (thus calling + // display_proxy::switch_buffers). + console_->step(elapsed_time); + + if (console_->get_status() != sys::status::success) + { + std::cerr << "Invalid state: " << to_string(console_->get_status()) << std::endl; + std::abort(); + } + } + else + { + // Directly let the scene render itself without rendering any gameplay (the scene is responsible for + // clearing the buffer). + display_.switch_buffers(); + } + } - if (console_.get_status() != sys::status::success) + auto application::handle_action(action const a) -> void + { + switch (a) { - std::cerr << "Invalid state: " << to_string(console_.get_status()) << std::endl; - std::abort(); + case action::none: + { + break; + } + case action::load_game: + { + console_.emplace(sys::cartridge::from_file(file), display_); + if (console_->get_status() != sys::status::success) + { + std::cerr << "Unable to load cartridge: " << to_string(console_->get_status()) << std::endl; + std::abort(); + } + + display_.screen = nullptr; + break; + } } } } // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/application.hh b/lib/nes/app/application.hh index 5cd6ac7..1fc1c6b 100644 --- a/lib/nes/app/application.hh +++ b/lib/nes/app/application.hh @@ -1,16 +1,16 @@ #pragma once #include "nes/app/input/input-manager.hh" +#include "nes/app/ui/screen-title.hh" #include "nes/sys/nes.hh" +#include "nes/common/box.hh" +#include "nes/common/display.hh" #include -namespace nes -{ - class display; -} // namespace nes - namespace nes::app { + class screen; + /// Entrypoint for the application. /// /// This class takes care of presenting a UI screen or the emulator, handling any errors and implementing the main @@ -19,9 +19,28 @@ namespace nes::app /// It delegates some platform-specific tasks to interfaces. class application { - display& display_; + /// Acts like a regular display, but renders a screen on top of the back buffer when requested to switch + /// buffers. + class display_proxy final : public display + { + public: + display& base; + screen* screen{ nullptr }; + + explicit display_proxy(display& base) + : base{ base } + { + } + + auto get(unsigned const x, unsigned const y) const -> rgb override { return base.get(x, y); } + auto set(unsigned const x, unsigned const y, rgb const value) -> void override { base.set(x, y, value); } + auto switch_buffers() -> void override; + }; + + display_proxy display_; input_manager input_manager_; - sys::nes console_; + box console_; + screen_title screen_title_; public: explicit application(display&, input_device_keyboard&); @@ -36,5 +55,8 @@ namespace nes::app auto add_controller(input_device_controller& c) -> void { input_manager_.add_controller(c); } auto remove_controller(input_device_controller& c) -> void { input_manager_.remove_controller(c); } + + private: + auto handle_action(action) -> void; }; } // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/graphics/image-tile.hh b/lib/nes/app/graphics/image-tile.hh index 5633a0e..0663e3f 100644 --- a/lib/nes/app/graphics/image-tile.hh +++ b/lib/nes/app/graphics/image-tile.hh @@ -11,19 +11,4 @@ namespace nes::app { color data[8 * 8]{}; }; - - namespace tiles - { - inline constexpr auto test = image_tile - { - color{ 0 }, color{ 0 }, color{ 9 }, color{ 9 }, color{ 9 }, color{ 9 }, color{ 0 }, color{ 0 }, - color{ 0 }, color{ 9 }, color{ 9 }, color{ 8 }, color{ 8 }, color{ 9 }, color{ 9 }, color{ 0 }, - color{ 9 }, color{ 9 }, color{ 8 }, color{ 8 }, color{ 8 }, color{ 8 }, color{ 9 }, color{ 9 }, - color{ 9 }, color{ 8 }, color{ 8 }, color{ 9 }, color{ 9 }, color{ 8 }, color{ 8 }, color{ 9 }, - color{ 9 }, color{ 8 }, color{ 8 }, color{ 9 }, color{ 9 }, color{ 8 }, color{ 8 }, color{ 9 }, - color{ 9 }, color{ 9 }, color{ 8 }, color{ 8 }, color{ 8 }, color{ 8 }, color{ 9 }, color{ 9 }, - color{ 0 }, color{ 9 }, color{ 9 }, color{ 8 }, color{ 8 }, color{ 9 }, color{ 9 }, color{ 0 }, - color{ 0 }, color{ 0 }, color{ 9 }, color{ 9 }, color{ 9 }, color{ 9 }, color{ 0 }, color{ 0 }, - }; - } // namespace tiles } // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/graphics/renderer.cc b/lib/nes/app/graphics/renderer.cc index c53ea12..8d1a168 100644 --- a/lib/nes/app/graphics/renderer.cc +++ b/lib/nes/app/graphics/renderer.cc @@ -68,7 +68,7 @@ namespace nes::app auto const resolved = resolve_color(c); auto const x_stop = std::min(x + width, renderer::width); - auto const y_stop = std::min(y + width, renderer::height); + auto const y_stop = std::min(y + height, renderer::height); for (auto y_px = unsigned{ y * 8 }; y_px < y_stop * 8; ++y_px) { for (auto x_px = unsigned{ x * 8 }; x_px < x_stop * 8; ++x_px) diff --git a/lib/nes/app/ui/CMakeLists.txt b/lib/nes/app/ui/CMakeLists.txt new file mode 100644 index 0000000..2faed29 --- /dev/null +++ b/lib/nes/app/ui/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources( + nes + PRIVATE + action.hh + screen.hh + screen-title.hh + screen-title.cc) \ No newline at end of file diff --git a/lib/nes/app/ui/action.hh b/lib/nes/app/ui/action.hh new file mode 100644 index 0000000..2994f37 --- /dev/null +++ b/lib/nes/app/ui/action.hh @@ -0,0 +1,11 @@ +#pragma once + +namespace nes::app +{ + /// An action for the application to process after a scene has handled its events. + enum class action + { + none, + load_game, + }; +} // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/ui/screen-title.cc b/lib/nes/app/ui/screen-title.cc new file mode 100644 index 0000000..2a7bc5c --- /dev/null +++ b/lib/nes/app/ui/screen-title.cc @@ -0,0 +1,30 @@ +#include "nes/app/ui/screen-title.hh" +#include "nes/app/graphics/renderer.hh" +#include "nes/app/graphics/color.hh" +#include "nes/app/graphics/mask-tile.hh" +#include "nes/app/input/input-manager.hh" + +namespace nes::app +{ + screen_title::screen_title(input_manager& input_manager) + : input_manager_{ input_manager } + { + } + + auto screen_title::render(renderer& renderer) -> void + { + renderer.render_fill(color::background_primary); + renderer.render_rect(2, 4, 28, 8, color::accent_primary); + renderer.render_mask_tile(10, 16, tiles::letter_a, color::foreground_primary); + } + + auto screen_title::process_events() -> action + { + if (input_manager_.get_keyboard().read_key_start()) + { + return action::load_game; + } + + return action::none; + } +} // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/ui/screen-title.hh b/lib/nes/app/ui/screen-title.hh new file mode 100644 index 0000000..a8dfdc5 --- /dev/null +++ b/lib/nes/app/ui/screen-title.hh @@ -0,0 +1,22 @@ +#pragma once + +#include "nes/app/ui/screen.hh" + +namespace nes::app +{ + class input_manager; + + /// The main title screen for the emulator, used as an entrypoint for the UI. + /// + /// Presents options to load games, adjust settings, and more. + class screen_title final : public screen + { + input_manager& input_manager_; + + public: + explicit screen_title(input_manager&); + + auto render(renderer&) -> void override; + auto process_events() -> action override; + }; +} // namespace nes::app \ No newline at end of file diff --git a/lib/nes/app/ui/screen.hh b/lib/nes/app/ui/screen.hh new file mode 100644 index 0000000..07e9d6f --- /dev/null +++ b/lib/nes/app/ui/screen.hh @@ -0,0 +1,31 @@ +#pragma once + +#include "nes/app/ui/action.hh" + +namespace nes::app +{ + class renderer; + + /// A part of the UI which takes care of rendering and handling input events. + /// + /// One screen may be shown at a time. It may be used as an overlay over an active game. + class screen + { + public: + virtual ~screen() = default; + + screen(screen const&) = delete; + screen(screen&&) = delete; + auto operator=(screen const&) -> screen& = delete; + auto operator=(screen&&) -> screen& = delete; + + /// Render a frame with the current UI state. + virtual auto render(renderer&) -> void = 0; + + /// Handle events and return an action for the application to execute. + virtual auto process_events() -> action = 0; + + protected: + explicit screen() = default; + }; +} // namespace nes::app \ No newline at end of file diff --git a/lib/nes/common/CMakeLists.txt b/lib/nes/common/CMakeLists.txt index 553d468..1a66929 100644 --- a/lib/nes/common/CMakeLists.txt +++ b/lib/nes/common/CMakeLists.txt @@ -4,4 +4,5 @@ target_sources( debug.hh rgb.hh list.hh + box.hh display.hh) \ No newline at end of file diff --git a/lib/nes/common/box.hh b/lib/nes/common/box.hh new file mode 100644 index 0000000..442d60f --- /dev/null +++ b/lib/nes/common/box.hh @@ -0,0 +1,55 @@ +#pragma once + +namespace nes +{ + /// Wrapper around an object, allowing for it to be uninitialized or reassigned even if the object type doesn't + /// support it. + /// + /// Basically a std::unique_ptr but stored in-place (and thus not movable). + template + class box + { + bool initialized_{ false }; + alignas(T) unsigned char buffer_[sizeof(T)]; + + public: + /// Create an empty box. + explicit box() = default; + + ~box() + { + clear(); + } + + box(box const&) = delete; + box(box&&) = delete; + auto operator=(box const&) -> box& = delete; + auto operator=(box&&) -> box& = delete; + + auto operator*() -> T& { return *get(); } + auto operator*() const -> T const& { return *get(); } + auto operator->() -> T* { return get(); } + auto operator->() const -> T const* { return get(); } + operator bool() const { return has_value(); } + + auto has_value() const -> bool { return initialized_; } + auto get() -> T* { return has_value() ? std::launder(reinterpret_cast(buffer_)) : nullptr; } + auto get() const -> T const* { return has_value() ? std::launder(reinterpret_cast(buffer_)) : nullptr; } + + /// Construct a new value in-place, potentially replacing the old value. + template + auto emplace(Args&&... args) -> void + { + clear(); + new (buffer_) T(std::forward(args)...); + initialized_ = true; + } + + /// Clear the boxed value. + auto clear() -> void + { + if (initialized_) { (*this)->~T(); } + initialized_ = false; + } + }; +} // namespace nes \ No newline at end of file diff --git a/lib/nes/common/list.hh b/lib/nes/common/list.hh index 5f0d258..d7a7d9b 100644 --- a/lib/nes/common/list.hh +++ b/lib/nes/common/list.hh @@ -39,7 +39,7 @@ namespace nes auto operator==(iterator const& other) -> bool { return current_ == other.current_; } auto operator!=(iterator const& other) -> bool { return current_ != other.current_; } - auto operator*() -> T* { return current_; } + auto operator*() const -> T* { return current_; } auto operator++() -> iterator& { -- cgit v1.3.1