summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/mac/CMakeLists.txt7
-rw-r--r--app/mac/controller-adapter-gamepad.hh18
-rw-r--r--app/mac/controller-adapter-gamepad.mm29
-rw-r--r--app/mac/controller-adapter-keyboard.hh15
-rw-r--r--app/mac/controller-adapter-keyboard.mm44
-rw-r--r--app/mac/controller-adapter.hh16
-rw-r--r--app/mac/main.mm10
-rw-r--r--app/mac/scene.hh9
-rw-r--r--app/mac/scene.mm171
-rw-r--r--app/mac/view-controller.hh3
-rw-r--r--app/mac/view-controller.mm83
-rw-r--r--lib/nes/CMakeLists.txt3
-rw-r--r--lib/nes/app/CMakeLists.txt7
-rw-r--r--lib/nes/app/application.cc38
-rw-r--r--lib/nes/app/application.hh37
-rw-r--r--lib/nes/app/input/CMakeLists.txt8
-rw-r--r--lib/nes/app/input/input-device-controller.hh38
-rw-r--r--lib/nes/app/input/input-device-keyboard.hh16
-rw-r--r--lib/nes/app/input/input-device.hh44
-rw-r--r--lib/nes/app/input/input-manager.cc102
-rw-r--r--lib/nes/app/input/input-manager.hh45
-rw-r--r--lib/nes/common/CMakeLists.txt1
-rw-r--r--lib/nes/common/list.hh130
23 files changed, 622 insertions, 252 deletions
diff --git a/app/mac/CMakeLists.txt b/app/mac/CMakeLists.txt
index e0d42d0..f529e3d 100644
--- a/app/mac/CMakeLists.txt
+++ b/app/mac/CMakeLists.txt
@@ -21,9 +21,4 @@ target_sources(
scene.hh
scene.mm
view-controller.hh
- view-controller.mm
- controller-adapter.hh
- controller-adapter-gamepad.hh
- controller-adapter-gamepad.mm
- controller-adapter-keyboard.hh
- controller-adapter-keyboard.mm) \ No newline at end of file
+ view-controller.mm) \ No newline at end of file
diff --git a/app/mac/controller-adapter-gamepad.hh b/app/mac/controller-adapter-gamepad.hh
deleted file mode 100644
index c0caeed..0000000
--- a/app/mac/controller-adapter-gamepad.hh
+++ /dev/null
@@ -1,18 +0,0 @@
-#pragma once
-
-#import "controller-adapter.hh"
-
-#import <GameController/GameController.h>
-
-NS_ASSUME_NONNULL_BEGIN
-
-@interface ControllerAdapterGamepad : NSObject <ControllerAdapter>
-
-- (instancetype)initWithProfile:(GCExtendedGamepad*)profile;
-- (instancetype)init NS_UNAVAILABLE;
-
-@property (readonly) GCExtendedGamepad* profile;
-
-@end
-
-NS_ASSUME_NONNULL_END \ No newline at end of file
diff --git a/app/mac/controller-adapter-gamepad.mm b/app/mac/controller-adapter-gamepad.mm
deleted file mode 100644
index f1d8db8..0000000
--- a/app/mac/controller-adapter-gamepad.mm
+++ /dev/null
@@ -1,29 +0,0 @@
-#include "controller-adapter-gamepad.hh"
-
-@implementation ControllerAdapterGamepad
-
-- (instancetype)initWithProfile:(GCExtendedGamepad*)profile
-{
- self = [super init];
- if (self)
- {
- _profile = profile;
- }
- return self;
-}
-
-- (nes::button_mask)readPressedButtons
-{
- auto res = nes::button_mask{};
- if ([[[_profile dpad] up] isPressed]) { res.add(nes::buttons::up); }
- if ([[[_profile dpad] down] isPressed]) { res.add(nes::buttons::down); }
- if ([[[_profile dpad] left] isPressed]) { res.add(nes::buttons::left); }
- if ([[[_profile dpad] right] isPressed]) { res.add(nes::buttons::right); }
- if ([[_profile buttonA] isPressed]) { res.add(nes::buttons::a); }
- if ([[_profile buttonB] isPressed]) { res.add(nes::buttons::b); }
- if ([[_profile leftTrigger] isPressed]) { res.add(nes::buttons::select); }
- if ([[_profile rightTrigger] isPressed]) { res.add(nes::buttons::start); }
- return res;
-}
-
-@end \ No newline at end of file
diff --git a/app/mac/controller-adapter-keyboard.hh b/app/mac/controller-adapter-keyboard.hh
deleted file mode 100644
index 7c3ef84..0000000
--- a/app/mac/controller-adapter-keyboard.hh
+++ /dev/null
@@ -1,15 +0,0 @@
-#pragma once
-
-#import "controller-adapter.hh"
-
-#import <GameController/GameController.h>
-
-NS_ASSUME_NONNULL_BEGIN
-
-@interface ControllerAdapterKeyboard : NSObject <ControllerAdapter>
-
-@property (readonly, nullable) GCKeyboard* keyboard;
-
-@end
-
-NS_ASSUME_NONNULL_END \ No newline at end of file
diff --git a/app/mac/controller-adapter-keyboard.mm b/app/mac/controller-adapter-keyboard.mm
deleted file mode 100644
index ad3dda8..0000000
--- a/app/mac/controller-adapter-keyboard.mm
+++ /dev/null
@@ -1,44 +0,0 @@
-#import "controller-adapter-keyboard.hh"
-
-@implementation ControllerAdapterKeyboard
-
-- (instancetype)init
-{
- self = [super init];
- if (self)
- {
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(handleKeyboardConnected:)
- name:GCKeyboardDidConnectNotification
- object:nil];
- }
- return self;
-}
-
-- (void)dealloc
-{
- [[NSNotificationCenter defaultCenter] removeObserver:self];
-}
-
-- (void)handleKeyboardConnected:(NSNotification*)notification
-{
- _keyboard = [GCKeyboard coalescedKeyboard];
-}
-
-- (nes::button_mask)readPressedButtons
-{
- auto profile = [[self keyboard] keyboardInput];
-
- auto res = nes::button_mask{};
- if ([[profile buttonForKeyCode:GCKeyCodeUpArrow] isPressed]) { res.add(nes::buttons::up); }
- if ([[profile buttonForKeyCode:GCKeyCodeDownArrow] isPressed]) { res.add(nes::buttons::down); }
- if ([[profile buttonForKeyCode:GCKeyCodeLeftArrow] isPressed]) { res.add(nes::buttons::left); }
- if ([[profile buttonForKeyCode:GCKeyCodeRightArrow] isPressed]) { res.add(nes::buttons::right); }
- if ([[profile buttonForKeyCode:GCKeyCodeKeyZ] isPressed]) { res.add(nes::buttons::a); }
- if ([[profile buttonForKeyCode:GCKeyCodeKeyX] isPressed]) { res.add(nes::buttons::b); }
- if ([[profile buttonForKeyCode:GCKeyCodeReturnOrEnter] isPressed]) { res.add(nes::buttons::start); }
- if ([[profile buttonForKeyCode:GCKeyCodeSpacebar] isPressed]) { res.add(nes::buttons::select); }
- return res;
-}
-
-@end \ No newline at end of file
diff --git a/app/mac/controller-adapter.hh b/app/mac/controller-adapter.hh
deleted file mode 100644
index 09e8b6d..0000000
--- a/app/mac/controller-adapter.hh
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma once
-
-#import "nes/sys/types/button-mask.hh"
-
-#import <Foundation/Foundation.h>
-
-NS_ASSUME_NONNULL_BEGIN
-
-/// Adapts inputs from a GCPhysicalInputProfile to a NES button mask.
-@protocol ControllerAdapter <NSObject>
-
-- (nes::button_mask)readPressedButtons;
-
-@end
-
-NS_ASSUME_NONNULL_END \ No newline at end of file
diff --git a/app/mac/main.mm b/app/mac/main.mm
index 4e96cea..55e00b1 100644
--- a/app/mac/main.mm
+++ b/app/mac/main.mm
@@ -5,14 +5,6 @@
int main(int argc, char** argv)
{
- if (argc != 2)
- {
- std::cerr << "Usage: " << argv[0] << " <rom-path>" << std::endl;
- return 1;
- }
-
- auto filePath = [NSString stringWithUTF8String:argv[1]];
-
auto app = [NSApplication sharedApplication];
[app setActivationPolicy:NSApplicationActivationPolicyRegular];
@@ -33,7 +25,7 @@ int main(int argc, char** argv)
[mainMenu addItem:item];
[app setMainMenu:mainMenu];
- auto viewController = [[ViewController alloc] initWithFilePath:filePath];
+ auto viewController = [[ViewController alloc] init];
auto window = [NSWindow windowWithContentViewController:viewController];
[window setTitle:@"NES"];
[window setContentAspectRatio:[viewController minimumDisplaySize]];
diff --git a/app/mac/scene.hh b/app/mac/scene.hh
index ba8eb31..e4f0a45 100644
--- a/app/mac/scene.hh
+++ b/app/mac/scene.hh
@@ -1,22 +1,15 @@
#pragma once
-#import "controller-adapter.hh"
-
#import <SpriteKit/SpriteKit.h>
+#import <GameController/GameController.h>
NS_ASSUME_NONNULL_BEGIN
@interface Scene : SKScene
-- (instancetype)initWithFilePath:(NSString*)filePath NS_DESIGNATED_INITIALIZER;
-
+ (instancetype)sceneWithSize:(CGSize)size NS_UNAVAILABLE;
- (instancetype)initWithSize:(CGSize)size NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;
-- (instancetype)init NS_UNAVAILABLE;
-
-@property (nonatomic, nullable) id<ControllerAdapter> primaryController;
-@property (nonatomic, nullable) id<ControllerAdapter> secondaryController;
@end
diff --git a/app/mac/scene.mm b/app/mac/scene.mm
index 97caadf..de9c587 100644
--- a/app/mac/scene.mm
+++ b/app/mac/scene.mm
@@ -1,13 +1,14 @@
#import "scene.hh"
-#import "nes/sys/nes.hh"
+#import "nes/app/application.hh"
+#import "nes/app/input/input-device-keyboard.hh"
+#import "nes/app/input/input-device-controller.hh"
#import "nes/common/display.hh"
#import <memory>
#import <optional>
-#import <iostream>
namespace
{
- class display_impl : public nes::display
+ class display_impl final : public nes::display
{
SKSpriteNode* node_;
NSMutableData* buffer_front_;
@@ -63,16 +64,97 @@ namespace
return [NSMutableData dataWithLength:length];
}
};
+
+ class input_device_keyboard_impl final : public nes::input_device_keyboard
+ {
+ GCKeyboardInput* profile_;
+
+ public:
+ explicit input_device_keyboard_impl(GCKeyboardInput* profile)
+ : profile_{ profile }
+ {
+ }
+
+ auto set_profile(GCKeyboardInput* profile) -> void { profile_ = profile; }
+
+ auto read_key_up() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeUpArrow] isPressed]; }
+ auto read_key_down() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeDownArrow] isPressed]; }
+ auto read_key_left() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeLeftArrow] isPressed]; }
+ auto read_key_right() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeRightArrow] isPressed]; }
+ auto read_key_a() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeKeyZ] isPressed]; }
+ auto read_key_b() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeKeyX] isPressed]; }
+ auto read_key_select() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeSpacebar] isPressed]; }
+ auto read_key_start() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeReturnOrEnter] isPressed]; }
+ auto read_key_escape() -> bool override { return [[profile_ buttonForKeyCode:GCKeyCodeEscape] isPressed]; }
+ };
+
+ class input_device_controller_impl final : public nes::input_device_controller
+ {
+ GCExtendedGamepad* profile_;
+
+ public:
+ explicit input_device_controller_impl(GCExtendedGamepad* profile)
+ : profile_{ 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 { return [[get_controller() vendorName] UTF8String]; }
+
+ auto get_index() const -> index override
+ {
+ 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 set_index(index idx) -> void override
+ {
+ 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 read_key_up() -> bool override { return [[[profile_ dpad] up] isPressed]; }
+ auto read_key_down() -> bool override { return [[[profile_ dpad] down] isPressed]; }
+ auto read_key_left() -> bool override { return [[[profile_ dpad] left] isPressed]; }
+ auto read_key_right() -> bool override { return [[[profile_ dpad] right] isPressed]; }
+ auto read_key_a() -> bool override { return [[profile_ buttonA] isPressed]; }
+ auto read_key_b() -> bool override { return [[profile_ buttonB] isPressed]; }
+ auto read_key_select() -> bool override { return [[profile_ leftTrigger] isPressed]; }
+ auto read_key_start() -> bool override { return [[profile_ rightTrigger] isPressed]; }
+ };
} // namespace
@implementation Scene
{
std::unique_ptr<display_impl> _display;
- std::unique_ptr<nes::nes> _nes;
+ std::unique_ptr<input_device_keyboard_impl> _keyboard;
+ std::vector<std::unique_ptr<input_device_controller_impl>> _controllers;
+ std::unique_ptr<nes::application> _app;
std::optional<NSTimeInterval> _lastTimestamp;
}
-- (instancetype)initWithFilePath:(NSString*)filePath
+- (instancetype)init
{
self = [super initWithSize:CGSizeMake(nes::display::width, nes::display::height)];
if (self)
@@ -84,19 +166,42 @@ namespace
[self setScaleMode:SKSceneScaleModeAspectFit];
[self addChild:node];
- auto cartridge = nes::cartridge::from_file([filePath UTF8String]);
- if (cartridge.get_status() != nes::status::success)
- {
- std::cerr << "Unable to load cartridge: " << to_string(cartridge.get_status()) << std::endl;
- std::abort();
- }
-
_display = std::make_unique<display_impl>(node);
- _nes = std::make_unique<nes::nes>(std::move(cartridge), *self->_display);
+ _keyboard = std::make_unique<input_device_keyboard_impl>([[GCKeyboard coalescedKeyboard] keyboardInput]);
+ _app = std::make_unique<nes::application>(*_display, *_keyboard);
+
+ for (GCController* controller in [GCController controllers])
+ {
+ auto gamepad = [controller extendedGamepad];
+ if (gamepad)
+ {
+ auto adapter = std::make_unique<input_device_controller_impl>(gamepad);
+ _app->add_controller(*adapter);
+ _controllers.emplace_back(std::move(adapter));
+ }
+ }
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(handleKeyboardConnected:)
+ name:GCKeyboardDidConnectNotification
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(handleControllerConnected:)
+ name:GCControllerDidConnectNotification
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(handleControllerDisconnected:)
+ name:GCControllerDidDisconnectNotification
+ object:nil];
}
return self;
}
+- (void)dealloc
+{
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
- (void)keyUp:(NSEvent*)event
{
// Handled by Game Controller
@@ -109,20 +214,42 @@ namespace
- (void)update:(NSTimeInterval)currentTime
{
- _nes->ref_controller_1().set_pressed(_primaryController ? [_primaryController readPressedButtons] : nes::button_mask{});
- _nes->ref_controller_2().set_pressed(_secondaryController ? [_secondaryController readPressedButtons] : nes::button_mask{});
-
auto const lastTimestamp = _lastTimestamp.value_or(currentTime);
_lastTimestamp = currentTime;
auto deltaMicroseconds = static_cast<std::uint64_t>((currentTime - lastTimestamp) * 1000. * 1000.);
deltaMicroseconds = std::min(deltaMicroseconds, std::uint64_t{ 50000 });
- _nes->step(std::chrono::microseconds{ deltaMicroseconds });
+ _app->frame(std::chrono::microseconds{ deltaMicroseconds });
+}
- if (_nes->get_status() != nes::status::success)
- {
- std::cerr << "Invalid state: " << to_string(_nes->get_status()) << std::endl;
- std::abort();
- }
+- (void)handleKeyboardConnected:(NSNotification*)notification
+{
+ _keyboard->set_profile([[GCKeyboard coalescedKeyboard] keyboardInput]);
+}
+
+- (void)handleControllerConnected:(NSNotification*)notification
+{
+ auto controller = (GCController*)[notification object];
+ auto gamepad = [controller extendedGamepad];
+ if (gamepad)
+ {
+ auto adapter = std::make_unique<input_device_controller_impl>(gamepad);
+ _app->add_controller(*adapter);
+ _controllers.emplace_back(std::move(adapter));
+ }
+}
+
+- (void)handleControllerDisconnected:(NSNotification*)notification
+{
+ auto controller = (GCController*)[notification object];
+ for (auto i = _controllers.size(); i > 0; --i)
+ {
+ auto& adapter = *_controllers[i - 1];
+ if (adapter.get_controller() == controller)
+ {
+ _app->remove_controller(adapter);
+ _controllers.erase(_controllers.begin() + static_cast<int>(i - 1));
+ }
+ }
}
@end \ No newline at end of file
diff --git a/app/mac/view-controller.hh b/app/mac/view-controller.hh
index 2eecd4e..bed3207 100644
--- a/app/mac/view-controller.hh
+++ b/app/mac/view-controller.hh
@@ -7,12 +7,9 @@ NS_ASSUME_NONNULL_BEGIN
@interface ViewController : GCEventViewController
-- (instancetype)initWithFilePath:(NSString*)filePath NS_DESIGNATED_INITIALIZER;
-
- (instancetype)initWithNibName:(nullable NSString*)nibNameOrNil
bundle:(nullable NSBundle*)nibBundleOrNil NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;
-- (instancetype)init NS_UNAVAILABLE;
@property (readonly) NSSize minimumDisplaySize;
@property (readonly) NSSize preferredDisplaySize;
diff --git a/app/mac/view-controller.mm b/app/mac/view-controller.mm
index eb81b6b..8695634 100644
--- a/app/mac/view-controller.mm
+++ b/app/mac/view-controller.mm
@@ -1,54 +1,23 @@
#import "view-controller.hh"
-#import "controller-adapter-keyboard.hh"
-#import "controller-adapter-gamepad.hh"
#import "scene.hh"
@implementation ViewController
{
Scene* _scene;
NSSize _sceneSize;
- ControllerAdapterKeyboard* _keyboardController;
- NSMutableArray<ControllerAdapterGamepad*>* _gamepadControllers;
}
-- (instancetype)initWithFilePath:(NSString*)filePath
+- (instancetype)init
{
self = [super initWithNibName:nil bundle:nil];
if (self)
{
- _scene = [[Scene alloc] initWithFilePath:filePath];
+ _scene = [[Scene alloc] init];
_sceneSize = [_scene size];
-
- _keyboardController = [[ControllerAdapterKeyboard alloc] init];
- _gamepadControllers = [NSMutableArray array];
- for (GCController* controller in [GCController controllers])
- {
- auto gamepad = [controller extendedGamepad];
- if (gamepad)
- {
- auto adapter = [[ControllerAdapterGamepad alloc] initWithProfile:gamepad];
- [_gamepadControllers addObject:adapter];
- }
- }
- [self updateControllers];
-
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(handleControllerConnected:)
- name:GCControllerDidConnectNotification
- object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self
- selector:@selector(handleControllerDisconnected:)
- name:GCControllerDidDisconnectNotification
- object:nil];
}
return self;
}
-- (void)dealloc
-{
- [[NSNotificationCenter defaultCenter] removeObserver:self];
-}
-
- (void)loadView
{
auto view = [[SKView alloc] init];
@@ -59,54 +28,6 @@
[self setView:view];
}
-- (void)updateControllers
-{
- // Prefer controllers, then fallback to keyboard input.
- if ([_gamepadControllers count] >= 1)
- {
- [_scene setPrimaryController:[_gamepadControllers objectAtIndex:0]];
- if ([_gamepadControllers count] >= 2)
- {
- [_scene setSecondaryController:[_gamepadControllers objectAtIndex:1]];
- }
- else
- {
- [_scene setSecondaryController:_keyboardController];
- }
- }
- else
- {
- [_scene setPrimaryController:_keyboardController];
- [_scene setSecondaryController:nil];
- }
-}
-
-- (void)handleControllerConnected:(NSNotification*)notification
-{
- auto controller = (GCController*)[notification object];
- auto gamepad = [controller extendedGamepad];
- if (gamepad)
- {
- auto adapter = [[ControllerAdapterGamepad alloc] initWithProfile:gamepad];
- [_gamepadControllers addObject:adapter];
- }
- [self updateControllers];
-}
-
-- (void)handleControllerDisconnected:(NSNotification*)notification
-{
- auto controller = (GCController*)[notification object];
- for (auto i = NSUInteger{ [_gamepadControllers count] }; i > 0; --i)
- {
- auto adapter = [_gamepadControllers objectAtIndex:(i - 1)];
- if ([[adapter profile] controller] == controller)
- {
- [_gamepadControllers removeObjectAtIndex:(i - 1)];
- }
- }
- [self updateControllers];
-}
-
- (NSSize)minimumDisplaySize
{
return _sceneSize;
diff --git a/lib/nes/CMakeLists.txt b/lib/nes/CMakeLists.txt
index 78c09fe..f24c27f 100644
--- a/lib/nes/CMakeLists.txt
+++ b/lib/nes/CMakeLists.txt
@@ -1,2 +1,3 @@
add_subdirectory(common)
-add_subdirectory(sys) \ No newline at end of file
+add_subdirectory(sys)
+add_subdirectory(app) \ No newline at end of file
diff --git a/lib/nes/app/CMakeLists.txt b/lib/nes/app/CMakeLists.txt
new file mode 100644
index 0000000..18b2a2c
--- /dev/null
+++ b/lib/nes/app/CMakeLists.txt
@@ -0,0 +1,7 @@
+target_sources(
+ nes
+ PRIVATE
+ application.hh
+ application.cc)
+
+add_subdirectory(input) \ No newline at end of file
diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc
new file mode 100644
index 0000000..db43854
--- /dev/null
+++ b/lib/nes/app/application.cc
@@ -0,0 +1,38 @@
+#include "nes/app/application.hh"
+#include "nes/app/input/input-device-keyboard.hh"
+#include "nes/app/input/input-device-controller.hh"
+#include <iostream>
+
+namespace nes
+{
+ namespace
+ {
+ constexpr auto file = std::string_view{ "/Users/hannes/Documents/temp/supermario.nes" };
+ } // namespace
+
+ application::application(display& display, input_device_keyboard& keyboard)
+ : display_{ display }
+ , input_manager_{ keyboard }
+ , console_{ cartridge::from_file(file), display_ }
+ {
+ if (console_.get_status() != status::success)
+ {
+ std::cerr << "Unable to load cartridge: " << to_string(console_.get_status()) << std::endl;
+ std::abort();
+ }
+ }
+
+ 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());
+
+ console_.step(elapsed_time);
+
+ if (console_.get_status() != status::success)
+ {
+ std::cerr << "Invalid state: " << to_string(console_.get_status()) << std::endl;
+ std::abort();
+ }
+ }
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/application.hh b/lib/nes/app/application.hh
new file mode 100644
index 0000000..9f6bd16
--- /dev/null
+++ b/lib/nes/app/application.hh
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "nes/app/input/input-manager.hh"
+#include "nes/sys/nes.hh"
+#include <chrono>
+
+namespace nes
+{
+ class display;
+
+ /// Entrypoint for the application.
+ ///
+ /// This class takes care of presenting a UI screen or the emulator, handling any errors and implementing the main
+ /// application logic.
+ ///
+ /// It delegates some platform-specific tasks to interfaces.
+ class application
+ {
+ display& display_;
+ input_manager input_manager_;
+ nes console_;
+
+ public:
+ explicit application(display&, input_device_keyboard&);
+
+ application(application const&) = delete;
+ application(application&&) = delete;
+ auto operator=(application const&) -> application& = delete;
+ auto operator=(application&&) -> application& = delete;
+
+ /// Request a new frame for the application.
+ auto frame(std::chrono::microseconds elapsed_time) -> void;
+
+ 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); }
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/CMakeLists.txt b/lib/nes/app/input/CMakeLists.txt
new file mode 100644
index 0000000..7150745
--- /dev/null
+++ b/lib/nes/app/input/CMakeLists.txt
@@ -0,0 +1,8 @@
+target_sources(
+ nes
+ PRIVATE
+ input-device.hh
+ input-device-keyboard.hh
+ input-device-controller.hh
+ input-manager.hh
+ input-manager.cc) \ No newline at end of file
diff --git a/lib/nes/app/input/input-device-controller.hh b/lib/nes/app/input/input-device-controller.hh
new file mode 100644
index 0000000..6adb79c
--- /dev/null
+++ b/lib/nes/app/input/input-device-controller.hh
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "nes/app/input/input-device.hh"
+#include "nes/common/list.hh"
+#include <string_view>
+
+namespace nes
+{
+ /// A game controller input device.
+ class input_device_controller : public input_device, public list<input_device_controller>::node
+ {
+ public:
+ enum class index
+ {
+ unused = -1,
+ player_1 = 0,
+ player_2 = 1,
+ };
+
+ /// Indicates whether the controller can be trusted to actually exist.
+ ///
+ /// If this returns false, the controller is not automatically selected on startup if there is no other
+ /// controller already selected.
+ virtual auto is_reliable() const -> bool = 0;
+
+ /// Human-readable name for the controller.
+ virtual auto get_name() const -> std::string_view = 0;
+
+ /// Get the (persistent) player index for the controller, if already set before.
+ virtual auto get_index() const -> index = 0;
+
+ /// Set the (persistent) player index for the controller.
+ virtual auto set_index(index) -> void = 0;
+
+ protected:
+ explicit input_device_controller() = default;
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/input-device-keyboard.hh b/lib/nes/app/input/input-device-keyboard.hh
new file mode 100644
index 0000000..d61417d
--- /dev/null
+++ b/lib/nes/app/input/input-device-keyboard.hh
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "nes/app/input/input-device.hh"
+
+namespace nes
+{
+ /// A keyboard input device.
+ class input_device_keyboard : public input_device
+ {
+ public:
+ virtual auto read_key_escape() -> bool = 0;
+
+ protected:
+ explicit input_device_keyboard() = default;
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/input-device.hh b/lib/nes/app/input/input-device.hh
new file mode 100644
index 0000000..9ac7423
--- /dev/null
+++ b/lib/nes/app/input/input-device.hh
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "nes/sys/types/button-mask.hh"
+
+namespace nes
+{
+ /// An abstract input device which can be used to play games.
+ class input_device
+ {
+ public:
+ virtual ~input_device() = default;
+
+ input_device(input_device const&) = delete;
+ input_device(input_device&&) = delete;
+ auto operator=(input_device const&) -> input_device& = delete;
+ auto operator=(input_device&&) -> input_device& = delete;
+
+ virtual auto read_key_up() -> bool = 0;
+ virtual auto read_key_down() -> bool = 0;
+ virtual auto read_key_left() -> bool = 0;
+ virtual auto read_key_right() -> bool = 0;
+ virtual auto read_key_a() -> bool = 0;
+ virtual auto read_key_b() -> bool = 0;
+ virtual auto read_key_select() -> bool = 0;
+ virtual auto read_key_start() -> bool = 0;
+
+ auto read_mask() -> button_mask
+ {
+ auto res = button_mask{};
+ if (read_key_up()) { res.add(buttons::up); }
+ if (read_key_down()) { res.add(buttons::down); }
+ if (read_key_left()) { res.add(buttons::left); }
+ if (read_key_right()) { res.add(buttons::right); }
+ if (read_key_a()) { res.add(buttons::a); }
+ if (read_key_b()) { res.add(buttons::b); }
+ if (read_key_select()) { res.add(buttons::select); }
+ if (read_key_start()) { res.add(buttons::start); }
+ return res;
+ }
+
+ protected:
+ explicit input_device() = default;
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/input-manager.cc b/lib/nes/app/input/input-manager.cc
new file mode 100644
index 0000000..1c9db9e
--- /dev/null
+++ b/lib/nes/app/input/input-manager.cc
@@ -0,0 +1,102 @@
+#include "nes/app/input/input-manager.hh"
+
+namespace nes
+{
+ namespace
+ {
+ class input_device_dummy final : public input_device
+ {
+ public:
+ explicit input_device_dummy() = default;
+
+ static auto get_instance() -> input_device_dummy&
+ {
+ static auto res = input_device_dummy{};
+ return res;
+ }
+
+ auto read_key_up() -> bool override { return false; }
+ auto read_key_down() -> bool override { return false; }
+ auto read_key_left() -> bool override { return false; }
+ auto read_key_right() -> bool override { return false; }
+ auto read_key_a() -> bool override { return false; }
+ auto read_key_b() -> bool override { return false; }
+ auto read_key_select() -> bool override { return false; }
+ auto read_key_start() -> bool override { return false; }
+ };
+ } // namespace
+
+ input_manager::input_manager(input_device_keyboard& keyboard)
+ : keyboard_{ &keyboard }
+ {
+ fill_unassigned_inputs();
+ }
+
+ auto input_manager::add_controller(input_device_controller& controller) -> void
+ {
+ controllers_.append(controller);
+ if (controller.is_reliable())
+ {
+ auto const index = controller.get_index();
+ if (index != input_device_controller::index::unused)
+ {
+ // This controller was previously used and assigned an index -> replace the current device with this
+ // (known) controller.
+ auto const i = static_cast<std::size_t>(index);
+ if (inputs_[i].controller)
+ {
+ inputs_[i].controller->set_index(input_device_controller::index::unused);
+ }
+
+ inputs_[i].controller = &controller;
+ inputs_[i].device = &controller;
+ }
+
+ // There may be another free controller now.
+ fill_unassigned_inputs();
+ }
+ }
+
+ auto input_manager::remove_controller(input_device_controller& controller) -> void
+ {
+ controllers_.remove(controller);
+ fill_unassigned_inputs();
+ }
+
+ auto input_manager::fill_unassigned_inputs() -> void
+ {
+ auto it = controllers_.begin();
+ input_device* fallback = keyboard_;
+
+ for (auto i = std::size_t{ 0 }; i < input_count; ++i)
+ {
+ if (inputs_[i].controller != nullptr) { continue; }
+
+ // This input is not assigned -> use the next available controller if available.
+ while (it != controllers_.end())
+ {
+ auto controller = *it;
+ if (controller->is_reliable() && controller->get_index() == input_device_controller::index::unused)
+ {
+ break;
+ }
+ ++it;
+ }
+
+ if (it != controllers_.end())
+ {
+ // We've found an unused controller -> assign it.
+ inputs_[i].controller = *it;
+ inputs_[i].controller->set_index(static_cast<input_device_controller::index>(i));
+ inputs_[i].device = *it;
+ }
+ else
+ {
+ // We're all out of controllers -> fall back to the keyboard (if this is the first missing controller)
+ // or nothing.
+ inputs_[i].device = fallback;
+ fallback = &input_device_dummy::get_instance(); // Only use the keyboard as a fallback once.
+ }
+ }
+ }
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/app/input/input-manager.hh b/lib/nes/app/input/input-manager.hh
new file mode 100644
index 0000000..10308c7
--- /dev/null
+++ b/lib/nes/app/input/input-manager.hh
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "nes/app/input/input-device-keyboard.hh"
+#include "nes/app/input/input-device-controller.hh"
+#include "nes/common/list.hh"
+
+namespace nes
+{
+ /// Manages input devices.
+ ///
+ /// The main purpose of this class is to assign newly plugged in controllers as the primary/secondary input and fall
+ /// back to the keyboard if needed.
+ class input_manager
+ {
+ static constexpr auto input_count = std::size_t{ 2 };
+
+ input_device_keyboard* keyboard_;
+ list<input_device_controller> controllers_;
+ struct
+ {
+ input_device* device{ nullptr };
+ input_device_controller* controller{ nullptr };
+ } inputs_[input_count]{};
+
+ public:
+ explicit input_manager(input_device_keyboard&);
+
+ input_manager(input_manager const&) = delete;
+ input_manager(input_manager&&) = delete;
+ auto operator=(input_manager const&) -> input_manager& = delete;
+ auto operator=(input_manager&&) -> input_manager& = delete;
+
+ auto get_input_1() const -> input_device& { return *inputs_[0].device; }
+ auto get_input_2() const -> input_device& { return *inputs_[1].device; }
+
+ auto get_keyboard() const -> input_device_keyboard& { return *keyboard_; }
+ auto get_controllers() const -> list<input_device_controller> const& { return controllers_; }
+
+ auto add_controller(input_device_controller&) -> void;
+ auto remove_controller(input_device_controller&) -> void;
+
+ private:
+ auto fill_unassigned_inputs() -> void;
+ };
+} // namespace nes \ No newline at end of file
diff --git a/lib/nes/common/CMakeLists.txt b/lib/nes/common/CMakeLists.txt
index 69ae435..553d468 100644
--- a/lib/nes/common/CMakeLists.txt
+++ b/lib/nes/common/CMakeLists.txt
@@ -3,4 +3,5 @@ target_sources(
PRIVATE
debug.hh
rgb.hh
+ list.hh
display.hh) \ No newline at end of file
diff --git a/lib/nes/common/list.hh b/lib/nes/common/list.hh
new file mode 100644
index 0000000..5f0d258
--- /dev/null
+++ b/lib/nes/common/list.hh
@@ -0,0 +1,130 @@
+#pragma once
+
+namespace nes
+{
+ /// An intrusive doubly-linked list.
+ template<typename T>
+ class list
+ {
+ public:
+ /// Base class of nodes in the list.
+ class node
+ {
+ friend list;
+
+ list* list_{ nullptr };
+ T* prev_{ nullptr };
+ T* next_{ nullptr };
+
+ public:
+ virtual ~node() = default;
+
+ node(node const&) = delete;
+ node(node&&) = delete;
+ auto operator=(node const&) -> node& = delete;
+ auto operator=(node&&) -> node& = delete;
+
+ protected:
+ explicit node() = default;
+ };
+
+ /// An iterator for the linked list.
+ class iterator
+ {
+ friend list;
+
+ T* current_{ nullptr };
+
+ public:
+ 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++() -> iterator&
+ {
+ if (current_)
+ {
+ node& current_node = *current_;
+ current_ = current_node.next_;
+ }
+ return *this;
+ }
+
+ auto operator++(int) -> iterator
+ {
+ auto res = *this;
+ ++*this;
+ return res;
+ }
+
+ private:
+ explicit iterator() = default;
+ explicit iterator(T* current)
+ : current_{ current }
+ {
+ }
+ };
+
+ private:
+ T* head_{ nullptr };
+ T* tail_{ nullptr };
+
+ public:
+ explicit list() = default;
+
+ ~list()
+ {
+ while (begin() != end()) { remove(begin()); }
+ }
+
+ list(list const&) = delete;
+ list(list&&) = delete;
+ auto operator=(list const&) -> list& = delete;
+ auto operator=(list&&) -> list& = delete;
+
+ auto begin() const -> iterator { return iterator{ head_ }; }
+ auto end() const -> iterator { return iterator{}; }
+
+ /// Add an item to the end of the list, removing it from any other list it may be in.
+ auto append(T& item) -> void
+ {
+ node& item_node = item;
+ if (item_node.list_) { item_node.list_->remove(item); }
+
+ item_node.list_ = this;
+ if (tail_)
+ {
+ item_node.prev_ = tail_;
+ tail_->next_ = &item;
+ }
+ else
+ {
+ head_ = &item;
+ }
+ tail_ = &item;
+ }
+
+ /// Remove an item from the list.
+ auto remove(T& item) -> void
+ {
+ node& item_node = item;
+ if (!item_node.list_) { return; }
+
+ if (head_ == &item) { head_ = item_node.next_; }
+ if (tail_ == &item) { tail_ = item_node.prev_; }
+ if (item_node.prev_) { item_node.prev_->next_ = item_node.next_; }
+ if (item_node.next_) { item_node.next_->prev_ = item_node.prev_; }
+
+ item_node.next_ = nullptr;
+ item_node.prev_ = nullptr;
+ item_node.list_ = nullptr;
+ }
+
+ /// Remove an item from the list by its iterator.
+ auto remove(iterator it) -> void
+ {
+ if (it != end()) { remove(**it); }
+ }
+ };
+} // namespace nes \ No newline at end of file