diff options
| author | Hannes Schulze <haschu0103@outlook.de> | 2025-01-23 22:02:11 +0100 |
|---|---|---|
| committer | Hannes Schulze <haschu0103@outlook.de> | 2025-01-23 22:02:11 +0100 |
| commit | b1d1573702f21e3104606d6ade5f5d39ec86a33a (patch) | |
| tree | d703be130a5d6f9ff2626313d3380ed34de44dd7 /app | |
| parent | 6512dd599d13035f6a90b2009344e02c488f5aae (diff) | |
restructure project
Diffstat (limited to 'app')
| -rw-r--r-- | app/CMakeLists.txt | 5 | ||||
| -rw-r--r-- | app/mac/CMakeLists.txt | 20 | ||||
| -rw-r--r-- | app/mac/main.mm | 60 | ||||
| -rw-r--r-- | app/mac/scene.hh | 12 | ||||
| -rw-r--r-- | app/mac/scene.mm | 179 | ||||
| -rw-r--r-- | app/test/CMakeLists.txt | 14 | ||||
| -rw-r--r-- | app/test/main.cc | 76 |
7 files changed, 366 insertions, 0 deletions
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..8ff9434 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,5 @@ +if(APPLE) + add_subdirectory(mac) +endif() + +add_subdirectory(test)
\ No newline at end of file diff --git a/app/mac/CMakeLists.txt b/app/mac/CMakeLists.txt new file mode 100644 index 0000000..a4f25c9 --- /dev/null +++ b/app/mac/CMakeLists.txt @@ -0,0 +1,20 @@ +add_executable(nes_mac) + +find_library(FRAMEWORK_COCOA Cocoa REQUIRED) +find_library(FRAMEWORK_SPRITEKIT SpriteKit REQUIRED) + +target_include_directories(nes_mac PRIVATE .) +target_link_libraries( + nes_mac + PRIVATE + nes::options + nes::nes + ${FRAMEWORK_COCOA} + ${FRAMEWORK_SPRITEKIT}) + +target_sources( + nes_mac + PRIVATE + main.mm + scene.hh + scene.mm)
\ No newline at end of file diff --git a/app/mac/main.mm b/app/mac/main.mm new file mode 100644 index 0000000..5ca7c5c --- /dev/null +++ b/app/mac/main.mm @@ -0,0 +1,60 @@ +#import "scene.hh" +#import <iostream> + +#import <Cocoa/Cocoa.h> +#import <SpriteKit/SpriteKit.h> + +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]; + + auto quitItem = [[NSMenuItem alloc] init]; + [quitItem setTitle:@"Quit NES"]; + [quitItem setKeyEquivalent:@"q"]; + [quitItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; + [quitItem setTarget:app]; + [quitItem setAction:@selector(terminate:)]; + + auto menu = [[NSMenu alloc] init]; + [menu addItem:quitItem]; + + auto item = [[NSMenuItem alloc] init]; + [item setSubmenu:menu]; + + auto mainMenu = [[NSMenu alloc] init]; + [mainMenu addItem:item]; + [app setMainMenu:mainMenu]; + + auto scene = [[Scene alloc] initWithFilePath:filePath]; + auto minSize = [scene size]; + + auto view = [[SKView alloc] init]; + [view setIgnoresSiblingOrder:YES]; + [view setShowsFPS:YES]; + [view presentScene:scene]; + + auto 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 setContentAspectRatio:minSize]; + [window setContentMinSize:minSize]; + [window setContentView:view]; + [window makeKeyAndOrderFront:nil]; + + [app activateIgnoringOtherApps:YES]; + [app run]; + + return 0; +}
\ No newline at end of file diff --git a/app/mac/scene.hh b/app/mac/scene.hh new file mode 100644 index 0000000..1660e66 --- /dev/null +++ b/app/mac/scene.hh @@ -0,0 +1,12 @@ +#pragma once + +#import <SpriteKit/SpriteKit.h> + +@interface Scene : SKScene + +- (instancetype)initWithFilePath:(NSString*)filePath 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/app/mac/scene.mm b/app/mac/scene.mm new file mode 100644 index 0000000..f858185 --- /dev/null +++ b/app/mac/scene.mm @@ -0,0 +1,179 @@ +#import "scene.hh" +#import "nes/nes.hh" +#import "nes/display.hh" +#import "nes/util/rgb.hh" +#import <memory> +#import <optional> + +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<std::uint8_t*>([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<std::uint8_t*>([buffer_back_ mutableBytes]); + } + + auto actually_switch_buffers() -> void + { + auto texture = [SKTexture textureWithData:buffer_front_ size:CGSizeMake(width, height) flipped:YES]; + [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_impl> display_; + std::unique_ptr<nes::nes> nes_; + std::optional<NSTimeInterval> last_time_; +} + +- (instancetype)initWithFilePath:(NSString*)filePath +{ + self = [super initWithSize:CGSizeMake(nes::display::width, nes::display::height)]; + if (self) + { + auto node = [[SKSpriteNode alloc] init]; + [node setAnchorPoint:CGPointZero]; + [node setSize:[self size]]; + + [self setScaleMode:SKSceneScaleModeAspectFit]; + [self addChild:node]; + + auto cartridge = nes::cartridge::from_file([filePath UTF8String]); + + self->display_ = std::make_unique<display_impl>(node); + self->nes_ = std::make_unique<nes::nes>(std::move(cartridge), *self->display_); + } + return self; +} + +- (void)keyDown:(NSEvent*)event +{ + auto& controller_1 = self->nes_->get_controller_1(); + + switch ([event keyCode]) + { + case 0x7e: // up arrow + controller_1.get_pressed().add(nes::buttons::up); + break; + case 0x7d: // down arrow + controller_1.get_pressed().add(nes::buttons::down); + break; + case 0x7b: // left arrow + controller_1.get_pressed().add(nes::buttons::left); + break; + case 0x7c: // right arrow + controller_1.get_pressed().add(nes::buttons::right); + break; + case 0x6: // "Z" + controller_1.get_pressed().add(nes::buttons::a); + break; + case 0x7: // "X" + controller_1.get_pressed().add(nes::buttons::b); + break; + case 0x24: // return + controller_1.get_pressed().add(nes::buttons::start); + break; + case 0x31: // space + controller_1.get_pressed().add(nes::buttons::select); + break; + default: + [super keyDown:event]; + break; + } +} + +- (void)keyUp:(NSEvent*)event +{ + auto& controller_1 = self->nes_->get_controller_1(); + + switch ([event keyCode]) + { + case 0x7e: // up arrow + controller_1.get_pressed().remove(nes::buttons::up); + break; + case 0x7d: // down arrow + controller_1.get_pressed().remove(nes::buttons::down); + break; + case 0x7b: // left arrow + controller_1.get_pressed().remove(nes::buttons::left); + break; + case 0x7c: // right arrow + controller_1.get_pressed().remove(nes::buttons::right); + break; + case 0x6: // "Z" + controller_1.get_pressed().remove(nes::buttons::a); + break; + case 0x7: // "X" + controller_1.get_pressed().remove(nes::buttons::b); + break; + case 0x24: // return + controller_1.get_pressed().remove(nes::buttons::start); + break; + case 0x31: // space + controller_1.get_pressed().remove(nes::buttons::select); + break; + default: + [super keyUp:event]; + break; + } +} + +- (void)update:(NSTimeInterval)currentTime +{ + auto const last_time = self->last_time_.value_or(currentTime); + last_time_ = currentTime; + auto const delta_us = static_cast<std::uint64_t>((currentTime - last_time) * 1000. * 1000.); + self->nes_->step(std::chrono::microseconds{ delta_us }); + self->display_->actually_switch_buffers(); +} + +@end
\ No newline at end of file diff --git a/app/test/CMakeLists.txt b/app/test/CMakeLists.txt new file mode 100644 index 0000000..b88acc2 --- /dev/null +++ b/app/test/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(nes_test) + +target_include_directories(nes_test PRIVATE .) +target_link_libraries( + nes_test + PRIVATE + nes::options + nes::nes + nes::ref) + +target_sources( + nes_test + PRIVATE + main.cc)
\ No newline at end of file diff --git a/app/test/main.cc b/app/test/main.cc new file mode 100644 index 0000000..3bb785f --- /dev/null +++ b/app/test/main.cc @@ -0,0 +1,76 @@ +#include "nes/display.hh" +#include "nes/nes.hh" +#include "nes/util/rgb.hh" +#include "nes/util/snapshot.hh" +#include <iostream> + +namespace +{ + class dummy_display final : public nes::display + { + std::uint8_t buffer_a_[width * height * 3]{}; + std::uint8_t buffer_b_[width * height * 3]{}; + std::uint8_t* front_buffer_{ buffer_a_ }; + std::uint8_t* back_buffer_{ buffer_b_ }; + + public: + explicit dummy_display() = default; + + auto get(unsigned const x, unsigned const y) const -> nes::rgb override + { + auto const offset = ((y * width) + x) * 3; + return nes::rgb{ back_buffer_[offset + 0], back_buffer_[offset + 1], back_buffer_[offset + 2] }; + } + + auto set(unsigned const x, unsigned const y, nes::rgb const value) -> void override + { + auto const offset = ((y * width) + x) * 3; + back_buffer_[offset + 0] = value.r; + back_buffer_[offset + 1] = value.g; + back_buffer_[offset + 2] = value.b; + } + + auto switch_buffers() -> void override + { + std::swap(front_buffer_, back_buffer_); + } + }; +} // namespace + +int main() +{ + auto const path = "/Users/hannes/Documents/temp/supermario.nes"; + + auto cartridge = nes::cartridge::from_file(path); + if (!cartridge.is_valid()) + { + std::cerr << "Could not load cartridge!" << std::endl; + std::abort(); + } + + // Initialize the system to be tested. + auto display_a = std::make_unique<dummy_display>(); + auto sys_a = nes::nes{ cartridge, *display_a }; + + // Initialize the (assumed to be working) reference implementation. + auto display_b = std::make_unique<dummy_display>(); + auto sys_b = nes::nes{ cartridge, *display_b }; + + // Run. + while (true) + { + auto const status_a = sys_a.get_snapshot(); + auto const status_b = sys_b.get_snapshot(); + + if (status_a != status_b) + { + std::cout << "Different snapshot!\n"; + return 1; + } + + std::cout << "Cycle: " << status_a.cpu_cycle.to_cpu() << ", " << status_b.cpu_cycle.to_cpu() << '\n'; + + sys_a.step_to_nmi(); + sys_b.step_to_nmi(); + } +}
\ No newline at end of file |
