From 02f963a4bab95b3a328b6a792c9a7e027ade0e26 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Mon, 13 Jan 2025 22:03:10 +0100 Subject: implement display --- .clang-format | 3 +- CMakeLists.txt | 4 +- src/CMakeLists.txt | 7 +++- src/main.mm | 28 +++++++++---- src/nes/CMakeLists.txt | 9 +++++ src/nes/display.cc | 16 ++++++++ src/nes/display.hh | 33 ++++++++++++++++ src/nes/nes.cc | 13 ++++++ src/nes/nes.hh | 19 +++++++++ src/nes/util/CMakeLists.txt | 4 ++ src/nes/util/rgb.hh | 33 ++++++++++++++++ src/scene.hh | 12 ++++++ src/scene.mm | 96 +++++++++++++++++++++++++++++++++++++++++++++ src/view-controller.hh | 6 --- src/view-controller.mm | 15 ------- 15 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 src/nes/CMakeLists.txt create mode 100644 src/nes/display.cc create mode 100644 src/nes/display.hh create mode 100644 src/nes/nes.cc create mode 100644 src/nes/nes.hh create mode 100644 src/nes/util/CMakeLists.txt create mode 100644 src/nes/util/rgb.hh create mode 100644 src/scene.hh create mode 100644 src/scene.mm delete mode 100644 src/view-controller.hh delete mode 100644 src/view-controller.mm diff --git a/.clang-format b/.clang-format index a8bb62b..32cf393 100644 --- a/.clang-format +++ b/.clang-format @@ -40,6 +40,7 @@ ContinuationIndentWidth: 4 DerivePointerAlignment: false DisableFormat: false ExperimentalAutoDetectBinPacking: false +NamespaceIndentation: All IncludeBlocks: Preserve IncludeCategories: - Regex: '^"(llvm|llvm-c|clang|clang-c)/' @@ -64,7 +65,7 @@ PenaltyBreakString: 1000 PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 1000 -PointerAlignment: Right +PointerAlignment: Left ReflowComments: true Cpp11BracedListStyle: false SortIncludes: Never diff --git a/CMakeLists.txt b/CMakeLists.txt index 34552bf..5bfbf5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,8 @@ project( set(CMAKE_CXX_STANDARD 17) add_executable(nes) - add_subdirectory(src) find_library(FRAMEWORK_COCOA Cocoa REQUIRED) -target_link_libraries(nes PRIVATE ${FRAMEWORK_COCOA}) \ No newline at end of file +find_library(FRAMEWORK_SPRITEKIT SpriteKit REQUIRED) +target_link_libraries(nes PRIVATE ${FRAMEWORK_COCOA} ${FRAMEWORK_SPRITEKIT}) \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 34c0ed3..68ba329 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,10 @@ +add_subdirectory(nes) + target_sources( nes PRIVATE main.mm - view-controller.mm) \ No newline at end of file + scene.hh + scene.mm) + +target_include_directories(nes PRIVATE .) \ No newline at end of file diff --git a/src/main.mm b/src/main.mm index a526e66..5207e62 100644 --- a/src/main.mm +++ b/src/main.mm @@ -1,36 +1,48 @@ +#import "scene.hh" + #import -#import "view-controller.hh" +#import int main(int argc, char** argv) { NSApplication* app = [NSApplication sharedApplication]; [app setActivationPolicy:NSApplicationActivationPolicyRegular]; - NSMenuItem *quitItem = [[NSMenuItem alloc] init]; + NSMenuItem* quitItem = [[NSMenuItem alloc] init]; [quitItem setTitle:@"Quit NES"]; [quitItem setKeyEquivalent:@"q"]; [quitItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; [quitItem setTarget:app]; [quitItem setAction:@selector(terminate:)]; - NSMenu *menu = [[NSMenu alloc] init]; + NSMenu* menu = [[NSMenu alloc] init]; [menu addItem:quitItem]; - NSMenuItem *item = [[NSMenuItem alloc] init]; + NSMenuItem* item = [[NSMenuItem alloc] init]; [item setSubmenu:menu]; - NSMenu *mainMenu = [[NSMenu alloc] init]; + NSMenu* mainMenu = [[NSMenu alloc] init]; [mainMenu addItem:item]; [app setMainMenu:mainMenu]; - NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0., 0., 640., 480.) + Scene* scene = [[Scene alloc] init]; + CGSize minSize = [scene size]; + + + SKView* view = [[SKView alloc] init]; + [view setIgnoresSiblingOrder:YES]; + [view setShowsFPS:YES]; + [view presentScene:scene]; + + NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0., 0., minSize.width * 2, minSize.height * 2) styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowMiniaturizeButton | NSWindowStyleMaskResizable) backing:NSBackingStoreBuffered defer:false]; [window setTitle:@"NES"]; - [window setContentViewController:[[ViewController alloc] init]]; - [window setContentSize:NSMakeSize(640., 480.)]; + [window setContentAspectRatio:minSize]; + [window setContentMinSize:minSize]; + [window setContentView:view]; [window makeKeyAndOrderFront:nil]; [app activateIgnoringOtherApps:YES]; diff --git a/src/nes/CMakeLists.txt b/src/nes/CMakeLists.txt new file mode 100644 index 0000000..560dcdd --- /dev/null +++ b/src/nes/CMakeLists.txt @@ -0,0 +1,9 @@ +add_subdirectory(util) + +target_sources( + nes + PRIVATE + nes.hh + nes.cc + display.hh + display.cc) \ No newline at end of file diff --git a/src/nes/display.cc b/src/nes/display.cc new file mode 100644 index 0000000..4ff1d5f --- /dev/null +++ b/src/nes/display.cc @@ -0,0 +1,16 @@ +#include "nes/display.hh" +#include "nes/util/rgb.hh" + +namespace nes +{ + auto display::clear(rgb const color) -> void + { + for (auto y = unsigned{ 0 }; y < height; ++y) + { + for (auto x = unsigned{ 0 }; x < width; ++x) + { + set(x, y, color); + } + } + } +} // namespace nes \ No newline at end of file diff --git a/src/nes/display.hh b/src/nes/display.hh new file mode 100644 index 0000000..14ccff1 --- /dev/null +++ b/src/nes/display.hh @@ -0,0 +1,33 @@ +#pragma once + +namespace nes +{ + class rgb; + + /// Abstraction of the display logic. + class display + { + public: + static constexpr auto width = unsigned{ 256 }; + static constexpr auto height = unsigned{ 240 }; + + virtual ~display() = default; + + display(display const&) = delete; + display(display&&) = delete; + auto operator=(display const&) -> display& = delete; + auto operator=(display&&) -> display& = delete; + + /// Switch between front and back buffers. + virtual auto switch_buffers() -> void = 0; + /// Update the pixel at the given position in the back buffer. + virtual auto set(unsigned x, unsigned y, rgb value) -> void = 0; + /// Load the pixel value at the given position in the back buffer. + virtual auto get(unsigned x, unsigned y) const -> rgb = 0; + /// Clear the entire back buffer with a color. + virtual auto clear(rgb color) -> void; + + protected: + explicit display() = default; + }; +} // namespace nes \ No newline at end of file diff --git a/src/nes/nes.cc b/src/nes/nes.cc new file mode 100644 index 0000000..c503d65 --- /dev/null +++ b/src/nes/nes.cc @@ -0,0 +1,13 @@ +#include "nes/nes.hh" +#include "nes/display.hh" +#include "nes/util/rgb.hh" + +namespace nes +{ + nes::nes(display& display) + : display_{ display } + { + display.clear(rgb::from_hex(0xFF0000)); + display.switch_buffers(); + } +} // namespace nes \ No newline at end of file diff --git a/src/nes/nes.hh b/src/nes/nes.hh new file mode 100644 index 0000000..ab975f0 --- /dev/null +++ b/src/nes/nes.hh @@ -0,0 +1,19 @@ +#pragma once + +namespace nes +{ + class display; + + class nes + { + display& display_; + + public: + explicit nes(display&); + + nes(nes const&) = delete; + nes(nes&&) = delete; + auto operator=(nes const&) -> nes& = delete; + auto operator=(nes&&) -> nes& = delete; + }; +} // namespace nes \ No newline at end of file diff --git a/src/nes/util/CMakeLists.txt b/src/nes/util/CMakeLists.txt new file mode 100644 index 0000000..c2255dd --- /dev/null +++ b/src/nes/util/CMakeLists.txt @@ -0,0 +1,4 @@ +target_sources( + nes + PRIVATE + rgb.hh) \ No newline at end of file diff --git a/src/nes/util/rgb.hh b/src/nes/util/rgb.hh new file mode 100644 index 0000000..d62c70c --- /dev/null +++ b/src/nes/util/rgb.hh @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace nes +{ + /// An RGB value. + class rgb + { + public: + explicit rgb() = default; + + explicit rgb(std::uint8_t const r, std::uint8_t const g, std::uint8_t const b) + : r{ r } + , g{ g } + , b{ b } + { + } + + static auto from_hex(std::uint32_t const hex) -> rgb + { + auto res = rgb{}; + res.r = (hex & 0x00FF0000) >> 16; + res.g = (hex & 0x0000FF00) >> 8; + res.b = (hex & 0x000000FF) >> 0; + return res; + } + + std::uint8_t r{ 0 }; + std::uint8_t g{ 0 }; + std::uint8_t b{ 0 }; + }; +} // namespace nes \ No newline at end of file diff --git a/src/scene.hh b/src/scene.hh new file mode 100644 index 0000000..967a4fe --- /dev/null +++ b/src/scene.hh @@ -0,0 +1,12 @@ +#pragma once + +#import + +@interface Scene : SKScene + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + ++ (instancetype)sceneWithSize:(CGSize)size NS_UNAVAILABLE; +- (instancetype)initWithSize:(CGSize)size NS_UNAVAILABLE; + +@end \ No newline at end of file diff --git a/src/scene.mm b/src/scene.mm new file mode 100644 index 0000000..944d01a --- /dev/null +++ b/src/scene.mm @@ -0,0 +1,96 @@ +#import "scene.hh" +#import "nes/nes.hh" +#import "nes/display.hh" +#import "nes/util/rgb.hh" +#import + +namespace +{ + class display_impl : public nes::display + { + SKSpriteNode* node_; + NSMutableData* buffer_front_; + NSMutableData* buffer_back_; + std::uint8_t* bytes_back_; + + public: + explicit display_impl(SKSpriteNode* node) + : node_{ node } + , buffer_front_{ make_buffer() } + , buffer_back_{ make_buffer() } + , bytes_back_{ static_cast([buffer_back_ mutableBytes]) } + { + } + + ~display_impl() = default; + + virtual auto switch_buffers() -> void override + { + auto tmp = buffer_back_; + buffer_back_ = buffer_front_; + buffer_front_ = tmp; + bytes_back_ = static_cast([buffer_back_ mutableBytes]); + + auto texture = [SKTexture textureWithData:buffer_front_ size:CGSizeMake(width, height)]; + [texture setFilteringMode:SKTextureFilteringNearest]; + [node_ setTexture:texture]; + } + + virtual auto set(unsigned x, unsigned y, nes::rgb value) -> void override + { + 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; + } + + virtual auto get(unsigned x, unsigned y) const -> nes::rgb override + { + auto const offset = (y * width + x) * 4; + auto res = nes::rgb{}; + res.r = bytes_back_[offset + 0]; + res.g = bytes_back_[offset + 1]; + res.b = bytes_back_[offset + 2]; + return res; + } + + private: + static auto make_buffer() -> NSMutableData* + { + auto const length = NSUInteger{ width * height * 4 }; + return [NSMutableData dataWithLength:length]; + } + }; +} // namespace + +@implementation Scene +{ + std::unique_ptr display_; + std::unique_ptr nes_; +} + +- (instancetype)init +{ + self = [super initWithSize:CGSizeMake(nes::display::width, nes::display::height)]; + if (self) + { + SKSpriteNode* node = [[SKSpriteNode alloc] init]; + [node setAnchorPoint:CGPointZero]; + [node setSize:[self size]]; + + [self setScaleMode:SKSceneScaleModeAspectFit]; + [self addChild:node]; + + self->display_ = std::make_unique(node); + self->nes_ = std::make_unique(*self->display_); + } + return self; +} + +- (void)update:(NSTimeInterval)currentTime +{ + [super update:currentTime]; +} + +@end \ No newline at end of file diff --git a/src/view-controller.hh b/src/view-controller.hh deleted file mode 100644 index a89a0c4..0000000 --- a/src/view-controller.hh +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#import - -@interface ViewController : NSViewController -@end \ No newline at end of file diff --git a/src/view-controller.mm b/src/view-controller.mm deleted file mode 100644 index 5f24326..0000000 --- a/src/view-controller.mm +++ /dev/null @@ -1,15 +0,0 @@ -#import "view-controller.hh" - -@implementation ViewController - -- (void)loadView -{ - [self setView:[[NSView alloc] init]]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; -} - -@end \ No newline at end of file -- cgit v1.3.1