diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a4c3a1b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: LIJI32 diff --git a/.github/actions/install_deps.sh b/.github/actions/install_deps.sh index 1c9749e..991f2e1 100755 --- a/.github/actions/install_deps.sh +++ b/.github/actions/install_deps.sh @@ -4,7 +4,7 @@ case `echo $1 | cut -d '-' -f 1` in sudo apt-get install -yq bison libpng-dev pkg-config libsdl2-dev ( cd `mktemp -d` - curl -L https://github.com/rednex/rgbds/archive/v0.4.0.zip > rgbds.zip + curl -L https://github.com/rednex/rgbds/archive/v0.6.0.zip > rgbds.zip unzip rgbds.zip cd rgbds-* make -sj @@ -12,9 +12,22 @@ case `echo $1 | cut -d '-' -f 1` in cd .. rm -rf * ) + + ( + cd `mktemp -d` + curl -L https://github.com/BR903/cppp/archive/refs/heads/master.zip > cppp.zip + unzip cppp.zip + cd cppp-* + make -sj + sudo make install + cd .. + rm -rf * + ) + + ;; macos) - brew install rgbds sdl2 + brew install rgbds sdl2 cppp ;; *) echo "Unsupported OS" diff --git a/.github/actions/sanity_tests.sh b/.github/actions/sanity_tests.sh index 13b5e39..0cd4686 100755 --- a/.github/actions/sanity_tests.sh +++ b/.github/actions/sanity_tests.sh @@ -1,3 +1,5 @@ +#!/bin/sh + set -e ./build/bin/tester/sameboy_tester --jobs 5 \ @@ -7,7 +9,7 @@ set -e --dmg --length 45 .github/actions/dmg_sound-2.gb \ --dmg --length 20 .github/actions/oam_bug-2.gb -mv .github/actions/dmg{,-mode}-acid2.bmp +mv .github/actions/dmg-acid2.bmp .github/actions/dmg-mode-acid2.bmp ./build/bin/tester/sameboy_tester \ --dmg --length 10 .github/actions/dmg-acid2.gb @@ -16,10 +18,10 @@ set +e FAILED_TESTS=` shasum .github/actions/*.bmp | grep -E -v \(\ -5283564df0cf5bb78a7a90aff026c1a4692fd39e\ \ .github/actions/cgb-acid2.bmp\|\ +64c3fd9a5fe9aee40fe15f3371029c0d2f20f5bc\ \ .github/actions/cgb-acid2.bmp\|\ dbcc438dcea13b5d1b80c5cd06bda2592cc5d9e0\ \ .github/actions/cgb_sound.bmp\|\ 0caadf9634e40247ae9c15ff71992e8f77bbf89e\ \ .github/actions/dmg-acid2.bmp\|\ -a732077f98f43d9231453b1764d9f797a836924d\ \ .github/actions/dmg-mode-acid2.bmp\|\ +fbdb5e342bfdd2edda3ea5601d35d0ca60d18034\ \ .github/actions/dmg-mode-acid2.bmp\|\ c9e944b7e01078bdeba1819bc2fa9372b111f52d\ \ .github/actions/dmg_sound-2.bmp\|\ f0172cc91867d3343fbd113a2bb98100074be0de\ \ .github/actions/oam_bug-2.bmp\ \)` diff --git a/.github/actions/update_libretro.sh b/.github/actions/update_libretro.sh new file mode 100755 index 0000000..a33633e --- /dev/null +++ b/.github/actions/update_libretro.sh @@ -0,0 +1,30 @@ +set -ex + +git fetch --tags +LATEST=$(git tag --sort=-creatordate | grep "^v" | grep -v libretro | head -n 1) + +if [ $(git tag -l "$LATEST"-libretro) ]; then + echo "The libretro branch is already up-to-date" + exit 0 +fi + +git config --global --add --bool push.autoSetupRemote true +git config --global user.name 'Libretro Updater' +git config --global user.email '<>' + +cp libretro/gitlab-ci.yml .gitlab-ci.yml + +echo "Switching to tag $LATEST" +git branch --delete libretro || true +git checkout tags/$LATEST -b libretro + +echo "Building boot ROMs..." +make -j bootroms + +echo "Updating branch" +mv build/bin/BootROMs BootROMs/prebuilt +git add BootROMs/prebuilt/* .gitlab-ci.yml +git commit -m "Update libretro branch to $LATEST" +git tag "$LATEST"-libretro +git push --force +git push --tags diff --git a/.github/workflows/libretro.yml b/.github/workflows/libretro.yml new file mode 100644 index 0000000..b249f70 --- /dev/null +++ b/.github/workflows/libretro.yml @@ -0,0 +1,28 @@ +name: "libretro branch update" + +on: + push: + branches: + - master + +jobs: + libretro-prebuilt-update: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + name: Checkout + with: + repository: LIJI32/SameBoy + token: ${{ secrets.WEBSITETOKEN }} + submodules: false + - name: Install Deps + shell: bash + run: | + ./.github/actions/install_deps.sh ${{ matrix.os }} + - name: Build Boot ROMs and Push + run: | + ./.github/actions/update_libretro.sh diff --git a/.github/workflows/sanity.yml b/.github/workflows/sanity.yml index ac37323..414b31f 100644 --- a/.github/workflows/sanity.yml +++ b/.github/workflows/sanity.yml @@ -1,36 +1,44 @@ name: "Bulidability and Sanity" -on: push +on: + push: + branches: + - master + - '*' + - '!libretro' jobs: sanity: strategy: fail-fast: false matrix: - os: [macos-latest, ubuntu-latest, ubuntu-18.04] + os: [macos-latest, ubuntu-latest, ubuntu-20.04] cc: [gcc, clang] include: - os: macos-latest cc: clang - extra_target: cocoa + extra_targets: cocoa ios-ipa ios-deb exclude: - os: macos-latest cc: gcc runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install deps shell: bash run: | ./.github/actions/install_deps.sh ${{ matrix.os }} - name: Build run: | - ${{ matrix.cc }} -v; (make -j sdl tester libretro ${{ matrix.extra_target }} CONF=release CC=${{ matrix.cc }} || (echo "==== Build Failed ==="; make sdl tester libretro ${{ matrix.extra_target }} CONF=release CC=${{ matrix.cc }})) + ${{ matrix.cc }} -v; (make -j all CONF=release CC=${{ matrix.cc }} || (echo "==== Build Failed ==="; make all CONF=release CC=${{ matrix.cc }})) - name: Sanity tests shell: bash run: | ./.github/actions/sanity_tests.sh - name: Upload binaries - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v3 with: name: sameboy-canary-${{ matrix.os }}-${{ matrix.cc }} - path: build/bin + path: | + build/bin + build/lib + build/include diff --git a/Cocoa/GBAudioClient.h b/AppleCommon/GBAudioClient.h similarity index 100% rename from Cocoa/GBAudioClient.h rename to AppleCommon/GBAudioClient.h diff --git a/Cocoa/GBAudioClient.m b/AppleCommon/GBAudioClient.m similarity index 81% rename from Cocoa/GBAudioClient.m rename to AppleCommon/GBAudioClient.m index 81a51fd..e650aaf 100644 --- a/Cocoa/GBAudioClient.m +++ b/AppleCommon/GBAudioClient.m @@ -35,30 +35,43 @@ static OSStatus render( // kAudioUnitSubType_DefaultOutput on Mac OS X) AudioComponentDescription defaultOutputDescription; defaultOutputDescription.componentType = kAudioUnitType_Output; +#if TARGET_OS_IPHONE + defaultOutputDescription.componentSubType = kAudioUnitSubType_RemoteIO; +#else defaultOutputDescription.componentSubType = kAudioUnitSubType_DefaultOutput; +#endif defaultOutputDescription.componentManufacturer = kAudioUnitManufacturer_Apple; defaultOutputDescription.componentFlags = 0; defaultOutputDescription.componentFlagsMask = 0; // Get the default playback output unit AudioComponent defaultOutput = AudioComponentFindNext(NULL, &defaultOutputDescription); - NSAssert(defaultOutput, @"Can't find default output"); + if (!defaultOutput) { + NSLog(@"Can't find default output"); + return nil; + } // Create a new unit based on this that we'll use for output OSErr err = AudioComponentInstanceNew(defaultOutput, &audioUnit); - NSAssert1(audioUnit, @"Error creating unit: %hd", err); + if (!audioUnit) { + NSLog(@"Error creating unit: %hd", err); + return nil; + } // Set our tone rendering function on the unit AURenderCallbackStruct input; input.inputProc = (void*)render; - input.inputProcRefCon = (__bridge void * _Nullable)(self); + input.inputProcRefCon = (__bridge void *)(self); err = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &input, sizeof(input)); - NSAssert1(err == noErr, @"Error setting callback: %hd", err); + if (err) { + NSLog(@"Error setting callback: %hd", err); + return nil; + } AudioStreamBasicDescription streamFormat; streamFormat.mSampleRate = rate; @@ -76,9 +89,16 @@ static OSStatus render( 0, &streamFormat, sizeof(AudioStreamBasicDescription)); - NSAssert1(err == noErr, @"Error setting stream format: %hd", err); + + if (err) { + NSLog(@"Error setting stream format: %hd", err); + return nil; + } err = AudioUnitInitialize(audioUnit); - NSAssert1(err == noErr, @"Error initializing unit: %hd", err); + if (err) { + NSLog(@"Error initializing unit: %hd", err); + return nil; + } self.renderBlock = block; _rate = rate; @@ -89,7 +109,10 @@ static OSStatus render( -(void) start { OSErr err = AudioOutputUnitStart(audioUnit); - NSAssert1(err == noErr, @"Error starting unit: %hd", err); + if (err) { + NSLog(@"Error starting unit: %hd", err); + return; + } _playing = true; } diff --git a/AppleCommon/GBViewBase.h b/AppleCommon/GBViewBase.h new file mode 100644 index 0000000..94474cb --- /dev/null +++ b/AppleCommon/GBViewBase.h @@ -0,0 +1,36 @@ +#import + +#if TARGET_OS_IPHONE +#define NSView UIView +#import +#else +#import +#endif + +#import + +typedef enum { + GB_FRAME_BLENDING_MODE_DISABLED, + GB_FRAME_BLENDING_MODE_SIMPLE, + GB_FRAME_BLENDING_MODE_ACCURATE, + GB_FRAME_BLENDING_MODE_ACCURATE_EVEN = GB_FRAME_BLENDING_MODE_ACCURATE, + GB_FRAME_BLENDING_MODE_ACCURATE_ODD, +} GB_frame_blending_mode_t; + +@interface GBViewBase : NSView +{ + @public + GB_gameboy_t *_gb; +} + +@property (nonatomic) GB_gameboy_t *gb; +@property (nonatomic) GB_frame_blending_mode_t frameBlendingMode; +@property (nonatomic, strong) NSView *internalView; +- (void) flip; +- (uint32_t *) pixels; +- (void)screenSizeChanged; +- (void) createInternalView; +- (uint32_t *)currentBuffer; +- (uint32_t *)previousBuffer; + +@end diff --git a/AppleCommon/GBViewBase.m b/AppleCommon/GBViewBase.m new file mode 100644 index 0000000..de914dd --- /dev/null +++ b/AppleCommon/GBViewBase.m @@ -0,0 +1,85 @@ +#import "GBViewBase.h" + +@implementation GBViewBase +{ + uint32_t *_imageBuffers[3]; + unsigned _currentBuffer; + GB_frame_blending_mode_t _frameBlendingMode; + bool _oddFrame; +} + +- (void)screenSizeChanged +{ + if (_imageBuffers[0]) free(_imageBuffers[0]); + if (_imageBuffers[1]) free(_imageBuffers[1]); + if (_imageBuffers[2]) free(_imageBuffers[2]); + + size_t buffer_size = sizeof(_imageBuffers[0][0]) * GB_get_screen_width(_gb) * GB_get_screen_height(_gb); + + _imageBuffers[0] = calloc(1, buffer_size); + _imageBuffers[1] = calloc(1, buffer_size); + _imageBuffers[2] = calloc(1, buffer_size); +} + +- (void)flip +{ + _currentBuffer = (_currentBuffer + 1) % self.numberOfBuffers; + _oddFrame = GB_is_odd_frame(_gb); +} + +- (unsigned) numberOfBuffers +{ + return _frameBlendingMode? 3 : 2; +} + +- (void) createInternalView +{ + assert(false && "createInternalView must not be inherited"); +} + +- (uint32_t *)currentBuffer +{ + return _imageBuffers[_currentBuffer]; +} + +- (uint32_t *)previousBuffer +{ + return _imageBuffers[(_currentBuffer + 2) % self.numberOfBuffers]; +} + +- (uint32_t *) pixels +{ + return _imageBuffers[(_currentBuffer + 1) % self.numberOfBuffers]; +} + +- (void) setFrameBlendingMode:(GB_frame_blending_mode_t)frameBlendingMode +{ + _frameBlendingMode = frameBlendingMode; + [self setNeedsDisplay]; +} + +- (GB_frame_blending_mode_t)frameBlendingMode +{ + if (_frameBlendingMode == GB_FRAME_BLENDING_MODE_ACCURATE) { + if (!_gb || GB_is_sgb(_gb)) { + return GB_FRAME_BLENDING_MODE_SIMPLE; + } + return _oddFrame ? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN; + } + return _frameBlendingMode; +} + +- (void)dealloc +{ + free(_imageBuffers[0]); + free(_imageBuffers[1]); + free(_imageBuffers[2]); +} + +#if !TARGET_OS_IPHONE +- (void)setNeedsDisplay +{ + [self setNeedsDisplay:true]; +} +#endif +@end diff --git a/AppleCommon/GBViewMetal.h b/AppleCommon/GBViewMetal.h new file mode 100644 index 0000000..c865b3b --- /dev/null +++ b/AppleCommon/GBViewMetal.h @@ -0,0 +1,11 @@ +#import +#import +#if TARGET_OS_IPHONE +#import "../iOS/GBView.h" +#else +#import "../Cocoa/GBView.h" +#endif + +@interface GBViewMetal : GBView ++ (bool) isSupported; +@end diff --git a/Cocoa/GBViewMetal.m b/AppleCommon/GBViewMetal.m similarity index 62% rename from Cocoa/GBViewMetal.m rename to AppleCommon/GBViewMetal.m index ae7443f..ad715bb 100644 --- a/Cocoa/GBViewMetal.m +++ b/AppleCommon/GBViewMetal.m @@ -1,7 +1,9 @@ #import #import "GBViewMetal.h" #pragma clang diagnostic ignored "-Wpartial-availability" - +#if !TARGET_OS_IPHONE +#import "../Cocoa/NSObject+DefaultsObserver.h" +#endif static const vector_float2 rect[] = { @@ -13,27 +15,32 @@ static const vector_float2 rect[] = @implementation GBViewMetal { - id device; - id texture, previous_texture; - id vertices; - id pipeline_state; - id command_queue; - id frame_blending_mode_buffer; - id output_resolution_buffer; - vector_float2 output_resolution; + id _device; + id _texture, _previousTexture; + id _vertices; + id _pipelineState; + id _commandQueue; + id _frameBlendingModeBuffer; + id _outputResolutionBuffer; + vector_float2 _outputResolution; + id _commandBuffer; + bool _waitedForFrame; } + (bool)isSupported { +#if TARGET_OS_IPHONE + return true; +#else if (MTLCopyAllDevices) { return [MTLCopyAllDevices() count]; } return false; +#endif } - - (void) allocateTextures { - if (!device) return; + if (!_device) return; MTLTextureDescriptor *texture_descriptor = [[MTLTextureDescriptor alloc] init]; @@ -42,39 +49,44 @@ static const vector_float2 rect[] = texture_descriptor.width = GB_get_screen_width(self.gb); texture_descriptor.height = GB_get_screen_height(self.gb); - texture = [device newTextureWithDescriptor:texture_descriptor]; - previous_texture = [device newTextureWithDescriptor:texture_descriptor]; + _texture = [_device newTextureWithDescriptor:texture_descriptor]; + _previousTexture = [_device newTextureWithDescriptor:texture_descriptor]; } - (void)createInternalView { - MTKView *view = [[MTKView alloc] initWithFrame:self.frame device:(device = MTLCreateSystemDefaultDevice())]; + MTKView *view = [[MTKView alloc] initWithFrame:self.frame device:(_device = MTLCreateSystemDefaultDevice())]; view.delegate = self; self.internalView = view; view.paused = true; view.enableSetNeedsDisplay = true; view.framebufferOnly = false; - vertices = [device newBufferWithBytes:rect + _vertices = [_device newBufferWithBytes:rect length:sizeof(rect) options:MTLResourceStorageModeShared]; static const GB_frame_blending_mode_t default_blending_mode = GB_FRAME_BLENDING_MODE_DISABLED; - frame_blending_mode_buffer = [device newBufferWithBytes:&default_blending_mode + _frameBlendingModeBuffer = [_device newBufferWithBytes:&default_blending_mode length:sizeof(default_blending_mode) options:MTLResourceStorageModeShared]; - output_resolution_buffer = [device newBufferWithBytes:&output_resolution - length:sizeof(output_resolution) + _outputResolutionBuffer = [_device newBufferWithBytes:&_outputResolution + length:sizeof(_outputResolution) options:MTLResourceStorageModeShared]; - output_resolution = (simd_float2){view.drawableSize.width, view.drawableSize.height}; + _outputResolution = (simd_float2){view.drawableSize.width, view.drawableSize.height}; + /* TODO: NSObject+DefaultsObserver can replace the less flexible `addDefaultObserver` in iOS */ +#if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loadShader) name:@"GBFilterChanged" object:nil]; [self loadShader]; +#else + [self observeStandardDefaultsKey:@"GBFilter" selector:@selector(loadShader)]; +#endif } -- (void) loadShader +- (void)loadShader { NSError *error = nil; NSString *shader_source = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"MasterShader" @@ -95,7 +107,7 @@ static const vector_float2 rect[] = MTLCompileOptions *options = [[MTLCompileOptions alloc] init]; options.fastMathEnabled = true; - id library = [device newLibraryWithSource:shader_source + id library = [_device newLibraryWithSource:shader_source options:options error:&error]; if (error) { @@ -115,19 +127,19 @@ static const vector_float2 rect[] = pipeline_state_descriptor.colorAttachments[0].pixelFormat = ((MTKView *)self.internalView).colorPixelFormat; error = nil; - pipeline_state = [device newRenderPipelineStateWithDescriptor:pipeline_state_descriptor + _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipeline_state_descriptor error:&error]; if (error) { NSLog(@"Failed to created pipeline state, error %@", error); return; } - command_queue = [device newCommandQueue]; + _commandQueue = [_device newCommandQueue]; } - (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size { - output_resolution = (vector_float2){size.width, size.height}; + _outputResolution = (vector_float2){size.width, size.height}; dispatch_async(dispatch_get_main_queue(), ^{ [(MTKView *)self.internalView draw]; }); @@ -135,90 +147,107 @@ static const vector_float2 rect[] = - (void)drawInMTKView:(MTKView *)view { +#if !TARGET_OS_IPHONE if (!(view.window.occlusionState & NSWindowOcclusionStateVisible)) return; +#endif if (!self.gb) return; - if (texture.width != GB_get_screen_width(self.gb) || - texture.height != GB_get_screen_height(self.gb)) { + if (_texture.width != GB_get_screen_width(self.gb) || + _texture.height != GB_get_screen_height(self.gb)) { [self allocateTextures]; } MTLRegion region = { {0, 0, 0}, // MTLOrigin - {texture.width, texture.height, 1} // MTLSize + {_texture.width, _texture.height, 1} // MTLSize }; + + /* Don't start rendering if the previous frame hasn't finished yet. Either wait, or skip the frame */ + if (_commandBuffer && _commandBuffer.status != MTLCommandBufferStatusCompleted) { + if (_waitedForFrame) return; + [_commandBuffer waitUntilCompleted]; + _waitedForFrame = true; + } + else { + _waitedForFrame = false; + } - [texture replaceRegion:region + GB_frame_blending_mode_t mode = [self frameBlendingMode]; + + [_texture replaceRegion:region mipmapLevel:0 withBytes:[self currentBuffer] - bytesPerRow:texture.width * 4]; - if ([self frameBlendingMode]) { - [previous_texture replaceRegion:region + bytesPerRow:_texture.width * 4]; + + if (mode) { + [_previousTexture replaceRegion:region mipmapLevel:0 withBytes:[self previousBuffer] - bytesPerRow:texture.width * 4]; + bytesPerRow:_texture.width * 4]; } - - MTLRenderPassDescriptor *render_pass_descriptor = view.currentRenderPassDescriptor; - id command_buffer = [command_queue commandBuffer]; - - if (render_pass_descriptor != nil) { - *(GB_frame_blending_mode_t *)[frame_blending_mode_buffer contents] = [self frameBlendingMode]; - *(vector_float2 *)[output_resolution_buffer contents] = output_resolution; - - id render_encoder = - [command_buffer renderCommandEncoderWithDescriptor:render_pass_descriptor]; - [render_encoder setViewport:(MTLViewport){0.0, 0.0, - output_resolution.x, - output_resolution.y, + MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor; + _commandBuffer = [_commandQueue commandBuffer]; + + if (renderPassDescriptor) { + *(GB_frame_blending_mode_t *)[_frameBlendingModeBuffer contents] = mode; + *(vector_float2 *)[_outputResolutionBuffer contents] = _outputResolution; + + id renderEncoder = + [_commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; + + [renderEncoder setViewport:(MTLViewport){0.0, 0.0, + _outputResolution.x, + _outputResolution.y, -1.0, 1.0}]; - [render_encoder setRenderPipelineState:pipeline_state]; + [renderEncoder setRenderPipelineState:_pipelineState]; - [render_encoder setVertexBuffer:vertices + [renderEncoder setVertexBuffer:_vertices offset:0 atIndex:0]; - [render_encoder setFragmentBuffer:frame_blending_mode_buffer + [renderEncoder setFragmentBuffer:_frameBlendingModeBuffer offset:0 atIndex:0]; - [render_encoder setFragmentBuffer:output_resolution_buffer + [renderEncoder setFragmentBuffer:_outputResolutionBuffer offset:0 atIndex:1]; - [render_encoder setFragmentTexture:texture + [renderEncoder setFragmentTexture:_texture atIndex:0]; - [render_encoder setFragmentTexture:previous_texture + [renderEncoder setFragmentTexture:_previousTexture atIndex:1]; - [render_encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip + [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; - [render_encoder endEncoding]; + [renderEncoder endEncoding]; - [command_buffer presentDrawable:view.currentDrawable]; + [_commandBuffer presentDrawable:view.currentDrawable]; } - [command_buffer commit]; + [_commandBuffer commit]; } - (void)flip { [super flip]; dispatch_async(dispatch_get_main_queue(), ^{ - [(MTKView *)self.internalView setNeedsDisplay:true]; + [(MTKView *)self.internalView draw]; }); } +#if !TARGET_OS_IPHONE - (NSImage *)renderToImage { CIImage *ciImage = [CIImage imageWithMTLTexture:[[(MTKView *)self.internalView currentDrawable] texture] options:@{ - kCIImageColorSpace: (__bridge_transfer id)CGColorSpaceCreateDeviceRGB() + kCIImageColorSpace: (__bridge_transfer id)CGColorSpaceCreateDeviceRGB(), + kCIImageProperties: [NSNull null] }]; ciImage = [ciImage imageByApplyingTransform:CGAffineTransformTranslate(CGAffineTransformMakeScale(1, -1), 0, ciImage.extent.size.height)]; @@ -228,5 +257,6 @@ static const vector_float2 rect[] = CGImageRelease(cgImage); return ret; } +#endif @end diff --git a/BESS.md b/BESS.md index 7c9296e..1807943 100644 --- a/BESS.md +++ b/BESS.md @@ -176,6 +176,48 @@ The length of this block is 0x11 bytes long and it follows the following structu | 0x0E | Scheduled alarm time days (16-bit) | | 0x10 | Alarm enabled flag (8-bits, either 0 or 1) | +#### TPP1 block +The TPP1 block uses the `'TPP1'` identifier, and is an optional block that is used while emulating a TPP1 cartridge to store RTC information. This block can be omitted if the ROM header does not specify the inclusion of a RTC. + +The length of this block is 0x11 bytes long and it follows the following structure: + +| Offset | Content | +|--------|-------------------------------------------------------| +| 0x00 | UNIX timestamp at the time of the save state (64-bit) | +| 0x08 | The current RTC data (4 bytes) | +| 0x0C | The latched RTC data (4 bytes) | +| 0x10 | The value of the MR4 register (8-bits) | + + +#### MBC7 block +The MBC7 block uses the `'MBC7'` identifier, and is an optional block that is used while emulating an MBC7 cartridge to store the EEPROM communication state and motion control state. + +The length of this block is 0xA bytes long and it follows the following structure: + +| Offset | Content | +|--------|-------------------------------------------------------| +| 0x00 | Flags (8-bits) | +| 0x01 | Argument bits left (8-bits) | +| 0x02 | Current EEPROM command (16-bits) | +| 0x04 | Pending bits to read (16-bits) | +| 0x06 | Latched gyro X value (16-bits) | +| 0x08 | Latched gyro Y value (16-bits) | + +The meaning of the individual bits in flags are: + * Bit 0: Latch ready; set after writing `0x55` to `0xAX0X` and reset after writing `0xAA` to `0xAX1X` + * Bit 1: EEPROM DO line + * Bit 2: EEPROM DI line + * Bit 3: EEPROM CLK line + * Bit 4: EEPROM CS line + * Bit 5: EEPROM write enable; set after an `EWEN` command, reset after an `EWDS` command + * Bits 6-7: Unused. + +The current EEPROM command field has bits pushed to its LSB first, padded with zeros. For example, if the ROM clocked a single `1` bit, this field should contain `0b1`; if the ROM later clocks a `0` bit, this field should contain `0b10`. + +If the currently transmitted command has an argument, the "Argument bits left" field should contain the number argument bits remaining. Otherwise, it should contain 0. + +The "Pending bits to read" field contains the pending bits waiting to be shifted into the DO signal, MSB first, padded with ones. + #### SGB block The SGB block uses the `'SGB '` identifier, and is an optional block that is only used while emulating an SGB or SGB2 *and* SGB commands enabled. Implementations must not save this block on other models or when SGB commands are disabled, and should assume SGB commands are disabled if this block is missing. diff --git a/BootROMs/SameBoyLogo.png b/BootROMs/SameBoyLogo.png index c7cfc08..ad1a760 100644 Binary files a/BootROMs/SameBoyLogo.png and b/BootROMs/SameBoyLogo.png differ diff --git a/BootROMs/agb_boot.asm b/BootROMs/agb_boot.asm index 95a2c78..a4e3ee8 100644 --- a/BootROMs/agb_boot.asm +++ b/BootROMs/agb_boot.asm @@ -1,2 +1,2 @@ -AGB EQU 1 -include "cgb_boot.asm" \ No newline at end of file +DEF AGB = 1 +include "cgb_boot.asm" diff --git a/BootROMs/cgb0_boot.asm b/BootROMs/cgb0_boot.asm index d49166d..af9c8d5 100644 --- a/BootROMs/cgb0_boot.asm +++ b/BootROMs/cgb0_boot.asm @@ -1,2 +1,2 @@ -CGB0 EQU 1 -include "cgb_boot.asm" \ No newline at end of file +DEF CGB0 = 1 +include "cgb_boot.asm" diff --git a/BootROMs/cgb_boot.asm b/BootROMs/cgb_boot.asm index ae40e24..8fc2ca4 100644 --- a/BootROMs/cgb_boot.asm +++ b/BootROMs/cgb_boot.asm @@ -1,16 +1,18 @@ ; SameBoy CGB bootstrap ROM -; Todo: use friendly names for HW registers instead of magic numbers -SECTION "BootCode", ROM0[$0] + +include "sameboot.inc" + +SECTION "BootCode", ROM0[$0000] Start: ; Init stack pointer - ld sp, $fffe + ld sp, $FFFE ; Clear memory VRAM - call ClearMemoryPage8000 + call ClearMemoryVRAM ; Clear OAM - ld h, $fe - ld c, $a0 + ld h, HIGH(_OAMRAM) + ld c, sizeof_OAM_ATTRS * OAM_COUNT .clearOAMLoop ldi [hl], a dec c @@ -18,8 +20,8 @@ Start: IF !DEF(CGB0) ; Init waveform - ld c, $10 - ld hl, $FF30 + ld c, 16 + ld hl, _AUD3WAVERAM .waveformLoop ldi [hl], a cpl @@ -28,30 +30,32 @@ IF !DEF(CGB0) ENDC ; Clear chosen input palette - ldh [InputPalette], a + ldh [hInputPalette], a ; Clear title checksum - ldh [TitleChecksum], a + ldh [hTitleChecksum], a - ld a, $80 - ldh [$26], a - ldh [$11], a - ld a, $f3 - ldh [$12], a - ldh [$25], a +; Init Audio + ld a, AUDENA_ON + ldh [rNR52], a + assert AUDENA_ON == AUDLEN_DUTY_50 + ldh [rNR11], a + ld a, $F3 + ldh [rNR12], a ; Envelope $F, decreasing, sweep $3 + ldh [rNR51], a ; Channels 1+2+3+4 left, channels 1+2 right ld a, $77 - ldh [$24], a + ldh [rNR50], a ; Volume $7, left and right ; Init BG palette - ld a, $fc - ldh [$47], a + ld a, %11_11_11_00 + ldh [rBGP], a ; Load logo from ROM. ; A nibble represents a 4-pixels line, 2 bytes represent a 4x4 tile, scaled to 8x8. ; Tiles are ordered left to right, top to bottom. ; These tiles are not used, but are required for DMG compatibility. This is done ; by the original CGB Boot ROM as well. - ld de, $104 ; Logo start - ld hl, $8010 ; This is where we load the tiles in VRAM + ld de, NintendoLogo + ld hl, _VRAM + $10 ; This is where we load the tiles in VRAM .loadLogoLoop ld a, [de] ; Read 2 rows @@ -59,28 +63,28 @@ ENDC call DoubleBitsAndWriteRowTwice inc de ld a, e - cp $34 ; End of logo + cp LOW(NintendoLogoEnd) jr nz, .loadLogoLoop call ReadTrademarkSymbol ; Clear the second VRAM bank ld a, 1 - ldh [$4F], a - call ClearMemoryPage8000 + ldh [rVBK], a + call ClearMemoryVRAM call LoadTileset ld b, 3 IF DEF(FAST) xor a - ldh [$4F], a + ldh [rVBK], a ELSE ; Load Tilemap - ld hl, $98C2 + ld hl, _SCRN0 + 6 * SCRN_VX_B + 2 ld d, 3 ld a, 8 .tilemapLoop - ld c, $10 + ld c, 16 .tilemapRowLoop @@ -114,19 +118,19 @@ ELSE dec d ld a, $38 - ld l, $a7 - ld bc, $0107 + ld l, $A7 + lb bc, 1, 7 ; $0107 jr .tilemapRowLoop .write_with_palette push af ; Switch to second VRAM Bank ld a, 1 - ldh [$4F], a + ldh [rVBK], a ld [hl], 8 ; Switch to back first VRAM Bank xor a - ldh [$4F], a + ldh [rVBK], a pop af ldi [hl], a ret @@ -136,7 +140,7 @@ ENDC ; Expand Palettes ld de, AnimationColors ld c, 8 - ld hl, BgPalettes + ld hl, hBgPalettes xor a .expandPalettesLoop: cpl @@ -179,14 +183,14 @@ ENDC call LoadPalettesFromHRAM ; Turn on LCD - ld a, $91 - ldh [$40], a + ld a, LCDCF_ON | LCDCF_BLK01 | LCDCF_BGON + ldh [rLCDC], a IF !DEF(FAST) call DoIntroAnimation ld a, 48 ; frames to wait after playing the chime - ldh [WaitLoopCounter], a + ldh [hWaitLoopCounter], a ld b, 4 ; frames to wait before playing the chime call WaitBFrames @@ -196,34 +200,39 @@ IF !DEF(FAST) ld b, 5 call WaitBFrames ; Play second sound - ld a, $c1 + ld a, $C1 call PlaySound .waitLoop call GetInputPaletteIndex call WaitFrame - ld hl, WaitLoopCounter + ld hl, hWaitLoopCounter dec [hl] jr nz, .waitLoop ELSE - ld a, $c1 + ld a, $C1 call PlaySound ENDC call Preboot IF DEF(AGB) - ld b, 1 + inc b ENDC jr BootGame HDMAData: - db $D0, $00, $98, $A0, $12 - db $D0, $00, $80, $00, $40 +MACRO hdma_data ; source, destination, length + db HIGH(\1), LOW(\1) + db HIGH(\2), LOW(\2) + db (\3) +ENDM + hdma_data _RAMBANK, _SCRN0 + 5 * SCRN_VX_B + 0, 18 + hdma_data _RAMBANK, _VRAM, 64 -SECTION "BootGame", ROM0[$fe] +SECTION "BootGame", ROM0[$00FE] BootGame: - ldh [$50], a + ldh [rBANK], a ; unmap boot ROM -SECTION "MoreStuff", ROM0[$200] +SECTION "BootData", ROM0[$0200] ; Game Palettes Data TitleChecksums: db $00 ; Default @@ -326,103 +335,107 @@ FirstChecksumWithDuplicate: ChecksumsEnd: PalettePerChecksum: -palette_index: MACRO ; palette, flags - db ((\1)) | (\2) ; | $80 means game requires DMG boot tilemap +MACRO palette_index ; palette[, flags] + IF _NARG == 1 + db (\1) + ELSE + db (\1) | (\2) ; flag $80 means game requires DMG boot tilemap + ENDC ENDM - palette_index 0, 0 ; Default Palette - palette_index 4, 0 ; ALLEY WAY - palette_index 5, 0 ; YAKUMAN - palette_index 35, 0 ; BASEBALL, (Game and Watch 2) - palette_index 34, 0 ; TENNIS - palette_index 3, 0 ; TETRIS - palette_index 31, 0 ; QIX - palette_index 15, 0 ; DR.MARIO - palette_index 10, 0 ; RADARMISSION - palette_index 5, 0 ; F1RACE - palette_index 19, 0 ; YOSSY NO TAMAGO - palette_index 36, 0 ; - palette_index 7, $80 ; X - palette_index 37, 0 ; MARIOLAND2 - palette_index 30, 0 ; YOSSY NO COOKIE - palette_index 44, 0 ; ZELDA - palette_index 21, 0 ; - palette_index 32, 0 ; - palette_index 31, 0 ; TETRIS FLASH - palette_index 20, 0 ; DONKEY KONG - palette_index 5, 0 ; MARIO'S PICROSS - palette_index 33, 0 ; - palette_index 13, 0 ; POKEMON RED, (GAMEBOYCAMERA G) - palette_index 14, 0 ; POKEMON GREEN - palette_index 5, 0 ; PICROSS 2 - palette_index 29, 0 ; YOSSY NO PANEPON - palette_index 5, 0 ; KIRAKIRA KIDS - palette_index 18, 0 ; GAMEBOY GALLERY - palette_index 9, 0 ; POCKETCAMERA - palette_index 3, 0 ; - palette_index 2, 0 ; BALLOON KID - palette_index 26, 0 ; KINGOFTHEZOO - palette_index 25, 0 ; DMG FOOTBALL - palette_index 25, 0 ; WORLD CUP - palette_index 41, 0 ; OTHELLO - palette_index 42, 0 ; SUPER RC PRO-AM - palette_index 26, 0 ; DYNABLASTER - palette_index 45, 0 ; BOY AND BLOB GB2 - palette_index 42, 0 ; MEGAMAN - palette_index 45, 0 ; STAR WARS-NOA - palette_index 36, 0 ; - palette_index 38, 0 ; WAVERACE + palette_index 0 ; Default Palette + palette_index 4 ; ALLEY WAY + palette_index 5 ; YAKUMAN + palette_index 35 ; BASEBALL, (Game and Watch 2) + palette_index 34 ; TENNIS + palette_index 3 ; TETRIS + palette_index 31 ; QIX + palette_index 15 ; DR.MARIO + palette_index 10 ; RADARMISSION + palette_index 5 ; F1RACE + palette_index 19 ; YOSSY NO TAMAGO + palette_index 36 ; + palette_index 7, $80 ; X + palette_index 37 ; MARIOLAND2 + palette_index 30 ; YOSSY NO COOKIE + palette_index 44 ; ZELDA + palette_index 21 ; + palette_index 32 ; + palette_index 31 ; TETRIS FLASH + palette_index 20 ; DONKEY KONG + palette_index 5 ; MARIO'S PICROSS + palette_index 33 ; + palette_index 13 ; POKEMON RED, (GAMEBOYCAMERA G) + palette_index 14 ; POKEMON GREEN + palette_index 5 ; PICROSS 2 + palette_index 29 ; YOSSY NO PANEPON + palette_index 5 ; KIRAKIRA KIDS + palette_index 18 ; GAMEBOY GALLERY + palette_index 9 ; POCKETCAMERA + palette_index 3 ; + palette_index 2 ; BALLOON KID + palette_index 26 ; KINGOFTHEZOO + palette_index 25 ; DMG FOOTBALL + palette_index 25 ; WORLD CUP + palette_index 41 ; OTHELLO + palette_index 42 ; SUPER RC PRO-AM + palette_index 26 ; DYNABLASTER + palette_index 45 ; BOY AND BLOB GB2 + palette_index 42 ; MEGAMAN + palette_index 45 ; STAR WARS-NOA + palette_index 36 ; + palette_index 38 ; WAVERACE palette_index 26, $80 ; - palette_index 42, 0 ; LOLO2 - palette_index 30, 0 ; YOSHI'S COOKIE - palette_index 41, 0 ; MYSTIC QUEST - palette_index 34, 0 ; - palette_index 34, 0 ; TOPRANKINGTENNIS - palette_index 5, 0 ; MANSELL - palette_index 42, 0 ; MEGAMAN3 - palette_index 6, 0 ; SPACE INVADERS - palette_index 5, 0 ; GAME&WATCH - palette_index 33, 0 ; DONKEYKONGLAND95 - palette_index 25, 0 ; ASTEROIDS/MISCMD - palette_index 42, 0 ; STREET FIGHTER 2 - palette_index 42, 0 ; DEFENDER/JOUST - palette_index 40, 0 ; KILLERINSTINCT95 - palette_index 2, 0 ; TETRIS BLAST - palette_index 16, 0 ; PINOCCHIO - palette_index 25, 0 ; - palette_index 42, 0 ; BA.TOSHINDEN - palette_index 42, 0 ; NETTOU KOF 95 - palette_index 5, 0 ; - palette_index 0, 0 ; TETRIS PLUS - palette_index 39, 0 ; DONKEYKONGLAND 3 - palette_index 36, 0 ; - palette_index 22, 0 ; SUPER MARIOLAND - palette_index 25, 0 ; GOLF - palette_index 6, 0 ; SOLARSTRIKER - palette_index 32, 0 ; GBWARS - palette_index 12, 0 ; KAERUNOTAMENI - palette_index 36, 0 ; - palette_index 11, 0 ; POKEMON BLUE - palette_index 39, 0 ; DONKEYKONGLAND - palette_index 18, 0 ; GAMEBOY GALLERY2 - palette_index 39, 0 ; DONKEYKONGLAND 2 - palette_index 24, 0 ; KID ICARUS - palette_index 31, 0 ; TETRIS2 - palette_index 50, 0 ; - palette_index 17, 0 ; MOGURANYA - palette_index 46, 0 ; - palette_index 6, 0 ; GALAGA&GALAXIAN - palette_index 27, 0 ; BT2RAGNAROKWORLD - palette_index 0, 0 ; KEN GRIFFEY JR - palette_index 47, 0 ; - palette_index 41, 0 ; MAGNETIC SOCCER - palette_index 41, 0 ; VEGAS STAKES - palette_index 0, 0 ; - palette_index 0, 0 ; MILLI/CENTI/PEDE - palette_index 19, 0 ; MARIO & YOSHI - palette_index 34, 0 ; SOCCER - palette_index 23, 0 ; POKEBOM - palette_index 18, 0 ; G&W GALLERY - palette_index 29, 0 ; TETRIS ATTACK + palette_index 42 ; LOLO2 + palette_index 30 ; YOSHI'S COOKIE + palette_index 41 ; MYSTIC QUEST + palette_index 34 ; + palette_index 34 ; TOPRANKINGTENNIS + palette_index 5 ; MANSELL + palette_index 42 ; MEGAMAN3 + palette_index 6 ; SPACE INVADERS + palette_index 5 ; GAME&WATCH + palette_index 33 ; DONKEYKONGLAND95 + palette_index 25 ; ASTEROIDS/MISCMD + palette_index 42 ; STREET FIGHTER 2 + palette_index 42 ; DEFENDER/JOUST + palette_index 40 ; KILLERINSTINCT95 + palette_index 2 ; TETRIS BLAST + palette_index 16 ; PINOCCHIO + palette_index 25 ; + palette_index 42 ; BA.TOSHINDEN + palette_index 42 ; NETTOU KOF 95 + palette_index 5 ; + palette_index 0 ; TETRIS PLUS + palette_index 39 ; DONKEYKONGLAND 3 + palette_index 36 ; + palette_index 22 ; SUPER MARIOLAND + palette_index 25 ; GOLF + palette_index 6 ; SOLARSTRIKER + palette_index 32 ; GBWARS + palette_index 12 ; KAERUNOTAMENI + palette_index 36 ; + palette_index 11 ; POKEMON BLUE + palette_index 39 ; DONKEYKONGLAND + palette_index 18 ; GAMEBOY GALLERY2 + palette_index 39 ; DONKEYKONGLAND 2 + palette_index 24 ; KID ICARUS + palette_index 31 ; TETRIS2 + palette_index 50 ; + palette_index 17 ; MOGURANYA + palette_index 46 ; + palette_index 6 ; GALAGA&GALAXIAN + palette_index 27 ; BT2RAGNAROKWORLD + palette_index 0 ; KEN GRIFFEY JR + palette_index 47 ; + palette_index 41 ; MAGNETIC SOCCER + palette_index 41 ; VEGAS STAKES + palette_index 0 ; + palette_index 0 ; MILLI/CENTI/PEDE + palette_index 19 ; MARIO & YOSHI + palette_index 34 ; SOCCER + palette_index 23 ; POKEBOM + palette_index 18 ; G&W GALLERY + palette_index 29 ; TETRIS ATTACK Dups4thLetterArray: db "BEFAARBEKEK R-URAR INAILICE R" @@ -430,125 +443,139 @@ Dups4thLetterArray: ; We assume the last three arrays fit in the same $100 byte page! PaletteCombinations: -palette_comb: MACRO ; Obj0, Obj1, Bg +MACRO palette_comb ; Obj0, Obj1, Bg db (\1) * 8, (\2) * 8, (\3) *8 ENDM -raw_palette_comb: MACRO ; Obj0, Obj1, Bg +MACRO raw_palette_comb ; Obj0, Obj1, Bg db (\1) * 2, (\2) * 2, (\3) * 2 ENDM - palette_comb 4, 4, 29 - palette_comb 18, 18, 18 - palette_comb 20, 20, 20 - palette_comb 24, 24, 24 - palette_comb 9, 9, 9 - palette_comb 0, 0, 0 - palette_comb 27, 27, 27 - palette_comb 5, 5, 5 - palette_comb 12, 12, 12 - palette_comb 26, 26, 26 - palette_comb 16, 8, 8 - palette_comb 4, 28, 28 - palette_comb 4, 2, 2 - palette_comb 3, 4, 4 - palette_comb 4, 29, 29 - palette_comb 28, 4, 28 - palette_comb 2, 17, 2 - palette_comb 16, 16, 8 - palette_comb 4, 4, 7 - palette_comb 4, 4, 18 - palette_comb 4, 4, 20 - palette_comb 19, 19, 9 - raw_palette_comb 4 * 4 - 1, 4 * 4 - 1, 11 * 4 - palette_comb 17, 17, 2 - palette_comb 4, 4, 2 - palette_comb 4, 4, 3 - palette_comb 28, 28, 0 - palette_comb 3, 3, 0 - palette_comb 0, 0, 1 - palette_comb 18, 22, 18 - palette_comb 20, 22, 20 - palette_comb 24, 22, 24 - palette_comb 16, 22, 8 - palette_comb 17, 4, 13 - raw_palette_comb 28 * 4 - 1, 0 * 4, 14 * 4 - raw_palette_comb 28 * 4 - 1, 4 * 4, 15 * 4 - raw_palette_comb 19 * 4, 23 * 4 - 1, 9 * 4 - palette_comb 16, 28, 10 - palette_comb 4, 23, 28 - palette_comb 17, 22, 2 - palette_comb 4, 0, 2 - palette_comb 4, 28, 3 - palette_comb 28, 3, 0 - palette_comb 3, 28, 4 - palette_comb 21, 28, 4 - palette_comb 3, 28, 0 - palette_comb 25, 3, 28 - palette_comb 0, 28, 8 - palette_comb 4, 3, 28 - palette_comb 28, 3, 6 - palette_comb 4, 28, 29 + palette_comb 4, 4, 29 ; 0, Right + A + palette_comb 18, 18, 18 ; 1, Right + palette_comb 20, 20, 20 ; 2 + palette_comb 24, 24, 24 ; 3, Down + A + palette_comb 9, 9, 9 ; 4 + palette_comb 0, 0, 0 ; 5, Up + palette_comb 27, 27, 27 ; 6, Right + B + palette_comb 5, 5, 5 ; 7, Left + B + palette_comb 12, 12, 12 ; 8, Down + palette_comb 26, 26, 26 ; 9 + palette_comb 16, 8, 8 ; 10 + palette_comb 4, 28, 28 ; 11 + palette_comb 4, 2, 2 ; 12 + palette_comb 3, 4, 4 ; 13 + palette_comb 4, 29, 29 ; 14 + palette_comb 28, 4, 28 ; 15 + palette_comb 2, 17, 2 ; 16 + palette_comb 16, 16, 8 ; 17 + palette_comb 4, 4, 7 ; 18 + palette_comb 4, 4, 18 ; 19 + palette_comb 4, 4, 20 ; 20 + palette_comb 19, 19, 9 ; 21 + raw_palette_comb 4 * 4 - 1, 4 * 4 - 1, 11 * 4 ; 22 + palette_comb 17, 17, 2 ; 23 + palette_comb 4, 4, 2 ; 24 + palette_comb 4, 4, 3 ; 25 + palette_comb 28, 28, 0 ; 26 + palette_comb 3, 3, 0 ; 27 + palette_comb 0, 0, 1 ; 28, Up + B + palette_comb 18, 22, 18 ; 29 + palette_comb 20, 22, 20 ; 30 + palette_comb 24, 22, 24 ; 31 + palette_comb 16, 22, 8 ; 32 + palette_comb 17, 4, 13 ; 33 + raw_palette_comb 28 * 4 - 1, 0 * 4, 14 * 4 ; 34 + raw_palette_comb 28 * 4 - 1, 4 * 4, 15 * 4 ; 35 + raw_palette_comb 19 * 4, 23 * 4 - 1, 9 * 4 ; 36 + palette_comb 16, 28, 10 ; 37 + palette_comb 4, 23, 28 ; 38 + palette_comb 17, 22, 2 ; 39 + palette_comb 4, 0, 2 ; 40, Left + A + palette_comb 4, 28, 3 ; 41 + palette_comb 28, 3, 0 ; 42 + palette_comb 3, 28, 4 ; 43, Up + A + palette_comb 21, 28, 4 ; 44 + palette_comb 3, 28, 0 ; 45 + palette_comb 25, 3, 28 ; 46 + palette_comb 0, 28, 8 ; 47 + palette_comb 4, 3, 28 ; 48, Left + palette_comb 28, 3, 6 ; 49, Down + B + palette_comb 4, 28, 29 ; 50 ; SameBoy "Exclusives" - palette_comb 30, 30, 30 ; CGA - palette_comb 31, 31, 31 ; DMG LCD - palette_comb 28, 4, 1 - palette_comb 0, 0, 2 + palette_comb 30, 30, 30 ; 51, Right + A + B, CGA + palette_comb 31, 31, 31 ; 52, Left + A + B, DMG LCD + palette_comb 28, 4, 1 ; 53, Up + A + B + palette_comb 0, 0, 2 ; 54, Down + A + B Palettes: - dw $7FFF, $32BF, $00D0, $0000 - dw $639F, $4279, $15B0, $04CB - dw $7FFF, $6E31, $454A, $0000 - dw $7FFF, $1BEF, $0200, $0000 - dw $7FFF, $421F, $1CF2, $0000 - dw $7FFF, $5294, $294A, $0000 - dw $7FFF, $03FF, $012F, $0000 - dw $7FFF, $03EF, $01D6, $0000 - dw $7FFF, $42B5, $3DC8, $0000 - dw $7E74, $03FF, $0180, $0000 - dw $67FF, $77AC, $1A13, $2D6B - dw $7ED6, $4BFF, $2175, $0000 - dw $53FF, $4A5F, $7E52, $0000 - dw $4FFF, $7ED2, $3A4C, $1CE0 - dw $03ED, $7FFF, $255F, $0000 - dw $036A, $021F, $03FF, $7FFF - dw $7FFF, $01DF, $0112, $0000 - dw $231F, $035F, $00F2, $0009 - dw $7FFF, $03EA, $011F, $0000 - dw $299F, $001A, $000C, $0000 - dw $7FFF, $027F, $001F, $0000 - dw $7FFF, $03E0, $0206, $0120 - dw $7FFF, $7EEB, $001F, $7C00 - dw $7FFF, $3FFF, $7E00, $001F - dw $7FFF, $03FF, $001F, $0000 - dw $03FF, $001F, $000C, $0000 - dw $7FFF, $033F, $0193, $0000 - dw $0000, $4200, $037F, $7FFF - dw $7FFF, $7E8C, $7C00, $0000 - dw $7FFF, $1BEF, $6180, $0000 + dw $7FFF, $32BF, $00D0, $0000 ; 0 + dw $639F, $4279, $15B0, $04CB ; 1 + dw $7FFF, $6E31, $454A, $0000 ; 2 + dw $7FFF, $1BEF, $0200, $0000 ; 3 + dw $7FFF, $421F, $1CF2, $0000 ; 4 + dw $7FFF, $5294, $294A, $0000 ; 5 + dw $7FFF, $03FF, $012F, $0000 ; 6 + dw $7FFF, $03EF, $01D6, $0000 ; 7 + dw $7FFF, $42B5, $3DC8, $0000 ; 8 + dw $7E74, $03FF, $0180, $0000 ; 9 + dw $67FF, $77AC, $1A13, $2D6B ; 10 + dw $7ED6, $4BFF, $2175, $0000 ; 11 + dw $53FF, $4A5F, $7E52, $0000 ; 12 + dw $4FFF, $7ED2, $3A4C, $1CE0 ; 13 + dw $03ED, $7FFF, $255F, $0000 ; 14 + dw $036A, $021F, $03FF, $7FFF ; 15 + dw $7FFF, $01DF, $0112, $0000 ; 16 + dw $231F, $035F, $00F2, $0009 ; 17 + dw $7FFF, $03EA, $011F, $0000 ; 18 + dw $299F, $001A, $000C, $0000 ; 19 + dw $7FFF, $027F, $001F, $0000 ; 20 + dw $7FFF, $03E0, $0206, $0120 ; 21 + dw $7FFF, $7EEB, $001F, $7C00 ; 22 + dw $7FFF, $3FFF, $7E00, $001F ; 23 + dw $7FFF, $03FF, $001F, $0000 ; 24 + dw $03FF, $001F, $000C, $0000 ; 25 + dw $7FFF, $033F, $0193, $0000 ; 26 + dw $0000, $4200, $037F, $7FFF ; 27 + dw $7FFF, $7E8C, $7C00, $0000 ; 28 + dw $7FFF, $1BEF, $6180, $0000 ; 29 ; SameBoy "Exclusives" - dw $7FFF, $7FEA, $7D5F, $0000 ; CGA 1 - dw $4778, $3290, $1D87, $0861 ; DMG LCD + dw $7FFF, $7FEA, $7D5F, $0000 ; 30, CGA 1 + dw $4778, $3290, $1D87, $0861 ; 31, DMG LCD KeyCombinationPalettes: - db 1 * 3 ; Right - db 48 * 3 ; Left - db 5 * 3 ; Up - db 8 * 3 ; Down - db 0 * 3 ; Right + A - db 40 * 3 ; Left + A - db 43 * 3 ; Up + A - db 3 * 3 ; Down + A - db 6 * 3 ; Right + B - db 7 * 3 ; Left + B - db 28 * 3 ; Up + B - db 49 * 3 ; Down + B +MACRO palette_comb_id ; PaletteCombinations ID + db (\1) * 3 +ENDM + palette_comb_id 1 ; 1, Right + palette_comb_id 48 ; 2, Left + palette_comb_id 5 ; 3, Up + palette_comb_id 8 ; 4, Down + palette_comb_id 0 ; 5, Right + A + palette_comb_id 40 ; 6, Left + A + palette_comb_id 43 ; 7, Up + A + palette_comb_id 3 ; 8, Down + A + palette_comb_id 6 ; 9, Right + B + palette_comb_id 7 ; 10, Left + B + palette_comb_id 28 ; 11, Up + B + palette_comb_id 49 ; 12, Down + B ; SameBoy "Exclusives" - db 51 * 3 ; Right + A + B - db 52 * 3 ; Left + A + B - db 53 * 3 ; Up + A + B - db 54 * 3 ; Down + A + B + palette_comb_id 51 ; 13, Right + A + B + palette_comb_id 52 ; 14, Left + A + B + palette_comb_id 53 ; 15, Up + A + B + palette_comb_id 54 ; 16, Down + A + B TrademarkSymbol: - db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c + pusho + opt b.X + db %..XXXX.. + db %.X....X. + db %X.XXX..X + db %X.X..X.X + db %X.XXX..X + db %X.X..X.X + db %.X....X. + db %..XXXX.. + popo +TrademarkSymbolEnd: SameBoyLogo: incbin "SameBoyLogo.pb12" @@ -562,7 +589,12 @@ AnimationColors: dw $017D ; Orange dw $241D ; Red dw $6D38 ; Purple - dw $7102 ; Blue +IF DEF(AGB) + dw $6D60 ; Blue +ELSE + dw $5500 ; Blue +ENDC + AnimationColorsEnd: ; Helper Functions @@ -590,7 +622,7 @@ DoubleBitsAndWriteRowTwice: WaitFrame: push hl - ld hl, $FF0F + ld hl, rIF res 0, [hl] .wait bit 0, [hl] @@ -606,13 +638,13 @@ WaitBFrames: ret PlaySound: - ldh [$13], a + ldh [rNR13], a ld a, $87 - ldh [$14], a + ldh [rNR14], a ret -ClearMemoryPage8000: - ld hl, $8000 +ClearMemoryVRAM: + ld hl, _VRAM ; Clear from HL to HL | 0x2000 ClearMemoryPage: xor a @@ -623,7 +655,7 @@ ClearMemoryPage: ReadTwoTileLines: call ReadTileLine -; c = $f0 for even lines, $f for odd lines. +; c = $F0 for even lines, $0F for odd lines. ReadTileLine: ld a, [de] and c @@ -657,13 +689,10 @@ ReadCGBLogoHalfTile: ; LoadTileset using PB12 codec, 2020 Jakub Kądziołka ; (based on PB8 codec, 2019 Damian Yerrick) -SameBoyLogo_dst = $8080 -SameBoyLogo_length = (128 * 24) / 64 - LoadTileset: - ld hl, SameBoyLogo - ld de, SameBoyLogo_dst - 1 - ld c, SameBoyLogo_length + ld hl, SameBoyLogo ; source + ld de, _VRAM + $80 - 1 ; destination + ld c, (128 * 24) / (8 * 8) ; length .refill ; Register map for PB12 decompression ; HL: source address in boot ROM @@ -701,13 +730,13 @@ LoadTileset: ld c, a jr nc, .shift_left srl a - db $fe ; eat the add a with cp d8 + db $FE ; eat the `add a` with `cp d8` .shift_left add a sla b jr c, .go_and or c - db $fe ; eat the and c with cp d8 + db $FE ; eat the `and c` with `cp d8` .go_and and c jr .got_byte @@ -731,26 +760,26 @@ LoadTileset: ld l, $80 ; Copy (unresized) ROM logo - ld de, $104 + ld de, NintendoLogo .CGBROMLogoLoop - ld c, $f0 + ld c, $F0 call ReadCGBLogoHalfTile add a, 22 ld e, a call ReadCGBLogoHalfTile sub a, 22 ld e, a - cp $1c + cp $1C jr nz, .CGBROMLogoLoop inc hl ; fallthrough ReadTrademarkSymbol: ld de, TrademarkSymbol - ld c,$08 + ld c, TrademarkSymbolEnd - TrademarkSymbol .loadTrademarkSymbolLoop: - ld a,[de] + ld a, [de] inc de - ldi [hl],a + ldi [hl], a inc hl dec c jr nz, .loadTrademarkSymbolLoop @@ -759,12 +788,12 @@ ReadTrademarkSymbol: DoIntroAnimation: ; Animate the intro ld a, 1 - ldh [$4F], a + ldh [rVBK], a ld d, 26 .animationLoop ld b, 2 call WaitBFrames - ld hl, $98C0 + ld hl, _SCRN0 + 6 * SCRN_VX_B + 0 ld c, 3 ; Row count .loop ld a, [hl] @@ -791,8 +820,8 @@ Preboot: IF !DEF(FAST) ld b, 32 ; 32 times to fade .fadeLoop - ld c, 32 ; 32 colors to fade - ld hl, BgPalettes + ld c, (hBgPalettesEnd - hBgPalettes) / 2 ; 32 colors to fade + ld hl, hBgPalettes .frameLoop push bc @@ -802,7 +831,7 @@ IF !DEF(FAST) ld a, [hld] ld d, a ; RGB(1,1,1) - ld bc, $421 + ld bc, $0421 ; Is blue maxed? ld a, e @@ -849,35 +878,35 @@ IF !DEF(FAST) jr nz, .fadeLoop ENDC ld a, 2 - ldh [$70], a + ldh [rSVBK], a ; Clear RAM Bank 2 (Like the original boot ROM) - ld hl, $D000 + ld hl, _RAMBANK call ClearMemoryPage inc a call ClearVRAMViaHDMA call _ClearVRAMViaHDMA call ClearVRAMViaHDMA ; A = $40, so it's bank 0 xor a - ldh [$70], a + ldh [rSVBK], a cpl - ldh [$00], a + ldh [rJOYP], a ; Final values for CGB mode ld d, a ld e, c - ld l, $0d + ld l, $0D - ld a, [$143] + ld a, [CGBFlag] bit 7, a call z, EmulateDMG bit 7, a - ldh [$4C], a - ldh a, [TitleChecksum] + ldh [rKEY0], a ; write CGB compatibility byte, CGB mode + ldh a, [hTitleChecksum] ld b, a jr z, .skipDMGForCGBCheck - ldh a, [InputPalette] + ldh a, [hInputPalette] and a jr nz, .emulateDMGForCGBGame .skipDMGForCGBCheck @@ -886,23 +915,23 @@ IF DEF(AGB) ; AF = $1100, C = 0 xor a ld c, a - add a, $11 + add a, BOOTUP_A_CGB ld h, c - ; B is set to 1 after ret + ; B is set to BOOTUP_B_AGB (1) after ret ELSE ; Set registers to match the original CGB boot ; AF = $1180, C = 0 xor a ld c, a - ld a, $11 + ld a, BOOTUP_A_CGB ld h, c - ; B is set to the title checksum + ; B is set to the title checksum (BOOTUP_B_CGB, 0) ENDC ret .emulateDMGForCGBGame call EmulateDMG - ldh [$4C], a + ldh [rKEY0], a ; write $04, DMG emulation mode ld a, $1 ret @@ -916,7 +945,7 @@ GetKeyComboPalette: EmulateDMG: ld a, 1 - ldh [$6C], a ; DMG Emulation + ldh [rOPRI], a ; DMG Emulation sprite priority call GetPaletteIndex bit 7, a call nz, LoadDMGTilemap @@ -925,7 +954,7 @@ EmulateDMG: add b add b ld b, a - ldh a, [InputPalette] + ldh a, [hInputPalette] and a jr z, .nothingDown call GetKeyComboPalette @@ -938,19 +967,19 @@ EmulateDMG: ld a, 4 ; Set the final values for DMG mode ld de, 8 - ld l, $7c + ld l, $7C ret GetPaletteIndex: - ld hl, $14B - ld a, [hl] ; Old Licensee + ld hl, OldLicenseeCode + ld a, [hl] cp $33 jr z, .newLicensee dec a ; 1 = Nintendo jr nz, .notNintendo jr .doChecksum .newLicensee - ld l, $44 + ld l, LOW(NewLicenseeCode) ld a, [hli] cp "0" jr nz, .notNintendo @@ -959,15 +988,15 @@ GetPaletteIndex: jr nz, .notNintendo .doChecksum - ld l, $34 - ld c, $10 + ld l, LOW(Title) + ld c, 16 xor a - .checksumLoop add [hl] inc l dec c jr nz, .checksumLoop + ldh [hTitleChecksum], a ld b, a ; c = 0 @@ -993,7 +1022,7 @@ GetPaletteIndex: ld a, [hl] pop hl ld c, a - ld a, [$134 + 3] ; Get 4th letter + ld a, [Title + 3] ; Get 4th letter cp c jr nz, .searchLoop ; Not a match, continue @@ -1002,7 +1031,7 @@ GetPaletteIndex: add PalettePerChecksum - TitleChecksums - 1; -1 since hl was incremented ld l, a ld a, b - ldh [TitleChecksum], a + ldh [hTitleChecksum], a ld a, [hl] ret @@ -1030,13 +1059,13 @@ LoadPalettesFromIndex: ; a = index of combination ; b is already 0 ld c, a add hl, bc - ld d, 8 - ld c, $6A + ld d, 4 * 2 + ld c, LOW(rOBPI) call LoadPalettes pop hl - bit 3, e + bit OAMB_BANK1, e jr nz, .loadBGPalette - ld e, 8 + ld e, OAMF_BANK1 jr .loadObjPalette .loadBGPalette ;BG Palette @@ -1048,13 +1077,11 @@ LoadPalettesFromIndex: ; a = index of combination jr LoadBGPalettes LoadPalettesFromHRAM: - ld hl, BgPalettes - ld d, 64 - + ld hl, hBgPalettes + ld d, hBgPalettesEnd - hBgPalettes LoadBGPalettes: ld e, 0 - ld c, $68 - + ld c, LOW(rBGPI) LoadPalettes: ld a, $80 or e @@ -1068,11 +1095,11 @@ LoadPalettes: ret ClearVRAMViaHDMA: - ldh [$4F], a + ldh [rVBK], a ld hl, HDMAData _ClearVRAMViaHDMA: call WaitFrame ; Wait for vblank - ld c, $51 + ld c, LOW(rHDMA1) ld b, 5 .loop ld a, [hli] @@ -1084,9 +1111,9 @@ _ClearVRAMViaHDMA: ; clobbers AF and HL GetInputPaletteIndex: - ld a, $20 ; Select directions - ldh [$00], a - ldh a, [$00] + ld a, P1F_GET_DPAD + ldh [rJOYP], a + ldh a, [rJOYP] cpl and $F ret z ; No direction keys pressed, no palette @@ -1099,20 +1126,20 @@ GetInputPaletteIndex: ; c = 1: Right, 2: Left, 3: Up, 4: Down - ld a, $10 ; Select buttons - ldh [$00], a - ldh a, [$00] + ld a, P1F_GET_BTN + ldh [rJOYP], a + ldh a, [rJOYP] cpl rla rla and $C add l ld l, a - ldh a, [InputPalette] + ldh a, [hInputPalette] cp l ret z ; No change, don't load ld a, l - ldh [InputPalette], a + ldh [hInputPalette], a ; Slide into change Animation Palette ChangeAnimationPalette: @@ -1135,21 +1162,21 @@ ChangeAnimationPalette: ld a, [hli] push hl - ld hl, BgPalettes ; First color, all palettes + ld hl, hBgPalettes ; First color, all palettes call ReplaceColorInAllPalettes - ld l, LOW(BgPalettes + 2) ; Second color, all palettes + ld l, LOW(hBgPalettes + 2) ; Second color, all palettes call ReplaceColorInAllPalettes pop hl - ldh [BgPalettes + 6], a ; Fourth color, first palette + ldh [hBgPalettes + 6], a ; Fourth color, first palette ld a, [hli] push hl - ld hl, BgPalettes + 1 ; First color, all palettes + ld hl, hBgPalettes + 1 ; First color, all palettes call ReplaceColorInAllPalettes - ld l, LOW(BgPalettes + 3) ; Second color, all palettes + ld l, LOW(hBgPalettes + 3) ; Second color, all palettes call ReplaceColorInAllPalettes pop hl - ldh [BgPalettes + 7], a ; Fourth color, first palette + ldh [hBgPalettes + 7], a ; Fourth color, first palette pop af jr z, .isNotWhite @@ -1157,41 +1184,41 @@ ChangeAnimationPalette: inc hl .isNotWhite ; Mixing code by ISSOtm - ldh a, [BgPalettes + 7 * 8 + 2] + ldh a, [hBgPalettes + 7 * 8 + 2] and ~$21 ld b, a ld a, [hli] and ~$21 add a, b ld b, a - ld a, [BgPalettes + 7 * 8 + 3] + ld a, [hBgPalettes + 7 * 8 + 3] res 2, a ; and ~$04, but not touching carry ld c, [hl] res 2, c ; and ~$04, but not touching carry adc a, c rra ; Carry sort of "extends" the accumulator, we're bringing that bit back home - ld [BgPalettes + 7 * 8 + 3], a + ld [hBgPalettes + 7 * 8 + 3], a ld a, b rra - ld [BgPalettes + 7 * 8 + 2], a + ld [hBgPalettes + 7 * 8 + 2], a dec l ld a, [hli] - ldh [BgPalettes + 7 * 8 + 6], a ; Fourth color, 7th palette + ldh [hBgPalettes + 7 * 8 + 6], a ; Fourth color, 7th palette ld a, [hli] - ldh [BgPalettes + 7 * 8 + 7], a ; Fourth color, 7th palette + ldh [hBgPalettes + 7 * 8 + 7], a ; Fourth color, 7th palette ld a, [hli] - ldh [BgPalettes + 4], a ; Third color, first palette + ldh [hBgPalettes + 4], a ; Third color, first palette ld a, [hli] - ldh [BgPalettes + 5], a ; Third color, first palette + ldh [hBgPalettes + 5], a ; Third color, first palette call WaitFrame call LoadPalettesFromHRAM ; Delay the wait loop while the user is selecting a palette ld a, 48 - ldh [WaitLoopCounter], a + ldh [hWaitLoopCounter], a pop de pop bc ret @@ -1209,33 +1236,35 @@ ReplaceColorInAllPalettes: LoadDMGTilemap: push af call WaitFrame - ld a, $19 ; Trademark symbol - ld [$9910], a ; ... put in the superscript position - ld hl,$992f ; Bottom right corner of the logo - ld c,$c ; Tiles in a logo row + ld a, $19 ; Trademark symbol tile ID + ld [_SCRN0 + 8 * SCRN_VX_B + 16], a ; ... put in the superscript position + ld hl, _SCRN0 + 9 * SCRN_VX_B + 15 ; Bottom right corner of the logo + ld c, 12 ; Tiles in a logo row .tilemapLoop dec a jr z, .tilemapDone ldd [hl], a dec c jr nz, .tilemapLoop - ld l, $0f ; Jump to top row + ld l, $0F ; Jump to top row jr .tilemapLoop .tilemapDone pop af ret BootEnd: -IF BootEnd > $900 +IF BootEnd > $0900 FAIL "BootROM overflowed: {BootEnd}" ENDC + ds $100 + $800 - @ ; Ensure that the ROM is padded up to standard size. -SECTION "HRAM", HRAM[$FF80] -TitleChecksum: +SECTION "HRAM", HRAM[_HRAM] +hTitleChecksum: ds 1 -BgPalettes: +hBgPalettes: ds 8 * 4 * 2 -InputPalette: +hBgPalettesEnd: +hInputPalette: ds 1 -WaitLoopCounter: +hWaitLoopCounter: ds 1 diff --git a/BootROMs/cgb_boot_fast.asm b/BootROMs/cgb_boot_fast.asm index cddb475..c0d6eab 100644 --- a/BootROMs/cgb_boot_fast.asm +++ b/BootROMs/cgb_boot_fast.asm @@ -1,2 +1,2 @@ -FAST EQU 1 -include "cgb_boot.asm" \ No newline at end of file +DEF FAST = 1 +include "cgb_boot.asm" diff --git a/BootROMs/dmg_boot.asm b/BootROMs/dmg_boot.asm index 5517683..7013033 100644 --- a/BootROMs/dmg_boot.asm +++ b/BootROMs/dmg_boot.asm @@ -1,12 +1,15 @@ ; SameBoy DMG bootstrap ROM -; Todo: use friendly names for HW registers instead of magic numbers -SECTION "BootCode", ROM0[$0] + +include "sameboot.inc" + +SECTION "BootCode", ROM0[$0000] Start: ; Init stack pointer - ld sp, $fffe + ld sp, $FFFE ; Clear memory VRAM - ld hl, $8000 + ld hl, _VRAM + xor a .clearVRAMLoop ldi [hl], a @@ -14,24 +17,25 @@ Start: jr z, .clearVRAMLoop ; Init Audio - ld a, $80 - ldh [$26], a - ldh [$11], a - ld a, $f3 - ldh [$12], a - ldh [$25], a + ld a, AUDENA_ON + ldh [rNR52], a + assert AUDENA_ON == AUDLEN_DUTY_50 + ldh [rNR11], a + ld a, $F3 + ldh [rNR12], a ; Envelope $F, decreasing, sweep $3 + ldh [rNR51], a ; Channels 1+2+3+4 left, channels 1+2 right ld a, $77 - ldh [$24], a + ldh [rNR50], a ; Volume $7, left and right ; Init BG palette - ld a, $54 - ldh [$47], a + ld a, %01_01_01_00 + ldh [rBGP], a ; Load logo from ROM. ; A nibble represents a 4-pixels line, 2 bytes represent a 4x4 tile, scaled to 8x8. ; Tiles are ordered left to right, top to bottom. - ld de, $104 ; Logo start - ld hl, $8010 ; This is where we load the tiles in VRAM + ld de, NintendoLogo + ld hl, _VRAM + $10 ; This is where we load the tiles in VRAM .loadLogoLoop ld a, [de] ; Read 2 rows @@ -40,92 +44,92 @@ Start: call DoubleBitsAndWriteRow inc de ld a, e - xor $34 ; End of logo + xor LOW(NintendoLogoEnd) jr nz, .loadLogoLoop ; Load trademark symbol ld de, TrademarkSymbol - ld c,$08 + ld c, TrademarkSymbolEnd - TrademarkSymbol .loadTrademarkSymbolLoop: - ld a,[de] + ld a, [de] inc de - ldi [hl],a + ldi [hl], a inc hl dec c jr nz, .loadTrademarkSymbolLoop ; Set up tilemap - ld a,$19 ; Trademark symbol - ld [$9910], a ; ... put in the superscript position - ld hl,$992f ; Bottom right corner of the logo - ld c,$c ; Tiles in a logo row + ld a, $19 ; Trademark symbol tile ID + ld [_SCRN0 + 8 * SCRN_VX_B + 16], a ; ... put in the superscript position + ld hl, _SCRN0 + 9 * SCRN_VX_B + 15 ; Bottom right corner of the logo + ld c, 12 ; Tiles in a logo row .tilemapLoop dec a jr z, .tilemapDone ldd [hl], a dec c jr nz, .tilemapLoop - ld l,$0f ; Jump to top row + ld l, $0F ; Jump to top row jr .tilemapLoop .tilemapDone ld a, 30 - ldh [$ff42], a - - ; Turn on LCD - ld a, $91 - ldh [$40], a + ldh [rSCY], a - ld d, (-119) & $FF + ; Turn on LCD + ld a, LCDCF_ON | LCDCF_BLK01 | LCDCF_BGON + ldh [rLCDC], a + + ld d, LOW(-119) ld c, 15 - + .animate call WaitFrame ld a, d sra a sra a - ldh [$ff42], a + ldh [rSCY], a ld a, d add c ld d, a ld a, c cp 8 jr nz, .noPaletteChange - ld a, $A8 - ldh [$47], a + ld a, %10_10_10_00 + ldh [rBGP], a .noPaletteChange dec c jr nz, .animate - ld a, $fc - ldh [$47], a - + ld a, %11_11_11_00 + ldh [rBGP], a + ; Play first sound ld a, $83 call PlaySound ld b, 5 call WaitBFrames ; Play second sound - ld a, $c1 + ld a, $C1 call PlaySound - + ; Wait ~1 second ld b, 60 call WaitBFrames - + ; Set registers to match the original DMG boot IF DEF(MGB) - ld hl, $FFB0 + lb hl, BOOTUP_A_MGB, %10110000 ELSE - ld hl, $01B0 + lb hl, BOOTUP_A_DMG, %10110000 ENDC push hl pop af - ld hl, $014D - ld bc, $0013 - ld de, $00D8 - + ld hl, HeaderChecksum + lb bc, 0, LOW(rNR13) ; $0013 + lb de, 0, $D8 ; $00D8 + ; Boot the game jp BootGame @@ -152,7 +156,7 @@ DoubleBitsAndWriteRow: WaitFrame: push hl - ld hl, $FF0F + ld hl, rIF res 0, [hl] .wait bit 0, [hl] @@ -167,15 +171,26 @@ WaitBFrames: ret PlaySound: - ldh [$13], a - ld a, $87 - ldh [$14], a + ldh [rNR13], a + ld a, AUDHIGH_RESTART | $7 + ldh [rNR14], a ret TrademarkSymbol: -db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c + pusho + opt b.X + db %..XXXX.. + db %.X....X. + db %X.XXX..X + db %X.X..X.X + db %X.XXX..X + db %X.X..X.X + db %.X....X. + db %..XXXX.. + popo +TrademarkSymbolEnd: -SECTION "BootGame", ROM0[$fe] +SECTION "BootGame", ROM0[$00FE] BootGame: - ldh [$50], a \ No newline at end of file + ldh [rBANK], a ; unmap boot ROM diff --git a/BootROMs/hardware.inc b/BootROMs/hardware.inc new file mode 100755 index 0000000..c1a8c41 --- /dev/null +++ b/BootROMs/hardware.inc @@ -0,0 +1,1113 @@ +;* +;* Gameboy Hardware definitions +;* https://github.com/gbdev/hardware.inc +;* +;* Based on Jones' hardware.inc +;* And based on Carsten Sorensen's ideas. +;* +;* To the extent possible under law, the authors of this work have +;* waived all copyright and related or neighboring rights to the work. +;* See https://creativecommons.org/publicdomain/zero/1.0/ for details. +;* +;* SPDX-License-Identifier: CC0-1.0 +;* +;* Rev 1.1 - 15-Jul-97 : Added define check +;* Rev 1.2 - 18-Jul-97 : Added revision check macro +;* Rev 1.3 - 19-Jul-97 : Modified for RGBASM V1.05 +;* Rev 1.4 - 27-Jul-97 : Modified for new subroutine prefixes +;* Rev 1.5 - 15-Aug-97 : Added _HRAM, PAD, CART defines +;* : and Nintendo Logo +;* Rev 1.6 - 30-Nov-97 : Added rDIV, rTIMA, rTMA, & rTAC +;* Rev 1.7 - 31-Jan-98 : Added _SCRN0, _SCRN1 +;* Rev 1.8 - 15-Feb-98 : Added rSB, rSC +;* Rev 1.9 - 16-Feb-98 : Converted I/O registers to $FFXX format +;* Rev 2.0 - : Added GBC registers +;* Rev 2.1 - : Added MBC5 & cart RAM enable/disable defines +;* Rev 2.2 - : Fixed NR42,NR43, & NR44 equates +;* Rev 2.3 - : Fixed incorrect _HRAM equate +;* Rev 2.4 - 27-Apr-13 : Added some cart defines (AntonioND) +;* Rev 2.5 - 03-May-15 : Fixed format (AntonioND) +;* Rev 2.6 - 09-Apr-16 : Added GBC OAM and cart defines (AntonioND) +;* Rev 2.7 - 19-Jan-19 : Added rPCMXX (ISSOtm) +;* Rev 2.8 - 03-Feb-19 : Added audio registers flags (Álvaro Cuesta) +;* Rev 2.9 - 28-Feb-20 : Added utility rP1 constants +;* Rev 3.0 - 27-Aug-20 : Register ordering, byte-based sizes, OAM additions, general cleanup (Blitter Object) +;* Rev 4.0 - 03-May-21 : Updated to use RGBDS 0.5.0 syntax, changed IEF_LCDC to IEF_STAT (Eievui) +;* Rev 4.1 - 16-Aug-21 : Added more flags, bit number defines, and offset constants for OAM and window positions (rondnelson99) +;* Rev 4.2 - 04-Sep-21 : Added CH3- and CH4-specific audio registers flags (ISSOtm) +;* Rev 4.3 - 07-Nov-21 : Deprecate VRAM address constants (Eievui) +;* Rev 4.4 - 11-Jan-22 : Deprecate VRAM CART_SRAM_2KB constant (avivace) +;* Rev 4.5 - 03-Mar-22 : Added bit number definitions for OCPS, BCPS and LCDC (sukus) +;* Rev 4.6 - 15-Jun-22 : Added MBC3 registers and special values +;* Rev 4.7.0 - 27-Jun-22 : Added alternate names for some constants +;* Rev 4.7.1 - 05-Jul-22 : Added RPB_LED_ON constant +;* Rev 4.8.0 - 25-Oct-22 : Changed background addressing constants (zlago) +;* Rev 4.8.1 - 29-Apr-23 : Added rOPRI (rbong) +;* Rev 4.9.0 - 24-Jun-23 : Added definitions for interrupt vectors (sukus) +;* Rev 4.9.1 - 11-Sep-23 : Added repository link and CC0 waiver notice + + +; NOTE: REVISION NUMBER CHANGES MUST BE REFLECTED +; IN `rev_Check_hardware_inc` BELOW! + +IF __RGBDS_MAJOR__ == 0 && __RGBDS_MINOR__ < 5 + FAIL "This version of 'hardware.inc' requires RGBDS version 0.5.0 or later." +ENDC + +; If all of these are already defined, don't do it again. + + IF !DEF(HARDWARE_INC) +DEF HARDWARE_INC EQU 1 + +; Usage: rev_Check_hardware_inc +; Examples: rev_Check_hardware_inc 4.1.2 +; rev_Check_hardware_inc 4.1 (equivalent to 4.1.0) +; rev_Check_hardware_inc 4 (equivalent to 4.0.0) +MACRO rev_Check_hardware_inc + DEF CUR_VER equs "4,9,1" ; ** UPDATE THIS LINE WHEN CHANGING THE REVISION NUMBER ** + + DEF MIN_VER equs STRRPL("\1", ".", ",") + DEF INTERNAL_CHK equs """MACRO ___internal + IF \\1 != \\4 || \\2 < \\5 || (\\2 == \\5 && \\3 < \\6) + FAIL "Version \\1.\\2.\\3 of 'hardware.inc' is incompatible with requested version \\4.\\5.\\6" + ENDC +\nENDM""" + INTERNAL_CHK + ___internal {CUR_VER}, {MIN_VER},0,0 + PURGE CUR_VER, MIN_VER, INTERNAL_CHK, ___internal +ENDM + + +;*************************************************************************** +;* +;* General memory region constants +;* +;*************************************************************************** + +DEF _VRAM EQU $8000 ; $8000->$9FFF +DEF _SCRN0 EQU $9800 ; $9800->$9BFF +DEF _SCRN1 EQU $9C00 ; $9C00->$9FFF +DEF _SRAM EQU $A000 ; $A000->$BFFF +DEF _RAM EQU $C000 ; $C000->$CFFF / $C000->$DFFF +DEF _RAMBANK EQU $D000 ; $D000->$DFFF +DEF _OAMRAM EQU $FE00 ; $FE00->$FE9F +DEF _IO EQU $FF00 ; $FF00->$FF7F,$FFFF +DEF _AUD3WAVERAM EQU $FF30 ; $FF30->$FF3F +DEF _HRAM EQU $FF80 ; $FF80->$FFFE + + +;*************************************************************************** +;* +;* MBC registers +;* +;*************************************************************************** + +; *** Common *** + +; -- +; -- RAMG ($0000-$1FFF) +; -- Controls whether access to SRAM (and the MBC3 RTC registers) is allowed (W) +; -- +DEF rRAMG EQU $0000 + +DEF CART_SRAM_ENABLE EQU $0A +DEF CART_SRAM_DISABLE EQU $00 + + +; -- +; -- ROMB0 ($2000-$3FFF) +; -- Selects which ROM bank is mapped to the ROMX space ($4000-$7FFF) (W) +; -- +; -- The range of accepted values, as well as the behavior of writing $00, +; -- varies depending on the MBC. +; -- +DEF rROMB0 EQU $2000 + +; -- +; -- RAMB ($4000-$5FFF) +; -- Selects which SRAM bank is mapped to the SRAM space ($A000-$BFFF) (W) +; -- +; -- The range of accepted values varies depending on the cartridge configuration. +; -- +DEF rRAMB EQU $4000 + + +; *** MBC3-specific registers *** + +; Write one of these to rRAMG to map the corresponding RTC register to all SRAM space +DEF RTC_S EQU $08 ; Seconds (0-59) +DEF RTC_M EQU $09 ; Minutes (0-59) +DEF RTC_H EQU $0A ; Hours (0-23) +DEF RTC_DL EQU $0B ; Lower 8 bits of Day Counter ($00-$FF) +DEF RTC_DH EQU $0C ; Bit 7 - Day Counter Carry Bit (1=Counter Overflow) + ; Bit 6 - Halt (0=Active, 1=Stop Timer) + ; Bit 0 - Most significant bit of Day Counter (Bit 8) + + +; -- +; -- RTCLATCH ($6000-$7FFF) +; -- Write $00 then $01 to latch the current time into the RTC registers (W) +; -- +DEF rRTCLATCH EQU $6000 + + +; *** MBC5-specific register *** + +; -- +; -- ROMB1 ($3000-$3FFF) +; -- A 9th bit that "extends" ROMB0 if more than 256 banks are present (W) +; -- +; -- Also note that rROMB0 thus only spans $2000-$2FFF. +; -- +DEF rROMB1 EQU $3000 + + +; Bit 3 of RAMB enables the rumble motor (if any) +DEF CART_RUMBLE_ON EQU 1 << 3 + + +;*************************************************************************** +;* +;* Memory-mapped registers +;* +;*************************************************************************** + +; -- +; -- P1 ($FF00) +; -- Register for reading joy pad info. (R/W) +; -- +DEF rP1 EQU $FF00 + +DEF P1F_5 EQU %00100000 ; P15 out port, set to 0 to get buttons +DEF P1F_4 EQU %00010000 ; P14 out port, set to 0 to get dpad +DEF P1F_3 EQU %00001000 ; P13 in port +DEF P1F_2 EQU %00000100 ; P12 in port +DEF P1F_1 EQU %00000010 ; P11 in port +DEF P1F_0 EQU %00000001 ; P10 in port + +DEF P1F_GET_DPAD EQU P1F_5 +DEF P1F_GET_BTN EQU P1F_4 +DEF P1F_GET_NONE EQU P1F_4 | P1F_5 + + +; -- +; -- SB ($FF01) +; -- Serial Transfer Data (R/W) +; -- +DEF rSB EQU $FF01 + + +; -- +; -- SC ($FF02) +; -- Serial I/O Control (R/W) +; -- +DEF rSC EQU $FF02 + +DEF SCF_START EQU %10000000 ; Transfer Start Flag (1=Transfer in progress, or requested) +DEF SCF_SPEED EQU %00000010 ; Clock Speed (0=Normal, 1=Fast) ** CGB Mode Only ** +DEF SCF_SOURCE EQU %00000001 ; Shift Clock (0=External Clock, 1=Internal Clock) + +DEF SCB_START EQU 7 +DEF SCB_SPEED EQU 1 +DEF SCB_SOURCE EQU 0 + +; -- +; -- DIV ($FF04) +; -- Divider register (R/W) +; -- +DEF rDIV EQU $FF04 + + +; -- +; -- TIMA ($FF05) +; -- Timer counter (R/W) +; -- +DEF rTIMA EQU $FF05 + + +; -- +; -- TMA ($FF06) +; -- Timer modulo (R/W) +; -- +DEF rTMA EQU $FF06 + + +; -- +; -- TAC ($FF07) +; -- Timer control (R/W) +; -- +DEF rTAC EQU $FF07 + +DEF TACF_START EQU %00000100 +DEF TACF_STOP EQU %00000000 +DEF TACF_4KHZ EQU %00000000 +DEF TACF_16KHZ EQU %00000011 +DEF TACF_65KHZ EQU %00000010 +DEF TACF_262KHZ EQU %00000001 + +DEF TACB_START EQU 2 + + +; -- +; -- IF ($FF0F) +; -- Interrupt Flag (R/W) +; -- +DEF rIF EQU $FF0F + + +; -- +; -- AUD1SWEEP/NR10 ($FF10) +; -- Sweep register (R/W) +; -- +; -- Bit 6-4 - Sweep Time +; -- Bit 3 - Sweep Increase/Decrease +; -- 0: Addition (frequency increases???) +; -- 1: Subtraction (frequency increases???) +; -- Bit 2-0 - Number of sweep shift (# 0-7) +; -- Sweep Time: (n*7.8ms) +; -- +DEF rNR10 EQU $FF10 +DEF rAUD1SWEEP EQU rNR10 + +DEF AUD1SWEEP_UP EQU %00000000 +DEF AUD1SWEEP_DOWN EQU %00001000 + + +; -- +; -- AUD1LEN/NR11 ($FF11) +; -- Sound length/Wave pattern duty (R/W) +; -- +; -- Bit 7-6 - Wave Pattern Duty (00:12.5% 01:25% 10:50% 11:75%) +; -- Bit 5-0 - Sound length data (# 0-63) +; -- +DEF rNR11 EQU $FF11 +DEF rAUD1LEN EQU rNR11 + + +; -- +; -- AUD1ENV/NR12 ($FF12) +; -- Envelope (R/W) +; -- +; -- Bit 7-4 - Initial value of envelope +; -- Bit 3 - Envelope UP/DOWN +; -- 0: Decrease +; -- 1: Range of increase +; -- Bit 2-0 - Number of envelope sweep (# 0-7) +; -- +DEF rNR12 EQU $FF12 +DEF rAUD1ENV EQU rNR12 + + +; -- +; -- AUD1LOW/NR13 ($FF13) +; -- Frequency low byte (W) +; -- +DEF rNR13 EQU $FF13 +DEF rAUD1LOW EQU rNR13 + + +; -- +; -- AUD1HIGH/NR14 ($FF14) +; -- Frequency high byte (W) +; -- +; -- Bit 7 - Initial (when set, sound restarts) +; -- Bit 6 - Counter/consecutive selection +; -- Bit 2-0 - Frequency's higher 3 bits +; -- +DEF rNR14 EQU $FF14 +DEF rAUD1HIGH EQU rNR14 + + +; -- +; -- AUD2LEN/NR21 ($FF16) +; -- Sound Length; Wave Pattern Duty (R/W) +; -- +; -- see AUD1LEN for info +; -- +DEF rNR21 EQU $FF16 +DEF rAUD2LEN EQU rNR21 + + +; -- +; -- AUD2ENV/NR22 ($FF17) +; -- Envelope (R/W) +; -- +; -- see AUD1ENV for info +; -- +DEF rNR22 EQU $FF17 +DEF rAUD2ENV EQU rNR22 + + +; -- +; -- AUD2LOW/NR23 ($FF18) +; -- Frequency low byte (W) +; -- +DEF rNR23 EQU $FF18 +DEF rAUD2LOW EQU rNR23 + + +; -- +; -- AUD2HIGH/NR24 ($FF19) +; -- Frequency high byte (W) +; -- +; -- see AUD1HIGH for info +; -- +DEF rNR24 EQU $FF19 +DEF rAUD2HIGH EQU rNR24 + + +; -- +; -- AUD3ENA/NR30 ($FF1A) +; -- Sound on/off (R/W) +; -- +; -- Bit 7 - Sound ON/OFF (1=ON,0=OFF) +; -- +DEF rNR30 EQU $FF1A +DEF rAUD3ENA EQU rNR30 + +DEF AUD3ENA_OFF EQU %00000000 +DEF AUD3ENA_ON EQU %10000000 + + +; -- +; -- AUD3LEN/NR31 ($FF1B) +; -- Sound length (R/W) +; -- +; -- Bit 7-0 - Sound length +; -- +DEF rNR31 EQU $FF1B +DEF rAUD3LEN EQU rNR31 + + +; -- +; -- AUD3LEVEL/NR32 ($FF1C) +; -- Select output level +; -- +; -- Bit 6-5 - Select output level +; -- 00: 0/1 (mute) +; -- 01: 1/1 +; -- 10: 1/2 +; -- 11: 1/4 +; -- +DEF rNR32 EQU $FF1C +DEF rAUD3LEVEL EQU rNR32 + +DEF AUD3LEVEL_MUTE EQU %00000000 +DEF AUD3LEVEL_100 EQU %00100000 +DEF AUD3LEVEL_50 EQU %01000000 +DEF AUD3LEVEL_25 EQU %01100000 + + +; -- +; -- AUD3LOW/NR33 ($FF1D) +; -- Frequency low byte (W) +; -- +; -- see AUD1LOW for info +; -- +DEF rNR33 EQU $FF1D +DEF rAUD3LOW EQU rNR33 + + +; -- +; -- AUD3HIGH/NR34 ($FF1E) +; -- Frequency high byte (W) +; -- +; -- see AUD1HIGH for info +; -- +DEF rNR34 EQU $FF1E +DEF rAUD3HIGH EQU rNR34 + + +; -- +; -- AUD4LEN/NR41 ($FF20) +; -- Sound length (R/W) +; -- +; -- Bit 5-0 - Sound length data (# 0-63) +; -- +DEF rNR41 EQU $FF20 +DEF rAUD4LEN EQU rNR41 + + +; -- +; -- AUD4ENV/NR42 ($FF21) +; -- Envelope (R/W) +; -- +; -- see AUD1ENV for info +; -- +DEF rNR42 EQU $FF21 +DEF rAUD4ENV EQU rNR42 + + +; -- +; -- AUD4POLY/NR43 ($FF22) +; -- Polynomial counter (R/W) +; -- +; -- Bit 7-4 - Selection of the shift clock frequency of the (scf) +; -- polynomial counter (0000-1101) +; -- freq=drf*1/2^scf (not sure) +; -- Bit 3 - Selection of the polynomial counter's step +; -- 0: 15 steps +; -- 1: 7 steps +; -- Bit 2-0 - Selection of the dividing ratio of frequencies (drf) +; -- 000: f/4 001: f/8 010: f/16 011: f/24 +; -- 100: f/32 101: f/40 110: f/48 111: f/56 (f=4.194304 Mhz) +; -- +DEF rNR43 EQU $FF22 +DEF rAUD4POLY EQU rNR43 + +DEF AUD4POLY_15STEP EQU %00000000 +DEF AUD4POLY_7STEP EQU %00001000 + + +; -- +; -- AUD4GO/NR44 ($FF23) +; -- +; -- Bit 7 - Initial (when set, sound restarts) +; -- Bit 6 - Counter/consecutive selection +; -- +DEF rNR44 EQU $FF23 +DEF rAUD4GO EQU rNR44 + + +; -- +; -- AUDVOL/NR50 ($FF24) +; -- Channel control / ON-OFF / Volume (R/W) +; -- +; -- Bit 7 - Vin->SO2 ON/OFF (left) +; -- Bit 6-4 - SO2 output level (left speaker) (# 0-7) +; -- Bit 3 - Vin->SO1 ON/OFF (right) +; -- Bit 2-0 - SO1 output level (right speaker) (# 0-7) +; -- +DEF rNR50 EQU $FF24 +DEF rAUDVOL EQU rNR50 + +DEF AUDVOL_VIN_LEFT EQU %10000000 ; SO2 +DEF AUDVOL_VIN_RIGHT EQU %00001000 ; SO1 + + +; -- +; -- AUDTERM/NR51 ($FF25) +; -- Selection of Sound output terminal (R/W) +; -- +; -- Bit 7 - Output channel 4 to SO2 terminal (left) +; -- Bit 6 - Output channel 3 to SO2 terminal (left) +; -- Bit 5 - Output channel 2 to SO2 terminal (left) +; -- Bit 4 - Output channel 1 to SO2 terminal (left) +; -- Bit 3 - Output channel 4 to SO1 terminal (right) +; -- Bit 2 - Output channel 3 to SO1 terminal (right) +; -- Bit 1 - Output channel 2 to SO1 terminal (right) +; -- Bit 0 - Output channel 1 to SO1 terminal (right) +; -- +DEF rNR51 EQU $FF25 +DEF rAUDTERM EQU rNR51 + +; SO2 +DEF AUDTERM_4_LEFT EQU %10000000 +DEF AUDTERM_3_LEFT EQU %01000000 +DEF AUDTERM_2_LEFT EQU %00100000 +DEF AUDTERM_1_LEFT EQU %00010000 +; SO1 +DEF AUDTERM_4_RIGHT EQU %00001000 +DEF AUDTERM_3_RIGHT EQU %00000100 +DEF AUDTERM_2_RIGHT EQU %00000010 +DEF AUDTERM_1_RIGHT EQU %00000001 + + +; -- +; -- AUDENA/NR52 ($FF26) +; -- Sound on/off (R/W) +; -- +; -- Bit 7 - All sound on/off (sets all audio regs to 0!) +; -- Bit 3 - Sound 4 ON flag (read only) +; -- Bit 2 - Sound 3 ON flag (read only) +; -- Bit 1 - Sound 2 ON flag (read only) +; -- Bit 0 - Sound 1 ON flag (read only) +; -- +DEF rNR52 EQU $FF26 +DEF rAUDENA EQU rNR52 + +DEF AUDENA_ON EQU %10000000 +DEF AUDENA_OFF EQU %00000000 ; sets all audio regs to 0! + + +; -- +; -- LCDC ($FF40) +; -- LCD Control (R/W) +; -- +DEF rLCDC EQU $FF40 + +DEF LCDCF_OFF EQU %00000000 ; LCD Control Operation +DEF LCDCF_ON EQU %10000000 ; LCD Control Operation +DEF LCDCF_WIN9800 EQU %00000000 ; Window Tile Map Display Select +DEF LCDCF_WIN9C00 EQU %01000000 ; Window Tile Map Display Select +DEF LCDCF_WINOFF EQU %00000000 ; Window Display +DEF LCDCF_WINON EQU %00100000 ; Window Display +DEF LCDCF_BLK21 EQU %00000000 ; BG & Window Tile Data Select +DEF LCDCF_BLK01 EQU %00010000 ; BG & Window Tile Data Select +DEF LCDCF_BG9800 EQU %00000000 ; BG Tile Map Display Select +DEF LCDCF_BG9C00 EQU %00001000 ; BG Tile Map Display Select +DEF LCDCF_OBJ8 EQU %00000000 ; OBJ Construction +DEF LCDCF_OBJ16 EQU %00000100 ; OBJ Construction +DEF LCDCF_OBJOFF EQU %00000000 ; OBJ Display +DEF LCDCF_OBJON EQU %00000010 ; OBJ Display +DEF LCDCF_BGOFF EQU %00000000 ; BG Display +DEF LCDCF_BGON EQU %00000001 ; BG Display + +DEF LCDCB_ON EQU 7 ; LCD Control Operation +DEF LCDCB_WIN9C00 EQU 6 ; Window Tile Map Display Select +DEF LCDCB_WINON EQU 5 ; Window Display +DEF LCDCB_BLKS EQU 4 ; BG & Window Tile Data Select +DEF LCDCB_BG9C00 EQU 3 ; BG Tile Map Display Select +DEF LCDCB_OBJ16 EQU 2 ; OBJ Construction +DEF LCDCB_OBJON EQU 1 ; OBJ Display +DEF LCDCB_BGON EQU 0 ; BG Display +; "Window Character Data Select" follows BG + + +; -- +; -- STAT ($FF41) +; -- LCDC Status (R/W) +; -- +DEF rSTAT EQU $FF41 + +DEF STATF_LYC EQU %01000000 ; LYC=LY Coincidence (Selectable) +DEF STATF_MODE10 EQU %00100000 ; Mode 10 +DEF STATF_MODE01 EQU %00010000 ; Mode 01 (V-Blank) +DEF STATF_MODE00 EQU %00001000 ; Mode 00 (H-Blank) +DEF STATF_LYCF EQU %00000100 ; Coincidence Flag +DEF STATF_HBL EQU %00000000 ; H-Blank +DEF STATF_VBL EQU %00000001 ; V-Blank +DEF STATF_OAM EQU %00000010 ; OAM-RAM is used by system +DEF STATF_LCD EQU %00000011 ; Both OAM and VRAM used by system +DEF STATF_BUSY EQU %00000010 ; When set, VRAM access is unsafe + +DEF STATB_LYC EQU 6 +DEF STATB_MODE10 EQU 5 +DEF STATB_MODE01 EQU 4 +DEF STATB_MODE00 EQU 3 +DEF STATB_LYCF EQU 2 +DEF STATB_BUSY EQU 1 + +; -- +; -- SCY ($FF42) +; -- Scroll Y (R/W) +; -- +DEF rSCY EQU $FF42 + + +; -- +; -- SCX ($FF43) +; -- Scroll X (R/W) +; -- +DEF rSCX EQU $FF43 + + +; -- +; -- LY ($FF44) +; -- LCDC Y-Coordinate (R) +; -- +; -- Values range from 0->153. 144->153 is the VBlank period. +; -- +DEF rLY EQU $FF44 + + +; -- +; -- LYC ($FF45) +; -- LY Compare (R/W) +; -- +; -- When LY==LYC, STATF_LYCF will be set in STAT +; -- +DEF rLYC EQU $FF45 + + +; -- +; -- DMA ($FF46) +; -- DMA Transfer and Start Address (W) +; -- +DEF rDMA EQU $FF46 + + +; -- +; -- BGP ($FF47) +; -- BG Palette Data (W) +; -- +; -- Bit 7-6 - Intensity for %11 +; -- Bit 5-4 - Intensity for %10 +; -- Bit 3-2 - Intensity for %01 +; -- Bit 1-0 - Intensity for %00 +; -- +DEF rBGP EQU $FF47 + + +; -- +; -- OBP0 ($FF48) +; -- Object Palette 0 Data (W) +; -- +; -- See BGP for info +; -- +DEF rOBP0 EQU $FF48 + + +; -- +; -- OBP1 ($FF49) +; -- Object Palette 1 Data (W) +; -- +; -- See BGP for info +; -- +DEF rOBP1 EQU $FF49 + + +; -- +; -- WY ($FF4A) +; -- Window Y Position (R/W) +; -- +; -- 0 <= WY <= 143 +; -- When WY = 0, the window is displayed from the top edge of the LCD screen. +; -- +DEF rWY EQU $FF4A + + +; -- +; -- WX ($FF4B) +; -- Window X Position (R/W) +; -- +; -- 7 <= WX <= 166 +; -- When WX = 7, the window is displayed from the left edge of the LCD screen. +; -- Values of 0-6 and 166 are unreliable due to hardware bugs. +; -- +DEF rWX EQU $FF4B + +DEF WX_OFS EQU 7 ; add this to a screen position to get a WX position + + +; -- +; -- SPEED ($FF4D) +; -- Select CPU Speed (R/W) +; -- +DEF rKEY1 EQU $FF4D +DEF rSPD EQU rKEY1 + +DEF KEY1F_DBLSPEED EQU %10000000 ; 0=Normal Speed, 1=Double Speed (R) +DEF KEY1F_PREPARE EQU %00000001 ; 0=No, 1=Prepare (R/W) + + +; -- +; -- VBK ($FF4F) +; -- Select Video RAM Bank (R/W) +; -- +; -- Bit 0 - Bank Specification (0: Specify Bank 0; 1: Specify Bank 1) +; -- +DEF rVBK EQU $FF4F + + +; -- +; -- HDMA1 ($FF51) +; -- High byte for Horizontal Blanking/General Purpose DMA source address (W) +; -- CGB Mode Only +; -- +DEF rHDMA1 EQU $FF51 + + +; -- +; -- HDMA2 ($FF52) +; -- Low byte for Horizontal Blanking/General Purpose DMA source address (W) +; -- CGB Mode Only +; -- +DEF rHDMA2 EQU $FF52 + + +; -- +; -- HDMA3 ($FF53) +; -- High byte for Horizontal Blanking/General Purpose DMA destination address (W) +; -- CGB Mode Only +; -- +DEF rHDMA3 EQU $FF53 + + +; -- +; -- HDMA4 ($FF54) +; -- Low byte for Horizontal Blanking/General Purpose DMA destination address (W) +; -- CGB Mode Only +; -- +DEF rHDMA4 EQU $FF54 + + +; -- +; -- HDMA5 ($FF55) +; -- Transfer length (in tiles minus 1)/mode/start for Horizontal Blanking, General Purpose DMA (R/W) +; -- CGB Mode Only +; -- +DEF rHDMA5 EQU $FF55 + +DEF HDMA5F_MODE_GP EQU %00000000 ; General Purpose DMA (W) +DEF HDMA5F_MODE_HBL EQU %10000000 ; HBlank DMA (W) +DEF HDMA5B_MODE EQU 7 ; DMA mode select (W) + +; -- Once DMA has started, use HDMA5F_BUSY to check when the transfer is complete +DEF HDMA5F_BUSY EQU %10000000 ; 0=Busy (DMA still in progress), 1=Transfer complete (R) + + +; -- +; -- RP ($FF56) +; -- Infrared Communications Port (R/W) +; -- CGB Mode Only +; -- +DEF rRP EQU $FF56 + +DEF RPF_ENREAD EQU %11000000 +DEF RPF_DATAIN EQU %00000010 ; 0=Receiving IR Signal, 1=Normal +DEF RPF_WRITE_HI EQU %00000001 +DEF RPF_WRITE_LO EQU %00000000 + +DEF RPB_LED_ON EQU 0 +DEF RPB_DATAIN EQU 1 + + +; -- +; -- BCPS/BGPI ($FF68) +; -- Background Color Palette Specification (aka Background Palette Index) (R/W) +; -- +DEF rBCPS EQU $FF68 +DEF rBGPI EQU rBCPS + +DEF BCPSF_AUTOINC EQU %10000000 ; Auto Increment (0=Disabled, 1=Increment after Writing) +DEF BCPSB_AUTOINC EQU 7 +DEF BGPIF_AUTOINC EQU BCPSF_AUTOINC +DEF BGPIB_AUTOINC EQU BCPSB_AUTOINC + + +; -- +; -- BCPD/BGPD ($FF69) +; -- Background Color Palette Data (aka Background Palette Data) (R/W) +; -- +DEF rBCPD EQU $FF69 +DEF rBGPD EQU rBCPD + + +; -- +; -- OCPS/OBPI ($FF6A) +; -- Object Color Palette Specification (aka Object Background Palette Index) (R/W) +; -- +DEF rOCPS EQU $FF6A +DEF rOBPI EQU rOCPS + +DEF OCPSF_AUTOINC EQU %10000000 ; Auto Increment (0=Disabled, 1=Increment after Writing) +DEF OCPSB_AUTOINC EQU 7 +DEF OBPIF_AUTOINC EQU OCPSF_AUTOINC +DEF OBPIB_AUTOINC EQU OCPSB_AUTOINC + + +; -- +; -- OCPD/OBPD ($FF6B) +; -- Object Color Palette Data (aka Object Background Palette Data) (R/W) +; -- +DEF rOCPD EQU $FF6B +DEF rOBPD EQU rOCPD + + +; -- +; -- OPRI ($FF6C) +; -- Object Priority Mode (R/W) +; -- CGB Only + +; -- +; -- Priority can be changed only from the boot ROM +; -- +DEF rOPRI EQU $FF6C + +DEF OPRI_OAM EQU 0 ; Prioritize objects by location in OAM (CGB Mode default) +DEF OPRI_COORD EQU 1 ; Prioritize objects by x-coordinate (Non-CGB Mode default) + + + +; -- +; -- SMBK/SVBK ($FF70) +; -- Select Main RAM Bank (R/W) +; -- +; -- Bit 2-0 - Bank Specification (0,1: Specify Bank 1; 2-7: Specify Banks 2-7) +; -- +DEF rSVBK EQU $FF70 +DEF rSMBK EQU rSVBK + + +; -- +; -- PCM12 ($FF76) +; -- Sound channel 1&2 PCM amplitude (R) +; -- +; -- Bit 7-4 - Copy of sound channel 2's PCM amplitude +; -- Bit 3-0 - Copy of sound channel 1's PCM amplitude +; -- +DEF rPCM12 EQU $FF76 + + +; -- +; -- PCM34 ($FF77) +; -- Sound channel 3&4 PCM amplitude (R) +; -- +; -- Bit 7-4 - Copy of sound channel 4's PCM amplitude +; -- Bit 3-0 - Copy of sound channel 3's PCM amplitude +; -- +DEF rPCM34 EQU $FF77 + + +; -- +; -- IE ($FFFF) +; -- Interrupt Enable (R/W) +; -- +DEF rIE EQU $FFFF + +DEF IEF_HILO EQU %00010000 ; Transition from High to Low of Pin number P10-P13 +DEF IEF_SERIAL EQU %00001000 ; Serial I/O transfer end +DEF IEF_TIMER EQU %00000100 ; Timer Overflow +DEF IEF_STAT EQU %00000010 ; STAT +DEF IEF_VBLANK EQU %00000001 ; V-Blank + +DEF IEB_HILO EQU 4 +DEF IEB_SERIAL EQU 3 +DEF IEB_TIMER EQU 2 +DEF IEB_STAT EQU 1 +DEF IEB_VBLANK EQU 0 + + +;*************************************************************************** +;* +;* Flags common to multiple sound channels +;* +;*************************************************************************** + +; -- +; -- Square wave duty cycle +; -- +; -- Can be used with AUD1LEN and AUD2LEN +; -- See AUD1LEN for more info +; -- +DEF AUDLEN_DUTY_12_5 EQU %00000000 ; 12.5% +DEF AUDLEN_DUTY_25 EQU %01000000 ; 25% +DEF AUDLEN_DUTY_50 EQU %10000000 ; 50% +DEF AUDLEN_DUTY_75 EQU %11000000 ; 75% + + +; -- +; -- Audio envelope flags +; -- +; -- Can be used with AUD1ENV, AUD2ENV, AUD4ENV +; -- See AUD1ENV for more info +; -- +DEF AUDENV_UP EQU %00001000 +DEF AUDENV_DOWN EQU %00000000 + + +; -- +; -- Audio trigger flags +; -- +; -- Can be used with AUD1HIGH, AUD2HIGH, AUD3HIGH +; -- See AUD1HIGH for more info +; -- +DEF AUDHIGH_RESTART EQU %10000000 +DEF AUDHIGH_LENGTH_ON EQU %01000000 +DEF AUDHIGH_LENGTH_OFF EQU %00000000 + + +;*************************************************************************** +;* +;* CPU values on bootup (a=type, b=qualifier) +;* +;*************************************************************************** + +DEF BOOTUP_A_DMG EQU $01 ; Dot Matrix Game +DEF BOOTUP_A_CGB EQU $11 ; Color GameBoy +DEF BOOTUP_A_MGB EQU $FF ; Mini GameBoy (Pocket GameBoy) + +; if a=BOOTUP_A_CGB, bit 0 in b can be checked to determine if real CGB or +; other system running in GBC mode +DEF BOOTUP_B_CGB EQU %00000000 +DEF BOOTUP_B_AGB EQU %00000001 ; GBA, GBA SP, Game Boy Player, or New GBA SP + + +;*************************************************************************** +;* +;* Interrupt vector addresses +;* +;*************************************************************************** + +DEF INT_HANDLER_VBLANK EQU $0040 +DEF INT_HANDLER_STAT EQU $0048 +DEF INT_HANDLER_TIMER EQU $0050 +DEF INT_HANDLER_SERIAL EQU $0058 +DEF INT_HANDLER_JOYPAD EQU $0060 + + +;*************************************************************************** +;* +;* Header +;* +;*************************************************************************** + +;* +;* Nintendo scrolling logo +;* (Code won't work on a real GameBoy) +;* (if next lines are altered.) +MACRO NINTENDO_LOGO + DB $CE,$ED,$66,$66,$CC,$0D,$00,$0B,$03,$73,$00,$83,$00,$0C,$00,$0D + DB $00,$08,$11,$1F,$88,$89,$00,$0E,$DC,$CC,$6E,$E6,$DD,$DD,$D9,$99 + DB $BB,$BB,$67,$63,$6E,$0E,$EC,$CC,$DD,$DC,$99,$9F,$BB,$B9,$33,$3E +ENDM + +; $0143 Color GameBoy compatibility code +DEF CART_COMPATIBLE_DMG EQU $00 +DEF CART_COMPATIBLE_DMG_GBC EQU $80 +DEF CART_COMPATIBLE_GBC EQU $C0 + +; $0146 GameBoy/Super GameBoy indicator +DEF CART_INDICATOR_GB EQU $00 +DEF CART_INDICATOR_SGB EQU $03 + +; $0147 Cartridge type +DEF CART_ROM EQU $00 +DEF CART_ROM_MBC1 EQU $01 +DEF CART_ROM_MBC1_RAM EQU $02 +DEF CART_ROM_MBC1_RAM_BAT EQU $03 +DEF CART_ROM_MBC2 EQU $05 +DEF CART_ROM_MBC2_BAT EQU $06 +DEF CART_ROM_RAM EQU $08 +DEF CART_ROM_RAM_BAT EQU $09 +DEF CART_ROM_MMM01 EQU $0B +DEF CART_ROM_MMM01_RAM EQU $0C +DEF CART_ROM_MMM01_RAM_BAT EQU $0D +DEF CART_ROM_MBC3_BAT_RTC EQU $0F +DEF CART_ROM_MBC3_RAM_BAT_RTC EQU $10 +DEF CART_ROM_MBC3 EQU $11 +DEF CART_ROM_MBC3_RAM EQU $12 +DEF CART_ROM_MBC3_RAM_BAT EQU $13 +DEF CART_ROM_MBC5 EQU $19 +DEF CART_ROM_MBC5_BAT EQU $1A +DEF CART_ROM_MBC5_RAM_BAT EQU $1B +DEF CART_ROM_MBC5_RUMBLE EQU $1C +DEF CART_ROM_MBC5_RAM_RUMBLE EQU $1D +DEF CART_ROM_MBC5_RAM_BAT_RUMBLE EQU $1E +DEF CART_ROM_MBC7_RAM_BAT_GYRO EQU $22 +DEF CART_ROM_POCKET_CAMERA EQU $FC +DEF CART_ROM_BANDAI_TAMA5 EQU $FD +DEF CART_ROM_HUDSON_HUC3 EQU $FE +DEF CART_ROM_HUDSON_HUC1 EQU $FF + +; $0148 ROM size +; these are kilobytes +DEF CART_ROM_32KB EQU $00 ; 2 banks +DEF CART_ROM_64KB EQU $01 ; 4 banks +DEF CART_ROM_128KB EQU $02 ; 8 banks +DEF CART_ROM_256KB EQU $03 ; 16 banks +DEF CART_ROM_512KB EQU $04 ; 32 banks +DEF CART_ROM_1024KB EQU $05 ; 64 banks +DEF CART_ROM_2048KB EQU $06 ; 128 banks +DEF CART_ROM_4096KB EQU $07 ; 256 banks +DEF CART_ROM_8192KB EQU $08 ; 512 banks +DEF CART_ROM_1152KB EQU $52 ; 72 banks +DEF CART_ROM_1280KB EQU $53 ; 80 banks +DEF CART_ROM_1536KB EQU $54 ; 96 banks + +; $0149 SRAM size +; these are kilobytes +DEF CART_SRAM_NONE EQU 0 +DEF CART_SRAM_8KB EQU 2 ; 1 bank +DEF CART_SRAM_32KB EQU 3 ; 4 banks +DEF CART_SRAM_128KB EQU 4 ; 16 banks + +; $014A Destination code +DEF CART_DEST_JAPANESE EQU $00 +DEF CART_DEST_NON_JAPANESE EQU $01 + + +;*************************************************************************** +;* +;* Keypad related +;* +;*************************************************************************** + +DEF PADF_DOWN EQU $80 +DEF PADF_UP EQU $40 +DEF PADF_LEFT EQU $20 +DEF PADF_RIGHT EQU $10 +DEF PADF_START EQU $08 +DEF PADF_SELECT EQU $04 +DEF PADF_B EQU $02 +DEF PADF_A EQU $01 + +DEF PADB_DOWN EQU $7 +DEF PADB_UP EQU $6 +DEF PADB_LEFT EQU $5 +DEF PADB_RIGHT EQU $4 +DEF PADB_START EQU $3 +DEF PADB_SELECT EQU $2 +DEF PADB_B EQU $1 +DEF PADB_A EQU $0 + + +;*************************************************************************** +;* +;* Screen related +;* +;*************************************************************************** + +DEF SCRN_X EQU 160 ; Width of screen in pixels +DEF SCRN_Y EQU 144 ; Height of screen in pixels. Also corresponds to the value in LY at the beginning of VBlank. +DEF SCRN_X_B EQU 20 ; Width of screen in bytes +DEF SCRN_Y_B EQU 18 ; Height of screen in bytes + +DEF SCRN_VX EQU 256 ; Virtual width of screen in pixels +DEF SCRN_VY EQU 256 ; Virtual height of screen in pixels +DEF SCRN_VX_B EQU 32 ; Virtual width of screen in bytes +DEF SCRN_VY_B EQU 32 ; Virtual height of screen in bytes + + +;*************************************************************************** +;* +;* OAM related +;* +;*************************************************************************** + +; OAM attributes +; each entry in OAM RAM is 4 bytes (sizeof_OAM_ATTRS) +RSRESET +DEF OAMA_Y RB 1 ; y pos plus 16 +DEF OAMA_X RB 1 ; x pos plus 8 +DEF OAMA_TILEID RB 1 ; tile id +DEF OAMA_FLAGS RB 1 ; flags (see below) +DEF sizeof_OAM_ATTRS RB 0 + +DEF OAM_Y_OFS EQU 16 ; add this to a screen-relative Y position to get an OAM Y position +DEF OAM_X_OFS EQU 8 ; add this to a screen-relative X position to get an OAM X position + +DEF OAM_COUNT EQU 40 ; number of OAM entries in OAM RAM + +; flags +DEF OAMF_PRI EQU %10000000 ; Priority +DEF OAMF_YFLIP EQU %01000000 ; Y flip +DEF OAMF_XFLIP EQU %00100000 ; X flip +DEF OAMF_PAL0 EQU %00000000 ; Palette number; 0,1 (DMG) +DEF OAMF_PAL1 EQU %00010000 ; Palette number; 0,1 (DMG) +DEF OAMF_BANK0 EQU %00000000 ; Bank number; 0,1 (GBC) +DEF OAMF_BANK1 EQU %00001000 ; Bank number; 0,1 (GBC) + +DEF OAMF_PALMASK EQU %00000111 ; Palette (GBC) + +DEF OAMB_PRI EQU 7 ; Priority +DEF OAMB_YFLIP EQU 6 ; Y flip +DEF OAMB_XFLIP EQU 5 ; X flip +DEF OAMB_PAL1 EQU 4 ; Palette number; 0,1 (DMG) +DEF OAMB_BANK1 EQU 3 ; Bank number; 0,1 (GBC) + + +; Deprecated constants. Please avoid using. + +DEF IEF_LCDC EQU %00000010 ; LCDC (see STAT) +DEF _VRAM8000 EQU _VRAM +DEF _VRAM8800 EQU _VRAM+$800 +DEF _VRAM9000 EQU _VRAM+$1000 +DEF CART_SRAM_2KB EQU 1 ; 1 incomplete bank +DEF LCDCF_BG8800 EQU %00000000 ; BG & Window Tile Data Select +DEF LCDCF_BG8000 EQU %00010000 ; BG & Window Tile Data Select +DEF LCDCB_BG8000 EQU 4 ; BG & Window Tile Data Select + + + ENDC ;HARDWARE_INC diff --git a/BootROMs/mgb_boot.asm b/BootROMs/mgb_boot.asm index 3a98aef..c202303 100644 --- a/BootROMs/mgb_boot.asm +++ b/BootROMs/mgb_boot.asm @@ -1,2 +1,2 @@ -MGB EQU 1 -include "dmg_boot.asm" \ No newline at end of file +DEF MGB = 1 +include "dmg_boot.asm" diff --git a/BootROMs/pb12.c b/BootROMs/pb12.c index cfedf6b..7de3de2 100644 --- a/BootROMs/pb12.c +++ b/BootROMs/pb12.c @@ -5,7 +5,7 @@ #include #include -void opts(uint8_t byte, uint8_t *options) +static void opts(uint8_t byte, uint8_t *options) { *(options++) = byte | ((byte << 1) & 0xff); *(options++) = byte & (byte << 1); @@ -13,7 +13,7 @@ void opts(uint8_t byte, uint8_t *options) *(options++) = byte & (byte >> 1); } -void write_all(int fd, const void *buf, size_t count) { +static void write_all(int fd, const void *buf, size_t count) { while (count) { ssize_t written = write(fd, buf, count); if (written < 0) { @@ -25,7 +25,7 @@ void write_all(int fd, const void *buf, size_t count) { } } -int main() +int main(void) { static uint8_t source[0x4000]; size_t size = read(STDIN_FILENO, &source, sizeof(source)); @@ -87,7 +87,7 @@ int main() prev[1] = byte; if (bits >= 8) { uint8_t outctl = control >> (bits - 8); - assert(outctl != 1); + assert(outctl != 1); // 1 is reserved as the end byte write_all(STDOUT_FILENO, &outctl, 1); write_all(STDOUT_FILENO, literals, literals_size); bits -= 8; diff --git a/BootROMs/sameboot.inc b/BootROMs/sameboot.inc new file mode 100644 index 0000000..b7eecc7 --- /dev/null +++ b/BootROMs/sameboot.inc @@ -0,0 +1,40 @@ +IF !DEF(SAMEBOY_INC) +DEF SAMEBOY_INC EQU 1 + +include "hardware.inc" + +DEF rKEY0 EQU $FF4C +DEF rBANK EQU $FF50 + +DEF rJOYP EQU rP1 + + +MACRO lb ; r16, high, low + ld \1, LOW(\2) << 8 | LOW(\3) +ENDM + + +MACRO header_section ; name, address + PUSHS + SECTION "\1", ROM0[\2] + \1: + POPS +ENDM + header_section EntryPoint, $0100 + header_section NintendoLogo, $0104 + header_section NintendoLogoEnd, $0134 + header_section Title, $0134 + header_section ManufacturerCode, $013F + header_section CGBFlag, $0143 + header_section NewLicenseeCode, $0144 + header_section SGBFlag, $0146 + header_section CartridgeType, $0147 + header_section ROMSize, $0148 + header_section RAMSize, $0149 + header_section DestinationCode, $014A + header_section OldLicenseeCode, $014B + header_section MaskRomVersion, $014C + header_section HeaderChecksum, $014D + header_section GlobalChecksum, $014E + +ENDC diff --git a/BootROMs/sgb2_boot.asm b/BootROMs/sgb2_boot.asm index 1c3d858..d81de18 100644 --- a/BootROMs/sgb2_boot.asm +++ b/BootROMs/sgb2_boot.asm @@ -1,2 +1,2 @@ -SGB2 EQU 1 -include "sgb_boot.asm" \ No newline at end of file +DEF SGB2 = 1 +include "sgb_boot.asm" diff --git a/BootROMs/sgb_boot.asm b/BootROMs/sgb_boot.asm index cdb9d77..ba42b82 100644 --- a/BootROMs/sgb_boot.asm +++ b/BootROMs/sgb_boot.asm @@ -1,12 +1,15 @@ ; SameBoy SGB bootstrap ROM -; Todo: use friendly names for HW registers instead of magic numbers -SECTION "BootCode", ROM0[$0] + +include "sameboot.inc" + +SECTION "BootCode", ROM0[$0000] Start: ; Init stack pointer - ld sp, $fffe + ld sp, $FFFE ; Clear memory VRAM - ld hl, $8000 + ld hl, _VRAM + xor a .clearVRAMLoop ldi [hl], a @@ -14,24 +17,25 @@ Start: jr z, .clearVRAMLoop ; Init Audio - ld a, $80 - ldh [$26], a - ldh [$11], a - ld a, $f3 - ldh [$12], a - ldh [$25], a + ld a, AUDENA_ON + ldh [rNR52], a + assert AUDENA_ON == AUDLEN_DUTY_50 + ldh [rNR11], a + ld a, $F3 + ldh [rNR12], a ; Envelope $F, decreasing, sweep $3 + ldh [rNR51], a ; Channels 1+2+3+4 left, channels 1+2 right ld a, $77 - ldh [$24], a + ldh [rNR50], a ; Volume $7, left and right ; Init BG palette to white - ld a, $0 - ldh [$47], a + ld a, %00_00_00_00 + ldh [rBGP], a ; Load logo from ROM. ; A nibble represents a 4-pixels line, 2 bytes represent a 4x4 tile, scaled to 8x8. ; Tiles are ordered left to right, top to bottom. - ld de, $104 ; Logo start - ld hl, $8010 ; This is where we load the tiles in VRAM + ld de, NintendoLogo + ld hl, _VRAM + $10 ; This is where we load the tiles in VRAM .loadLogoLoop ld a, [de] ; Read 2 rows @@ -40,43 +44,43 @@ Start: call DoubleBitsAndWriteRow inc de ld a, e - xor $34 ; End of logo + xor LOW(NintendoLogoEnd) jr nz, .loadLogoLoop ; Load trademark symbol ld de, TrademarkSymbol - ld c,$08 + ld c, TrademarkSymbolEnd - TrademarkSymbol .loadTrademarkSymbolLoop: - ld a,[de] + ld a, [de] inc de - ldi [hl],a + ldi [hl], a inc hl dec c jr nz, .loadTrademarkSymbolLoop ; Set up tilemap - ld a,$19 ; Trademark symbol - ld [$9910], a ; ... put in the superscript position - ld hl,$992f ; Bottom right corner of the logo - ld c,$c ; Tiles in a logo row + ld a, $19 ; Trademark symbol tile ID + ld [_SCRN0 + 8 * SCRN_VX_B + 16], a ; ... put in the superscript position + ld hl, _SCRN0 + 9 * SCRN_VX_B + 15 ; Bottom right corner of the logo + ld c, 12 ; Tiles in a logo row .tilemapLoop dec a jr z, .tilemapDone ldd [hl], a dec c jr nz, .tilemapLoop - ld l,$0f ; Jump to top row + ld l, $0F ; Jump to top row jr .tilemapLoop .tilemapDone ; Turn on LCD - ld a, $91 - ldh [$40], a + ld a, LCDCF_ON | LCDCF_BLK01 | LCDCF_BGON + ldh [rLCDC], a + + ld a, $F1 ; Packet magic, increases by 2 for every packet + ldh [hCommand], a + ld hl, NintendoLogo ; Header start - ld a, $f1 ; Packet magic, increases by 2 for every packet - ldh [$80], a - ld hl, $104 ; Header start - xor a ld c, a ; JOYP @@ -85,66 +89,79 @@ Start: ld [c], a ld a, $30 ld [c], a - - ldh a, [$80] + + ldh a, [hCommand] call SendByte push hl - ld b, $e + + ld b, 14 ld d, 0 - .checksumLoop call ReadHeaderByte add d ld d, a dec b jr nz, .checksumLoop - + ; Send checksum call SendByte pop hl - - ld b, $e + + ld b, 14 .sendLoop call ReadHeaderByte call SendByte dec b jr nz, .sendLoop - + ; Done bit ld a, $20 ld [c], a ld a, $30 ld [c], a - + + ; Wait 4 frames + ld e, 4 + ld a, 1 + ldh [rIE], a + xor a +.waitLoop + ldh [rIF], a + halt + nop + dec e + jr nz, .waitLoop + ldh [rIE], a + ; Update command - ldh a, [$80] + ldh a, [hCommand] add 2 - ldh [$80], a - + ldh [hCommand], a + ld a, $58 cp l jr nz, .sendCommand - + ; Write to sound registers for DMG compatibility - ld c, $13 - ld a, $c1 + ld c, LOW(rNR13) + ld a, $C1 ld [c], a inc c - ld a, 7 + ld a, $7 ld [c], a - + ; Init BG palette - ld a, $fc - ldh [$47], a - + ld a, %11_11_11_00 + ldh [rBGP], a + ; Set registers to match the original SGB boot IF DEF(SGB2) - ld a, $FF + ld a, BOOTUP_A_MGB ELSE - ld a, 1 + ld a, BOOTUP_A_DMG ENDC - ld hl, $c060 - + ld hl, $C060 + ; Boot the game jp BootGame @@ -195,19 +212,24 @@ DoubleBitsAndWriteRow: inc hl ret -WaitFrame: - push hl - ld hl, $FF0F - res 0, [hl] -.wait - bit 0, [hl] - jr z, .wait - pop hl - ret - TrademarkSymbol: -db $3c,$42,$b9,$a5,$b9,$a5,$42,$3c + pusho + opt b.X + db %..XXXX.. + db %.X....X. + db %X.XXX..X + db %X.X..X.X + db %X.XXX..X + db %X.X..X.X + db %.X....X. + db %..XXXX.. + popo +TrademarkSymbolEnd: -SECTION "BootGame", ROM0[$fe] +SECTION "BootGame", ROM0[$00FE] BootGame: - ldh [$50], a \ No newline at end of file + ldh [rBANK], a + +SECTION "HRAM", HRAM[_HRAM] +hCommand: + ds 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 94627d1..23f4a7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ SameBoy's main target compiler is Clang, but GCC is also supported when targetin ### Third Party Libraries and Tools -Avoid adding new required dependencies; run-time and compile-time dependencies alike. Most importantly, avoid linking against GPL licensed libraries (LGPL libraries are fine), so SameBoy can retain its MIT license. +Avoid adding new required dependencies; run-time and compile-time dependencies alike. Most importantly, avoid linking against GPL licensed libraries (LGPL libraries are fine), so SameBoy can retain its Expat license. ### Spacing, Indentation and Formatting diff --git a/Cocoa/AppIcon.icns b/Cocoa/AppIcon.icns index 92ad4c6..2a85022 100644 Binary files a/Cocoa/AppIcon.icns and b/Cocoa/AppIcon.icns differ diff --git a/Cocoa/AudioRecordingAccessoryView.xib b/Cocoa/AudioRecordingAccessoryView.xib new file mode 100644 index 0000000..6dda38b --- /dev/null +++ b/Cocoa/AudioRecordingAccessoryView.xib @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Cocoa/BackstepTemplate.png b/Cocoa/BackstepTemplate.png new file mode 100644 index 0000000..b5e4115 Binary files /dev/null and b/Cocoa/BackstepTemplate.png differ diff --git a/Cocoa/BackstepTemplate@2x.png b/Cocoa/BackstepTemplate@2x.png new file mode 100644 index 0000000..be08590 Binary files /dev/null and b/Cocoa/BackstepTemplate@2x.png differ diff --git a/Cocoa/BigSurToolbar.h b/Cocoa/BigSurToolbar.h index 9057d34..a136b1b 100644 --- a/Cocoa/BigSurToolbar.h +++ b/Cocoa/BigSurToolbar.h @@ -1,6 +1,4 @@ #import -#ifndef BigSurToolbar_h -#define BigSurToolbar_h /* Backport the toolbarStyle property to allow compilation with older SDKs*/ #ifndef __MAC_10_16 @@ -26,5 +24,3 @@ typedef NS_ENUM(NSInteger, NSWindowToolbarStyle) { @end #endif - -#endif diff --git a/Cocoa/CPU.png b/Cocoa/CPU.png index 7f13621..f289a07 100644 Binary files a/Cocoa/CPU.png and b/Cocoa/CPU.png differ diff --git a/Cocoa/CPU@2x.png b/Cocoa/CPU@2x.png index 3c86883..855fac3 100644 Binary files a/Cocoa/CPU@2x.png and b/Cocoa/CPU@2x.png differ diff --git a/Cocoa/CPU~solid.png b/Cocoa/CPU~solid.png new file mode 100644 index 0000000..57b90ab Binary files /dev/null and b/Cocoa/CPU~solid.png differ diff --git a/Cocoa/CPU~solid@2x.png b/Cocoa/CPU~solid@2x.png new file mode 100644 index 0000000..24cbcc3 Binary files /dev/null and b/Cocoa/CPU~solid@2x.png differ diff --git a/Cocoa/CPU~solid~dark.png b/Cocoa/CPU~solid~dark.png new file mode 100644 index 0000000..cc36c50 Binary files /dev/null and b/Cocoa/CPU~solid~dark.png differ diff --git a/Cocoa/CPU~solid~dark@2x.png b/Cocoa/CPU~solid~dark@2x.png new file mode 100644 index 0000000..426a741 Binary files /dev/null and b/Cocoa/CPU~solid~dark@2x.png differ diff --git a/Cocoa/Cartridge.icns b/Cocoa/Cartridge.icns index 6e0c78d..1dae2b4 100644 Binary files a/Cocoa/Cartridge.icns and b/Cocoa/Cartridge.icns differ diff --git a/Cocoa/ContinueTemplate.png b/Cocoa/ContinueTemplate.png new file mode 100644 index 0000000..eb72962 Binary files /dev/null and b/Cocoa/ContinueTemplate.png differ diff --git a/Cocoa/ContinueTemplate@2x.png b/Cocoa/ContinueTemplate@2x.png new file mode 100644 index 0000000..586ab3d Binary files /dev/null and b/Cocoa/ContinueTemplate@2x.png differ diff --git a/Cocoa/Display.png b/Cocoa/Display.png index 5753f55..5008a82 100644 Binary files a/Cocoa/Display.png and b/Cocoa/Display.png differ diff --git a/Cocoa/Display@2x.png b/Cocoa/Display@2x.png index 6a71d22..8813e11 100644 Binary files a/Cocoa/Display@2x.png and b/Cocoa/Display@2x.png differ diff --git a/Cocoa/Display~solid.png b/Cocoa/Display~solid.png new file mode 100644 index 0000000..2e9c4b6 Binary files /dev/null and b/Cocoa/Display~solid.png differ diff --git a/Cocoa/Display~solid@2x.png b/Cocoa/Display~solid@2x.png new file mode 100644 index 0000000..4188f55 Binary files /dev/null and b/Cocoa/Display~solid@2x.png differ diff --git a/Cocoa/Display~solid~dark.png b/Cocoa/Display~solid~dark.png new file mode 100644 index 0000000..b874541 Binary files /dev/null and b/Cocoa/Display~solid~dark.png differ diff --git a/Cocoa/Display~solid~dark@2x.png b/Cocoa/Display~solid~dark@2x.png new file mode 100644 index 0000000..a18ea64 Binary files /dev/null and b/Cocoa/Display~solid~dark@2x.png differ diff --git a/Cocoa/Document.h b/Cocoa/Document.h index 2cfaa87..2546cb9 100644 --- a/Cocoa/Document.h +++ b/Cocoa/Document.h @@ -1,57 +1,76 @@ #import -#include "GBView.h" -#include "GBImageView.h" -#include "GBSplitView.h" -#include "GBVisualizerView.h" -#include "GBOSDView.h" +#import "GBView.h" +#import "GBImageView.h" +#import "GBSplitView.h" +#import "GBVisualizerView.h" +#import "GBOSDView.h" +#import "GBDebuggerButton.h" @class GBCheatWindowController; +@class GBPaletteView; +@class GBObjectView; -@interface Document : NSDocument -@property (nonatomic, readonly) GB_gameboy_t *gb; -@property (nonatomic, strong) IBOutlet GBView *view; -@property (nonatomic, strong) IBOutlet NSTextView *consoleOutput; -@property (nonatomic, strong) IBOutlet NSPanel *consoleWindow; -@property (nonatomic, strong) IBOutlet NSTextField *consoleInput; -@property (nonatomic, strong) IBOutlet NSWindow *mainWindow; -@property (nonatomic, strong) IBOutlet NSView *memoryView; -@property (nonatomic, strong) IBOutlet NSPanel *memoryWindow; -@property (nonatomic, readonly) GB_gameboy_t *gameboy; -@property (nonatomic, strong) IBOutlet NSTextField *memoryBankInput; -@property (nonatomic, strong) IBOutlet NSToolbarItem *memoryBankItem; -@property (nonatomic, strong) IBOutlet GBImageView *tilesetImageView; -@property (nonatomic, strong) IBOutlet NSPopUpButton *tilesetPaletteButton; -@property (nonatomic, strong) IBOutlet GBImageView *tilemapImageView; -@property (nonatomic, strong) IBOutlet NSPopUpButton *tilemapPaletteButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *tilemapMapButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *TilemapSetButton; -@property (nonatomic, strong) IBOutlet NSButton *gridButton; -@property (nonatomic, strong) IBOutlet NSTabView *vramTabView; -@property (nonatomic, strong) IBOutlet NSPanel *vramWindow; -@property (nonatomic, strong) IBOutlet NSTextField *vramStatusLabel; -@property (nonatomic, strong) IBOutlet NSTableView *paletteTableView; -@property (nonatomic, strong) IBOutlet NSTableView *objectsTableView; -@property (nonatomic, strong) IBOutlet NSPanel *printerFeedWindow; -@property (nonatomic, strong) IBOutlet NSImageView *feedImageView; -@property (nonatomic, strong) IBOutlet NSTextView *debuggerSideViewInput; -@property (nonatomic, strong) IBOutlet NSTextView *debuggerSideView; -@property (nonatomic, strong) IBOutlet GBSplitView *debuggerSplitView; -@property (nonatomic, strong) IBOutlet NSBox *debuggerVerticalLine; -@property (nonatomic, strong) IBOutlet NSPanel *cheatsWindow; -@property (nonatomic, strong) IBOutlet GBCheatWindowController *cheatWindowController; -@property (nonatomic, readonly) Document *partner; -@property (nonatomic, readonly) bool isSlave; -@property (strong) IBOutlet NSView *gbsPlayerView; -@property (strong) IBOutlet NSTextField *gbsTitle; -@property (strong) IBOutlet NSTextField *gbsAuthor; -@property (strong) IBOutlet NSTextField *gbsCopyright; -@property (strong) IBOutlet NSPopUpButton *gbsTracks; -@property (strong) IBOutlet NSButton *gbsPlayPauseButton; -@property (strong) IBOutlet NSButton *gbsRewindButton; -@property (strong) IBOutlet NSSegmentedControl *gbsNextPrevButton; -@property (strong) IBOutlet GBVisualizerView *gbsVisualizer; -@property (strong) IBOutlet GBOSDView *osdView; +@interface Document : NSDocument +@property (readonly) GB_gameboy_t *gb; +@property IBOutlet GBView *view; +@property IBOutlet NSTextView *consoleOutput; +@property IBOutlet NSPanel *consoleWindow; +@property IBOutlet NSTextField *consoleInput; +@property IBOutlet NSWindow *mainWindow; +@property IBOutlet NSView *memoryView; +@property IBOutlet NSPanel *memoryWindow; +@property (readonly) GB_gameboy_t *gameboy; +@property IBOutlet NSTextField *memoryBankInput; +@property IBOutlet NSToolbarItem *memoryBankItem; +@property IBOutlet NSPopUpButton *memorySpaceButton; +@property IBOutlet GBImageView *tilesetImageView; +@property IBOutlet NSPopUpButton *tilesetPaletteButton; +@property IBOutlet GBImageView *tilemapImageView; +@property IBOutlet NSPopUpButton *tilemapPaletteButton; +@property IBOutlet NSPopUpButton *tilemapMapButton; +@property IBOutlet NSPopUpButton *TilemapSetButton; +@property IBOutlet NSButton *gridButton; +@property IBOutlet NSTabView *vramTabView; +@property IBOutlet NSPanel *vramWindow; +@property IBOutlet NSTextField *vramStatusLabel; +@property IBOutlet GBPaletteView *paletteView; +@property IBOutlet GBObjectView *objectView; +@property IBOutlet NSPanel *printerFeedWindow; +@property IBOutlet NSProgressIndicator *printerSpinner; +@property IBOutlet NSImageView *feedImageView; +@property IBOutlet NSTextView *debuggerSideViewInput; +@property IBOutlet NSTextView *debuggerSideView; +@property IBOutlet GBSplitView *debuggerSplitView; +@property IBOutlet NSBox *debuggerVerticalLine; +@property IBOutlet NSPanel *cheatsWindow; +@property IBOutlet GBCheatWindowController *cheatWindowController; +@property (readonly) Document *partner; +@property (readonly) bool isSlave; +@property IBOutlet NSView *gbsPlayerView; +@property IBOutlet NSTextField *gbsTitle; +@property IBOutlet NSTextField *gbsAuthor; +@property IBOutlet NSTextField *gbsCopyright; +@property IBOutlet NSPopUpButton *gbsTracks; +@property IBOutlet NSButton *gbsPlayPauseButton; +@property IBOutlet NSButton *gbsRewindButton; +@property IBOutlet NSSegmentedControl *gbsNextPrevButton; +@property IBOutlet GBVisualizerView *gbsVisualizer; +@property IBOutlet GBOSDView *osdView; +@property (readonly) GB_oam_info_t *oamInfo; +@property uint8_t oamCount; +@property uint8_t oamHeight; +@property IBOutlet NSView *audioRecordingAccessoryView; +@property IBOutlet NSPopUpButton *audioFormatButton; +@property IBOutlet NSVisualEffectView *debuggerSidebarEffectView API_AVAILABLE(macos(10.10)); +@property IBOutlet GBDebuggerButton *debuggerContinueButton; +@property IBOutlet GBDebuggerButton *debuggerNextButton; +@property IBOutlet GBDebuggerButton *debuggerStepButton; +@property IBOutlet GBDebuggerButton *debuggerFinishButton; +@property (strong) IBOutlet GBDebuggerButton *debuggerBackstepButton; + + ++ (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale; -(uint8_t) readMemory:(uint16_t) addr; -(void) writeMemory:(uint16_t) addr value:(uint8_t)value; -(void) performAtomicBlock: (void (^)())block; diff --git a/Cocoa/Document.m b/Cocoa/Document.m index 80da097..aa9e276 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -1,16 +1,47 @@ -#include -#include -#include -#include "GBAudioClient.h" -#include "Document.h" -#include "AppDelegate.h" -#include "HexFiend/HexFiend.h" -#include "GBMemoryByteArray.h" -#include "GBWarningPopover.h" -#include "GBCheatWindowController.h" -#include "GBTerminalTextFieldCell.h" -#include "BigSurToolbar.h" +#import +#import +#import +#import "GBAudioClient.h" +#import "Document.h" +#import "GBApp.h" +#import "HexFiend/HexFiend.h" +#import "GBMemoryByteArray.h" +#import "GBWarningPopover.h" +#import "GBCheatWindowController.h" +#import "GBTerminalTextFieldCell.h" +#import "BigSurToolbar.h" #import "GBPaletteEditorController.h" +#import "GBObjectView.h" +#import "GBPaletteView.h" +#import "GBHexStatusBarRepresenter.h" +#import "NSObject+DefaultsObserver.h" + +#define likely(x) GB_likely(x) +#define unlikely(x) GB_unlikely(x) + +@implementation NSString (relativePath) + +- (NSString *)pathRelativeToDirectory:(NSString *)directory +{ + NSMutableArray *baseComponents = [[directory pathComponents] mutableCopy]; + NSMutableArray *selfComponents = [[self pathComponents] mutableCopy]; + + while (baseComponents.count) { + if (![baseComponents.firstObject isEqualToString:selfComponents.firstObject]) { + break; + } + + [baseComponents removeObjectAtIndex:0]; + [selfComponents removeObjectAtIndex:0]; + } + while (baseComponents.count) { + [baseComponents removeObjectAtIndex:0]; + [selfComponents insertObject:@".." atIndex:0]; + } + return [selfComponents componentsJoinedByString:@"/"]; +} + +@end #define GB_MODEL_PAL_BIT_OLD 0x1000 @@ -24,91 +55,89 @@ enum model { MODEL_AGB, MODEL_SGB, MODEL_MGB, + + MODEL_QUICK_RESET = -1, }; @interface Document () +@property GBAudioClient *audioClient; +@end + +@implementation Document { + GB_gameboy_t _gb; + volatile bool _running; + volatile bool _stopping; + NSConditionLock *_hasDebuggerInput; + NSMutableArray *_debuggerInputQueue; - NSMutableAttributedString *pending_console_output; - NSRecursiveLock *console_output_lock; - NSTimer *console_output_timer; - NSTimer *hex_timer; + NSMutableAttributedString *_pendingConsoleOutput; + NSRecursiveLock *_consoleOutputLock; + NSTimer *_consoleOutputTimer; + NSTimer *_hexTimer; - bool fullScreen; - bool in_sync_input; - HFController *hex_controller; - - NSString *lastConsoleInput; - HFLineCountingRepresenter *lineRep; - - CVImageBufferRef cameraImage; - AVCaptureSession *cameraSession; - AVCaptureConnection *cameraConnection; - AVCaptureStillImageOutput *cameraOutput; + bool _fullScreen; + bool _inSyncInput; + NSString *_debuggerCommandWhilePaused; + HFController *_hexController; - GB_oam_info_t oamInfo[40]; - uint16_t oamCount; - uint8_t oamHeight; - bool oamUpdating; + NSString *_lastConsoleInput; + HFLineCountingRepresenter *_lineRep; + GBHexStatusBarRepresenter *_statusRep; - NSMutableData *currentPrinterImageData; - enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy, GBAccessoryLinkCable} accessory; + CVImageBufferRef _cameraImage; + AVCaptureSession *_cameraSession; + AVCaptureConnection *_cameraConnection; + AVCaptureStillImageOutput *_cameraOutput; - bool rom_warning_issued; + GB_oam_info_t _oamInfo[40]; - NSMutableString *capturedOutput; - bool logToSideView; - bool shouldClearSideView; - enum model current_model; + NSMutableData *_currentPrinterImageData; - bool rewind; - bool modelsChanging; + bool _romWarningIssued; - NSCondition *audioLock; - GB_sample_t *audioBuffer; - size_t audioBufferSize; - size_t audioBufferPosition; - size_t audioBufferNeeded; + NSMutableString *_capturedOutput; + bool _logToSideView; + bool _shouldClearSideView; + enum model _currentModel; + + bool _rewind; + bool _modelsChanging; + + NSCondition *_audioLock; + GB_sample_t *_audioBuffer; + size_t _audioBufferSize; + size_t _audioBufferPosition; + size_t _audioBufferNeeded; double _volume; - bool borderModeChanged; + bool _borderModeChanged; /* Link cable*/ - Document *master; - Document *slave; - signed linkOffset; - bool linkCableBit; + Document *_master; + Document *_slave; + signed _linkOffset; + bool _linkCableBit; + + NSSavePanel *_audioSavePanel; + bool _isRecordingAudio; + + void (^ volatile _pendingAtomicBlock)(); + + NSDate *_fileModificationTime; + __weak NSThread *_emulationThread; } -@property GBAudioClient *audioClient; -- (void) vblank; -- (void) log: (const char *) log withAttributes: (GB_log_attributes) attributes; -- (char *) getDebuggerInput; -- (char *) getAsyncDebuggerInput; -- (void) cameraRequestUpdate; -- (uint8_t) cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y; -- (void) printImage:(uint32_t *)image height:(unsigned) height - topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin - exposure:(unsigned) exposure; -- (void) gotNewSample:(GB_sample_t *)sample; -- (void) rumbleChanged:(double)amp; -- (void) loadBootROM:(GB_boot_rom_t)type; -- (void)linkCableBitStart:(bool)bit; -- (bool)linkCableBitEnd; -- (void)infraredStateChanged:(bool)state; - -@end - static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self loadBootROM: type]; } -static void vblank(GB_gameboy_t *gb) +static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type) { Document *self = (__bridge Document *)GB_get_user_data(gb); - [self vblank]; + [self vblankWithType:type]; } static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) @@ -154,6 +183,12 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure]; } +static void printDone(GB_gameboy_t *gb) +{ + Document *self = (__bridge Document *)GB_get_user_data(gb); + [self printDone]; +} + static void setWorkboyTime(GB_gameboy_t *gb, time_t t) { [[NSUserDefaults standardUserDefaults] setInteger:time(NULL) - t forKey:@"GBWorkboyTimeOffset"]; @@ -177,16 +212,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) } -static void linkCableBitStart(GB_gameboy_t *gb, bool bit_to_send) +static void _linkCableBitStart(GB_gameboy_t *gb, bool bit_to_send) { Document *self = (__bridge Document *)GB_get_user_data(gb); - [self linkCableBitStart:bit_to_send]; + [self _linkCableBitStart:bit_to_send]; } -static bool linkCableBitEnd(GB_gameboy_t *gb) +static bool _linkCableBitEnd(GB_gameboy_t *gb) { Document *self = (__bridge Document *)GB_get_user_data(gb); - return [self linkCableBitEnd]; + return [self _linkCableBitEnd]; } static void infraredStateChanged(GB_gameboy_t *gb, bool on) @@ -195,51 +230,39 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on) [self infraredStateChanged:on]; } - -@implementation Document +static void debuggerReloadCallback(GB_gameboy_t *gb) { - GB_gameboy_t gb; - volatile bool running; - volatile bool stopping; - NSConditionLock *has_debugger_input; - NSMutableArray *debugger_input_queue; + Document *self = (__bridge Document *)GB_get_user_data(gb); + dispatch_sync(dispatch_get_main_queue(), ^{ + bool wasRunning = self->_running; + self->_running = false; // Hack for output capture + [self loadROM]; + self->_running = wasRunning; + GB_reset(gb); + }); } - (instancetype)init { self = [super init]; if (self) { - has_debugger_input = [[NSConditionLock alloc] initWithCondition:0]; - debugger_input_queue = [[NSMutableArray alloc] init]; - console_output_lock = [[NSRecursiveLock alloc] init]; - audioLock = [[NSCondition alloc] init]; + _hasDebuggerInput = [[NSConditionLock alloc] initWithCondition:0]; + _debuggerInputQueue = [[NSMutableArray alloc] init]; + _consoleOutputLock = [[NSRecursiveLock alloc] init]; + _audioLock = [[NSCondition alloc] init]; _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; } return self; } -- (NSString *)bootROMPathForName:(NSString *)name -{ - NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"]; - if (url) { - NSString *path = [url path]; - path = [path stringByAppendingPathComponent:name]; - path = [path stringByAppendingPathExtension:@"bin"]; - if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { - return path; - } - } - - return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"]; -} - - (GB_model_t)internalModel { - switch (current_model) { + switch (_currentModel) { case MODEL_DMG: return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"]; case MODEL_NONE: + case MODEL_QUICK_RESET: case MODEL_CGB: return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]; @@ -255,81 +278,105 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on) return GB_MODEL_MGB; case MODEL_AGB: - return GB_MODEL_AGB; + return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBAGBModel"]; } } -- (void) updatePalette +- (void)updatePalette { - GB_set_palette(&gb, [GBPaletteEditorController userPalette]); + GB_set_palette(&_gb, [GBPaletteEditorController userPalette]); } -- (void) updateBorderMode +- (void)initCommon { - borderModeChanged = true; -} - -- (void) updateRumbleMode -{ - GB_set_rumble_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRumbleMode"]); -} - -- (void) initCommon -{ - GB_init(&gb, [self internalModel]); - GB_set_user_data(&gb, (__bridge void *)(self)); - GB_set_boot_rom_load_callback(&gb, (GB_boot_rom_load_callback_t)boot_rom_load); - GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); - GB_set_log_callback(&gb, (GB_log_callback_t) consoleLog); - GB_set_input_callback(&gb, (GB_input_callback_t) consoleInput); - GB_set_async_input_callback(&gb, (GB_input_callback_t) asyncConsoleInput); - GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]); - GB_set_light_temperature(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]); - GB_set_interference_volume(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]); - GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); + GB_init(&_gb, [self internalModel]); + GB_set_user_data(&_gb, (__bridge void *)(self)); + GB_set_boot_rom_load_callback(&_gb, (GB_boot_rom_load_callback_t)boot_rom_load); + GB_set_vblank_callback(&_gb, (GB_vblank_callback_t) vblank); + GB_set_log_callback(&_gb, (GB_log_callback_t) consoleLog); + GB_set_input_callback(&_gb, (GB_input_callback_t) consoleInput); + GB_set_async_input_callback(&_gb, (GB_input_callback_t) asyncConsoleInput); [self updatePalette]; - GB_set_rgb_encode_callback(&gb, rgbEncode); - GB_set_camera_get_pixel_callback(&gb, cameraGetPixel); - GB_set_camera_update_request_callback(&gb, cameraRequestUpdate); - GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]); - GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]); - GB_set_rtc_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]); - GB_apu_set_sample_callback(&gb, audioCallback); - GB_set_rumble_callback(&gb, rumbleCallback); - GB_set_infrared_callback(&gb, infraredStateChanged); - [self updateRumbleMode]; + GB_set_rgb_encode_callback(&_gb, rgbEncode); + GB_set_camera_get_pixel_callback(&_gb, cameraGetPixel); + GB_set_camera_update_request_callback(&_gb, cameraRequestUpdate); + GB_apu_set_sample_callback(&_gb, audioCallback); + GB_set_rumble_callback(&_gb, rumbleCallback); + GB_set_infrared_callback(&_gb, infraredStateChanged); + GB_set_debugger_reload_callback(&_gb, debuggerReloadCallback); + + GB_gameboy_t *gb = &_gb; + __unsafe_unretained Document *weakSelf = self; + + [self observeStandardDefaultsKey:@"GBColorCorrection" withBlock:^(NSNumber *value) { + GB_set_color_correction_mode(gb, value.unsignedIntValue); + }]; + + [self observeStandardDefaultsKey:@"GBLightTemperature" withBlock:^(NSNumber *value) { + GB_set_light_temperature(gb, value.doubleValue); + }]; + + [self observeStandardDefaultsKey:@"GBInterferenceVolume" withBlock:^(NSNumber *value) { + GB_set_interference_volume(gb, value.doubleValue); + }]; + + GB_set_border_mode(&_gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); + [self observeStandardDefaultsKey:@"GBBorderMode" withBlock:^(NSNumber *value) { + _borderModeChanged = true; + }]; + + [self observeStandardDefaultsKey:@"GBHighpassFilter" withBlock:^(NSNumber *value) { + GB_set_highpass_filter_mode(gb, value.unsignedIntValue); + }]; + + [self observeStandardDefaultsKey:@"GBRewindLength" withBlock:^(NSNumber *value) { + [weakSelf performAtomicBlock:^{ + GB_set_rewind_length(gb, value.unsignedIntValue); + }]; + }]; + + [self observeStandardDefaultsKey:@"GBRTCMode" withBlock:^(NSNumber *value) { + GB_set_rtc_mode(gb, value.unsignedIntValue); + }]; + + [self observeStandardDefaultsKey:@"GBRumbleMode" withBlock:^(NSNumber *value) { + GB_set_rumble_mode(gb, value.unsignedIntValue); + }]; } -- (void) updateMinSize +- (void)updateMinSize { - self.mainWindow.contentMinSize = NSMakeSize(GB_get_screen_width(&gb), GB_get_screen_height(&gb)); - if (self.mainWindow.contentView.bounds.size.width < GB_get_screen_width(&gb) || - self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&gb)) { + self.mainWindow.contentMinSize = NSMakeSize(GB_get_screen_width(&_gb), GB_get_screen_height(&_gb)); + if (self.mainWindow.contentView.bounds.size.width < GB_get_screen_width(&_gb) || + self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&_gb)) { [self.mainWindow zoom:nil]; } - self.osdView.usesSGBScale = GB_get_screen_width(&gb) == 256; + self.osdView.usesSGBScale = GB_get_screen_width(&_gb) == 256; } -- (void) vblank +- (void)vblankWithType:(GB_vblank_type_t)type { if (_gbsVisualizer) { dispatch_async(dispatch_get_main_queue(), ^{ [_gbsVisualizer setNeedsDisplay:true]; }); } - [self.view flip]; - if (borderModeChanged) { - dispatch_sync(dispatch_get_main_queue(), ^{ - size_t previous_width = GB_get_screen_width(&gb); - GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); - if (GB_get_screen_width(&gb) != previous_width) { - [self.view screenSizeChanged]; - [self updateMinSize]; - } - }); - borderModeChanged = false; + if (type != GB_VBLANK_TYPE_REPEAT) { + [self.view flip]; + if (_borderModeChanged) { + dispatch_sync(dispatch_get_main_queue(), ^{ + size_t previous_width = GB_get_screen_width(&_gb); + GB_set_border_mode(&_gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); + if (GB_get_screen_width(&_gb) != previous_width) { + [self.view screenSizeChanged]; + [self updateMinSize]; + } + }); + _borderModeChanged = false; + } + GB_set_pixels_output(&_gb, self.view.pixels); } - GB_set_pixels_output(&gb, self.view.pixels); + if (self.vramWindow.isVisible) { dispatch_async(dispatch_get_main_queue(), ^{ self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0; @@ -337,8 +384,8 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on) }); } if (self.view.isRewinding) { - rewind = true; - [self.osdView displayText:@"Rewinding..."]; + _rewind = true; + [self.osdView displayText:@"Rewinding…"]; } } @@ -347,34 +394,34 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on) if (_gbsVisualizer) { [_gbsVisualizer addSample:sample]; } - [audioLock lock]; + [_audioLock lock]; if (_audioClient.isPlaying) { - if (audioBufferPosition == audioBufferSize) { - if (audioBufferSize >= 0x4000) { - audioBufferPosition = 0; - [audioLock unlock]; + if (_audioBufferPosition == _audioBufferSize) { + if (_audioBufferSize >= 0x4000) { + _audioBufferPosition = 0; + [_audioLock unlock]; return; } - if (audioBufferSize == 0) { - audioBufferSize = 512; + if (_audioBufferSize == 0) { + _audioBufferSize = 512; } else { - audioBufferSize += audioBufferSize >> 2; + _audioBufferSize += _audioBufferSize >> 2; } - audioBuffer = realloc(audioBuffer, sizeof(*sample) * audioBufferSize); + _audioBuffer = realloc(_audioBuffer, sizeof(*sample) * _audioBufferSize); } if (_volume != 1) { sample->left *= _volume; sample->right *= _volume; } - audioBuffer[audioBufferPosition++] = *sample; + _audioBuffer[_audioBufferPosition++] = *sample; } - if (audioBufferPosition == audioBufferNeeded) { - [audioLock signal]; - audioBufferNeeded = 0; + if (_audioBufferPosition == _audioBufferNeeded) { + [_audioLock signal]; + _audioBufferNeeded = 0; } - [audioLock unlock]; + [_audioLock unlock]; } - (void)rumbleChanged:(double)amp @@ -382,46 +429,47 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on) [_view setRumble:amp]; } -- (void) preRun +- (void)preRun { - GB_set_pixels_output(&gb, self.view.pixels); - GB_set_sample_rate(&gb, 96000); + GB_set_pixels_output(&_gb, self.view.pixels); + GB_set_sample_rate(&_gb, 96000); _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { - [audioLock lock]; + [_audioLock lock]; - if (audioBufferPosition < nFrames) { - audioBufferNeeded = nFrames; - [audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.125]]; + if (_audioBufferPosition < nFrames) { + _audioBufferNeeded = nFrames; + [_audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:(double)(_audioBufferNeeded - _audioBufferPosition) / sampleRate]]; + _audioBufferNeeded = 0; } - if (stopping || GB_debugger_is_stopped(&gb)) { + if (_stopping || GB_debugger_is_stopped(&_gb)) { memset(buffer, 0, nFrames * sizeof(*buffer)); - [audioLock unlock]; + [_audioLock unlock]; return; } - if (audioBufferPosition < nFrames) { + if (_audioBufferPosition < nFrames) { // Not enough audio - memset(buffer, 0, (nFrames - audioBufferPosition) * sizeof(*buffer)); - memcpy(buffer, audioBuffer, audioBufferPosition * sizeof(*buffer)); - audioBufferPosition = 0; + memset(buffer, 0, (nFrames - _audioBufferPosition) * sizeof(*buffer)); + memcpy(buffer, _audioBuffer, _audioBufferPosition * sizeof(*buffer)); + // Do not reset the audio position to avoid more underflows } - else if (audioBufferPosition < nFrames + 4800) { - memcpy(buffer, audioBuffer, nFrames * sizeof(*buffer)); - memmove(audioBuffer, audioBuffer + nFrames, (audioBufferPosition - nFrames) * sizeof(*buffer)); - audioBufferPosition = audioBufferPosition - nFrames; + else if (_audioBufferPosition < nFrames + 4800) { + memcpy(buffer, _audioBuffer, nFrames * sizeof(*buffer)); + memmove(_audioBuffer, _audioBuffer + nFrames, (_audioBufferPosition - nFrames) * sizeof(*buffer)); + _audioBufferPosition = _audioBufferPosition - nFrames; } else { - memcpy(buffer, audioBuffer + (audioBufferPosition - nFrames), nFrames * sizeof(*buffer)); - audioBufferPosition = 0; + memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer)); + _audioBufferPosition = 0; } - [audioLock unlock]; + [_audioLock unlock]; } andSampleRate:96000]; if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) { [_audioClient start]; } - hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:true]; - [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode]; + _hexTimer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:true]; + [[NSRunLoop mainRunLoop] addTimer:_hexTimer forMode:NSDefaultRunLoopMode]; /* Clear pending alarms, don't play alarms while playing */ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { @@ -451,59 +499,65 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) return ret; } -- (void) run +- (void)run { - assert(!master); - running = true; + assert(!_master); [self preRun]; - if (slave) { - [slave preRun]; - unsigned *masterTable = multiplication_table_for_frequency(GB_get_clock_rate(&gb)); - unsigned *slaveTable = multiplication_table_for_frequency(GB_get_clock_rate(&slave->gb)); - while (running) { - if (linkOffset <= 0) { - linkOffset += slaveTable[GB_run(&gb)]; + if (_slave) { + [_slave preRun]; + unsigned *masterTable = multiplication_table_for_frequency(GB_get_clock_rate(&_gb)); + unsigned *slaveTable = multiplication_table_for_frequency(GB_get_clock_rate(&_slave->_gb)); + while (_running) { + if (_linkOffset <= 0) { + _linkOffset += slaveTable[GB_run(&_gb)]; } else { - linkOffset -= masterTable[GB_run(&slave->gb)]; + _linkOffset -= masterTable[GB_run(&_slave->_gb)]; + } + if (unlikely(_pendingAtomicBlock)) { + _pendingAtomicBlock(); + _pendingAtomicBlock = nil; } } free(masterTable); free(slaveTable); - [slave postRun]; + [_slave postRun]; } else { - while (running) { - if (rewind) { - rewind = false; - GB_rewind_pop(&gb); - if (!GB_rewind_pop(&gb)) { - rewind = self.view.isRewinding; + while (_running) { + if (_rewind) { + _rewind = false; + GB_rewind_pop(&_gb); + if (!GB_rewind_pop(&_gb)) { + _rewind = self.view.isRewinding; } } else { - GB_run(&gb); + GB_run(&_gb); + } + if (unlikely(_pendingAtomicBlock)) { + _pendingAtomicBlock(); + _pendingAtomicBlock = nil; } } } [self postRun]; - stopping = false; + _stopping = false; } - (void)postRun { - [hex_timer invalidate]; - [audioLock lock]; - memset(audioBuffer, 0, (audioBufferSize - audioBufferPosition) * sizeof(*audioBuffer)); - audioBufferPosition = audioBufferNeeded; - [audioLock signal]; - [audioLock unlock]; + [_hexTimer invalidate]; + [_audioLock lock]; + _audioBufferPosition = _audioBufferNeeded = 0; + [_audioLock signal]; + [_audioLock unlock]; [_audioClient stop]; _audioClient = nil; self.view.mouseHidingEnabled = false; - GB_save_battery(&gb, [[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path UTF8String]); - GB_save_cheats(&gb, [[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path UTF8String]); - unsigned time_to_alarm = GB_time_to_alarm(&gb); + GB_save_battery(&_gb, self.savPath.UTF8String); + GB_save_cheats(&_gb, self.chtPath.UTF8String); + unsigned time_to_alarm = GB_time_to_alarm(&_gb); if (time_to_alarm) { [NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate]; @@ -526,48 +580,74 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (void) start { + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateDebuggerButtons]; + [_slave updateDebuggerButtons]; + }); self.gbsPlayPauseButton.state = true; self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0; - if (master) { - [master start]; + if (_master) { + [_master start]; return; } - if (running) return; - [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start]; + if (_running) return; + _running = true; + NSThread *emulationThraed = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; + _emulationThread = emulationThraed; + [emulationThraed start]; } - (void) stop { + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateDebuggerButtons]; + [_slave updateDebuggerButtons]; + }); self.gbsPlayPauseButton.state = false; - if (master) { - if (!master->running) return; - GB_debugger_set_disabled(&gb, true); - if (GB_debugger_is_stopped(&gb)) { + if (_master) { + if (!_master->_running) return; + GB_debugger_set_disabled(&_gb, true); + if (GB_debugger_is_stopped(&_gb)) { [self interruptDebugInputRead]; } - [master stop]; - GB_debugger_set_disabled(&gb, false); + [_master stop]; + GB_debugger_set_disabled(&_gb, false); return; } - if (!running) return; - GB_debugger_set_disabled(&gb, true); - if (GB_debugger_is_stopped(&gb)) { + if (!_running) return; + GB_debugger_set_disabled(&_gb, true); + if (GB_debugger_is_stopped(&_gb)) { [self interruptDebugInputRead]; } - [audioLock lock]; - stopping = true; - [audioLock signal]; - [audioLock unlock]; - running = false; - while (stopping) { - [audioLock lock]; - [audioLock signal]; - [audioLock unlock]; + [_audioLock lock]; + _stopping = true; + [_audioLock signal]; + [_audioLock unlock]; + _running = false; + while (_stopping) { + [_audioLock lock]; + [_audioLock signal]; + [_audioLock unlock]; } - GB_debugger_set_disabled(&gb, false); + GB_debugger_set_disabled(&_gb, false); } -- (void) loadBootROM: (GB_boot_rom_t)type +- (NSString *)bootROMPathForName:(NSString *)name +{ + NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"]; + if (url) { + NSString *path = [url path]; + path = [path stringByAppendingPathComponent:name]; + path = [path stringByAppendingPathExtension:@"bin"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + return path; + } + } + + return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"]; +} + +- (void)loadBootROM: (GB_boot_rom_t)type { static NSString *const names[] = { [GB_BOOT_ROM_DMG_0] = @"dmg0_boot", @@ -577,34 +657,54 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [GB_BOOT_ROM_SGB2] = @"sgb2_boot", [GB_BOOT_ROM_CGB_0] = @"cgb0_boot", [GB_BOOT_ROM_CGB] = @"cgb_boot", + [GB_BOOT_ROM_CGB_E] = @"cgbE_boot", + [GB_BOOT_ROM_AGB_0] = @"agb0_boot", [GB_BOOT_ROM_AGB] = @"agb_boot", }; - GB_load_boot_rom(&gb, [[self bootROMPathForName:names[type]] UTF8String]); + NSString *name = names[type]; + NSString *path = [self bootROMPathForName:name]; + /* These boot types are not commonly available, and they are indentical + from an emulator perspective, so fall back to the more common variants + if they can't be found. */ + if (!path && type == GB_BOOT_ROM_CGB_E) { + [self loadBootROM:GB_BOOT_ROM_CGB]; + return; + } + if (!path && type == GB_BOOT_ROM_AGB_0) { + [self loadBootROM:GB_BOOT_ROM_AGB]; + return; + } + GB_load_boot_rom(&_gb, [path UTF8String]); } - (IBAction)reset:(id)sender { [self stop]; - size_t old_width = GB_get_screen_width(&gb); + size_t old_width = GB_get_screen_width(&_gb); if ([sender tag] != MODEL_NONE) { - current_model = (enum model)[sender tag]; + _currentModel = (enum model)[sender tag]; } - GB_switch_model_and_reset(&gb, [self internalModel]); + if ([sender tag] == MODEL_QUICK_RESET) { + GB_quick_reset(&_gb); + } + else { + GB_switch_model_and_reset(&_gb, [self internalModel]); + } - if (old_width != GB_get_screen_width(&gb)) { + if (old_width != GB_get_screen_width(&_gb)) { [self.view screenSizeChanged]; } [self updateMinSize]; - if ([sender tag] != 0) { + if ([sender tag] > MODEL_NONE) { /* User explictly selected a model, save the preference */ - [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_DMG forKey:@"EmulateDMG"]; - [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_SGB forKey:@"EmulateSGB"]; - [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_AGB forKey:@"EmulateAGB"]; - [[NSUserDefaults standardUserDefaults] setBool:current_model == MODEL_MGB forKey:@"EmulateMGB"]; + [[NSUserDefaults standardUserDefaults] setBool:_currentModel == MODEL_DMG forKey:@"EmulateDMG"]; + [[NSUserDefaults standardUserDefaults] setBool:_currentModel == MODEL_SGB forKey:@"EmulateSGB"]; + [[NSUserDefaults standardUserDefaults] setBool:_currentModel == MODEL_AGB forKey:@"EmulateAGB"]; + [[NSUserDefaults standardUserDefaults] setBool:_currentModel == MODEL_MGB forKey:@"EmulateMGB"]; } /* Reload the ROM, SAV and SYM files */ @@ -612,24 +712,24 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [self start]; - if (hex_controller) { + if (_hexController) { /* Verify bank sanity, especially when switching models. */ - [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:0]; + [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:0]; [self hexUpdateBank:self.memoryBankInput ignoreErrors:true]; } char title[17]; - GB_get_rom_title(&gb, title); - [self.osdView displayText:[NSString stringWithFormat:@"SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&gb)]]; + GB_get_rom_title(&_gb, title); + [self.osdView displayText:[NSString stringWithFormat:@"SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&_gb)]]; } - (IBAction)togglePause:(id)sender { - if (master) { - [master togglePause:sender]; + if (_master) { + [_master togglePause:sender]; return; } - if (running) { + if (_running) { [self stop]; } else { @@ -639,14 +739,14 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (void)dealloc { - [cameraSession stopRunning]; + [_cameraSession stopRunning]; self.view.gb = NULL; - GB_free(&gb); - if (cameraImage) { - CVBufferRelease(cameraImage); + GB_free(&_gb); + if (_cameraImage) { + CVBufferRelease(_cameraImage); } - if (audioBuffer) { - free(audioBuffer); + if (_audioBuffer) { + free(_audioBuffer); } } @@ -669,7 +769,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) self.debuggerSideViewInput.textColor = [NSColor whiteColor]; self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style; [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"]; - ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &gb; + ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &_gb; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateSideView) name:NSTextDidChangeNotification @@ -693,105 +793,100 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]]; - self.debuggerSplitView.dividerColor = [NSColor clearColor]; + self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [self.fileURL.path lastPathComponent]]; + self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [self.fileURL.path lastPathComponent]]; + + self.consoleWindow.level = NSNormalWindowLevel; + + self.debuggerSplitView.dividerColor = self.debuggerVerticalLine.borderColor; + [self.debuggerVerticalLine removeFromSuperview]; // No longer used, just there for the color if (@available(macOS 11.0, *)) { self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded; self.printerFeedWindow.toolbarStyle = NSWindowToolbarStyleUnifiedCompact; - [self.printerFeedWindow.toolbar removeItemAtIndex:1]; - self.printerFeedWindow.toolbar.items.firstObject.image = + self.printerFeedWindow.toolbar.items[1].image = [NSImage imageWithSystemSymbolName:@"square.and.arrow.down" accessibilityDescription:@"Save"]; - self.printerFeedWindow.toolbar.items.lastObject.image = + self.printerFeedWindow.toolbar.items[2].image = [NSImage imageWithSystemSymbolName:@"printer" accessibilityDescription:@"Print"]; + self.printerFeedWindow.toolbar.items[1].bordered = false; + self.printerFeedWindow.toolbar.items[2].bordered = false; + } + else { + NSToolbarItem *spinner = self.printerFeedWindow.toolbar.items[0]; + [self.printerFeedWindow.toolbar removeItemAtIndex:0]; + [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:spinner.itemIdentifier atIndex:2]; + [self.printerFeedWindow.toolbar removeItemAtIndex:1]; + [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:NSToolbarPrintItemIdentifier + atIndex:1]; + [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier + atIndex:2]; } - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateHighpassFilter) - name:@"GBHighpassFilterChanged" - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateColorCorrectionMode) - name:@"GBColorCorrectionChanged" - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateLightTemperature) - name:@"GBLightTemperatureChanged" - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateInterferenceVolume) - name:@"GBInterferenceVolumeChanged" - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateFrameBlendingMode) - name:@"GBFrameBlendingModeChanged" - object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatePalette) name:@"GBColorPaletteChanged" object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateBorderMode) - name:@"GBBorderModeChanged" - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateRumbleMode) - name:@"GBRumbleModeChanged" - object:nil]; + __unsafe_unretained Document *weakSelf = self; + [self observeStandardDefaultsKey:@"GBFrameBlendingMode" + withBlock:^(NSNumber *value) { + weakSelf.view.frameBlendingMode = (GB_frame_blending_mode_t)value.unsignedIntValue; + }]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateRewindLength) - name:@"GBRewindLengthChanged" - object:nil]; + [self observeStandardDefaultsKey:@"GBDMGModel" withBlock:^(id newValue) { + weakSelf->_modelsChanging = true; + if (weakSelf->_currentModel == MODEL_DMG) { + [weakSelf reset:nil]; + } + weakSelf->_modelsChanging = false; + }]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateRTCMode) - name:@"GBRTCModeChanged" - object:nil]; - + [self observeStandardDefaultsKey:@"GBSGBModel" withBlock:^(id newValue) { + weakSelf->_modelsChanging = true; + if (weakSelf->_currentModel == MODEL_SGB) { + [weakSelf reset:nil]; + } + weakSelf->_modelsChanging = false; + }]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(dmgModelChanged) - name:@"GBDMGModelChanged" - object:nil]; + [self observeStandardDefaultsKey:@"GBCGBModel" withBlock:^(id newValue) { + weakSelf->_modelsChanging = true; + if (weakSelf->_currentModel == MODEL_CGB) { + [weakSelf reset:nil]; + } + weakSelf->_modelsChanging = false; + }]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(sgbModelChanged) - name:@"GBSGBModelChanged" - object:nil]; + [self observeStandardDefaultsKey:@"GBAGBModel" withBlock:^(id newValue) { + weakSelf->_modelsChanging = true; + if (weakSelf->_currentModel == MODEL_AGB) { + [weakSelf reset:nil]; + } + weakSelf->_modelsChanging = false; + }]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(cgbModelChanged) - name:@"GBCGBModelChanged" - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(updateVolume) - name:@"GBVolumeChanged" - object:nil]; + [self observeStandardDefaultsKey:@"GBVolume" withBlock:^(id newValue) { + weakSelf->_volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; + }]; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) { - current_model = MODEL_DMG; + _currentModel = MODEL_DMG; } else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateSGB"]) { - current_model = MODEL_SGB; + _currentModel = MODEL_SGB; } else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateMGB"]) { - current_model = MODEL_MGB; + _currentModel = MODEL_MGB; } else { - current_model = [[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateAGB"]? MODEL_AGB : MODEL_CGB; + _currentModel = [[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateAGB"]? MODEL_AGB : MODEL_CGB; } [self initCommon]; - self.view.gb = &gb; + self.view.gb = &_gb; self.view.osdView = _osdView; [self.view screenSizeChanged]; if ([self loadROM]) { @@ -807,36 +902,38 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (void) initMemoryView { - hex_controller = [[HFController alloc] init]; - [hex_controller setBytesPerColumn:1]; - [hex_controller setEditMode:HFOverwriteMode]; + _hexController = [[HFController alloc] init]; + [_hexController setBytesPerColumn:1]; + [_hexController setEditMode:HFOverwriteMode]; - [hex_controller setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]]; + [_hexController setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]]; /* Here we're going to make three representers - one for the hex, one for the ASCII, and one for the scrollbar. To lay these all out properly, we'll use a fourth HFLayoutRepresenter. */ HFLayoutRepresenter *layoutRep = [[HFLayoutRepresenter alloc] init]; HFHexTextRepresenter *hexRep = [[HFHexTextRepresenter alloc] init]; HFStringEncodingTextRepresenter *asciiRep = [[HFStringEncodingTextRepresenter alloc] init]; HFVerticalScrollerRepresenter *scrollRep = [[HFVerticalScrollerRepresenter alloc] init]; - lineRep = [[HFLineCountingRepresenter alloc] init]; - HFStatusBarRepresenter *statusRep = [[HFStatusBarRepresenter alloc] init]; + _lineRep = [[HFLineCountingRepresenter alloc] init]; + _statusRep = [[GBHexStatusBarRepresenter alloc] init]; + _statusRep.gb = &_gb; + _statusRep.bankForDescription = -1; - lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal; + _lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal; /* Add all our reps to the controller. */ - [hex_controller addRepresenter:layoutRep]; - [hex_controller addRepresenter:hexRep]; - [hex_controller addRepresenter:asciiRep]; - [hex_controller addRepresenter:scrollRep]; - [hex_controller addRepresenter:lineRep]; - [hex_controller addRepresenter:statusRep]; + [_hexController addRepresenter:layoutRep]; + [_hexController addRepresenter:hexRep]; + [_hexController addRepresenter:asciiRep]; + [_hexController addRepresenter:scrollRep]; + [_hexController addRepresenter:_lineRep]; + [_hexController addRepresenter:_statusRep]; /* Tell the layout rep which reps it should lay out. */ [layoutRep addRepresenter:hexRep]; [layoutRep addRepresenter:scrollRep]; [layoutRep addRepresenter:asciiRep]; - [layoutRep addRepresenter:lineRep]; - [layoutRep addRepresenter:statusRep]; + [layoutRep addRepresenter:_lineRep]; + [layoutRep addRepresenter:_statusRep]; [(NSView *)[hexRep view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; @@ -847,6 +944,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [layoutView setFrame:layoutViewFrame]; [layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin]; [self.memoryView addSubview:layoutView]; + self.memoryView = layoutView; self.memoryBankItem.enabled = false; } @@ -870,11 +968,11 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)changeGBSTrack:(id)sender { - if (!running) { + if (!_running) { [self start]; } [self performAtomicBlock:^{ - GB_gbs_switch_track(&gb, self.gbsTracks.indexOfSelectedItem); + GB_gbs_switch_track(&_gb, self.gbsTracks.indexOfSelectedItem); }]; } - (IBAction)gbsNextPrevPushed:(id)sender @@ -902,12 +1000,17 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (void)prepareGBSInterface: (GB_gbs_info_t *)info { - GB_set_rendering_disabled(&gb, true); + GB_set_rendering_disabled(&_gb, true); _view = nil; for (NSView *view in [_mainWindow.contentView.subviews copy]) { [view removeFromSuperview]; } - [[NSBundle mainBundle] loadNibNamed:@"GBS" owner:self topLevelObjects:nil]; + if (@available(macOS 11, *)) { + [[NSBundle mainBundle] loadNibNamed:@"GBS11" owner:self topLevelObjects:nil]; + } + else { + [[NSBundle mainBundle] loadNibNamed:@"GBS" owner:self topLevelObjects:nil]; + } [_mainWindow setContentSize:self.gbsPlayerView.bounds.size]; _mainWindow.styleMask &= ~NSWindowStyleMaskResizable; dispatch_async(dispatch_get_main_queue(), ^{ // Cocoa is weird, no clue why it's needed @@ -944,28 +1047,124 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) } } +- (bool)isCartContainer +{ + return [self.fileName.pathExtension.lowercaseString isEqualToString:@"gbcart"]; +} + +- (NSString *)savPath +{ + if (self.isCartContainer) { + return [self.fileName stringByAppendingPathComponent:@"battery.sav"]; + } + + return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path; +} + +- (NSString *)chtPath +{ + if (self.isCartContainer) { + return [self.fileName stringByAppendingPathComponent:@"cheats.cht"]; + } + + return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path; +} + +- (NSString *)saveStatePath:(unsigned)index +{ + if (self.isCartContainer) { + return [self.fileName stringByAppendingPathComponent:[NSString stringWithFormat:@"state.s%u", index]]; + } + return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%u", index]].path; +} + +- (NSString *)romPath +{ + NSString *fileName = self.fileName; + if (self.isCartContainer) { + NSArray *paths = [[NSString stringWithContentsOfFile:[fileName stringByAppendingPathComponent:@"rom.gbl"] + encoding:NSUTF8StringEncoding + error:nil] componentsSeparatedByString:@"\n"]; + fileName = nil; + bool needsRebuild = false; + for (NSString *path in paths) { + NSURL *url = [NSURL URLWithString:path relativeToURL:self.fileURL]; + if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) { + if (fileName && ![fileName isEqualToString:url.path]) { + needsRebuild = true; + break; + } + fileName = url.path; + } + else { + needsRebuild = true; + } + } + if (fileName && needsRebuild) { + [[NSString stringWithFormat:@"%@\n%@\n%@", + [fileName pathRelativeToDirectory:self.fileName], + fileName, + [[NSURL fileURLWithPath:fileName].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")]] + writeToFile:[self.fileName stringByAppendingPathComponent:@"rom.gbl"] + atomically:false + encoding:NSUTF8StringEncoding + error:nil]; + } + } + + return fileName; +} + +static bool is_path_writeable(const char *path) +{ + if (!access(path, W_OK)) return true; + int fd = creat(path, 0644); + if (fd == -1) return false; + close(fd); + unlink(path); + return true; +} + - (int) loadROM { __block int ret = 0; + NSString *fileName = self.romPath; + if (!fileName) { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Could not locate the ROM referenced by this Game Boy Cartridge"]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert runModal]; + return 1; + } + NSString *rom_warnings = [self captureOutputForBlock:^{ - GB_debugger_clear_symbols(&gb); - if ([[[self.fileType pathExtension] lowercaseString] isEqualToString:@"isx"]) { - ret = GB_load_isx(&gb, self.fileURL.path.UTF8String); - GB_load_battery(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"ram"].path.UTF8String); + GB_debugger_clear_symbols(&_gb); + if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"isx"]) { + ret = GB_load_isx(&_gb, fileName.UTF8String); + if (!self.isCartContainer) { + GB_load_battery(&_gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"ram"].path.UTF8String); + } } - else if ([[[self.fileType pathExtension] lowercaseString] isEqualToString:@"gbs"]) { + else if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"gbs"]) { __block GB_gbs_info_t info; - ret = GB_load_gbs(&gb, self.fileURL.path.UTF8String, &info); + ret = GB_load_gbs(&_gb, fileName.UTF8String, &info); [self prepareGBSInterface:&info]; } else { - ret = GB_load_rom(&gb, [self.fileURL.path UTF8String]); + ret = GB_load_rom(&_gb, [fileName UTF8String]); } - GB_load_battery(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path.UTF8String); - GB_load_cheats(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path.UTF8String); - [self.cheatWindowController cheatsUpdated]; - GB_debugger_load_symbol_file(&gb, [[[NSBundle mainBundle] pathForResource:@"registers" ofType:@"sym"] UTF8String]); - GB_debugger_load_symbol_file(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sym"].path.UTF8String); + if (GB_save_battery_size(&_gb)) { + if (!is_path_writeable(self.savPath.UTF8String)) { + GB_log(&_gb, "The save path for this ROM is not writeable, progress will not be saved.\n"); + } + } + GB_load_battery(&_gb, self.savPath.UTF8String); + GB_load_cheats(&_gb, self.chtPath.UTF8String); + dispatch_async(dispatch_get_main_queue(), ^{ + [self.cheatWindowController cheatsUpdated]; + }); + GB_debugger_load_symbol_file(&_gb, [[[NSBundle mainBundle] pathForResource:@"registers" ofType:@"sym"] UTF8String]); + GB_debugger_load_symbol_file(&_gb, [[fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sym"].UTF8String); }]; if (ret) { NSAlert *alert = [[NSAlert alloc] init]; @@ -973,13 +1172,24 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [alert setAlertStyle:NSAlertStyleCritical]; [alert runModal]; } - else if (rom_warnings && !rom_warning_issued) { - rom_warning_issued = true; + else if (rom_warnings && !_romWarningIssued) { + _romWarningIssued = true; [GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow]; } + _fileModificationTime = [[NSFileManager defaultManager] attributesOfItemAtPath:fileName error:nil][NSFileModificationDate]; return ret; } +- (void)showWindows +{ + if (GB_is_inited(&_gb)) { + if (![_fileModificationTime isEqualToDate:[[NSFileManager defaultManager] attributesOfItemAtPath:self.fileName error:nil][NSFileModificationDate]]) { + [self reset:nil]; + } + } + [super showWindows]; +} + - (void)close { [self disconnectLinkCable]; @@ -988,6 +1198,9 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"]; } [self stop]; + [_consoleOutputLock lock]; + [_consoleOutputTimer invalidate]; + [_consoleOutputLock unlock]; [self.consoleWindow close]; [self.memoryWindow close]; [self.vramWindow close]; @@ -999,7 +1212,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction) interrupt:(id)sender { [self log:"^C\n"]; - GB_debugger_break(&gb); + GB_debugger_break(&_gb); [self start]; [self.consoleWindow makeKeyAndOrderFront:nil]; [self.consoleInput becomeFirstResponder]; @@ -1019,20 +1232,25 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [[NSUserDefaults standardUserDefaults] setBool:!_audioClient.isPlaying forKey:@"Mute"]; } +- (bool) isPaused +{ + if (self.partner) { + return !self.partner->_running || GB_debugger_is_stopped(&_gb) || GB_debugger_is_stopped(&self.partner->_gb); + } + return (!_running) || GB_debugger_is_stopped(&_gb); +} + - (BOOL)validateUserInterfaceItem:(id)anItem { if ([anItem action] == @selector(mute:)) { [(NSMenuItem *)anItem setState:!_audioClient.isPlaying]; } else if ([anItem action] == @selector(togglePause:)) { - if (master) { - [(NSMenuItem *)anItem setState:(!master->running) || (GB_debugger_is_stopped(&gb)) || (GB_debugger_is_stopped(&gb))]; - } - [(NSMenuItem *)anItem setState:(!running) || (GB_debugger_is_stopped(&gb))]; - return !GB_debugger_is_stopped(&gb); + [(NSMenuItem *)anItem setState:self.isPaused]; + return !GB_debugger_is_stopped(&_gb); } - else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE) { - [(NSMenuItem*)anItem setState:anItem.tag == current_model]; + else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE && anItem.tag != MODEL_QUICK_RESET) { + [(NSMenuItem *)anItem setState:anItem.tag == _currentModel]; } else if ([anItem action] == @selector(interrupt:)) { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { @@ -1040,26 +1258,38 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) } } else if ([anItem action] == @selector(disconnectAllAccessories:)) { - [(NSMenuItem*)anItem setState:accessory == GBAccessoryNone]; + [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_NONE && !self.partner]; } else if ([anItem action] == @selector(connectPrinter:)) { - [(NSMenuItem*)anItem setState:accessory == GBAccessoryPrinter]; + [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_PRINTER]; } else if ([anItem action] == @selector(connectWorkboy:)) { - [(NSMenuItem*)anItem setState:accessory == GBAccessoryWorkboy]; + [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_WORKBOY]; } else if ([anItem action] == @selector(connectLinkCable:)) { - [(NSMenuItem*)anItem setState:[(NSMenuItem *)anItem representedObject] == master || - [(NSMenuItem *)anItem representedObject] == slave]; + [(NSMenuItem *)anItem setState:[(NSMenuItem *)anItem representedObject] == _master || + [(NSMenuItem *)anItem representedObject] == _slave]; } else if ([anItem action] == @selector(toggleCheats:)) { - [(NSMenuItem*)anItem setState:GB_cheats_enabled(&gb)]; + [(NSMenuItem *)anItem setState:GB_cheats_enabled(&_gb)]; } else if ([anItem action] == @selector(toggleDisplayBackground:)) { - [(NSMenuItem*)anItem setState:!GB_is_background_rendering_disabled(&gb)]; + [(NSMenuItem *)anItem setState:!GB_is_background_rendering_disabled(&_gb)]; } else if ([anItem action] == @selector(toggleDisplayObjects:)) { - [(NSMenuItem*)anItem setState:!GB_is_object_rendering_disabled(&gb)]; + [(NSMenuItem *)anItem setState:!GB_is_object_rendering_disabled(&_gb)]; + } + else if ([anItem action] == @selector(toggleAudioRecording:)) { + [(NSMenuItem *)anItem setTitle:_isRecordingAudio? @"Stop Audio Recording" : @"Start Audio Recording…"]; + } + else if ([anItem action] == @selector(toggleAudioChannel:)) { + [(NSMenuItem *)anItem setState:!GB_is_channel_muted(&_gb, [anItem tag])]; + } + else if ([anItem action] == @selector(increaseWindowSize:)) { + return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionIncrease]; + } + else if ([anItem action] == @selector(decreaseWindowSize:)) { + return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionDecrease]; } return [super validateUserInterfaceItem:anItem]; @@ -1068,75 +1298,154 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (void) windowWillEnterFullScreen:(NSNotification *)notification { - fullScreen = true; - self.view.mouseHidingEnabled = running; + _fullScreen = true; + self.view.mouseHidingEnabled = _running; } - (void) windowWillExitFullScreen:(NSNotification *)notification { - fullScreen = false; + _fullScreen = false; self.view.mouseHidingEnabled = false; } +enum GBWindowResizeAction +{ + GBWindowResizeActionZoom, + GBWindowResizeActionIncrease, + GBWindowResizeActionDecrease, +}; + +- (bool)newRect:(NSRect *)rect forWindow:(NSWindow *)window action:(enum GBWindowResizeAction)action +{ + if (_fullScreen) return false; + if (!rect) { + rect = alloca(sizeof(*rect)); + } + + size_t width = GB_get_screen_width(&_gb), + height = GB_get_screen_height(&_gb); + + *rect = window.contentView.frame; + + unsigned titlebarSize = window.contentView.superview.frame.size.height - rect->size.height; + + unsigned stepX = width / [[window screen] backingScaleFactor]; + unsigned stepY = height / [[window screen] backingScaleFactor]; + + if (action == GBWindowResizeActionDecrease) { + if (rect->size.width <= width || rect->size.height <= height) { + return false; + } + } + + typeof(floor) *roundFunc = action == GBWindowResizeActionDecrease? ceil : floor; + unsigned currentFactor = MIN(roundFunc(rect->size.width / stepX), roundFunc(rect->size.height / stepY)); + + rect->size.width = currentFactor * stepX; + rect->size.height = currentFactor * stepY + titlebarSize; + + if (action == GBWindowResizeActionDecrease) { + rect->size.width -= stepX; + rect->size.height -= stepY; + } + else { + rect->size.width += stepX; + rect->size.height += stepY; + } + + NSRect maxRect = [_mainWindow screen].visibleFrame; + + if (rect->size.width > maxRect.size.width || + rect->size.height > maxRect.size.height) { + if (action == GBWindowResizeActionIncrease) { + return false; + } + rect->size.width = width; + rect->size.height = height + titlebarSize; + } + + rect->origin = window.frame.origin; + if (action == GBWindowResizeActionZoom) { + rect->origin.y -= rect->size.height - window.frame.size.height; + } + else { + rect->origin.y -= (rect->size.height - window.frame.size.height) / 2; + rect->origin.x -= (rect->size.width - window.frame.size.width) / 2; + } + + if (rect->origin.x < maxRect.origin.x) { + rect->origin.x = maxRect.origin.x; + } + + if (rect->origin.y < maxRect.origin.y) { + rect->origin.y = maxRect.origin.y; + } + + if (rect->origin.x + rect->size.width > maxRect.origin.x + maxRect.size.width) { + rect->origin.x = maxRect.origin.x + maxRect.size.width - rect->size.width; + } + + if (rect->origin.y + rect->size.height > maxRect.origin.y + maxRect.size.height) { + rect->origin.y = maxRect.origin.y + maxRect.size.height - rect->size.height; + } + + return true; +} + - (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame { - if (fullScreen) { + if (_fullScreen) { return newFrame; } - size_t width = GB_get_screen_width(&gb), - height = GB_get_screen_height(&gb); - - NSRect rect = window.contentView.frame; + [self newRect:&newFrame forWindow:window action:GBWindowResizeActionZoom]; + return newFrame; +} - unsigned titlebarSize = window.contentView.superview.frame.size.height - rect.size.height; - unsigned step = width / [[window screen] backingScaleFactor]; - rect.size.width = floor(rect.size.width / step) * step + step; - rect.size.height = rect.size.width * height / width + titlebarSize; - - if (rect.size.width > newFrame.size.width) { - rect.size.width = width; - rect.size.height = height + titlebarSize; +- (IBAction)increaseWindowSize:(id)sender +{ + NSRect rect; + if ([self newRect:&rect forWindow:_mainWindow action:GBWindowResizeActionIncrease]) { + [_mainWindow setFrame:rect display:true animate:true]; } - else if (rect.size.height > newFrame.size.height) { - rect.size.width = width; - rect.size.height = height + titlebarSize; +} + +- (IBAction)decreaseWindowSize:(id)sender +{ + NSRect rect; + if ([self newRect:&rect forWindow:_mainWindow action:GBWindowResizeActionDecrease]) { + [_mainWindow setFrame:rect display:true animate:true]; } - - rect.origin = window.frame.origin; - rect.origin.y -= rect.size.height - window.frame.size.height; - - return rect; } - (void) appendPendingOutput { - [console_output_lock lock]; - if (shouldClearSideView) { - shouldClearSideView = false; + [_consoleOutputLock lock]; + if (_shouldClearSideView) { + _shouldClearSideView = false; [self.debuggerSideView setString:@""]; } - if (pending_console_output) { - NSTextView *textView = logToSideView? self.debuggerSideView : self.consoleOutput; + if (_pendingConsoleOutput) { + NSTextView *textView = _logToSideView? self.debuggerSideView : self.consoleOutput; - [hex_controller reloadData]; + [_hexController reloadData]; [self reloadVRAMData: nil]; - [textView.textStorage appendAttributedString:pending_console_output]; + [textView.textStorage appendAttributedString:_pendingConsoleOutput]; [textView scrollToEndOfDocument:nil]; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { [self.consoleWindow orderFront:nil]; } - pending_console_output = nil; + _pendingConsoleOutput = nil; } - [console_output_lock unlock]; + [_consoleOutputLock unlock]; } - (void) log: (const char *) string withAttributes: (GB_log_attributes) attributes { NSString *nsstring = @(string); // For ref-counting - if (capturedOutput) { - [capturedOutput appendString:nsstring]; + if (_capturedOutput) { + [_capturedOutput appendString:nsstring]; return; } @@ -1160,20 +1469,20 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) NSUnderlineStyleAttributeName: @(underline), NSParagraphStyleAttributeName: paragraph_style}]; - [console_output_lock lock]; - if (!pending_console_output) { - pending_console_output = attributed; + [_consoleOutputLock lock]; + if (!_pendingConsoleOutput) { + _pendingConsoleOutput = attributed; } else { - [pending_console_output appendAttributedString:attributed]; + [_pendingConsoleOutput appendAttributedString:attributed]; } - if (![console_output_timer isValid]) { - console_output_timer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:false]; - [[NSRunLoop mainRunLoop] addTimer:console_output_timer forMode:NSDefaultRunLoopMode]; + if (![_consoleOutputTimer isValid]) { + _consoleOutputTimer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:false]; + [[NSRunLoop mainRunLoop] addTimer:_consoleOutputTimer forMode:NSDefaultRunLoopMode]; } - [console_output_lock unlock]; + [_consoleOutputLock unlock]; /* Make sure mouse is not hidden while debugging */ self.view.mouseHidingEnabled = false; @@ -1184,41 +1493,53 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [self.consoleWindow orderBack:nil]; } +- (void)queueDebuggerCommand:(NSString *)command +{ + if (!_master && !_running && !GB_debugger_is_stopped(&_gb)) { + _debuggerCommandWhilePaused = command; + GB_debugger_break(&_gb); + [self start]; + return; + } + + if (!_inSyncInput) { + [self log:">"]; + } + [self log:[command UTF8String]]; + [self log:"\n"]; + [_hasDebuggerInput lock]; + [_debuggerInputQueue addObject:command]; + [_hasDebuggerInput unlockWithCondition:1]; +} + - (IBAction)consoleInput:(NSTextField *)sender { NSString *line = [sender stringValue]; - if ([line isEqualToString:@""] && lastConsoleInput) { - line = lastConsoleInput; + if ([line isEqualToString:@""] && _lastConsoleInput) { + line = _lastConsoleInput; } else if (line) { - lastConsoleInput = line; + _lastConsoleInput = line; } else { line = @""; } - - if (!in_sync_input) { - [self log:">"]; - } - [self log:[line UTF8String]]; - [self log:"\n"]; - [has_debugger_input lock]; - [debugger_input_queue addObject:line]; - [has_debugger_input unlockWithCondition:1]; + + [self queueDebuggerCommand: line]; [sender setStringValue:@""]; } - (void) interruptDebugInputRead { - [has_debugger_input lock]; - [debugger_input_queue addObject:[NSNull null]]; - [has_debugger_input unlockWithCondition:1]; + [_hasDebuggerInput lock]; + [_debuggerInputQueue addObject:[NSNull null]]; + [_hasDebuggerInput unlockWithCondition:1]; } - (void) updateSideView { - if (!GB_debugger_is_stopped(&gb)) { + if (!GB_debugger_is_stopped(&_gb)) { return; } @@ -1229,49 +1550,69 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) return; } - [console_output_lock lock]; - shouldClearSideView = true; + [_consoleOutputLock lock]; + _shouldClearSideView = true; [self appendPendingOutput]; - logToSideView = true; - [console_output_lock unlock]; + _logToSideView = true; + [_consoleOutputLock unlock]; for (NSString *line in [self.debuggerSideViewInput.string componentsSeparatedByString:@"\n"]) { NSString *stripped = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([stripped length]) { char *dupped = strdup([stripped UTF8String]); - GB_attributed_log(&gb, GB_LOG_BOLD, "%s:\n", dupped); - GB_debugger_execute_command(&gb, dupped); - GB_log(&gb, "\n"); + GB_attributed_log(&_gb, GB_LOG_BOLD, "%s:\n", dupped); + GB_debugger_execute_command(&_gb, dupped); + GB_log(&_gb, "\n"); free(dupped); } } - [console_output_lock lock]; + [_consoleOutputLock lock]; [self appendPendingOutput]; - logToSideView = false; - [console_output_lock unlock]; + _logToSideView = false; + [_consoleOutputLock unlock]; } -- (char *) getDebuggerInput +- (char *)getDebuggerInput { - [audioLock lock]; - [audioLock signal]; - [audioLock unlock]; + bool isPlaying = _audioClient.isPlaying; + if (isPlaying) { + [_audioClient stop]; + } + [_audioLock lock]; + [_audioLock signal]; + [_audioLock unlock]; + _inSyncInput = true; [self updateSideView]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateDebuggerButtons]; + }); + [self.partner updateDebuggerButtons]; [self log:">"]; - in_sync_input = true; - [has_debugger_input lockWhenCondition:1]; - NSString *input = [debugger_input_queue firstObject]; - [debugger_input_queue removeObjectAtIndex:0]; - [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; - in_sync_input = false; - shouldClearSideView = true; + if (_debuggerCommandWhilePaused) { + NSString *command = _debuggerCommandWhilePaused; + _debuggerCommandWhilePaused = nil; + dispatch_async(dispatch_get_main_queue(), ^{ + [self queueDebuggerCommand:command]; + }); + } + [_hasDebuggerInput lockWhenCondition:1]; + NSString *input = [_debuggerInputQueue firstObject]; + [_debuggerInputQueue removeObjectAtIndex:0]; + [_hasDebuggerInput unlockWithCondition:[_debuggerInputQueue count] != 0]; + _inSyncInput = false; + _shouldClearSideView = true; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC / 10)), dispatch_get_main_queue(), ^{ - if (shouldClearSideView) { - shouldClearSideView = false; + if (_shouldClearSideView) { + _shouldClearSideView = false; [self.debuggerSideView setString:@""]; } + [self updateDebuggerButtons]; + [self.partner updateDebuggerButtons]; }); + if (isPlaying) { + [_audioClient start]; + } if ((id) input == [NSNull null]) { return NULL; } @@ -1280,12 +1621,12 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (char *) getAsyncDebuggerInput { - [has_debugger_input lock]; - NSString *input = [debugger_input_queue firstObject]; + [_hasDebuggerInput lock]; + NSString *input = [_debuggerInputQueue firstObject]; if (input) { - [debugger_input_queue removeObjectAtIndex:0]; + [_debuggerInputQueue removeObjectAtIndex:0]; } - [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; + [_hasDebuggerInput unlockWithCondition:[_debuggerInputQueue count] != 0]; if ((id)input == [NSNull null]) { return NULL; } @@ -1296,7 +1637,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) { bool __block success = false; [self performAtomicBlock:^{ - success = GB_save_state(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]].path.UTF8String) == 0; + success = GB_save_state(&_gb, [self saveStatePath:[sender tag]].UTF8String) == 0; }]; if (!success) { @@ -1313,7 +1654,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) int __block result = false; NSString *error = [self captureOutputForBlock:^{ - result = GB_load_state(&gb, path); + result = GB_load_state(&_gb, path); }]; if (result == ENOENT && noErrorOnFileNotFound) { @@ -1334,8 +1675,8 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)loadState:(id)sender { - int ret = [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:true]; - if (ret == ENOENT) { + int ret = [self loadStateFile:[self saveStatePath:[sender tag]].UTF8String noErrorOnNotFound:true]; + if (ret == ENOENT && !self.isCartContainer) { [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"sn%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:false]; } } @@ -1352,79 +1693,82 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (uint8_t) readMemory:(uint16_t)addr { - while (!GB_is_inited(&gb)); - return GB_safe_read_memory(&gb, addr); + while (!GB_is_inited(&_gb)); + return GB_safe_read_memory(&_gb, addr); } - (void) writeMemory:(uint16_t)addr value:(uint8_t)value { - while (!GB_is_inited(&gb)); - GB_write_memory(&gb, addr, value); + while (!GB_is_inited(&_gb)); + GB_write_memory(&_gb, addr, value); } -- (void) performAtomicBlock: (void (^)())block +- (void)performAtomicBlock: (void (^)())block { - while (!GB_is_inited(&gb)); - bool was_running = running && !GB_debugger_is_stopped(&gb); - if (master) { - was_running |= master->running; + while (!GB_is_inited(&_gb)); + bool isRunning = _running && !GB_debugger_is_stopped(&_gb); + if (_master) { + isRunning |= _master->_running; } - if (was_running) { - [self stop]; + if (!isRunning) { + block(); + return; } - block(); - if (was_running) { - [self start]; + + if (_master) { + [_master performAtomicBlock:block]; + return; } + + if ([NSThread currentThread] == _emulationThread) { + block(); + return; + } + + _pendingAtomicBlock = block; + while (_pendingAtomicBlock); } -- (NSString *) captureOutputForBlock: (void (^)())block +- (NSString *)captureOutputForBlock: (void (^)())block { - capturedOutput = [[NSMutableString alloc] init]; + _capturedOutput = [[NSMutableString alloc] init]; [self performAtomicBlock:block]; - NSString *ret = [capturedOutput stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; - capturedOutput = nil; + NSString *ret = [_capturedOutput stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + _capturedOutput = nil; return [ret length]? ret : nil; } + (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale { - CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef) data); - CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; - CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; - - CGImageRef iref = CGImageCreate(width, - height, - 8, - 32, - 4 * width, - colorSpaceRef, - bitmapInfo, - provider, - NULL, - true, - renderingIntent); - CGDataProviderRelease(provider); - CGColorSpaceRelease(colorSpaceRef); - - NSImage *ret = [[NSImage alloc] initWithCGImage:iref size:NSMakeSize(width * scale, height * scale)]; - CGImageRelease(iref); + NSImage *ret = [[NSImage alloc] initWithSize:NSMakeSize(width * scale, height * scale)]; + NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL + pixelsWide:width + pixelsHigh:height + bitsPerSample:8 + samplesPerPixel:3 + hasAlpha:false + isPlanar:false + colorSpaceName:NSDeviceRGBColorSpace + bitmapFormat:0 + bytesPerRow:4 * width + bitsPerPixel:32]; + memcpy(rep.bitmapData, data.bytes, data.length); + [ret addRepresentation:rep]; return ret; } - (void) reloadMemoryView { if (self.memoryWindow.isVisible) { - [hex_controller reloadData]; + [_hexController reloadData]; } } - (IBAction) reloadVRAMData: (id) sender { if (self.vramWindow.isVisible) { - uint8_t *io_regs = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL); + uint8_t *io_regs = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_IO, NULL, NULL); switch ([self.vramTabView.tabViewItems indexOfObject:self.vramTabView.selectedTabViewItem]) { case 0: /* Tileset */ @@ -1437,7 +1781,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) size_t bufferLength = 256 * 192 * 4; NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength]; data.length = bufferLength; - GB_draw_tileset(&gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 1) & 7); + GB_draw_tileset(&_gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 1) & 7); self.tilesetImageView.image = [Document imageFromData:data width:256 height:192 scale:1.0]; self.tilesetImageView.layer.magnificationFilter = kCAFilterNearest; @@ -1459,7 +1803,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) size_t bufferLength = 256 * 256 * 4; NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength]; data.length = bufferLength; - GB_draw_tilemap(&gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 2) & 7, + GB_draw_tilemap(&_gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 2) & 7, (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem, (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem); @@ -1474,13 +1818,9 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) case 2: /* OAM */ { - oamCount = GB_get_oam_info(&gb, oamInfo, &oamHeight); + _oamCount = GB_get_oam_info(&_gb, _oamInfo, &_oamHeight); dispatch_async(dispatch_get_main_queue(), ^{ - if (!oamUpdating) { - oamUpdating = true; - [self.objectsTableView reloadData]; - oamUpdating = false; - } + [self.objectView reloadData:self]; }); } break; @@ -1489,7 +1829,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) /* Palettes */ { dispatch_async(dispatch_get_main_queue(), ^{ - [self.paletteTableView reloadData]; + [self.paletteView reloadData:self]; }); } break; @@ -1499,7 +1839,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction) showMemory:(id)sender { - if (!hex_controller) { + if (!_hexController) { [self initMemoryView]; } [self.memoryWindow makeKeyAndOrderFront:sender]; @@ -1509,17 +1849,65 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) { NSString *error = [self captureOutputForBlock:^{ uint16_t addr; - if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, NULL)) { + uint16_t bank; + if (GB_debugger_evaluate(&_gb, [[sender stringValue] UTF8String], &addr, &bank)) { return; } - addr -= lineRep.valueOffset; - if (addr >= hex_controller.byteArray.length) { - GB_log(&gb, "Value $%04x is out of range.\n", addr); + + if (bank != (typeof(bank))-1) { + GB_memory_mode_t mode = [(GBMemoryByteArray *)(_hexController.byteArray) mode]; + if (addr < 0x4000) { + if (bank == 0) { + if (mode != GBMemoryROM && mode != GBMemoryEntireSpace) { + mode = GBMemoryEntireSpace; + } + } + else { + addr |= 0x4000; + mode = GBMemoryROM; + } + } + else if (addr < 0x8000) { + mode = GBMemoryROM; + } + else if (addr < 0xA000) { + mode = GBMemoryVRAM; + } + else if (addr < 0xC000) { + mode = GBMemoryExternalRAM; + } + else if (addr < 0xD000) { + if (mode != GBMemoryRAM && mode != GBMemoryEntireSpace) { + mode = GBMemoryEntireSpace; + } + } + else if (addr < 0xE000) { + mode = GBMemoryRAM; + } + else { + mode = GBMemoryEntireSpace; + } + [_memorySpaceButton selectItemAtIndex:mode]; + [self hexUpdateSpace:_memorySpaceButton.cell]; + [_memoryBankInput setStringValue:[NSString stringWithFormat:@"$%02x", bank]]; + [self hexUpdateBank:_memoryBankInput]; + } + addr -= _lineRep.valueOffset; + if (addr >= _hexController.byteArray.length) { + GB_log(&_gb, "Value $%04x is out of range.\n", addr); return; } - [hex_controller setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]]; - [hex_controller _ensureVisibilityOfLocation:addr]; - [self.memoryWindow makeFirstResponder:self.memoryView.subviews[0].subviews[0]]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]]; + [_hexController _ensureVisibilityOfLocation:addr]; + for (HFRepresenter *representer in _hexController.representers) { + if ([representer isKindOfClass:[HFHexTextRepresenter class]]) { + [self.memoryWindow makeFirstResponder:representer.view]; + break; + } + } + }); }]; if (error) { NSBeep(); @@ -1531,7 +1919,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) { NSString *error = [self captureOutputForBlock:^{ uint16_t addr, bank; - if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, &bank)) { + if (GB_debugger_evaluate(&_gb, [[sender stringValue] UTF8String], &addr, &bank)) { return; } @@ -1540,24 +1928,24 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) } uint16_t n_banks = 1; - switch ([(GBMemoryByteArray *)(hex_controller.byteArray) mode]) { + switch ([(GBMemoryByteArray *)(_hexController.byteArray) mode]) { case GBMemoryROM: { size_t rom_size; - GB_get_direct_access(&gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL); + GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL); n_banks = rom_size / 0x4000; break; } case GBMemoryVRAM: - n_banks = GB_is_cgb(&gb) ? 2 : 1; + n_banks = GB_is_cgb(&_gb) ? 2 : 1; break; case GBMemoryExternalRAM: { size_t ram_size; - GB_get_direct_access(&gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL); + GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL); n_banks = (ram_size + 0x1FFF) / 0x2000; break; } case GBMemoryRAM: - n_banks = GB_is_cgb(&gb) ? 8 : 1; + n_banks = GB_is_cgb(&_gb) ? 8 : 1; break; case GBMemoryEntireSpace: break; @@ -1566,8 +1954,11 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) bank %= n_banks; [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]]; - [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:bank]; - [hex_controller reloadData]; + [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:bank]; + _statusRep.bankForDescription = bank; + dispatch_async(dispatch_get_main_queue(), ^{ + [_hexController reloadData]; + }); }]; if (error && !ignore_errors) { @@ -1584,40 +1975,46 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)hexUpdateSpace:(NSPopUpButtonCell *)sender { self.memoryBankItem.enabled = [sender indexOfSelectedItem] != GBMemoryEntireSpace; - GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(hex_controller.byteArray); + [_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(0, 0)]]]; + GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(_hexController.byteArray); [byteArray setMode:(GB_memory_mode_t)[sender indexOfSelectedItem]]; - uint16_t bank; + uint16_t bank = -1; switch ((GB_memory_mode_t)[sender indexOfSelectedItem]) { case GBMemoryEntireSpace: + _statusRep.baseAddress = _lineRep.valueOffset = 0; + break; case GBMemoryROM: - lineRep.valueOffset = 0; - GB_get_direct_access(&gb, GB_DIRECT_ACCESS_ROM, NULL, &bank); - byteArray.selectedBank = bank; + _statusRep.baseAddress = _lineRep.valueOffset = 0; + GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, &bank); break; case GBMemoryVRAM: - lineRep.valueOffset = 0x8000; - GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, &bank); - byteArray.selectedBank = bank; + _statusRep.baseAddress = _lineRep.valueOffset = 0x8000; + GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_VRAM, NULL, &bank); break; case GBMemoryExternalRAM: - lineRep.valueOffset = 0xA000; - GB_get_direct_access(&gb, GB_DIRECT_ACCESS_CART_RAM, NULL, &bank); - byteArray.selectedBank = bank; + _statusRep.baseAddress = _lineRep.valueOffset = 0xA000; + GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, NULL, &bank); break; case GBMemoryRAM: - lineRep.valueOffset = 0xC000; - GB_get_direct_access(&gb, GB_DIRECT_ACCESS_RAM, NULL, &bank); - byteArray.selectedBank = bank; + _statusRep.baseAddress = _lineRep.valueOffset = 0xC000; + GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_RAM, NULL, &bank); break; } - [self.memoryBankInput setStringValue:[NSString stringWithFormat:@"$%x", byteArray.selectedBank]]; - [hex_controller reloadData]; - [self.memoryView setNeedsDisplay:true]; + byteArray.selectedBank = bank; + _statusRep.bankForDescription = bank; + if (bank != (uint16_t)-1) { + [self.memoryBankInput setStringValue:[NSString stringWithFormat:@"$%x", byteArray.selectedBank]]; + } + + [_hexController reloadData]; + for (NSView *view in self.memoryView.subviews) { + [view setNeedsDisplay:true]; + } } - (GB_gameboy_t *) gameboy { - return &gb; + return &_gb; } + (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName @@ -1629,7 +2026,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @try { - if (!cameraSession) { + if (!_cameraSession) { if (@available(macOS 10.14, *)) { switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) { case AVAuthorizationStatusAuthorized: @@ -1642,7 +2039,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) } case AVAuthorizationStatusDenied: case AVAuthorizationStatusRestricted: - GB_camera_updated(&gb); + GB_camera_updated(&_gb); return; } } @@ -1650,63 +2047,65 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) NSError *error; AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error]; - CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions([[[device formats] firstObject] formatDescription]); + CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions([[[device formats] lastObject] formatDescription]); if (!input) { - GB_camera_updated(&gb); + GB_camera_updated(&_gb); return; } + + double ratio = MAX(130.0 / dimensions.width, 114.0 / dimensions.height); - cameraOutput = [[AVCaptureStillImageOutput alloc] init]; + _cameraOutput = [[AVCaptureStillImageOutput alloc] init]; /* Greyscale is not widely supported, so we use YUV, whose first element is the brightness. */ - [cameraOutput setOutputSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kYUVSPixelFormat), - (id)kCVPixelBufferWidthKey: @(MAX(128, 112 * dimensions.width / dimensions.height)), - (id)kCVPixelBufferHeightKey: @(MAX(112, 128 * dimensions.height / dimensions.width)),}]; + [_cameraOutput setOutputSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kYUVSPixelFormat), + (id)kCVPixelBufferWidthKey: @(round(dimensions.width * ratio)), + (id)kCVPixelBufferHeightKey: @(round(dimensions.height * ratio)),}]; - cameraSession = [AVCaptureSession new]; - cameraSession.sessionPreset = AVCaptureSessionPresetPhoto; + _cameraSession = [AVCaptureSession new]; + _cameraSession.sessionPreset = AVCaptureSessionPresetPhoto; - [cameraSession addInput: input]; - [cameraSession addOutput: cameraOutput]; - [cameraSession startRunning]; - cameraConnection = [cameraOutput connectionWithMediaType: AVMediaTypeVideo]; + [_cameraSession addInput: input]; + [_cameraSession addOutput: _cameraOutput]; + [_cameraSession startRunning]; + _cameraConnection = [_cameraOutput connectionWithMediaType: AVMediaTypeVideo]; } - [cameraOutput captureStillImageAsynchronouslyFromConnection: cameraConnection completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError *error) { + [_cameraOutput captureStillImageAsynchronouslyFromConnection: _cameraConnection completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError *error) { if (error) { - GB_camera_updated(&gb); + GB_camera_updated(&_gb); } else { - if (cameraImage) { - CVBufferRelease(cameraImage); - cameraImage = NULL; + if (_cameraImage) { + CVBufferRelease(_cameraImage); + _cameraImage = NULL; } - cameraImage = CVBufferRetain(CMSampleBufferGetImageBuffer(sampleBuffer)); + _cameraImage = CVBufferRetain(CMSampleBufferGetImageBuffer(sampleBuffer)); /* We only need the actual buffer, no need to ever unlock it. */ - CVPixelBufferLockBaseAddress(cameraImage, 0); + CVPixelBufferLockBaseAddress(_cameraImage, 0); } - GB_camera_updated(&gb); + GB_camera_updated(&_gb); }]; } @catch (NSException *exception) { /* I have not tested camera support on many devices, so we catch exceptions just in case. */ - GB_camera_updated(&gb); + GB_camera_updated(&_gb); } }); } -- (uint8_t)cameraGetPixelAtX:(uint8_t)x andY:(uint8_t) y +- (uint8_t)cameraGetPixelAtX:(unsigned)x andY:(unsigned)y { - if (!cameraImage) { + if (!_cameraImage) { return 0; } - uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(cameraImage); - size_t bytesPerRow = CVPixelBufferGetBytesPerRow(cameraImage); - uint8_t offsetX = (CVPixelBufferGetWidth(cameraImage) - 128) / 2; - uint8_t offsetY = (CVPixelBufferGetHeight(cameraImage) - 112) / 2; + uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(_cameraImage); + size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_cameraImage); + unsigned offsetX = (CVPixelBufferGetWidth(_cameraImage) - 128) / 2; + unsigned offsetY = (CVPixelBufferGetHeight(_cameraImage) - 112) / 2; uint8_t ret = baseAddress[(x + offsetX) * 2 + (y + offsetY) * bytesPerRow]; return ret; @@ -1756,14 +2155,14 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) window_rect.origin.y += window_rect.size.height; switch ([sender selectedSegment]) { case 0: + case 2: window_rect.size.height = 384 + height_diff + 48; break; case 1: - case 2: window_rect.size.height = 512 + height_diff + 48; break; case 3: - window_rect.size.height = 20 * 16 + height_diff + 34; + window_rect.size.height = 24 * 16 + height_diff; break; default: @@ -1791,15 +2190,15 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) uint16_t map_base = 0x1800; GB_map_type_t map_type = (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem; GB_tileset_type_t tileset_type = (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem; - uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC]; - uint8_t *vram = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL); + uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC]; + uint8_t *vram = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL); - if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & 0x08)) { - map_base = 0x1c00; + if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & GB_LCDC_BG_MAP)) { + map_base = 0x1C00; } if (tileset_type == GB_TILESET_AUTO) { - tileset_type = (lcdc & 0x10)? GB_TILESET_8800 : GB_TILESET_8000; + tileset_type = (lcdc & GB_LCDC_TILE_SEL)? GB_TILESET_8800 : GB_TILESET_8000; } uint8_t tile = vram[map_base + map_offset]; @@ -1811,7 +2210,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) tile_address = 0x9000 + (int8_t)tile * 0x10; } - if (GB_is_cgb(&gb)) { + if (GB_is_cgb(&_gb)) { uint8_t attributes = vram[map_base + map_offset + 0x2000]; self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)", tile, @@ -1836,79 +2235,9 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) } } -- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +- (GB_oam_info_t *)oamInfo { - if (tableView == self.paletteTableView) { - return 16; /* 8 BG palettes, 8 OBJ palettes*/ - } - else if (tableView == self.objectsTableView) { - return oamCount; - } - return 0; -} - -- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row -{ - NSUInteger columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; - if (tableView == self.paletteTableView) { - if (columnIndex == 0) { - return [NSString stringWithFormat:@"%s %u", row >= 8 ? "Object" : "Background", (unsigned)(row & 7)]; - } - - uint8_t *palette_data = GB_get_direct_access(&gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, NULL, NULL); - - uint16_t index = columnIndex - 1 + (row & 7) * 4; - return @((palette_data[(index << 1) + 1] << 8) | palette_data[(index << 1)]); - } - else if (tableView == self.objectsTableView) { - switch (columnIndex) { - case 0: - return [Document imageFromData:[NSData dataWithBytesNoCopy:oamInfo[row].image - length:64 * 4 * 2 - freeWhenDone:false] - width:8 - height:oamHeight - scale:16.0/oamHeight]; - case 1: - return @((unsigned)oamInfo[row].x - 8); - case 2: - return @((unsigned)oamInfo[row].y - 16); - case 3: - return [NSString stringWithFormat:@"$%02x", oamInfo[row].tile]; - case 4: - return [NSString stringWithFormat:@"$%04x", 0x8000 + oamInfo[row].tile * 0x10]; - case 5: - return [NSString stringWithFormat:@"$%04x", oamInfo[row].oam_addr]; - case 6: - if (GB_is_cgb(&gb)) { - return [NSString stringWithFormat:@"%c%c%c%d%d", - oamInfo[row].flags & 0x80? 'P' : '-', - oamInfo[row].flags & 0x40? 'Y' : '-', - oamInfo[row].flags & 0x20? 'X' : '-', - oamInfo[row].flags & 0x08? 1 : 0, - oamInfo[row].flags & 0x07]; - } - return [NSString stringWithFormat:@"%c%c%c%d", - oamInfo[row].flags & 0x80? 'P' : '-', - oamInfo[row].flags & 0x40? 'Y' : '-', - oamInfo[row].flags & 0x20? 'X' : '-', - oamInfo[row].flags & 0x10? 1 : 0]; - case 7: - return oamInfo[row].obscured_by_line_limit? @"Dropped: Too many objects in line": @""; - - } - } - return nil; -} - -- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row -{ - return tableView == self.objectsTableView; -} - -- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row -{ - return false; + return _oamInfo; } - (IBAction)showVRAMViewer:(id)sender @@ -1917,33 +2246,43 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [self reloadVRAMData: nil]; } -- (void) printImage:(uint32_t *)imageBytes height:(unsigned) height - topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin - exposure:(unsigned) exposure +- (void)printImage:(uint32_t *)imageBytes height:(unsigned) height + topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin + exposure:(unsigned) exposure { uint32_t paddedImage[160 * (topMargin + height + bottomMargin)]; memset(paddedImage, 0xFF, sizeof(paddedImage)); memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0])); if (!self.printerFeedWindow.isVisible) { - currentPrinterImageData = [[NSMutableData alloc] init]; + _currentPrinterImageData = [[NSMutableData alloc] init]; } - [currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)]; + [_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)]; /* UI related code must run on main thread. */ dispatch_async(dispatch_get_main_queue(), ^{ - self.feedImageView.image = [Document imageFromData:currentPrinterImageData + [_printerSpinner startAnimation:nil]; + self.feedImageView.image = [Document imageFromData:_currentPrinterImageData width:160 - height:currentPrinterImageData.length / 160 / sizeof(imageBytes[0]) + height:_currentPrinterImageData.length / 160 / sizeof(imageBytes[0]) scale:2.0]; NSRect frame = self.printerFeedWindow.frame; + double oldHeight = frame.size.height; frame.size = self.feedImageView.image.size; [self.printerFeedWindow setContentMaxSize:frame.size]; frame.size.height += self.printerFeedWindow.frame.size.height - self.printerFeedWindow.contentView.frame.size.height; + frame.origin.y -= frame.size.height - oldHeight; [self.printerFeedWindow setFrame:frame display:false animate: self.printerFeedWindow.isVisible]; [self.printerFeedWindow orderFront:NULL]; }); } +- (void)printDone +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [_printerSpinner stopAnimation:nil]; + }); +} + - (void)printDocument:(id)sender { if (self.feedImageView.image.size.height == 0) { @@ -1956,7 +2295,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)savePrinterFeed:(id)sender { - bool shouldResume = running; + bool shouldResume = _running; [self stop]; NSSavePanel *savePanel = [NSSavePanel savePanel]; [savePanel setAllowedFileTypes:@[@"png"]]; @@ -1982,8 +2321,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) { [self disconnectLinkCable]; [self performAtomicBlock:^{ - accessory = GBAccessoryNone; - GB_disconnect_serial(&gb); + GB_disconnect_serial(&_gb); }]; } @@ -1991,8 +2329,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) { [self disconnectLinkCable]; [self performAtomicBlock:^{ - accessory = GBAccessoryPrinter; - GB_connect_printer(&gb, printImage); + GB_connect_printer(&_gb, printImage, printDone); }]; } @@ -2000,97 +2337,16 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) { [self disconnectLinkCable]; [self performAtomicBlock:^{ - accessory = GBAccessoryWorkboy; - GB_connect_workboy(&gb, setWorkboyTime, getWorkboyTime); + GB_connect_workboy(&_gb, setWorkboyTime, getWorkboyTime); }]; } -- (void) updateVolume -{ - _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; -} - -- (void) updateHighpassFilter -{ - if (GB_is_inited(&gb)) { - GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]); - } -} - -- (void) updateColorCorrectionMode -{ - if (GB_is_inited(&gb)) { - GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]); - } -} - -- (void) updateLightTemperature -{ - if (GB_is_inited(&gb)) { - GB_set_light_temperature(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]); - } -} - -- (void) updateInterferenceVolume -{ - if (GB_is_inited(&gb)) { - GB_set_interference_volume(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]); - } -} - -- (void) updateFrameBlendingMode -{ - self.view.frameBlendingMode = (GB_frame_blending_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; -} - -- (void) updateRewindLength -{ - [self performAtomicBlock:^{ - if (GB_is_inited(&gb)) { - GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]); - } - }]; -} - -- (void) updateRTCMode -{ - if (GB_is_inited(&gb)) { - GB_set_rtc_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]); - } -} - -- (void)dmgModelChanged -{ - modelsChanging = true; - if (current_model == MODEL_DMG) { - [self reset:nil]; - } - modelsChanging = false; -} - -- (void)sgbModelChanged -{ - modelsChanging = true; - if (current_model == MODEL_SGB) { - [self reset:nil]; - } - modelsChanging = false; -} - -- (void)cgbModelChanged -{ - modelsChanging = true; - if (current_model == MODEL_CGB) { - [self reset:nil]; - } - modelsChanging = false; -} - - (void)setFileURL:(NSURL *)fileURL { [super setFileURL:fileURL]; self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[fileURL path] lastPathComponent]]; - + self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [[fileURL path] lastPathComponent]]; + self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [[fileURL path] lastPathComponent]]; } - (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview; @@ -2125,11 +2381,6 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) if ([[[splitview arrangedSubviews] firstObject] frame].size.width < 600) { [splitview setPosition:600 ofDividerAtIndex:0]; } - /* NSSplitView renders its separator without the proper vibrancy, so we made it transparent and move an - NSBox-based separator that renders properly so it acts like the split view's separator. */ - NSRect rect = self.debuggerVerticalLine.frame; - rect.origin.x = [[[splitview arrangedSubviews] firstObject] frame].size.width - 1; - self.debuggerVerticalLine.frame = rect; } - (IBAction)showCheats:(id)sender @@ -2139,27 +2390,26 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)toggleCheats:(id)sender { - GB_set_cheats_enabled(&gb, !GB_cheats_enabled(&gb)); + GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb)); } - (void)disconnectLinkCable { - bool wasRunning = self->running; - Document *partner = master ?: slave; + bool wasRunning = self->_running; + Document *partner = _master ?: _slave; if (partner) { + wasRunning |= partner->_running; [self stop]; - partner->master = nil; - partner->slave = nil; - master = nil; - slave = nil; + partner->_master = nil; + partner->_slave = nil; + _master = nil; + _slave = nil; if (wasRunning) { [partner start]; [self start]; } - GB_set_turbo_mode(&gb, false, false); - GB_set_turbo_mode(&partner->gb, false, false); - partner->accessory = GBAccessoryNone; - accessory = GBAccessoryNone; + GB_set_turbo_mode(&_gb, false, false); + GB_set_turbo_mode(&partner->_gb, false, false); } } @@ -2169,56 +2419,54 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) Document *partner = [sender representedObject]; [partner disconnectAllAccessories:sender]; - bool wasRunning = self->running; + bool wasRunning = self->_running; [self stop]; [partner stop]; - GB_set_turbo_mode(&partner->gb, true, true); - slave = partner; - partner->master = self; - linkOffset = 0; - partner->accessory = GBAccessoryLinkCable; - accessory = GBAccessoryLinkCable; - GB_set_serial_transfer_bit_start_callback(&gb, linkCableBitStart); - GB_set_serial_transfer_bit_start_callback(&partner->gb, linkCableBitStart); - GB_set_serial_transfer_bit_end_callback(&gb, linkCableBitEnd); - GB_set_serial_transfer_bit_end_callback(&partner->gb, linkCableBitEnd); + GB_set_turbo_mode(&partner->_gb, true, true); + _slave = partner; + partner->_master = self; + _linkOffset = 0; + GB_set_serial_transfer_bit_start_callback(&_gb, _linkCableBitStart); + GB_set_serial_transfer_bit_start_callback(&partner->_gb, _linkCableBitStart); + GB_set_serial_transfer_bit_end_callback(&_gb, _linkCableBitEnd); + GB_set_serial_transfer_bit_end_callback(&partner->_gb, _linkCableBitEnd); if (wasRunning) { [self start]; } } -- (void)linkCableBitStart:(bool)bit +- (void)_linkCableBitStart:(bool)bit { - linkCableBit = bit; + _linkCableBit = bit; } --(bool)linkCableBitEnd +-(bool)_linkCableBitEnd { - bool ret = GB_serial_get_data_bit(&self.partner->gb); - GB_serial_set_data_bit(&self.partner->gb, linkCableBit); + bool ret = GB_serial_get_data_bit(&self.partner->_gb); + GB_serial_set_data_bit(&self.partner->_gb, _linkCableBit); return ret; } - (void)infraredStateChanged:(bool)state { if (self.partner) { - GB_set_infrared_input(&self.partner->gb, state); + GB_set_infrared_input(&self.partner->_gb, state); } } -(Document *)partner { - return slave ?: master; + return _slave ?: _master; } - (bool)isSlave { - return master; + return _master; } - (GB_gameboy_t *)gb { - return &gb; + return &_gb; } - (NSImage *)takeScreenshot @@ -2226,19 +2474,13 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) NSImage *ret = nil; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]) { ret = [_view renderToImage]; - [ret lockFocus]; - NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0, - ret.size.width, ret.size.height)]; - [ret unlockFocus]; - ret = [[NSImage alloc] initWithSize:ret.size]; - [ret addRepresentation:bitmapRep]; } if (!ret) { ret = [Document imageFromData:[NSData dataWithBytesNoCopy:_view.currentBuffer - length:GB_get_screen_width(&gb) * GB_get_screen_height(&gb) * 4 + length:GB_get_screen_width(&_gb) * GB_get_screen_height(&_gb) * 4 freeWhenDone:false] - width:GB_get_screen_width(&gb) - height:GB_get_screen_height(&gb) + width:GB_get_screen_width(&_gb) + height:GB_get_screen_height(&_gb) scale:1.0]; } return ret; @@ -2264,7 +2506,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [[NSFileManager defaultManager] fileExistsAtPath:folder isDirectory:&isDirectory]; } if (!folder) { - bool shouldResume = running; + bool shouldResume = _running; [self stop]; NSOpenPanel *openPanel = [NSOpenPanel openPanel]; openPanel.canChooseFiles = false; @@ -2302,7 +2544,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)saveScreenshotAs:(id)sender { - bool shouldResume = running; + bool shouldResume = _running; [self stop]; NSImage *image = [self takeScreenshot]; NSSavePanel *savePanel = [NSSavePanel savePanel]; @@ -2333,12 +2575,202 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)toggleDisplayBackground:(id)sender { - GB_set_background_rendering_disabled(&gb, !GB_is_background_rendering_disabled(&gb)); + GB_set_background_rendering_disabled(&_gb, !GB_is_background_rendering_disabled(&_gb)); } - (IBAction)toggleDisplayObjects:(id)sender { - GB_set_object_rendering_disabled(&gb, !GB_is_object_rendering_disabled(&gb)); + GB_set_object_rendering_disabled(&_gb, !GB_is_object_rendering_disabled(&_gb)); +} + +- (IBAction)newCartridgeInstance:(id)sender +{ + bool shouldResume = _running; + [self stop]; + NSSavePanel *savePanel = [NSSavePanel savePanel]; + [savePanel setAllowedFileTypes:@[@"gbcart"]]; + [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) { + if (result == NSModalResponseOK) { + [savePanel orderOut:self]; + NSString *romPath = self.romPath; + [[NSFileManager defaultManager] trashItemAtURL:savePanel.URL resultingItemURL:nil error:nil]; + [[NSFileManager defaultManager] createDirectoryAtURL:savePanel.URL withIntermediateDirectories:false attributes:nil error:nil]; + [[NSString stringWithFormat:@"%@\n%@\n%@", + [romPath pathRelativeToDirectory:savePanel.URL.path], + romPath, + [[NSURL fileURLWithPath:romPath].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")] + ] writeToURL:[savePanel.URL URLByAppendingPathComponent:@"rom.gbl"] atomically:false encoding:NSUTF8StringEncoding error:nil]; + [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:savePanel.URL display:true completionHandler:nil]; + } + if (shouldResume) { + [self start]; + } + }]; +} + +- (IBAction)toggleAudioRecording:(id)sender +{ + + bool shouldResume = _running; + [self stop]; + if (_isRecordingAudio) { + _isRecordingAudio = false; + int error = GB_stop_audio_recording(&_gb); + if (error) { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithFormat:@"Could not finalize recording: %s", strerror(error)]]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert runModal]; + } + else { + [self.osdView displayText:@"Audio recording ended"]; + } + if (shouldResume) { + [self start]; + } + return; + } + _audioSavePanel = [NSSavePanel savePanel]; + if (!self.audioRecordingAccessoryView) { + [[NSBundle mainBundle] loadNibNamed:@"AudioRecordingAccessoryView" owner:self topLevelObjects:nil]; + } + _audioSavePanel.accessoryView = self.audioRecordingAccessoryView; + [self audioFormatChanged:self.audioFormatButton]; + + [_audioSavePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) { + if (result == NSModalResponseOK) { + [_audioSavePanel orderOut:self]; + int error = GB_start_audio_recording(&_gb, _audioSavePanel.URL.fileSystemRepresentation, self.audioFormatButton.selectedTag); + if (error) { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithFormat:@"Could not start recording: %s", strerror(error)]]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert runModal]; + } + else { + [self.osdView displayText:@"Audio recording started"]; + _isRecordingAudio = true; + } + } + if (shouldResume) { + [self start]; + } + _audioSavePanel = nil; + }]; +} + +- (IBAction)audioFormatChanged:(NSPopUpButton *)sender +{ + switch ((GB_audio_format_t)sender.selectedTag) { + case GB_AUDIO_FORMAT_RAW: + _audioSavePanel.allowedFileTypes = @[@"raw", @"pcm"]; + break; + case GB_AUDIO_FORMAT_AIFF: + _audioSavePanel.allowedFileTypes = @[@"aiff", @"aif", @"aifc"]; + break; + case GB_AUDIO_FORMAT_WAV: + _audioSavePanel.allowedFileTypes = @[@"wav"]; + break; + } +} + +- (IBAction)toggleAudioChannel:(NSMenuItem *)sender +{ + GB_set_channel_muted(&_gb, sender.tag, !GB_is_channel_muted(&_gb, sender.tag)); +} + +- (IBAction)cartSwap:(id)sender +{ + bool wasRunning = _running; + if (wasRunning) { + [self stop]; + } + [[NSDocumentController sharedDocumentController] beginOpenPanelWithCompletionHandler:^(NSArray *urls) { + if (urls.count == 1) { + bool ok = true; + for (Document *document in [NSDocumentController sharedDocumentController].documents) { + if (document == self) continue; + if ([document.fileURL isEqual:urls.firstObject]) { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:[NSString stringWithFormat:@"‘%@’ is already open in another window. Close ‘%@’ before hot swapping it into this instance.", + urls.firstObject.lastPathComponent, urls.firstObject.lastPathComponent]]; + [alert setAlertStyle:NSAlertStyleCritical]; + [alert runModal]; + ok = false; + break; + } + } + if (ok) { + GB_save_battery(&_gb, self.savPath.UTF8String); + self.fileURL = urls.firstObject; + [self loadROM]; + } + } + if (wasRunning) { + [self start]; + } + }]; +} + +- (IBAction)reloadROM:(id)sender +{ + bool wasRunning = _running; + if (wasRunning) { + [self stop]; + } + + [self loadROM]; + + if (wasRunning) { + [self start]; + } +} + +- (void)updateDebuggerButtons +{ + bool updateContinue = false; + if (@available(macOS 10.10, *)) { + if ([self.consoleInput.placeholderAttributedString.string isEqualToString:self.debuggerContinueButton.alternateTitle]) { + [self.debuggerContinueButton mouseExited:nil]; + updateContinue = true; + } + } + if (self.isPaused) { + self.debuggerContinueButton.toolTip = self.debuggerContinueButton.title = @"Continue"; + self.debuggerContinueButton.alternateTitle = @"continue"; + self.debuggerContinueButton.imagePosition = NSImageOnly; + if (@available(macOS 10.14, *)) { + self.debuggerContinueButton.contentTintColor = nil; + } + self.debuggerContinueButton.image = [NSImage imageNamed:@"ContinueTemplate"]; + + self.debuggerNextButton.enabled = true; + self.debuggerStepButton.enabled = true; + self.debuggerFinishButton.enabled = true; + self.debuggerBackstepButton.enabled = true; + } + else { + self.debuggerContinueButton.toolTip = self.debuggerContinueButton.title = @"Interrupt"; + self.debuggerContinueButton.alternateTitle = @"interrupt"; + self.debuggerContinueButton.imagePosition = NSImageOnly; + if (@available(macOS 10.14, *)) { + self.debuggerContinueButton.contentTintColor = [NSColor controlAccentColor]; + } + self.debuggerContinueButton.image = [NSImage imageNamed:@"InterruptTemplate"]; + + self.debuggerNextButton.enabled = false; + self.debuggerStepButton.enabled = false; + self.debuggerFinishButton.enabled = false; + self.debuggerBackstepButton.enabled = false; + } + if (updateContinue) { + [self.debuggerContinueButton mouseEntered:nil]; + } +} + +- (IBAction)debuggerButtonPressed:(NSButton *)sender +{ + [self queueDebuggerCommand:sender.alternateTitle]; } @end diff --git a/Cocoa/Document.xib b/Cocoa/Document.xib index 4c98007..dd5178f 100644 --- a/Cocoa/Document.xib +++ b/Cocoa/Document.xib @@ -1,8 +1,9 @@ - + - + + @@ -14,21 +15,29 @@ + + + + + + + - + - + + @@ -43,11 +52,11 @@ - + - + @@ -77,60 +86,41 @@ - + - + - + - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - + - + - + - + - - - + + + - - + + - - + + @@ -138,31 +128,126 @@ - - + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + - - + + - + - + - + - + @@ -186,22 +271,22 @@ - + - + - - - + + + - - + + - + @@ -216,7 +301,7 @@ - + @@ -235,11 +320,11 @@ - + - + @@ -318,11 +403,11 @@ - + - + @@ -341,17 +426,17 @@ - + - - + + - + @@ -360,7 +445,7 @@ - + @@ -392,7 +477,7 @@ - + @@ -431,7 +516,7 @@ - + @@ -464,7 +549,7 @@ - + @@ -482,7 +567,7 @@ - + @@ -504,250 +589,45 @@ - + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -792,7 +672,7 @@ - + @@ -808,18 +688,33 @@ + + - + + + + + + + + + + + + + + + - - + @@ -827,7 +722,7 @@ - + @@ -840,7 +735,7 @@ - + @@ -909,7 +804,7 @@ - - - - - - - - - - - - - - + - - + + @@ -114,6 +91,29 @@ + + + + + + + + + + + + + diff --git a/Cocoa/GBS11.xib b/Cocoa/GBS11.xib new file mode 100755 index 0000000..b7a69fd --- /dev/null +++ b/Cocoa/GBS11.xib @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Cocoa/GBSplitView.m b/Cocoa/GBSplitView.m index d24d580..ca51068 100644 --- a/Cocoa/GBSplitView.m +++ b/Cocoa/GBSplitView.m @@ -19,6 +19,12 @@ return [super dividerColor]; } +- (void)drawDividerInRect:(NSRect)rect +{ + [self.dividerColor set]; + NSRectFill(rect); +} + /* Mavericks comaptibility */ - (NSArray *)arrangedSubviews { diff --git a/Cocoa/GBTerminalTextFieldCell.h b/Cocoa/GBTerminalTextFieldCell.h index b760336..b1a3171 100644 --- a/Cocoa/GBTerminalTextFieldCell.h +++ b/Cocoa/GBTerminalTextFieldCell.h @@ -1,5 +1,5 @@ #import -#include +#import @interface GBTerminalTextFieldCell : NSTextFieldCell @property (nonatomic) GB_gameboy_t *gb; diff --git a/Cocoa/GBTerminalTextFieldCell.m b/Cocoa/GBTerminalTextFieldCell.m index e1ba957..a6f76a4 100644 --- a/Cocoa/GBTerminalTextFieldCell.m +++ b/Cocoa/GBTerminalTextFieldCell.m @@ -1,7 +1,11 @@ #import #import "GBTerminalTextFieldCell.h" +#import "NSTextFieldCell+Inset.h" @interface GBTerminalTextView : NSTextView +{ + @public __weak NSTextField *_field; +} @property GB_gameboy_t *gb; @end @@ -10,7 +14,7 @@ GBTerminalTextView *field_editor; } -- (NSTextView *)fieldEditorForView:(NSView *)controlView +- (NSTextView *)fieldEditorForView:(NSTextField *)controlView { if (field_editor) { field_editor.gb = self.gb; @@ -19,6 +23,10 @@ field_editor = [[GBTerminalTextView alloc] init]; [field_editor setFieldEditor:true]; field_editor.gb = self.gb; + field_editor->_field = (NSTextField *)controlView; + ((NSTextFieldCell *)controlView.cell).textInset = + field_editor.textContainerInset = + NSMakeSize(0, 2); return field_editor; } @@ -185,14 +193,21 @@ return [super resignFirstResponder]; } --(void)drawRect:(NSRect)dirtyRect +- (NSColor *)backgroundColor +{ + return nil; +} + +- (void)drawRect:(NSRect)dirtyRect { - [super drawRect:dirtyRect]; if (reverse_search_mode && [super string].length == 0) { NSMutableDictionary *attributes = [self.typingAttributes mutableCopy]; NSColor *color = [attributes[NSForegroundColorAttributeName] colorWithAlphaComponent:0.5]; [attributes setObject:color forKey:NSForegroundColorAttributeName]; - [[[NSAttributedString alloc] initWithString:@"Reverse search..." attributes:attributes] drawAtPoint:NSMakePoint(2, 0)]; + [[[NSAttributedString alloc] initWithString:@"Reverse search..." attributes:attributes] drawAtPoint:NSMakePoint(2, 2)]; + } + else { + [super drawRect:dirtyRect]; } } diff --git a/Cocoa/GBTintedImageCell.h b/Cocoa/GBTintedImageCell.h new file mode 100644 index 0000000..eb6c8b3 --- /dev/null +++ b/Cocoa/GBTintedImageCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBTintedImageCell : NSImageCell +@property NSColor *tint; +@end diff --git a/Cocoa/GBTintedImageCell.m b/Cocoa/GBTintedImageCell.m new file mode 100644 index 0000000..af4faa6 --- /dev/null +++ b/Cocoa/GBTintedImageCell.m @@ -0,0 +1,20 @@ +#import "GBTintedImageCell.h" + +@implementation GBTintedImageCell + +- (NSImage *)image +{ + if (!self.tint || !super.image.isTemplate) { + return [super image]; + } + + NSImage *tinted = [super.image copy]; + [tinted lockFocus]; + [self.tint set]; + NSRectFillUsingOperation((NSRect){.size = tinted.size}, NSCompositeSourceIn); + [tinted unlockFocus]; + tinted.template = false; + return tinted; +} + +@end diff --git a/Cocoa/GBView.h b/Cocoa/GBView.h index 01481a7..eff3268 100644 --- a/Cocoa/GBView.h +++ b/Cocoa/GBView.h @@ -1,31 +1,16 @@ #import -#include #import #import "GBOSDView.h" +#import "GBViewBase.h" + + @class Document; -typedef enum { - GB_FRAME_BLENDING_MODE_DISABLED, - GB_FRAME_BLENDING_MODE_SIMPLE, - GB_FRAME_BLENDING_MODE_ACCURATE, - GB_FRAME_BLENDING_MODE_ACCURATE_EVEN = GB_FRAME_BLENDING_MODE_ACCURATE, - GB_FRAME_BLENDING_MODE_ACCURATE_ODD, -} GB_frame_blending_mode_t; - -@interface GBView : NSView -- (void) flip; -- (uint32_t *) pixels; +@interface GBView : GBViewBase @property (nonatomic, weak) IBOutlet Document *document; -@property (nonatomic) GB_gameboy_t *gb; -@property (nonatomic) GB_frame_blending_mode_t frameBlendingMode; @property (nonatomic, getter=isMouseHidingEnabled) bool mouseHidingEnabled; @property (nonatomic) bool isRewinding; -@property (nonatomic, strong) NSView *internalView; @property (weak) GBOSDView *osdView; -- (void) createInternalView; -- (uint32_t *)currentBuffer; -- (uint32_t *)previousBuffer; -- (void)screenSizeChanged; -- (void)setRumble: (double)amp; - (NSImage *)renderToImage; +- (void)setRumble: (double)amp; @end diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m index 5ae9c79..14b66e1 100644 --- a/Cocoa/GBView.m +++ b/Cocoa/GBView.m @@ -5,6 +5,7 @@ #import "GBViewMetal.h" #import "GBButtons.h" #import "NSString+StringForKey.h" +#import "NSObject+DefaultsObserver.h" #import "Document.h" #define JOYSTICK_HIGH 0x4000 @@ -104,8 +105,6 @@ static const uint8_t workboy_vk_to_key[] = { @implementation GBView { - uint32_t *image_buffers[3]; - unsigned char current_buffer; bool mouse_hidden; NSTrackingArea *tracking_area; bool _mouseHidingEnabled; @@ -116,9 +115,10 @@ static const uint8_t workboy_vk_to_key[] = { bool analogClockMultiplierValid; NSEventModifierFlags previousModifiers; JOYController *lastController; - GB_frame_blending_mode_t _frameBlendingMode; bool _turbo; bool _mouseControlEnabled; + NSMutableDictionary *_controllerMapping; + unsigned _lastPlayerCount; } + (instancetype)alloc @@ -137,16 +137,17 @@ static const uint8_t workboy_vk_to_key[] = { return [super allocWithZone:zone]; } -- (void) createInternalView -{ - assert(false && "createInternalView must not be inherited"); -} - - (void) _init { [self registerForDraggedTypes:[NSArray arrayWithObjects: NSFilenamesPboardType, nil]]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ratioKeepingChanged) name:@"GBAspectChanged" object:nil]; + __unsafe_unretained GBView *weakSelf = self; + [self observeStandardDefaultsKey:@"GBAspectRatioUnkept" withBlock:^(id newValue) { + [weakSelf setFrame:weakSelf.superview.frame]; + }]; + [self observeStandardDefaultsKey:@"JoyKitDefaultControllers" withBlock:^(id newValue) { + [weakSelf reassignControllers]; + }]; tracking_area = [ [NSTrackingArea alloc] initWithRect:(NSRect){} options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseMoved owner:self @@ -158,57 +159,97 @@ static const uint8_t workboy_vk_to_key[] = { self.internalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; [JOYController registerListener:self]; _mouseControlEnabled = true; + [self reassignControllers]; +} + +- (void)controllerConnected:(JOYController *)controller +{ + [self reassignControllers]; +} + +- (void)controllerDisconnected:(JOYController *)controller +{ + [self reassignControllers]; +} + +- (unsigned)playerCount +{ + if (self.document.partner) { + return 2; + } + if (!_gb) { + return 1; + } + return GB_get_player_count(_gb); +} + +- (void)reassignControllers +{ + unsigned playerCount = self.playerCount; + /* Don't assign controlelrs if there's only one player, allow all controllers. */ + if (playerCount == 1) { + _controllerMapping = [NSMutableDictionary dictionary]; + return; + } + + if (!_controllerMapping) { + _controllerMapping = [NSMutableDictionary dictionary]; + } + + for (NSNumber *player in [_controllerMapping copy]) { + if (player.unsignedIntValue >= playerCount || !_controllerMapping[player].connected) { + [_controllerMapping removeObjectForKey:player]; + } + } + + _lastPlayerCount = playerCount; + for (unsigned i = 0; i < playerCount; i++) { + NSString *preferredJoypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"] + objectForKey:n2s(i)]; + for (JOYController *controller in [JOYController allControllers]) { + if (!controller.connected) continue; + if ([controller.uniqueID isEqual:preferredJoypad]) { + _controllerMapping[@(i)] = controller; + break; + } + } + } +} + +- (void)tryAssigningController:(JOYController *)controller +{ + unsigned playerCount = self.playerCount; + if (playerCount == 1) return; + if (_controllerMapping.count == playerCount) return; + if ([_controllerMapping.allValues containsObject:controller]) return; + for (unsigned i = 0; i < playerCount; i++) { + if (!_controllerMapping[@(i)]) { + _controllerMapping[@(i)] = controller; + return; + } + } +} + +- (NSDictionary *)controllerMapping +{ + if (_lastPlayerCount != self.playerCount) { + [self reassignControllers]; + } + + return _controllerMapping; } - (void)screenSizeChanged { - if (image_buffers[0]) free(image_buffers[0]); - if (image_buffers[1]) free(image_buffers[1]); - if (image_buffers[2]) free(image_buffers[2]); - - size_t buffer_size = sizeof(image_buffers[0][0]) * GB_get_screen_width(_gb) * GB_get_screen_height(_gb); - - image_buffers[0] = calloc(1, buffer_size); - image_buffers[1] = calloc(1, buffer_size); - image_buffers[2] = calloc(1, buffer_size); + [super screenSizeChanged]; dispatch_async(dispatch_get_main_queue(), ^{ [self setFrame:self.superview.frame]; }); } -- (void) ratioKeepingChanged -{ - [self setFrame:self.superview.frame]; -} - -- (void) setFrameBlendingMode:(GB_frame_blending_mode_t)frameBlendingMode -{ - _frameBlendingMode = frameBlendingMode; - [self setNeedsDisplay:true]; -} - - -- (GB_frame_blending_mode_t)frameBlendingMode -{ - if (_frameBlendingMode == GB_FRAME_BLENDING_MODE_ACCURATE) { - if (!_gb || GB_is_sgb(_gb)) { - return GB_FRAME_BLENDING_MODE_SIMPLE; - } - return GB_is_odd_frame(_gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN; - } - return _frameBlendingMode; -} -- (unsigned char) numberOfBuffers -{ - return _frameBlendingMode? 3 : 2; -} - - (void)dealloc { - free(image_buffers[0]); - free(image_buffers[1]); - free(image_buffers[2]); if (mouse_hidden) { mouse_hidden = false; [NSCursor unhide]; @@ -217,6 +258,7 @@ static const uint8_t workboy_vk_to_key[] = { [self setRumble:0]; [JOYController unregisterListener:self]; } + - (instancetype)initWithCoder:(NSCoder *)coder { if (!(self = [super initWithCoder:coder])) { @@ -295,18 +337,13 @@ static const uint8_t workboy_vk_to_key[] = { } if ((!analogClockMultiplierValid && clockMultiplier > 1) || _turbo || (analogClockMultiplierValid && analogClockMultiplier > 1)) { - [self.osdView displayText:@"Fast forwarding..."]; + [self.osdView displayText:@"Fast forwarding…"]; } else if ((!analogClockMultiplierValid && clockMultiplier < 1) || (analogClockMultiplierValid && analogClockMultiplier < 1)) { - [self.osdView displayText:@"Slow motion..."]; + [self.osdView displayText:@"Slow motion…"]; } - current_buffer = (current_buffer + 1) % self.numberOfBuffers; -} - -- (uint32_t *) pixels -{ - return image_buffers[(current_buffer + 1) % self.numberOfBuffers]; + [super flip]; } -(void)keyDown:(NSEvent *)theEvent @@ -474,10 +511,21 @@ static const uint8_t workboy_vk_to_key[] = { return true; } +- (bool)allowController +{ + if ([self.window isMainWindow]) return true; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAllowBackgroundControllers"]) { + if ([(Document *)[NSApplication sharedApplication].orderedDocuments.firstObject mainWindow] == self.window) { + return true; + } + } + return false; +} + - (void)controller:(JOYController *)controller movedAxis:(JOYAxis *)axis { if (!_gb) return; - if (![self.window isMainWindow]) return; + if (![self allowController]) return; NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; if (!mapping) { @@ -512,6 +560,28 @@ static const uint8_t workboy_vk_to_key[] = { if (!_gb) return; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7JoystickOverride"]) return; if (self.mouseControlsActive) return; + if (controller != lastController) return; + // When using Joy-Cons in dual-controller grip, ignore motion data from the left Joy-Con + if (controller.joyconType == JOYJoyConTypeDual) { + for (JOYController *child in [(JOYCombinedController *)controller children]) { + if (child.joyconType != JOYJoyConTypeRight && [child.axes3D containsObject:axes]) { + return; + } + } + } + + NSDictionary *controllerMapping = [self controllerMapping]; + GB_gameboy_t *effectiveGB = _gb; + + if (self.document.partner) { + if (controllerMapping[@1] == controller) { + effectiveGB = self.document.partner.gb; + } + if (controllerMapping[@0] != controller) { + return; + } + + } if (axes.usage == JOYAxes3DUsageOrientation) { for (JOYAxes3D *axes in controller.axes3D) { @@ -521,37 +591,35 @@ static const uint8_t workboy_vk_to_key[] = { } } JOYPoint3D point = axes.normalizedValue; - GB_set_accelerometer_values(_gb, point.x, point.z); + GB_set_accelerometer_values(effectiveGB, point.x, point.z); } else if (axes.usage == JOYAxes3DUsageAcceleration) { JOYPoint3D point = axes.gUnitsValue; - GB_set_accelerometer_values(_gb, point.x, point.z); + GB_set_accelerometer_values(effectiveGB, point.x, point.z); } } - (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button { if (!_gb) return; - if (![self.window isMainWindow]) return; + if (![self allowController]) return; _mouseControlEnabled = false; if (button.type == JOYButtonTypeAxes2DEmulated && [self shouldControllerUseJoystickForMotion:controller]) return; - unsigned player_count = GB_get_player_count(_gb); - if (self.document.partner) { - player_count = 2; - } + [self tryAssigningController:controller]; + + unsigned playerCount = self.playerCount; IOPMAssertionID assertionID; IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID); - for (unsigned player = 0; player < player_count; player++) { - NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"] - objectForKey:n2s(player)]; - if (player_count != 1 && // Single player, accpet inputs from all joypads - !(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads - ![preferred_joypad isEqualToString:controller.uniqueID]) { - continue; - } + + NSDictionary *controllerMapping = [self controllerMapping]; + for (unsigned player = 0; player < playerCount; player++) { + JOYController *preferredJoypad = controllerMapping[@(player)]; + if (preferredJoypad && preferredJoypad != controller) continue; // The player has a different assigned controller + if (!preferredJoypad && playerCount != 1) continue; // The player has no assigned controller in multiplayer mode, prevent controller inputs + dispatch_async(dispatch_get_main_queue(), ^{ [controller setPlayerLEDs:[controller LEDMaskForPlayer:player]]; }); @@ -562,7 +630,7 @@ static const uint8_t workboy_vk_to_key[] = { JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: button.usage; if (!mapping && usage >= JOYButtonUsageGeneric0) { - usage = (const JOYButtonUsage[]){JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX}[(usage - JOYButtonUsageGeneric0) & 3]; + usage = GB_inline_const(JOYButtonUsage[], {JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX})[(usage - JOYButtonUsageGeneric0) & 3]; } GB_gameboy_t *effectiveGB = _gb; @@ -740,16 +808,6 @@ static const uint8_t workboy_vk_to_key[] = { previousModifiers = event.modifierFlags; } -- (uint32_t *)currentBuffer -{ - return image_buffers[current_buffer]; -} - -- (uint32_t *)previousBuffer -{ - return image_buffers[(current_buffer + 2) % self.numberOfBuffers]; -} - -(NSDragOperation)draggingEntered:(id)sender { NSPasteboard *pboard = [sender draggingPasteboard]; diff --git a/Cocoa/GBViewMetal.h b/Cocoa/GBViewMetal.h deleted file mode 100644 index 521c3c7..0000000 --- a/Cocoa/GBViewMetal.h +++ /dev/null @@ -1,7 +0,0 @@ -#import -#import -#import "GBView.h" - -@interface GBViewMetal : GBView -+ (bool) isSupported; -@end diff --git a/Cocoa/GBVisualizerView.h b/Cocoa/GBVisualizerView.h index 5ee4638..f3ccc29 100644 --- a/Cocoa/GBVisualizerView.h +++ b/Cocoa/GBVisualizerView.h @@ -1,5 +1,5 @@ #import -#include +#import @interface GBVisualizerView : NSView - (void)addSample:(GB_sample_t *)sample; diff --git a/Cocoa/GBVisualizerView.m b/Cocoa/GBVisualizerView.m index 08f6024..185e1c7 100644 --- a/Cocoa/GBVisualizerView.m +++ b/Cocoa/GBVisualizerView.m @@ -1,6 +1,6 @@ #import "GBVisualizerView.h" #import "GBPaletteEditorController.h" -#include +#import #define SAMPLE_COUNT 1024 diff --git a/Cocoa/GBWarningPopover.h b/Cocoa/GBWarningPopover.h index 1d695b1..953aa3c 100644 --- a/Cocoa/GBWarningPopover.h +++ b/Cocoa/GBWarningPopover.h @@ -4,5 +4,6 @@ + (GBWarningPopover *) popoverWithContents:(NSString *)contents onView:(NSView *)view; + (GBWarningPopover *) popoverWithContents:(NSString *)contents onWindow:(NSWindow *)window; ++ (GBWarningPopover *) popoverWithContents:(NSString *)contents title:(NSString *)title onView:(NSView *)view timeout:(double)seconds preferredEdge:(NSRectEdge)preferredEdge; @end diff --git a/Cocoa/GBWarningPopover.m b/Cocoa/GBWarningPopover.m index 74f6444..b66186b 100644 --- a/Cocoa/GBWarningPopover.m +++ b/Cocoa/GBWarningPopover.m @@ -4,7 +4,7 @@ static GBWarningPopover *lastPopover; @implementation GBWarningPopover -+ (GBWarningPopover *) popoverWithContents:(NSString *)contents onView:(NSView *)view ++ (GBWarningPopover *) popoverWithContents:(NSString *)contents title:(NSString *)title onView:(NSView *)view timeout:(double)seconds preferredEdge:(NSRectEdge)preferredEdge { [lastPopover close]; lastPopover = [[self alloc] init]; @@ -13,7 +13,17 @@ static GBWarningPopover *lastPopover; [lastPopover setAnimates:true]; lastPopover.contentViewController = [[NSViewController alloc] initWithNibName:@"PopoverView" bundle:nil]; NSTextField *field = (NSTextField *)lastPopover.contentViewController.view; - [field setStringValue:contents]; + if (!title) { + [field setStringValue:contents]; + } + else { + NSMutableAttributedString *fullContents = [[NSMutableAttributedString alloc] initWithString:title + attributes:@{NSFontAttributeName: [NSFont boldSystemFontOfSize:[NSFont systemFontSize]]}]; + [fullContents appendAttributedString:[[NSAttributedString alloc] initWithString:[@"\n" stringByAppendingString:contents] + attributes:@{NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSize]]}]]; + [field setAttributedStringValue:fullContents]; + + } NSSize textSize = [field.cell cellSizeForBounds:[field.cell drawingRectForBounds:NSMakeRect(0, 0, 240, CGFLOAT_MAX)]]; textSize.width = ceil(textSize.width) + 16; textSize.height = ceil(textSize.height) + 12; @@ -25,11 +35,13 @@ static GBWarningPopover *lastPopover; [lastPopover showRelativeToRect:view.bounds ofView:view - preferredEdge:NSMinYEdge]; + preferredEdge:preferredEdge]; NSRect frame = field.frame; frame.origin.x += 8; - frame.origin.y -= 6; + frame.origin.y += 6; + frame.size.width -= 16; + frame.size.height -= 12; field.frame = frame; @@ -38,6 +50,11 @@ static GBWarningPopover *lastPopover; return lastPopover; } ++ (GBWarningPopover *) popoverWithContents:(NSString *)contents onView:(NSView *)view +{ + return [self popoverWithContents:contents title:nil onView:view timeout:3.0 preferredEdge:NSMinYEdge]; +} + + (GBWarningPopover *)popoverWithContents:(NSString *)contents onWindow:(NSWindow *)window { return [self popoverWithContents:contents onView:window.contentView.superview.subviews.lastObject]; diff --git a/Cocoa/HelpTemplate.png b/Cocoa/HelpTemplate.png new file mode 100644 index 0000000..6b12375 Binary files /dev/null and b/Cocoa/HelpTemplate.png differ diff --git a/Cocoa/HelpTemplate@2x.png b/Cocoa/HelpTemplate@2x.png new file mode 100644 index 0000000..d7f8237 Binary files /dev/null and b/Cocoa/HelpTemplate@2x.png differ diff --git a/Cocoa/HorizontalJoyConLeftTemplate.png b/Cocoa/HorizontalJoyConLeftTemplate.png new file mode 100644 index 0000000..7c4b597 Binary files /dev/null and b/Cocoa/HorizontalJoyConLeftTemplate.png differ diff --git a/Cocoa/HorizontalJoyConLeftTemplate@2x.png b/Cocoa/HorizontalJoyConLeftTemplate@2x.png new file mode 100644 index 0000000..816706d Binary files /dev/null and b/Cocoa/HorizontalJoyConLeftTemplate@2x.png differ diff --git a/Cocoa/HorizontalJoyConRightTemplate.png b/Cocoa/HorizontalJoyConRightTemplate.png new file mode 100644 index 0000000..866992b Binary files /dev/null and b/Cocoa/HorizontalJoyConRightTemplate.png differ diff --git a/Cocoa/HorizontalJoyConRightTemplate@2x.png b/Cocoa/HorizontalJoyConRightTemplate@2x.png new file mode 100644 index 0000000..908ba48 Binary files /dev/null and b/Cocoa/HorizontalJoyConRightTemplate@2x.png differ diff --git a/Cocoa/Icon.png b/Cocoa/Icon.png new file mode 100644 index 0000000..df0a4fd Binary files /dev/null and b/Cocoa/Icon.png differ diff --git a/Cocoa/Icon@2x.png b/Cocoa/Icon@2x.png new file mode 100644 index 0000000..a829567 Binary files /dev/null and b/Cocoa/Icon@2x.png differ diff --git a/Cocoa/Info.plist b/Cocoa/Info.plist index 5e409c9..c437485 100644 --- a/Cocoa/Info.plist +++ b/Cocoa/Info.plist @@ -22,6 +22,7 @@ LSItemContentTypes com.github.liji32.sameboy.gb + public.gbrom LSTypeIsPackage 0 @@ -88,6 +89,24 @@ NSDocumentClass Document + + CFBundleTypeExtensions + + gbcart + + CFBundleTypeIconFile + ColorCartridge + CFBundleTypeName + Game Boy Cartridge + CFBundleTypeRole + Viewer + LSItemContentTypes + + LSTypeIsPackage + 1 + NSDocumentClass + Document + CFBundleExecutable SameBoy @@ -95,6 +114,8 @@ AppIcon.icns CFBundleIdentifier com.github.liji32.sameboy + LSApplicationCategoryType + public.app-category.games CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -102,7 +123,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - Version @VERSION + @VERSION CFBundleSignature ???? CFBundleSupportedPlatforms @@ -112,11 +133,11 @@ LSMinimumSystemVersion 10.9 NSHumanReadableCopyright - Copyright © 2015-2021 Lior Halphon + Copyright © 2015-@COPYRIGHT_YEAR Lior Halphon NSMainNibFile MainMenu NSPrincipalClass - NSApplication + GBApp UTExportedTypeDeclarations @@ -197,7 +218,7 @@ NSCameraUsageDescription - SameBoy needs to access your camera to emulate the Game Boy Camera + SameBoy needs to access your device's camera to emulate the Game Boy Camera NSSupportsAutomaticGraphicsSwitching diff --git a/Cocoa/InterruptTemplate.png b/Cocoa/InterruptTemplate.png new file mode 100644 index 0000000..3530727 Binary files /dev/null and b/Cocoa/InterruptTemplate.png differ diff --git a/Cocoa/InterruptTemplate@2x.png b/Cocoa/InterruptTemplate@2x.png new file mode 100644 index 0000000..eb26243 Binary files /dev/null and b/Cocoa/InterruptTemplate@2x.png differ diff --git a/Cocoa/JoyConDualTemplate.png b/Cocoa/JoyConDualTemplate.png new file mode 100644 index 0000000..42e7a27 Binary files /dev/null and b/Cocoa/JoyConDualTemplate.png differ diff --git a/Cocoa/JoyConDualTemplate@2x.png b/Cocoa/JoyConDualTemplate@2x.png new file mode 100644 index 0000000..938fd7f Binary files /dev/null and b/Cocoa/JoyConDualTemplate@2x.png differ diff --git a/Cocoa/JoyConLeftTemplate.png b/Cocoa/JoyConLeftTemplate.png new file mode 100644 index 0000000..924c427 Binary files /dev/null and b/Cocoa/JoyConLeftTemplate.png differ diff --git a/Cocoa/JoyConLeftTemplate@2x.png b/Cocoa/JoyConLeftTemplate@2x.png new file mode 100644 index 0000000..6b2f996 Binary files /dev/null and b/Cocoa/JoyConLeftTemplate@2x.png differ diff --git a/Cocoa/JoyConRightTemplate.png b/Cocoa/JoyConRightTemplate.png new file mode 100644 index 0000000..1fccf5f Binary files /dev/null and b/Cocoa/JoyConRightTemplate.png differ diff --git a/Cocoa/JoyConRightTemplate@2x.png b/Cocoa/JoyConRightTemplate@2x.png new file mode 100644 index 0000000..d9c385c Binary files /dev/null and b/Cocoa/JoyConRightTemplate@2x.png differ diff --git a/Cocoa/Joypad.png b/Cocoa/Joypad.png index f30d8f9..7692cbf 100644 Binary files a/Cocoa/Joypad.png and b/Cocoa/Joypad.png differ diff --git a/Cocoa/Joypad@2x.png b/Cocoa/Joypad@2x.png index d91ee30..2909011 100644 Binary files a/Cocoa/Joypad@2x.png and b/Cocoa/Joypad@2x.png differ diff --git a/Cocoa/Joypad~dark.png b/Cocoa/Joypad~dark.png index 8a7687b..9ab114d 100644 Binary files a/Cocoa/Joypad~dark.png and b/Cocoa/Joypad~dark.png differ diff --git a/Cocoa/Joypad~dark@2x.png b/Cocoa/Joypad~dark@2x.png index ce2a07c..df22cfb 100644 Binary files a/Cocoa/Joypad~dark@2x.png and b/Cocoa/Joypad~dark@2x.png differ diff --git a/Cocoa/Joypad~solid.png b/Cocoa/Joypad~solid.png new file mode 100644 index 0000000..f05dca7 Binary files /dev/null and b/Cocoa/Joypad~solid.png differ diff --git a/Cocoa/Joypad~solid@2x.png b/Cocoa/Joypad~solid@2x.png new file mode 100644 index 0000000..f57e97e Binary files /dev/null and b/Cocoa/Joypad~solid@2x.png differ diff --git a/Cocoa/KeyboardShortcutPrivateAPIs.h b/Cocoa/KeyboardShortcutPrivateAPIs.h index a80dfde..8ce0b91 100644 --- a/Cocoa/KeyboardShortcutPrivateAPIs.h +++ b/Cocoa/KeyboardShortcutPrivateAPIs.h @@ -1,6 +1,3 @@ -#ifndef KeyboardShortcutPrivateAPIs_h -#define KeyboardShortcutPrivateAPIs_h - /* These are private APIs, but they are a very simple and comprehensive way to convert a key equivalent to its display name. */ @@ -22,5 +19,3 @@ @interface NSPrefPaneUtils : NSObject + (id)stringForVirtualKey:(unsigned int)key modifiers:(unsigned int)flags; @end - -#endif \ No newline at end of file diff --git a/Cocoa/License.html b/Cocoa/License.html index 9846514..b7616e7 100644 --- a/Cocoa/License.html +++ b/Cocoa/License.html @@ -29,8 +29,8 @@

SameBoy

-

MIT License

-

Copyright © 2015-2021 Lior Halphon

+

Expat License

+

Copyright © 2015-@COPYRIGHT_YEAR Lior Halphon

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Cocoa/MainMenu.xib b/Cocoa/MainMenu.xib index aafa8aa..ee78351 100644 --- a/Cocoa/MainMenu.xib +++ b/Cocoa/MainMenu.xib @@ -1,18 +1,17 @@ - + - + - + - - + @@ -27,13 +26,19 @@ - + + + + + + + - + @@ -77,6 +82,12 @@ + + + + + +

@@ -91,6 +102,12 @@ + + + + + + @@ -185,6 +202,17 @@ + + + + + + + + + + + @@ -364,9 +392,14 @@ - + - + + + + + + @@ -404,11 +437,11 @@ - + - + @@ -433,7 +466,7 @@ - + @@ -456,6 +489,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -493,6 +558,16 @@ + + + + + + + + + + @@ -513,9 +588,10 @@ - + + - + diff --git a/Cocoa/NSImageNamedDarkSupport.m b/Cocoa/NSImageNamedDarkSupport.m index 821ba3b..73e7b64 100644 --- a/Cocoa/NSImageNamedDarkSupport.m +++ b/Cocoa/NSImageNamedDarkSupport.m @@ -11,6 +11,13 @@ static NSImage * (*imageNamed)(Class self, SEL _cmd, NSString *name); + (NSImage *)imageNamedWithDark:(NSImageName)name { + if (@available(macOS 11.0, *)) { + if (![name containsString:@"~solid"]) { + NSImage *solid = [self imageNamed:[name stringByAppendingString:@"~solid"]]; + [solid setTemplate:true]; + if (solid) return solid; + } + } NSImage *light = imageNamed(self, _cmd, name); if (@available(macOS 10.14, *)) { NSImage *dark = imageNamed(self, _cmd, [name stringByAppendingString:@"~dark"]); diff --git a/Cocoa/NSObject+DefaultsObserver.h b/Cocoa/NSObject+DefaultsObserver.h new file mode 100644 index 0000000..18469f8 --- /dev/null +++ b/Cocoa/NSObject+DefaultsObserver.h @@ -0,0 +1,6 @@ +#import + +@interface NSObject (DefaultsObserver) +- (void)observeStandardDefaultsKey:(NSString *)key withBlock:(void(^)(id newValue))block; +- (void)observeStandardDefaultsKey:(NSString *)key selector:(SEL)selector; +@end diff --git a/Cocoa/NSObject+DefaultsObserver.m b/Cocoa/NSObject+DefaultsObserver.m new file mode 100644 index 0000000..8dac912 --- /dev/null +++ b/Cocoa/NSObject+DefaultsObserver.m @@ -0,0 +1,72 @@ +#import "NSObject+DefaultsObserver.h" +#import +#import + +@interface GBUserDefaultsObserverHelper : NSObject +@end + +@implementation GBUserDefaultsObserverHelper ++ (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + [[NSNotificationCenter defaultCenter] postNotificationName:[keyPath stringByAppendingString:@"Changed$DefaultsObserver"] + object:nil + userInfo:@{ + @"value": change[NSKeyValueChangeNewKey] + }]; +} + ++ (void)startObservingKey:(NSString *)key +{ + if (!NSThread.isMainThread) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [self startObservingKey:key]; + }); + return; + } + static NSMutableSet *set = nil; + if (!set) { + set = [NSMutableSet set]; + } + if ([set containsObject:key]) return; + [set addObject:key]; + [[NSUserDefaults standardUserDefaults] addObserver:(id)self + forKeyPath:key + options:NSKeyValueObservingOptionNew + context:nil]; +} +@end + +@implementation NSObject (DefaultsObserver) +- (void)observeStandardDefaultsKey:(NSString *)key selector:(SEL)selector +{ + __weak id weakSelf = self; + [self observeStandardDefaultsKey:key + withBlock:^(id newValue) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [weakSelf performSelector:selector withObject:newValue]; +#pragma clang diagnostic pop + }]; +} + +- (void)observeStandardDefaultsKey:(NSString *)key withBlock:(void(^)(id newValue))block +{ + NSString *notificationName = [key stringByAppendingString:@"Changed$DefaultsObserver"]; + objc_setAssociatedObject(self, sel_registerName(notificationName.UTF8String), block, OBJC_ASSOCIATION_RETAIN); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(standardDefaultsKeyChanged:) + name:notificationName + object:nil]; + [GBUserDefaultsObserverHelper startObservingKey:key]; + block([[NSUserDefaults standardUserDefaults] objectForKey:key]); +} + +- (void)standardDefaultsKeyChanged:(NSNotification *)notification +{ + SEL selector = sel_registerName(notification.name.UTF8String); + ((void(^)(id))objc_getAssociatedObject(self, selector))(notification.userInfo[@"value"]); +} +@end diff --git a/Cocoa/NSTextFieldCell+Inset.h b/Cocoa/NSTextFieldCell+Inset.h new file mode 100644 index 0000000..3b3cac8 --- /dev/null +++ b/Cocoa/NSTextFieldCell+Inset.h @@ -0,0 +1,6 @@ +#import +#import + +@interface NSTextFieldCell (Inset) +@property NSSize textInset; +@end diff --git a/Cocoa/NSTextFieldCell+Inset.m b/Cocoa/NSTextFieldCell+Inset.m new file mode 100644 index 0000000..acc6b3b --- /dev/null +++ b/Cocoa/NSTextFieldCell+Inset.m @@ -0,0 +1,39 @@ +#import "NSTextFieldCell+Inset.h" +#import +#import + +@interface NSTextFieldCell () +- (CGRect)_textLayerDrawingRectForCellFrame:(CGRect)rect; +@property NSSize textInset; +@end + +@implementation NSTextFieldCell (Inset) + +- (void)setTextInset:(NSSize)textInset +{ + objc_setAssociatedObject(self, @selector(textInset), @(textInset), OBJC_ASSOCIATION_RETAIN); +} + +- (NSSize)textInset +{ + return [objc_getAssociatedObject(self, _cmd) sizeValue]; +} + +- (CGRect)_textLayerDrawingRectForCellFrameHook:(CGRect)rect +{ + CGRect ret = [self _textLayerDrawingRectForCellFrameHook:rect]; + NSSize inset = self.textInset; + ret.origin.x += inset.width; + ret.origin.y += inset.height; + ret.size.width -= inset.width; + ret.size.height -= inset.height; + return ret; +} + ++ (void)load +{ + method_exchangeImplementations(class_getInstanceMethod(self, @selector(_textLayerDrawingRectForCellFrame:)), + class_getInstanceMethod(self, @selector(_textLayerDrawingRectForCellFrameHook:))); +} + +@end diff --git a/Cocoa/NSToolbarItem+NoOverflow.m b/Cocoa/NSToolbarItem+NoOverflow.m new file mode 100644 index 0000000..d5864bd --- /dev/null +++ b/Cocoa/NSToolbarItem+NoOverflow.m @@ -0,0 +1,24 @@ +#import +#import + +static id nop(id self, SEL _cmd) +{ + return nil; +} + +static double blah(id self, SEL _cmd) +{ + return 80.0; +} + +@implementation NSToolbarItem (NoOverflow) + ++ (void)load +{ + // Prevent collapsing toolbar items into menu items, they don't work in that form + method_setImplementation(class_getInstanceMethod(self, @selector(menuFormRepresentation)), (IMP)nop); + // Prevent over-agressive collapsing of the Printer Feed menu + method_setImplementation(class_getInstanceMethod(NSClassFromString(@"NSToolbarTitleView"), @selector(minSize)), (IMP)blah); +} + +@end diff --git a/Cocoa/Next.png b/Cocoa/Next.png index cd9a4c3..6776010 100644 Binary files a/Cocoa/Next.png and b/Cocoa/Next.png differ diff --git a/Cocoa/Next@2x.png b/Cocoa/Next@2x.png index 1debb1d..c6b9d3a 100644 Binary files a/Cocoa/Next@2x.png and b/Cocoa/Next@2x.png differ diff --git a/Cocoa/NextTemplate.png b/Cocoa/NextTemplate.png new file mode 100644 index 0000000..071e750 Binary files /dev/null and b/Cocoa/NextTemplate.png differ diff --git a/Cocoa/NextTemplate@2x.png b/Cocoa/NextTemplate@2x.png new file mode 100644 index 0000000..616fb2e Binary files /dev/null and b/Cocoa/NextTemplate@2x.png differ diff --git a/Cocoa/Pause.png b/Cocoa/Pause.png index 2bb380b..afb22e6 100644 Binary files a/Cocoa/Pause.png and b/Cocoa/Pause.png differ diff --git a/Cocoa/Pause@2x.png b/Cocoa/Pause@2x.png index 36b6da0..1c167f6 100644 Binary files a/Cocoa/Pause@2x.png and b/Cocoa/Pause@2x.png differ diff --git a/Cocoa/Play.png b/Cocoa/Play.png index 3f87092..65aae90 100644 Binary files a/Cocoa/Play.png and b/Cocoa/Play.png differ diff --git a/Cocoa/Play@2x.png b/Cocoa/Play@2x.png index 0de0553..a0d59ae 100644 Binary files a/Cocoa/Play@2x.png and b/Cocoa/Play@2x.png differ diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib index 706111e..3e302ca 100644 --- a/Cocoa/Preferences.xib +++ b/Cocoa/Preferences.xib @@ -1,12 +1,12 @@ - + - + - + @@ -52,7 +52,7 @@ - + @@ -67,47 +67,31 @@ - - - - - - + - - - - - - - - + + + + - - - - - - - - + - + @@ -115,56 +99,108 @@ - - + + - + + + + - - - - - - - - - - + + + + - - - - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + @@ -172,8 +208,8 @@ - - + + @@ -183,28 +219,34 @@ - - - - - + + + + + + + + - - - + + + - - + + - - - + + + + + + - + @@ -213,16 +255,16 @@ - + - + - - + + @@ -232,17 +274,17 @@ - - + +
- - - + + +
- + @@ -251,7 +293,7 @@ - + @@ -272,7 +314,7 @@ - + @@ -280,8 +322,8 @@ - - + + @@ -296,41 +338,53 @@
- - - + + +
- + - - + - + - + @@ -339,7 +393,7 @@ - + @@ -348,7 +402,7 @@ - + @@ -357,7 +411,7 @@ - + @@ -365,8 +419,8 @@ - - + + @@ -380,12 +434,12 @@
- - - + + +
- + @@ -393,8 +447,8 @@ - - + + @@ -410,12 +464,12 @@ - - - + + + - + @@ -424,29 +478,11 @@ - + - - - - - - - - - - - - - - - - - - - - + + @@ -456,16 +492,16 @@ - + - - - + + + - + @@ -488,8 +524,8 @@ - - + + @@ -508,12 +544,12 @@ - - - + + + - + @@ -521,23 +557,47 @@ - - + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -552,16 +612,19 @@ - - + + - - - + + + + + + - - + + @@ -571,17 +634,17 @@ - - + + - - - + + + - + @@ -598,23 +661,37 @@ - - + + - - - + + + + + + + - + - + - + @@ -622,24 +699,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - + @@ -658,7 +1013,7 @@ - + @@ -690,8 +1045,8 @@ - - + + @@ -699,8 +1054,8 @@ - - + + @@ -714,137 +1069,23 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - @@ -1080,7 +1321,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Text Cell +Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
- +
diff --git a/Cocoa/Previous.png b/Cocoa/Previous.png index cc91221..5b1fff4 100644 Binary files a/Cocoa/Previous.png and b/Cocoa/Previous.png differ diff --git a/Cocoa/Previous@2x.png b/Cocoa/Previous@2x.png index 77b0157..f0f7f65 100644 Binary files a/Cocoa/Previous@2x.png and b/Cocoa/Previous@2x.png differ diff --git a/Cocoa/Rewind.png b/Cocoa/Rewind.png index 999f358..3c72958 100644 Binary files a/Cocoa/Rewind.png and b/Cocoa/Rewind.png differ diff --git a/Cocoa/Rewind@2x.png b/Cocoa/Rewind@2x.png index d845b54..6cb3417 100644 Binary files a/Cocoa/Rewind@2x.png and b/Cocoa/Rewind@2x.png differ diff --git a/Cocoa/Speaker.png b/Cocoa/Speaker.png index 1f6b556..b969dbc 100644 Binary files a/Cocoa/Speaker.png and b/Cocoa/Speaker.png differ diff --git a/Cocoa/Speaker@2x.png b/Cocoa/Speaker@2x.png index 41c46ff..eef4c26 100644 Binary files a/Cocoa/Speaker@2x.png and b/Cocoa/Speaker@2x.png differ diff --git a/Cocoa/Speaker~dark.png b/Cocoa/Speaker~dark.png index f3f820a..b71c9ef 100644 Binary files a/Cocoa/Speaker~dark.png and b/Cocoa/Speaker~dark.png differ diff --git a/Cocoa/Speaker~dark@2x.png b/Cocoa/Speaker~dark@2x.png index bdc3eb7..8c004a5 100644 Binary files a/Cocoa/Speaker~dark@2x.png and b/Cocoa/Speaker~dark@2x.png differ diff --git a/Cocoa/Speaker~solid.png b/Cocoa/Speaker~solid.png new file mode 100644 index 0000000..3e9db09 Binary files /dev/null and b/Cocoa/Speaker~solid.png differ diff --git a/Cocoa/Speaker~solid@2x.png b/Cocoa/Speaker~solid@2x.png new file mode 100644 index 0000000..e728002 Binary files /dev/null and b/Cocoa/Speaker~solid@2x.png differ diff --git a/Cocoa/StepTemplate.png b/Cocoa/StepTemplate.png new file mode 100644 index 0000000..ca24d1c Binary files /dev/null and b/Cocoa/StepTemplate.png differ diff --git a/Cocoa/StepTemplate@2x.png b/Cocoa/StepTemplate@2x.png new file mode 100644 index 0000000..8d4b1af Binary files /dev/null and b/Cocoa/StepTemplate@2x.png differ diff --git a/Cocoa/UpdateWindow.xib b/Cocoa/UpdateWindow.xib index e34f8f2..0949af4 100644 --- a/Cocoa/UpdateWindow.xib +++ b/Cocoa/UpdateWindow.xib @@ -7,7 +7,7 @@ - + diff --git a/Cocoa/Updates.png b/Cocoa/Updates.png new file mode 100644 index 0000000..a27a663 Binary files /dev/null and b/Cocoa/Updates.png differ diff --git a/Cocoa/Updates@2x.png b/Cocoa/Updates@2x.png new file mode 100644 index 0000000..df0a4fd Binary files /dev/null and b/Cocoa/Updates@2x.png differ diff --git a/Cocoa/Updates~solid.png b/Cocoa/Updates~solid.png new file mode 100644 index 0000000..7d994b2 Binary files /dev/null and b/Cocoa/Updates~solid.png differ diff --git a/Cocoa/Updates~solid@2x.png b/Cocoa/Updates~solid@2x.png new file mode 100644 index 0000000..fac00bd Binary files /dev/null and b/Cocoa/Updates~solid@2x.png differ diff --git a/Cocoa/Updates~solid~dark.png b/Cocoa/Updates~solid~dark.png new file mode 100644 index 0000000..c11cdde Binary files /dev/null and b/Cocoa/Updates~solid~dark.png differ diff --git a/Cocoa/Updates~solid~dark@2x.png b/Cocoa/Updates~solid~dark@2x.png new file mode 100644 index 0000000..592603d Binary files /dev/null and b/Cocoa/Updates~solid~dark@2x.png differ diff --git a/Core/apu.c b/Core/apu.c index 77f2da9..3fb3bd2 100644 --- a/Core/apu.c +++ b/Core/apu.c @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include "gb.h" static const uint8_t duties[] = { @@ -11,7 +13,7 @@ static const uint8_t duties[] = { 0, 1, 1, 1, 1, 1, 1, 0, }; -static void refresh_channel(GB_gameboy_t *gb, unsigned index, unsigned cycles_offset) +static void refresh_channel(GB_gameboy_t *gb, GB_channel_t index, unsigned cycles_offset) { unsigned multiplier = gb->apu_output.cycles_since_render + cycles_offset - gb->apu_output.last_update[index]; gb->apu_output.summed_samples[index].left += gb->apu_output.current_sample[index].left * multiplier; @@ -19,7 +21,7 @@ static void refresh_channel(GB_gameboy_t *gb, unsigned index, unsigned cycles_of gb->apu_output.last_update[index] = gb->apu_output.cycles_since_render + cycles_offset; } -bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index) +bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, GB_channel_t index) { if (gb->model > GB_MODEL_CGB_E) { /* On the AGB, mixing is done digitally, so there are no per-channel @@ -47,7 +49,7 @@ bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index) return false; } -static uint8_t agb_bias_for_channel(GB_gameboy_t *gb, unsigned index) +static uint8_t agb_bias_for_channel(GB_gameboy_t *gb, GB_channel_t index) { if (!gb->apu.is_active[index]) return 0; @@ -63,15 +65,10 @@ static uint8_t agb_bias_for_channel(GB_gameboy_t *gb, unsigned index) nodefault; } - return 0; } -static void update_sample(GB_gameboy_t *gb, unsigned index, int8_t value, unsigned cycles_offset) +static void update_sample(GB_gameboy_t *gb, GB_channel_t index, int8_t value, unsigned cycles_offset) { - if (gb->model > GB_MODEL_CGB_E && index == GB_WAVE) { - /* For some reason, channel 3 is inverted on the AGB */ - value ^= 0xF; - } if (gb->model > GB_MODEL_CGB_E) { /* On the AGB, because no analog mixing is done, the behavior of NR51 is a bit different. @@ -82,25 +79,28 @@ static void update_sample(GB_gameboy_t *gb, unsigned index, int8_t value, unsign if (gb->apu_output.sample_rate) { unsigned right_volume = (gb->io_registers[GB_IO_NR50] & 7) + 1; unsigned left_volume = ((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1; + int8_t silence = 0; + if (index == GB_WAVE) { + /* For some reason, channel 3 is inverted on the AGB, and has a different "silence" value */ + value ^= 0xF; + silence = 7; + } - GB_sample_t output; uint8_t bias = agb_bias_for_channel(gb, index); - if (gb->io_registers[GB_IO_NR51] & (1 << index)) { - output.right = (0xf - value * 2 + bias) * right_volume; - } - else { - output.right = 0xf * right_volume; + bool left = gb->io_registers[GB_IO_NR51] & (0x10 << index); + bool right = gb->io_registers[GB_IO_NR51] & (1 << index); + + GB_sample_t output = { + .left = (0xF - (left? value : silence) * 2 + bias) * left_volume, + .right = (0xF - (right? value : silence) * 2 + bias) * right_volume + }; + + if (unlikely(gb->apu_output.channel_muted[index])) { + output.left = output.right = 0; } - if (gb->io_registers[GB_IO_NR51] & (0x10 << index)) { - output.left = (0xf - value * 2 + bias) * left_volume; - } - else { - output.left = 0xf * left_volume; - } - - if (*(uint32_t *)&(gb->apu_output.current_sample[index]) != *(uint32_t *)&output) { + if (gb->apu_output.current_sample[index].packed != output.packed) { refresh_channel(gb, index, cycles_offset); gb->apu_output.current_sample[index] = output; } @@ -127,8 +127,11 @@ static void update_sample(GB_gameboy_t *gb, unsigned index, int8_t value, unsign if (gb->io_registers[GB_IO_NR51] & (0x10 << index)) { left_volume = ((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1; } - GB_sample_t output = {(0xf - value * 2) * left_volume, (0xf - value * 2) * right_volume}; - if (*(uint32_t *)&(gb->apu_output.current_sample[index]) != *(uint32_t *)&output) { + GB_sample_t output = {0, 0}; + if (likely(!gb->apu_output.channel_muted[index])) { + output = (GB_sample_t){(0xF - value * 2) * left_volume, (0xF - value * 2) * right_volume}; + } + if (gb->apu_output.current_sample[index].packed != output.packed) { refresh_channel(gb, index, cycles_offset); gb->apu_output.current_sample[index] = output; } @@ -152,7 +155,7 @@ static signed interference(GB_gameboy_t *gb) ret -= MAX_CH_AMP / 12; } } - if (gb->io_registers[GB_IO_LCDC] & 0x80) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE) { ret += MAX_CH_AMP / 7; if ((gb->io_registers[GB_IO_STAT] & 3) == 3 && gb->model <= GB_MODEL_CGB_E) { ret += MAX_CH_AMP / 14; @@ -208,7 +211,7 @@ static void render(GB_gameboy_t *gb) } } - if (likely(gb->apu_output.last_update[i] == 0)) { + if (likely(gb->apu_output.last_update[i] == 0 || gb->apu_output.cycles_since_render == 0)) { output.left += gb->apu_output.current_sample[i].left * multiplier; output.right += gb->apu_output.current_sample[i].right * multiplier; } @@ -223,6 +226,8 @@ static void render(GB_gameboy_t *gb) gb->apu_output.last_update[i] = 0; } gb->apu_output.cycles_since_render = 0; + + if (gb->sgb && gb->sgb->intro_animation < GB_SGB_INTRO_ANIMATION_LENGTH) return; GB_sample_t filtered_output = gb->apu_output.highpass_mode? (GB_sample_t) {output.left - gb->apu_output.highpass_diff.left, @@ -243,18 +248,14 @@ static void render(GB_gameboy_t *gb) unsigned left_volume = 0; unsigned right_volume = 0; unrolled for (unsigned i = GB_N_CHANNELS; i--;) { - if (gb->apu.is_active[i]) { + if (GB_apu_is_DAC_enabled(gb, i)) { if (mask & 1) { - left_volume += (gb->io_registers[GB_IO_NR50] & 7) * CH_STEP * 0xF; + left_volume += ((gb->io_registers[GB_IO_NR50] & 7) + 1) * CH_STEP * 0xF; } if (mask & 0x10) { - right_volume += ((gb->io_registers[GB_IO_NR50] >> 4) & 7) * CH_STEP * 0xF; + right_volume += (((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1) * CH_STEP * 0xF; } } - else { - left_volume += gb->apu_output.current_sample[i].left * CH_STEP; - right_volume += gb->apu_output.current_sample[i].right * CH_STEP; - } mask >>= 1; } gb->apu_output.highpass_diff = (GB_double_sample_t) @@ -279,9 +280,22 @@ static void render(GB_gameboy_t *gb) } assert(gb->apu_output.sample_callback); gb->apu_output.sample_callback(gb, &filtered_output); + if (unlikely(gb->apu_output.output_file)) { +#ifdef GB_BIG_ENDIAN + if (gb->apu_output.output_format == GB_AUDIO_FORMAT_WAV) { + filtered_output.left = LE16(filtered_output.left); + filtered_output.right = LE16(filtered_output.right); + } +#endif + if (fwrite(&filtered_output, sizeof(filtered_output), 1, gb->apu_output.output_file) != 1) { + fclose(gb->apu_output.output_file); + gb->apu_output.output_file = NULL; + gb->apu_output.output_error = errno; + } + } } -static void update_square_sample(GB_gameboy_t *gb, unsigned index) +static void update_square_sample(GB_gameboy_t *gb, GB_channel_t index) { if (gb->apu.square_channels[index].sample_surpressed) { if (gb->model > GB_MODEL_CGB_E) { @@ -311,6 +325,19 @@ static inline void update_wave_sample(GB_gameboy_t *gb, unsigned cycles) } } +static inline void set_envelope_clock(GB_envelope_clock_t *clock, bool value, bool direction, uint8_t volume) +{ + if (clock->clock == value) return; + if (value) { + clock->clock = true; + clock->should_lock = (volume == 0xF && direction) || (volume == 0x0 && !direction); + } + else { + clock->clock = false; + clock->locked |= clock->should_lock; + } +} + static void _nrx2_glitch(uint8_t *volume, uint8_t value, uint8_t old_value, uint8_t *countdown, GB_envelope_clock_t *lock) { if (lock->clock) { @@ -324,7 +351,7 @@ static void _nrx2_glitch(uint8_t *volume, uint8_t value, uint8_t old_value, uint } if (should_invert) { - // The weird way and over-the-top way clocks for this counter are connected cause + // The weird and over-the-top way clocks for this counter are connected cause // some weird ways for it to invert if (value & 8) { if (!(old_value & 7) && !lock->locked) { @@ -351,27 +378,17 @@ static void _nrx2_glitch(uint8_t *volume, uint8_t value, uint8_t old_value, uint *volume &= 0xF; } else if (!(value & 7) && lock->clock) { - // *lock->locked = false; // Excepted from the schematics, but doesn't actually happen on any model? - if (!should_invert) { - if (*volume == 0xF && (value & 8)) { - lock->locked = true; - } - else if (*volume == 0 && !(value & 8)) { - lock->locked = true; - } - } - else if (*volume == 1 && !(value & 8)) { - lock->locked = true; - } - else if (*volume == 0xE && (value & 8)) { - lock->locked = true; - } - lock->clock = false; + set_envelope_clock(lock, false, 0, 0); } } static void nrx2_glitch(GB_gameboy_t *gb, uint8_t *volume, uint8_t value, uint8_t old_value, uint8_t *countdown, GB_envelope_clock_t *lock) { + /* Note: on pre-CGB models *some* of these are non-deterministic. Specifically, + $x0 writes seem to be non-deterministic while $x8 always work as expected. + TODO: Might be useful to find which cases are non-deterministic, and allow + the debugger to issue warnings when they're used. I suspect writes to/from + $xF are guaranteed to be deterministic. */ if (gb->model <= GB_MODEL_CGB_C) { _nrx2_glitch(volume, 0xFF, old_value, countdown, lock); _nrx2_glitch(volume, value, 0xFF, countdown, lock); @@ -381,11 +398,12 @@ static void nrx2_glitch(GB_gameboy_t *gb, uint8_t *volume, uint8_t value, uint8_ } } -static void tick_square_envelope(GB_gameboy_t *gb, enum GB_CHANNELS index) +static void tick_square_envelope(GB_gameboy_t *gb, GB_channel_t index) { + set_envelope_clock(&gb->apu.square_channels[index].envelope_clock, false, 0, 0); + if (gb->apu.square_channels[index].envelope_clock.locked) return; uint8_t nrx2 = gb->io_registers[index == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; - if (gb->apu.square_channels[index].envelope_clock.locked) return; if (!(nrx2 & 7)) return; if (gb->cgb_double_speed) { if (index == GB_SQUARE_1) { @@ -396,22 +414,14 @@ static void tick_square_envelope(GB_gameboy_t *gb, enum GB_CHANNELS index) } } + set_envelope_clock(&gb->apu.square_channels[index].envelope_clock, false, 0, 0); + if (nrx2 & 8) { - if (gb->apu.square_channels[index].current_volume < 0xF) { gb->apu.square_channels[index].current_volume++; } else { - gb->apu.square_channels[index].envelope_clock.locked = true; - } - } - else { - if (gb->apu.square_channels[index].current_volume > 0) { gb->apu.square_channels[index].current_volume--; } - else { - gb->apu.square_channels[index].envelope_clock.locked = true; - } - } if (gb->apu.is_active[index]) { update_square_sample(gb, index); @@ -420,9 +430,10 @@ static void tick_square_envelope(GB_gameboy_t *gb, enum GB_CHANNELS index) static void tick_noise_envelope(GB_gameboy_t *gb) { - uint8_t nr42 = gb->io_registers[GB_IO_NR42]; - + set_envelope_clock(&gb->apu.noise_channel.envelope_clock, false, 0, 0); if (gb->apu.noise_channel.envelope_clock.locked) return; + + uint8_t nr42 = gb->io_registers[GB_IO_NR42]; if (!(nr42 & 7)) return; if (gb->cgb_double_speed) { @@ -430,21 +441,11 @@ static void tick_noise_envelope(GB_gameboy_t *gb) } if (nr42 & 8) { - if (gb->apu.noise_channel.current_volume < 0xF) { gb->apu.noise_channel.current_volume++; } else { - gb->apu.noise_channel.envelope_clock.locked = true; - } - } - else { - if (gb->apu.noise_channel.current_volume > 0) { gb->apu.noise_channel.current_volume--; } - else { - gb->apu.noise_channel.envelope_clock.locked = true; - } - } if (gb->apu.is_active[GB_NOISE]) { update_sample(gb, GB_NOISE, @@ -454,6 +455,22 @@ static void tick_noise_envelope(GB_gameboy_t *gb) } } +static void sweep_calculation_done(GB_gameboy_t *gb, unsigned cycles) +{ + /* APU bug: sweep frequency is checked after adding the sweep delta twice */ + if (gb->apu.channel_1_restart_hold == 0) { + gb->apu.shadow_sweep_sample_length = gb->apu.square_channels[GB_SQUARE_1].sample_length; + } + if (gb->io_registers[GB_IO_NR10] & 8) { + gb->apu.sweep_length_addend ^= 0x7FF; + } + if (gb->apu.shadow_sweep_sample_length + gb->apu.sweep_length_addend > 0x7FF && !(gb->io_registers[GB_IO_NR10] & 8)) { + gb->apu.is_active[GB_SQUARE_1] = false; + update_sample(gb, GB_SQUARE_1, 0, gb->apu.square_sweep_calculate_countdown * 2 - cycles); + } + gb->apu.channel1_completed_addend = gb->apu.sweep_length_addend; +} + static void trigger_sweep_calculation(GB_gameboy_t *gb) { if ((gb->io_registers[GB_IO_NR10] & 0x70) && gb->apu.square_sweep_countdown == 7) { @@ -468,13 +485,17 @@ static void trigger_sweep_calculation(GB_gameboy_t *gb) } /* Recalculation and overflow check only occurs after a delay */ - gb->apu.square_sweep_calculate_countdown = (gb->io_registers[GB_IO_NR10] & 0x7) * 2 + 5 - gb->apu.lf_div; - if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { - // gb->apu.square_sweep_calculate_countdown += 2; + gb->apu.square_sweep_calculate_countdown = gb->io_registers[GB_IO_NR10] & 0x7; + // TODO: this is a hack because DIV write timing is inaccurate. Will probably break on odd mode. + gb->apu.square_sweep_calculate_countdown_reload_timer = 1 + gb->apu.lf_div; + if (!gb->cgb_double_speed && gb->during_div_write) { + gb->apu.square_sweep_calculate_countdown_reload_timer = 1; } - gb->apu.enable_zombie_calculate_stepping = false; gb->apu.unshifted_sweep = !(gb->io_registers[GB_IO_NR10] & 0x7); gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7) ^ 7; + if (gb->apu.square_sweep_calculate_countdown == 0) { + gb->apu.square_sweep_instant_calculation_done = true; + } } } @@ -509,13 +530,11 @@ void GB_apu_div_event(GB_gameboy_t *gb) unrolled for (unsigned i = GB_SQUARE_2 + 1; i--;) { if (gb->apu.square_channels[i].envelope_clock.clock) { tick_square_envelope(gb, i); - gb->apu.square_channels[i].envelope_clock.clock = false; } } if (gb->apu.noise_channel.envelope_clock.clock) { tick_noise_envelope(gb); - gb->apu.noise_channel.envelope_clock.clock = false; } if ((gb->apu.div_divider & 1) == 1) { @@ -573,12 +592,19 @@ void GB_apu_div_secondary_event(GB_gameboy_t *gb) unrolled for (unsigned i = GB_SQUARE_2 + 1; i--;) { uint8_t nrx2 = gb->io_registers[i == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; if (gb->apu.is_active[i] && gb->apu.square_channels[i].volume_countdown == 0) { - gb->apu.square_channels[i].envelope_clock.clock = (gb->apu.square_channels[i].volume_countdown = nrx2 & 7); + set_envelope_clock(&gb->apu.square_channels[i].envelope_clock, + (gb->apu.square_channels[i].volume_countdown = nrx2 & 7), + nrx2 & 8, + gb->apu.square_channels[i].current_volume); + } } if (gb->apu.is_active[GB_NOISE] && gb->apu.noise_channel.volume_countdown == 0) { - gb->apu.noise_channel.envelope_clock.clock = (gb->apu.noise_channel.volume_countdown = gb->io_registers[GB_IO_NR42] & 7); + set_envelope_clock(&gb->apu.noise_channel.envelope_clock, + (gb->apu.noise_channel.volume_countdown = gb->io_registers[GB_IO_NR42] & 7), + gb->io_registers[GB_IO_NR42] & 8, + gb->apu.noise_channel.current_volume); } } @@ -607,21 +633,62 @@ static void step_lfsr(GB_gameboy_t *gb, unsigned cycles_offset) void GB_apu_run(GB_gameboy_t *gb, bool force) { - uint32_t clock_rate = GB_get_clock_rate(gb) * 2; - if (!force || - (gb->apu.apu_cycles > 0x1000) || + uint32_t clock_rate = GB_get_clock_rate(gb); + bool orig_force = force; + +restart:; + uint16_t cycles = gb->apu.apu_cycles; + + if (force || + (cycles + gb->apu_output.cycles_since_render >= gb->apu_output.max_cycles_per_sample) || (gb->apu_output.sample_cycles >= clock_rate) || - (gb->apu.square_sweep_calculate_countdown || gb->apu.channel_1_restart_hold) || + (gb->apu.square_sweep_calculate_countdown || gb->apu.channel_1_restart_hold || gb->apu.square_sweep_calculate_countdown_reload_timer) || (gb->model <= GB_MODEL_CGB_E && (gb->apu.wave_channel.bugged_read_countdown || (gb->apu.wave_channel.enable && gb->apu.wave_channel.pulsed)))) { force = true; } if (!force) { return; } - /* Convert 4MHZ to 2MHz. apu_cycles is always divisable by 4. */ - uint16_t cycles = gb->apu.apu_cycles >> 2; + + /* Force renders to never be more than max_cycles_per_sample apart by spliting runs. */ + while (cycles + gb->apu_output.cycles_since_render > gb->apu_output.max_cycles_per_sample) { + /* We're already past max_cycles_per_sample. This can happen when changing clock rates, etc. + Let this sample render normally. */ + if (unlikely(gb->apu_output.cycles_since_render > gb->apu_output.max_cycles_per_sample)) break; + + gb->apu.apu_cycles = gb->apu_output.max_cycles_per_sample - gb->apu_output.cycles_since_render; + + if (gb->apu.apu_cycles) { + // Run for just enough cycles to reach max_cycles_per_sample + cycles -= gb->apu.apu_cycles; + GB_apu_run(gb, true); + // Re-evaluate force if needed + if (!orig_force) { + force = false; + gb->apu.apu_cycles = cycles; + goto restart; + } + // Check if we need another batch + continue; + } + + // Render if needed + if (gb->apu_output.sample_cycles >= clock_rate) { + gb->apu_output.sample_cycles -= clock_rate; + render(gb); + } + break; + } + gb->apu.apu_cycles = 0; - if (!cycles) return; + if (!cycles) { + /* This can happen in pre-CGB stop mode */ + while (unlikely(gb->apu_output.sample_cycles >= clock_rate)) { + gb->apu_output.sample_cycles -= clock_rate; + render(gb); + } + return; + } if (unlikely(gb->apu.wave_channel.bugged_read_countdown)) { uint16_t cycles_left = cycles; @@ -651,35 +718,40 @@ void GB_apu_run(GB_gameboy_t *gb, bool force) else { /* Split it into two */ cycles -= gb->apu.noise_channel.dmg_delayed_start; - gb->apu.apu_cycles = gb->apu.noise_channel.dmg_delayed_start * 4; + gb->apu.apu_cycles = gb->apu.noise_channel.dmg_delayed_start; GB_apu_run(gb, true); } } /* To align the square signal to 1MHz */ gb->apu.lf_div ^= cycles & 1; gb->apu.noise_channel.alignment += cycles; + + unsigned sweep_cycles = cycles / 2; + if ((cycles & 1) && !gb->apu.lf_div) { + sweep_cycles++; + } + if (gb->apu.square_sweep_calculate_countdown_reload_timer > sweep_cycles) { + gb->apu.square_sweep_calculate_countdown_reload_timer -= sweep_cycles; + sweep_cycles = 0; + } + else { + if (gb->apu.square_sweep_calculate_countdown_reload_timer && !gb->apu.square_sweep_calculate_countdown && gb->apu.square_sweep_instant_calculation_done) { + sweep_calculation_done(gb, cycles); + } + gb->apu.square_sweep_instant_calculation_done = false; + sweep_cycles -= gb->apu.square_sweep_calculate_countdown_reload_timer; + gb->apu.square_sweep_calculate_countdown_reload_timer = 0; + } + if (gb->apu.square_sweep_calculate_countdown && - (((gb->io_registers[GB_IO_NR10] & 7) || gb->apu.unshifted_sweep) || - gb->apu.square_sweep_calculate_countdown <= 3)) { // Calculation is paused if the lower bits are 0 - if (gb->apu.square_sweep_calculate_countdown > cycles) { - gb->apu.square_sweep_calculate_countdown -= cycles; + (((gb->io_registers[GB_IO_NR10] & 7) || gb->apu.unshifted_sweep))) { // Calculation is paused if the lower bits are 0 + if (gb->apu.square_sweep_calculate_countdown > sweep_cycles) { + gb->apu.square_sweep_calculate_countdown -= sweep_cycles; } else { - /* APU bug: sweep frequency is checked after adding the sweep delta twice */ - if (gb->apu.channel_1_restart_hold == 0) { - gb->apu.shadow_sweep_sample_length = gb->apu.square_channels[GB_SQUARE_1].sample_length; - } - if (gb->io_registers[GB_IO_NR10] & 8) { - gb->apu.sweep_length_addend ^= 0x7FF; - } - if (gb->apu.shadow_sweep_sample_length + gb->apu.sweep_length_addend > 0x7FF && !(gb->io_registers[GB_IO_NR10] & 8)) { - gb->apu.is_active[GB_SQUARE_1] = false; - update_sample(gb, GB_SQUARE_1, 0, gb->apu.square_sweep_calculate_countdown - cycles); - } - gb->apu.channel1_completed_addend = gb->apu.sweep_length_addend; - gb->apu.square_sweep_calculate_countdown = 0; + sweep_calculation_done(gb, cycles); } } @@ -695,6 +767,14 @@ void GB_apu_run(GB_gameboy_t *gb, bool force) unrolled for (unsigned i = GB_SQUARE_1; i <= GB_SQUARE_2; i++) { if (gb->apu.is_active[i]) { uint16_t cycles_left = cycles; + if (unlikely(gb->apu.square_channels[i].delay)) { + if (gb->apu.square_channels[i].delay < cycles_left) { + gb->apu.square_channels[i].delay = 0; + } + else { + gb->apu.square_channels[i].delay -= cycles_left; + } + } while (unlikely(cycles_left > gb->apu.square_channels[i].sample_countdown)) { cycles_left -= gb->apu.square_channels[i].sample_countdown + 1; gb->apu.square_channels[i].sample_countdown = (gb->apu.square_channels[i].sample_length ^ 0x7FF) * 2 + 1; @@ -704,9 +784,16 @@ void GB_apu_run(GB_gameboy_t *gb, bool force) if (cycles_left == 0 && gb->apu.samples[i] == 0) { gb->apu.pcm_mask[0] &= i == GB_SQUARE_1? 0xF0 : 0x0F; } - + gb->apu.square_channels[i].did_tick = true; update_square_sample(gb, i); + + uint8_t duty = gb->io_registers[i == GB_SQUARE_1? GB_IO_NR11 :GB_IO_NR21] >> 6; + uint8_t edge_sample_index = inline_const(uint8_t[], {7, 7, 5, 1})[duty]; + if (gb->apu.square_channels[i].current_sample_index == edge_sample_index) { + gb->apu_output.edge_triggered[i] = true; + } } + gb->apu.square_channels[i].just_reloaded = cycles_left == 0; if (cycles_left) { gb->apu.square_channels[i].sample_countdown -= cycles_left; } @@ -725,6 +812,9 @@ void GB_apu_run(GB_gameboy_t *gb, bool force) gb->io_registers[GB_IO_WAV_START + (gb->apu.wave_channel.current_sample_index >> 1)]; update_wave_sample(gb, cycles - cycles_left); gb->apu.wave_channel.wave_form_just_read = true; + if (gb->apu.wave_channel.current_sample_index == 0) { + gb->apu_output.edge_triggered[GB_WAVE] = true; + } } if (cycles_left) { gb->apu.wave_channel.sample_countdown -= cycles_left; @@ -784,6 +874,7 @@ void GB_apu_run(GB_gameboy_t *gb, bool force) } else { gb->apu.noise_channel.countdown_reloaded = true; + gb->apu_output.edge_triggered[GB_NOISE] = true; } } } @@ -804,6 +895,7 @@ void GB_apu_run(GB_gameboy_t *gb, bool force) void GB_apu_init(GB_gameboy_t *gb) { memset(&gb->apu, 0, sizeof(gb->apu)); + gb->apu.apu_cycles_in_2mhz = true; gb->apu.lf_div = 1; gb->apu.wave_channel.shift = 4; /* APU glitch: When turning the APU on while DIV's bit 4 (or 5 in double speed mode) is on, @@ -812,6 +904,8 @@ void GB_apu_init(GB_gameboy_t *gb) gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_SKIP; gb->apu.div_divider = 1; } + gb->apu.square_channels[GB_SQUARE_1].sample_countdown = -1; + gb->apu.square_channels[GB_SQUARE_2].sample_countdown = -1; } uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg) @@ -965,6 +1059,7 @@ static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb) } break; case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: /* TODO: AGBs are not affected, but AGSes are. They don't seem to follow a simple pattern like the other revisions. */ /* For the most part, AGS seems to do: @@ -977,6 +1072,76 @@ static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb) return effective_counter; } +static noinline void nr10_write_glitch(GB_gameboy_t *gb, uint8_t value) +{ + // TODO: Check all of these in APU odd mode + if (gb->model <= GB_MODEL_CGB_C) { + if (gb->apu.square_sweep_calculate_countdown_reload_timer == 1 && !gb->apu.lf_div) { + if (gb->cgb_double_speed) { + /* This is some instance-specific data corruption. It might also be affect by revision. + At least for my CGB-0 (haven't tested any other CGB-0s), the '3' case is non-deterministic. */ + static const uint8_t corruption[8] = {7, 7, 5, 7, 3, 3, 5, 7}; // Two of my CGB-Cs, CGB-A + // static const uint8_t corruption[8] = {7, 7, 1, 3, 3, 3, 5, 7}; // My other CGB-C, Coffee Bat's CGB-C + // static const uint8_t corruption[8] = {7, 1, 1, 3, 3, 5, 5, 7}; // My CGB-B + // static const uint8_t corruption[8] = {7, 7, 1, *, 3, 3, 5, 7}; // My CGB-0 + + // static const uint8_t corruption[8] = {7, 5, 1, 3, 3, 1, 5, 7}; // PinoBatch's CGB-B + // static const uint8_t corruption[8] = {7, 5, 1, 3, 3, *, 5, 7}; // GenericHeroGuy CGB-C + + + // TODO: How does this affect actual frequency calculation? + + gb->apu.square_sweep_calculate_countdown = corruption[gb->apu.square_sweep_calculate_countdown & 7]; + /* TODO: the value of 1 needs special handling, but it doesn't occur with the instance I'm emulating here */ + } + } + else if (gb->apu.square_sweep_calculate_countdown_reload_timer > 1) { + if (gb->cgb_double_speed) { + // TODO: How does this affect actual frequency calculation? + gb->apu.square_sweep_calculate_countdown = value & 7; + } + } + else if (gb->apu.square_sweep_calculate_countdown) { + // No clue why 1 is a special case here + bool should_zombie_step = false; + if (!(gb->io_registers[GB_IO_NR10] & 7)) { + should_zombie_step = gb->apu.lf_div ^ gb->cgb_double_speed; + } + else if (gb->cgb_double_speed && gb->apu.square_sweep_calculate_countdown == 1) { + should_zombie_step = true; + } + + if (should_zombie_step) { + gb->apu.square_sweep_calculate_countdown--; + if (gb->apu.square_sweep_calculate_countdown <= 1) { + gb->apu.square_sweep_calculate_countdown = 0; + sweep_calculation_done(gb, 0); + } + } + } + } + else { + if (gb->apu.square_sweep_calculate_countdown_reload_timer == 2) { + // Countdown just reloaded, re-reload it + gb->apu.square_sweep_calculate_countdown = value & 0x7; + if (!gb->apu.square_sweep_calculate_countdown) { + gb->apu.square_sweep_calculate_countdown_reload_timer = 0; + } + else { + // TODO: How does this affect actual frequency calculation? + } + } + if ((value & 7) && !(gb->io_registers[GB_IO_NR10] & 7) && !gb->apu.lf_div && gb->apu.square_sweep_calculate_countdown > 1) { + // TODO: Another odd glitch? Ditto + gb->apu.square_sweep_calculate_countdown--; + if (!gb->apu.square_sweep_calculate_countdown) { + sweep_calculation_done(gb, 0); + } + } + } + +} + void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) { GB_apu_run(gb, true); @@ -1031,6 +1196,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) memset(&gb->apu, 0, sizeof(gb->apu)); memset(gb->io_registers + GB_IO_NR10, 0, GB_IO_WAV_START - GB_IO_NR10); gb->apu.global_enable = false; + gb->apu.apu_cycles_in_2mhz = true; } if (!GB_is_cgb(gb) && (value & 0x80)) { @@ -1043,9 +1209,15 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) break; /* Square channels */ - case GB_IO_NR10:{ + case GB_IO_NR10: { + if (unlikely(gb->apu.square_sweep_calculate_countdown || gb->apu.square_sweep_calculate_countdown_reload_timer)) { + nr10_write_glitch(gb, value); + } bool old_negate = gb->io_registers[GB_IO_NR10] & 8; gb->io_registers[GB_IO_NR10] = value; + if (gb->model <= GB_MODEL_CGB_C) { + old_negate = true; + } if (gb->apu.shadow_sweep_sample_length + gb->apu.channel1_completed_addend + old_negate > 0x7FF && !(value & 8)) { gb->apu.is_active[GB_SQUARE_1] = false; @@ -1057,17 +1229,17 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR11: case GB_IO_NR21: { - unsigned index = reg == GB_IO_NR21? GB_SQUARE_2: GB_SQUARE_1; - gb->apu.square_channels[index].pulse_length = (0x40 - (value & 0x3f)); + GB_channel_t index = reg == GB_IO_NR21? GB_SQUARE_2: GB_SQUARE_1; + gb->apu.square_channels[index].pulse_length = (0x40 - (value & 0x3F)); if (!gb->apu.global_enable) { - value &= 0x3f; + value &= 0x3F; } break; } case GB_IO_NR12: case GB_IO_NR22: { - unsigned index = reg == GB_IO_NR22? GB_SQUARE_2: GB_SQUARE_1; + GB_channel_t index = reg == GB_IO_NR22? GB_SQUARE_2: GB_SQUARE_1; if ((value & 0xF8) == 0) { /* This disables the DAC */ gb->io_registers[reg] = value; @@ -1086,24 +1258,28 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR13: case GB_IO_NR23: { - unsigned index = reg == GB_IO_NR23? GB_SQUARE_2: GB_SQUARE_1; + GB_channel_t index = reg == GB_IO_NR23? GB_SQUARE_2: GB_SQUARE_1; gb->apu.square_channels[index].sample_length &= ~0xFF; gb->apu.square_channels[index].sample_length |= value & 0xFF; + if (gb->apu.square_channels[index].just_reloaded) { + gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 1; + } break; } case GB_IO_NR14: case GB_IO_NR24: { - unsigned index = reg == GB_IO_NR24? GB_SQUARE_2: GB_SQUARE_1; + GB_channel_t index = reg == GB_IO_NR24? GB_SQUARE_2: GB_SQUARE_1; bool was_active = gb->apu.is_active[index]; - /* TODO: When the sample length changes right before being updated, the countdown should change to the - old length, but the current sample should not change. Because our write timing isn't accurate to - the T-cycle, we hack around it by stepping the sample index backwards. */ - if ((value & 0x80) == 0 && gb->apu.is_active[index]) { + /* TODO: When the sample length changes right before being updated from ≥$700 to <$700, the countdown + should change to the old length, but the current sample should not change. Because our write + timing isn't accurate to the T-cycle, we hack around it by stepping the sample index backwards. */ + if ((value & 0x80) == 0 && gb->apu.is_active[index] && (gb->io_registers[reg] & 0x7) == 7 && (value & 7) != 7) { /* On an AGB, as well as on CGB C and earlier (TODO: Tested: 0, B and C), it behaves slightly different on double speed. */ if (gb->model == GB_MODEL_CGB_E || gb->model == GB_MODEL_CGB_D || gb->apu.square_channels[index].sample_countdown & 1) { - if (gb->apu.square_channels[index].sample_countdown >> 1 == (gb->apu.square_channels[index].sample_length ^ 0x7FF)) { + if (gb->apu.square_channels[index].did_tick && + gb->apu.square_channels[index].sample_countdown >> 1 == (gb->apu.square_channels[index].sample_length ^ 0x7FF)) { gb->apu.square_channels[index].current_sample_index--; gb->apu.square_channels[index].current_sample_index &= 7; gb->apu.square_channels[index].sample_surpressed = false; @@ -1114,21 +1290,31 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) uint16_t old_sample_length = gb->apu.square_channels[index].sample_length; gb->apu.square_channels[index].sample_length &= 0xFF; gb->apu.square_channels[index].sample_length |= (value & 7) << 8; + if (gb->apu.square_channels[index].just_reloaded) { + gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 1; + } if (value & 0x80) { /* Current sample index remains unchanged when restarting channels 1 or 2. It is only reset by turning the APU off. */ gb->apu.square_channels[index].envelope_clock.locked = false; gb->apu.square_channels[index].envelope_clock.clock = false; + gb->apu.square_channels[index].did_tick = false; + bool force_unsurpressed = false; if (!gb->apu.is_active[index]) { - gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 6 - gb->apu.lf_div; - if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { - gb->apu.square_channels[index].sample_countdown += 2; + if (gb->model == GB_MODEL_CGB_E || gb->model == GB_MODEL_CGB_D) { + if (!(value & 4) && !(((gb->apu.square_channels[index].sample_countdown - gb->apu.square_channels[index].delay) / 2) & 0x400)) { + gb->apu.square_channels[index].current_sample_index++; + gb->apu.square_channels[index].current_sample_index &= 0x7; + force_unsurpressed = true; + } } + gb->apu.square_channels[index].delay = 6 - gb->apu.lf_div; + gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + gb->apu.square_channels[index].delay; } else { unsigned extra_delay = 0; if (gb->model == GB_MODEL_CGB_E || gb->model == GB_MODEL_CGB_D) { - if (!(value & 4) && !(((gb->apu.square_channels[index].sample_countdown - 1) / 2) & 0x400)) { + if (!gb->apu.square_channels[index].just_reloaded && !(value & 4) && !(((gb->apu.square_channels[index].sample_countdown - 1 - gb->apu.square_channels[index].delay) / 2) & 0x400)) { gb->apu.square_channels[index].current_sample_index++; gb->apu.square_channels[index].current_sample_index &= 0x7; gb->apu.square_channels[index].sample_surpressed = false; @@ -1141,10 +1327,8 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } } /* Timing quirk: if already active, sound starts 2 (2MHz) ticks earlier.*/ - gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 4 - gb->apu.lf_div + extra_delay; - if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { - gb->apu.square_channels[index].sample_countdown += 2; - } + gb->apu.square_channels[index].delay = 4 - gb->apu.lf_div + extra_delay; + gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + gb->apu.square_channels[index].delay; } gb->apu.square_channels[index].current_volume = gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] >> 4; /* The volume changes caused by NRX4 sound start take effect instantly (i.e. the effect the previously @@ -1159,7 +1343,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) if ((gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] & 0xF8) != 0 && !gb->apu.is_active[index]) { gb->apu.is_active[index] = true; update_sample(gb, index, 0, 0); - gb->apu.square_channels[index].sample_surpressed = true; + gb->apu.square_channels[index].sample_surpressed = true && !force_unsurpressed; } if (gb->apu.square_channels[index].pulse_length == 0) { gb->apu.square_channels[index].pulse_length = 0x40; @@ -1167,21 +1351,21 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } if (index == GB_SQUARE_1) { + gb->apu.square_sweep_instant_calculation_done = false; gb->apu.shadow_sweep_sample_length = 0; gb->apu.channel1_completed_addend = 0; if (gb->io_registers[GB_IO_NR10] & 7) { /* APU bug: if shift is nonzero, overflow check also occurs on trigger */ - gb->apu.square_sweep_calculate_countdown = (gb->io_registers[GB_IO_NR10] & 0x7) * 2 + 5 - gb->apu.lf_div; - if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { - /* TODO: I used to think this is correct, but it caused several regressions. - More research is needed to figure how calculation time is different - in models prior to CGB-D */ - // gb->apu.square_sweep_calculate_countdown += 2; + gb->apu.square_sweep_calculate_countdown = gb->io_registers[GB_IO_NR10] & 0x7; + if ((gb->apu.lf_div ^ !gb->cgb_double_speed) && gb->model <= GB_MODEL_CGB_C) { + gb->apu.square_sweep_calculate_countdown_reload_timer = 3; + } + else { + gb->apu.square_sweep_calculate_countdown_reload_timer = 2; } - gb->apu.enable_zombie_calculate_stepping = false; gb->apu.unshifted_sweep = false; if (!was_active) { - gb->apu.square_sweep_calculate_countdown += 2; + gb->apu.square_sweep_calculate_countdown_reload_timer++; } gb->apu.sweep_length_addend = gb->apu.square_channels[GB_SQUARE_1].sample_length; gb->apu.sweep_length_addend >>= (gb->io_registers[GB_IO_NR10] & 7); @@ -1190,11 +1374,6 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.sweep_length_addend = 0; } gb->apu.channel_1_restart_hold = 2 - gb->apu.lf_div + (GB_is_cgb(gb) && gb->model != GB_MODEL_CGB_D) * 2; - /* - if (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { - // TODO: This if makes channel_1_sweep_restart_2 fail on CGB-C mode - gb->apu.channel_1_restart_hold += 2; - }*/ gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7) ^ 7; } } @@ -1241,7 +1420,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) gb->apu.wave_channel.pulse_length = (0x100 - value); break; case GB_IO_NR32: - gb->apu.wave_channel.shift = (uint8_t[]){4, 0, 1, 2}[(value >> 5) & 3]; + gb->apu.wave_channel.shift = inline_const(uint8_t[], {4, 0, 1, 2})[(value >> 5) & 3]; if (gb->apu.is_active[GB_WAVE]) { update_wave_sample(gb, 0); } @@ -1249,6 +1428,10 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR33: gb->apu.wave_channel.sample_length &= ~0xFF; gb->apu.wave_channel.sample_length |= value & 0xFF; + if (gb->apu.wave_channel.bugged_read_countdown == 1) { // Just reloaded countdown + /* TODO: not verified with a test ROM yet */ + gb->apu.wave_channel.sample_countdown = gb->apu.wave_channel.sample_length ^ 0x7FF; + } break; case GB_IO_NR34: gb->apu.wave_channel.sample_length &= 0xFF; @@ -1322,7 +1505,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) /* Noise Channel */ case GB_IO_NR41: { - gb->apu.noise_channel.pulse_length = (0x40 - (value & 0x3f)); + gb->apu.noise_channel.pulse_length = (0x40 - (value & 0x3F)); break; } @@ -1356,11 +1539,11 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) if (!divisor) divisor = 2; if (gb->model > GB_MODEL_CGB_C) { gb->apu.noise_channel.counter_countdown = - divisor + (divisor == 2? 0 : (uint8_t[]){2, 1, 0, 3}[(gb->apu.noise_channel.alignment) & 3]); + divisor + (divisor == 2? 0 : inline_const(uint8_t[], {2, 1, 0, 3})[(gb->apu.noise_channel.alignment) & 3]); } else { gb->apu.noise_channel.counter_countdown = - divisor + (divisor == 2? 0 : (uint8_t[]){2, 1, 4, 3}[(gb->apu.noise_channel.alignment) & 3]); + divisor + (divisor == 2? 0 : inline_const(uint8_t[], {2, 1, 4, 3})[(gb->apu.noise_channel.alignment) & 3]); } gb->apu.noise_channel.delta = 0; } @@ -1404,10 +1587,10 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } else { if (gb->model <= GB_MODEL_CGB_C) { - gb->apu.noise_channel.counter_countdown += (uint8_t[]){2, 1, 4, 3}[gb->apu.noise_channel.alignment & 3]; + gb->apu.noise_channel.counter_countdown += inline_const(uint8_t[], {2, 1, 4, 3})[gb->apu.noise_channel.alignment & 3]; } else { - gb->apu.noise_channel.counter_countdown += (uint8_t[]){2, 1, 0, 3}[gb->apu.noise_channel.alignment & 3]; + gb->apu.noise_channel.counter_countdown += inline_const(uint8_t[], {2, 1, 0, 3})[gb->apu.noise_channel.alignment & 3]; } if (((gb->apu.noise_channel.alignment + 1) & 3) < 2) { if ((gb->io_registers[GB_IO_NR43] & 0x07) == 1) { @@ -1487,10 +1670,13 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate) { - gb->apu_output.sample_rate = sample_rate; if (sample_rate) { gb->apu_output.highpass_rate = pow(0.999958, GB_get_clock_rate(gb) / (double)sample_rate); + gb->apu_output.max_cycles_per_sample = ceil(GB_get_clock_rate(gb) / 2.0 / sample_rate); + } + else { + gb->apu_output.max_cycles_per_sample = 0x400; } } @@ -1503,6 +1689,12 @@ void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample) } gb->apu_output.sample_rate = GB_get_clock_rate(gb) / cycles_per_sample * 2; gb->apu_output.highpass_rate = pow(0.999958, cycles_per_sample); + gb->apu_output.max_cycles_per_sample = ceil(cycles_per_sample / 4); +} + +unsigned GB_get_sample_rate(GB_gameboy_t *gb) +{ + return gb->apu_output.sample_rate; } void GB_apu_set_sample_callback(GB_gameboy_t *gb, GB_sample_callback_t callback) @@ -1519,3 +1711,249 @@ void GB_set_interference_volume(GB_gameboy_t *gb, double volume) { gb->apu_output.interference_volume = volume; } + +typedef struct __attribute__((packed)) { + uint32_t format_chunk; // = BE32('FORM') + uint32_t size; // = BE32(file size - 8) + uint32_t format; // = BE32('AIFC') + + uint32_t fver_chunk; // = BE32('FVER') + uint32_t fver_size; // = BE32(4) + uint32_t fver; + + uint32_t comm_chunk; // = BE32('COMM') + uint32_t comm_size; // = BE32(0x18) + + uint16_t channels; // = BE16(2) + uint32_t samples_per_channel; // = BE32(total number of samples / 2) + uint16_t bit_depth; // = BE16(16) + uint16_t frequency_exponent; + uint64_t frequency_significand; + uint32_t compression_type; // = 'NONE' (BE) or 'twos' (LE) + uint16_t compression_name; // = 0 + + uint32_t ssnd_chunk; // = BE32('SSND') + uint32_t ssnd_size; // = BE32(length of samples - 8) + uint32_t ssnd_offset; // = 0 + uint32_t ssnd_block; // = 0 +} aiff_header_t; + +typedef struct __attribute__((packed)) { + uint32_t marker; // = BE32('RIFF') + uint32_t size; // = LE32(file size - 8) + uint32_t type; // = BE32('WAVE') + + uint32_t fmt_chunk; // = BE32('fmt ') + uint32_t fmt_size; // = LE16(16) + uint16_t format; // = LE16(1) + uint16_t channels; // = LE16(2) + uint32_t sample_rate; // = LE32(sample_rate) + uint32_t byte_rate; // = LE32(sample_rate * 4) + uint16_t frame_size; // = LE32(4) + uint16_t bit_depth; // = LE16(16) + + uint32_t data_chunk; // = BE32('data') + uint32_t data_size; // = LE32(length of samples) +} wav_header_t; + + +int GB_start_audio_recording(GB_gameboy_t *gb, const char *path, GB_audio_format_t format) +{ + if (gb->apu_output.sample_rate == 0) { + return EINVAL; + } + + if (gb->apu_output.output_file) { + GB_stop_audio_recording(gb); + } + gb->apu_output.output_file = fopen(path, "wb"); + if (!gb->apu_output.output_file) return errno; + + gb->apu_output.output_format = format; + switch (format) { + case GB_AUDIO_FORMAT_RAW: + return 0; + case GB_AUDIO_FORMAT_AIFF: { + aiff_header_t header = {0,}; + if (fwrite(&header, sizeof(header), 1, gb->apu_output.output_file) != 1) { + fclose(gb->apu_output.output_file); + gb->apu_output.output_file = NULL; + return errno; + } + return 0; + } + case GB_AUDIO_FORMAT_WAV: { + wav_header_t header = {0,}; + if (fwrite(&header, sizeof(header), 1, gb->apu_output.output_file) != 1) { + fclose(gb->apu_output.output_file); + gb->apu_output.output_file = NULL; + return errno; + } + return 0; + } + default: + fclose(gb->apu_output.output_file); + gb->apu_output.output_file = NULL; + return EINVAL; + } +} +int GB_stop_audio_recording(GB_gameboy_t *gb) +{ + if (!gb->apu_output.output_file) { + int ret = gb->apu_output.output_error ?: -1; + gb->apu_output.output_error = 0; + return ret; + } + gb->apu_output.output_error = 0; + switch (gb->apu_output.output_format) { + case GB_AUDIO_FORMAT_RAW: + break; + case GB_AUDIO_FORMAT_AIFF: { + size_t file_size = ftell(gb->apu_output.output_file); + size_t frames = (file_size - sizeof(aiff_header_t)) / sizeof(GB_sample_t); + aiff_header_t header = { + .format_chunk = BE32('FORM'), + .size = BE32(file_size - 8), + .format = BE32('AIFC'), + + .fver_chunk = BE32('FVER'), + .fver_size = BE32(4), + .fver = BE32(0xA2805140), + + .comm_chunk = BE32('COMM'), + .comm_size = BE32(0x18), + .channels = BE16(2), + .samples_per_channel = BE32(frames), + .bit_depth = BE16(16), +#ifdef GB_BIG_ENDIAN + .compression_type = 'NONE', +#else + .compression_type = 'twos', +#endif + .compression_name = 0, + .ssnd_chunk = BE32('SSND'), + .ssnd_size = BE32(frames * sizeof(GB_sample_t) - 8), + .ssnd_offset = 0, + .ssnd_block = 0, + }; + + uint64_t significand = gb->apu_output.sample_rate; + uint16_t exponent = 0x403E; + while ((int64_t)significand > 0) { + significand <<= 1; + exponent--; + } + header.frequency_exponent = BE16(exponent); + header.frequency_significand = BE64(significand); + + fseek(gb->apu_output.output_file, 0, SEEK_SET); + if (fwrite(&header, sizeof(header), 1, gb->apu_output.output_file) != 1) { + gb->apu_output.output_error = errno; + } + break; + } + case GB_AUDIO_FORMAT_WAV: { + size_t file_size = ftell(gb->apu_output.output_file); + size_t frames = (file_size - sizeof(wav_header_t)) / sizeof(GB_sample_t); + wav_header_t header = { + .marker = BE32('RIFF'), + .size = LE32(file_size - 8), + .type = BE32('WAVE'), + + .fmt_chunk = BE32('fmt '), + .fmt_size = LE16(16), + .format = LE16(1), + .channels = LE16(2), + .sample_rate = LE32(gb->apu_output.sample_rate), + .byte_rate = LE32(gb->apu_output.sample_rate * 4), + .frame_size = LE32(4), + .bit_depth = LE16(16), + + .data_chunk = BE32('data'), + .data_size = LE32(frames * sizeof(GB_sample_t)), + }; + + fseek(gb->apu_output.output_file, 0, SEEK_SET); + if (fwrite(&header, sizeof(header), 1, gb->apu_output.output_file) != 1) { + gb->apu_output.output_error = errno; + } + break; + } + } + fclose(gb->apu_output.output_file); + gb->apu_output.output_file = NULL; + + int ret = gb->apu_output.output_error; + gb->apu_output.output_error = 0; + return ret; +} + + +void GB_set_channel_muted(GB_gameboy_t *gb, GB_channel_t channel, bool muted) +{ + assert(channel < GB_N_CHANNELS); + gb->apu_output.channel_muted[channel] = muted; +} + +bool GB_is_channel_muted(GB_gameboy_t *gb, GB_channel_t channel) +{ + return gb->apu_output.channel_muted[channel]; +} + +// Note: this intentionally does not check to see if the channel is muted. +uint8_t GB_get_channel_volume(GB_gameboy_t *gb, GB_channel_t channel) +{ + switch (channel) { + case GB_SQUARE_1: + case GB_SQUARE_2: + return gb->apu.square_channels[channel].current_volume; + + case GB_WAVE: + return inline_const(uint8_t[], {0xF, 8, 4, 0, 0})[gb->apu.wave_channel.shift]; + + case GB_NOISE: + return gb->apu.noise_channel.current_volume; + + default: + return 0; + } +} + +uint8_t GB_get_channel_amplitude(GB_gameboy_t *gb, GB_channel_t channel) +{ + return gb->apu.is_active[channel] ? gb->apu.samples[channel] : 0; +} + +uint16_t GB_get_channel_period(GB_gameboy_t *gb, GB_channel_t channel) +{ + switch (channel) { + case GB_SQUARE_1: + case GB_SQUARE_2: + return gb->apu.square_channels[channel].sample_length; + + case GB_WAVE: + return gb->apu.wave_channel.sample_length; + + case GB_NOISE: + return (gb->io_registers[GB_IO_NR43] & 7) << (gb->io_registers[GB_IO_NR43] >> 4); + + default: + return 0; + } +} + +// wave_table is a user allocated uint8_t[32] array +void GB_get_apu_wave_table(GB_gameboy_t *gb, uint8_t *wave_table) +{ + for (unsigned i = GB_IO_WAV_START; i <= GB_IO_WAV_END; i++) { + wave_table[2 * (i - GB_IO_WAV_START)] = gb->io_registers[i] >> 4; + wave_table[2 * (i - GB_IO_WAV_START) + 1] = gb->io_registers[i] & 0xF; + } +} + +bool GB_get_channel_edge_triggered(GB_gameboy_t *gb, GB_channel_t channel) +{ + bool edge_triggered = gb->apu_output.edge_triggered[channel]; + gb->apu_output.edge_triggered[channel] = false; + return edge_triggered; +} diff --git a/Core/apu.h b/Core/apu.h index 729ccce..312f884 100644 --- a/Core/apu.h +++ b/Core/apu.h @@ -1,8 +1,8 @@ -#ifndef apu_h -#define apu_h +#pragma once #include #include #include +#include #include "defs.h" #ifdef GB_INTERNAL @@ -25,11 +25,22 @@ /* APU ticks are 2MHz, triggered by an internal APU clock. */ +#ifdef GB_INTERNAL +typedef union +{ + struct { + int16_t left; + int16_t right; + }; + uint32_t packed; +} GB_sample_t; +#else typedef struct { int16_t left; int16_t right; } GB_sample_t; +#endif typedef struct { @@ -37,19 +48,20 @@ typedef struct double right; } GB_double_sample_t; -enum GB_CHANNELS { +typedef enum { GB_SQUARE_1, GB_SQUARE_2, GB_WAVE, GB_NOISE, GB_N_CHANNELS -}; +} GB_channel_t; typedef struct { - bool locked:1; + bool locked:1; // Represents FYNO's output on channel 4 bool clock:1; // Represents FOSY on channel 4 - unsigned padding:6; + bool should_lock:1; // Represents FYNO's input on channel 4 + uint8_t padding:5; } GB_envelope_clock_t; typedef void (*GB_sample_callback_t)(GB_gameboy_t *gb, GB_sample_t *sample); @@ -69,12 +81,13 @@ typedef struct // need to divide the signal. uint8_t square_sweep_countdown; // In 128Hz - uint8_t square_sweep_calculate_countdown; // In 2 MHz + uint8_t square_sweep_calculate_countdown; // In 1 MHz + uint8_t square_sweep_calculate_countdown_reload_timer; // In 1 Mhz, for glitches related to reloading square_sweep_calculate_countdown uint16_t sweep_length_addend; uint16_t shadow_sweep_sample_length; bool unshifted_sweep; - bool enable_zombie_calculate_stepping; - + bool square_sweep_instant_calculation_done; + uint8_t channel_1_restart_hold; uint16_t channel1_completed_addend; struct { @@ -88,6 +101,10 @@ typedef struct uint16_t sample_length; // From NRX3, NRX4, in APU ticks bool length_enabled; // NRX4 GB_envelope_clock_t envelope_clock; + uint8_t delay; // Hack for CGB D/E phantom step due to how sample_countdown is implemented in SameBoy + bool did_tick:1; + bool just_reloaded:1; + uint8_t padding:6; } square_channels[2]; struct { @@ -125,12 +142,14 @@ typedef struct GB_envelope_clock_t envelope_clock; } noise_channel; - enum { + GB_ENUM(uint8_t, { GB_SKIP_DIV_EVENT_INACTIVE, GB_SKIP_DIV_EVENT_SKIPPED, GB_SKIP_DIV_EVENT_SKIP, - } skip_div_event:8; + }) skip_div_event; uint8_t pcm_mask[2]; // For CGB-0 to CGB-C PCM read glitch + + bool apu_cycles_in_2mhz; // For compatibility with 0.16.x save states } GB_apu_t; typedef enum { @@ -140,10 +159,17 @@ typedef enum { GB_HIGHPASS_MAX } GB_highpass_mode_t; +typedef enum { + GB_AUDIO_FORMAT_RAW, // Native endian + GB_AUDIO_FORMAT_AIFF, // Native endian + GB_AUDIO_FORMAT_WAV, +} GB_audio_format_t; + typedef struct { unsigned sample_rate; unsigned sample_cycles; // Counts by sample_rate until it reaches the clock frequency + unsigned max_cycles_per_sample; // Samples are NOT normalized to MAX_CH_AMP * 4 at this stage! unsigned cycles_since_render; @@ -151,6 +177,8 @@ typedef struct { GB_sample_t current_sample[GB_N_CHANNELS]; GB_sample_t summed_samples[GB_N_CHANNELS]; double dac_discharge[GB_N_CHANNELS]; + bool channel_muted[GB_N_CHANNELS]; + bool edge_triggered[GB_N_CHANNELS]; GB_highpass_mode_t highpass_mode; double highpass_rate; @@ -160,16 +188,32 @@ typedef struct { double interference_volume; double interference_highpass; + + FILE *output_file; + GB_audio_format_t output_format; + int output_error; + + /* Not output related, but it's temp state so I'll put it here */ + bool square_sweep_disable_stepping; } GB_apu_output_t; +void GB_set_channel_muted(GB_gameboy_t *gb, GB_channel_t channel, bool muted); +bool GB_is_channel_muted(GB_gameboy_t *gb, GB_channel_t channel); void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate); +unsigned GB_get_sample_rate(GB_gameboy_t *gb); void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample); /* Cycles are in 8MHz units */ void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode); void GB_set_interference_volume(GB_gameboy_t *gb, double volume); void GB_apu_set_sample_callback(GB_gameboy_t *gb, GB_sample_callback_t callback); - +int GB_start_audio_recording(GB_gameboy_t *gb, const char *path, GB_audio_format_t format); +int GB_stop_audio_recording(GB_gameboy_t *gb); +uint8_t GB_get_channel_volume(GB_gameboy_t *gb, GB_channel_t channel); +uint8_t GB_get_channel_amplitude(GB_gameboy_t *gb, GB_channel_t channel); +uint16_t GB_get_channel_period(GB_gameboy_t *gb, GB_channel_t channel); +void GB_get_apu_wave_table(GB_gameboy_t *gb, uint8_t *wave_table); +bool GB_get_channel_edge_triggered(GB_gameboy_t *gb, GB_channel_t channel); #ifdef GB_INTERNAL -internal bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index); +internal bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, GB_channel_t index); internal void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value); internal uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg); internal void GB_apu_div_event(GB_gameboy_t *gb); @@ -177,5 +221,3 @@ internal void GB_apu_div_secondary_event(GB_gameboy_t *gb); internal void GB_apu_init(GB_gameboy_t *gb); internal void GB_apu_run(GB_gameboy_t *gb, bool force); #endif - -#endif /* apu_h */ diff --git a/Core/camera.c b/Core/camera.c index 7751f18..cbfc494 100644 --- a/Core/camera.c +++ b/Core/camera.c @@ -3,7 +3,7 @@ static uint32_t noise_seed = 0; /* This is not a complete emulation of the camera chip. Only the features used by the Game Boy Camera ROMs are supported. - We also do not emulate the timing of the real cart, as it might be actually faster than the webcam. */ + We also do not emulate the timing of the real cart when a webcam is used, as it might be actually faster than the webcam. */ static uint8_t generate_noise(uint8_t x, uint8_t y) { @@ -25,10 +25,17 @@ static uint8_t generate_noise(uint8_t x, uint8_t y) static long get_processed_color(GB_gameboy_t *gb, uint8_t x, uint8_t y) { - if (x >= 128) { + if (x == 128) { + x = 127; + } + else if (x > 128) { x = 0; } - if (y >= 112) { + + if (y == 112) { + y = 111; + } + else if (y >= 112) { y = 0; } @@ -55,10 +62,6 @@ static long get_processed_color(GB_gameboy_t *gb, uint8_t x, uint8_t y) uint8_t GB_camera_read_image(GB_gameboy_t *gb, uint16_t addr) { - if (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) { - /* Forbid reading the image while the camera is busy. */ - return 0xFF; - } uint8_t tile_x = addr / 0x10 % 0x10; uint8_t tile_y = addr / 0x10 / 0x10; @@ -112,6 +115,12 @@ void GB_set_camera_get_pixel_callback(GB_gameboy_t *gb, GB_camera_get_pixel_call void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback) { + if (gb->camera_countdown > 0 && callback) { + GB_log(gb, "Camera update request callback set while camera was proccessing, clearing camera countdown.\n"); + gb->camera_countdown = 0; + GB_camera_updated(gb); + } + gb->camera_update_request_callback = callback; } @@ -125,12 +134,25 @@ void GB_camera_write_register(GB_gameboy_t *gb, uint16_t addr, uint8_t value) addr &= 0x7F; if (addr == GB_CAMERA_SHOOT_AND_1D_FLAGS) { value &= 0x7; - noise_seed = rand(); - if ((value & 1) && !(gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) && gb->camera_update_request_callback) { - /* If no callback is set, ignore the write as if the camera is instantly done */ - gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] |= 1; - gb->camera_update_request_callback(gb); + noise_seed = GB_random(); + if ((value & 1) && !(gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1)) { + if (gb->camera_update_request_callback) { + gb->camera_update_request_callback(gb); + } + else { + /* If no callback is set, wait the amount of time the real camera would take before clearing the busy bit */ + uint16_t exposure = (gb->camera_registers[GB_CAMERA_EXPOSURE_HIGH] << 8) | gb->camera_registers[GB_CAMERA_EXPOSURE_LOW]; + gb->camera_countdown = 129792 + ((gb->camera_registers[GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS] & 0x80)? 0 : 2048) + (exposure * 64) + (gb->camera_alignment & 4); + } } + + if (!(value & 1) && (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1)) { + /* We don't support cancelling a camera shoot */ + GB_log(gb, "ROM attempted to cancel camera shoot, which is currently not supported. The camera shoot will not be cancelled.\n"); + value |= 1; + } + + gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] = value; } else { if (addr >= 0x36) { diff --git a/Core/camera.h b/Core/camera.h index 1461f3a..3811140 100644 --- a/Core/camera.h +++ b/Core/camera.h @@ -1,11 +1,19 @@ -#ifndef camera_h -#define camera_h +#pragma once #include #include "defs.h" typedef uint8_t (*GB_camera_get_pixel_callback_t)(GB_gameboy_t *gb, uint8_t x, uint8_t y); typedef void (*GB_camera_update_request_callback_t)(GB_gameboy_t *gb); +void GB_set_camera_get_pixel_callback(GB_gameboy_t *gb, GB_camera_get_pixel_callback_t callback); +void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback); +void GB_camera_updated(GB_gameboy_t *gb); + +#ifdef GB_INTERNAL +internal uint8_t GB_camera_read_image(GB_gameboy_t *gb, uint16_t addr); +internal void GB_camera_write_register(GB_gameboy_t *gb, uint16_t addr, uint8_t value); +internal uint8_t GB_camera_read_register(GB_gameboy_t *gb, uint16_t addr); + enum { GB_CAMERA_SHOOT_AND_1D_FLAGS = 0, GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS = 1, @@ -15,15 +23,4 @@ enum { GB_CAMERA_DITHERING_PATTERN_START = 6, GB_CAMERA_DITHERING_PATTERN_END = 0x35, }; - -uint8_t GB_camera_read_image(GB_gameboy_t *gb, uint16_t addr); - -void GB_set_camera_get_pixel_callback(GB_gameboy_t *gb, GB_camera_get_pixel_callback_t callback); -void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback); - -void GB_camera_updated(GB_gameboy_t *gb); - -void GB_camera_write_register(GB_gameboy_t *gb, uint16_t addr, uint8_t value); -uint8_t GB_camera_read_register(GB_gameboy_t *gb, uint16_t addr); - #endif diff --git a/Core/cheats.c b/Core/cheats.c index 8b5a7a0..6201f9f 100644 --- a/Core/cheats.c +++ b/Core/cheats.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include static inline uint8_t hash_addr(uint16_t addr) { @@ -30,23 +32,28 @@ static uint16_t bank_for_addr(GB_gameboy_t *gb, uint16_t addr) return 0; } +static noinline void apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value) +{ + if (unlikely(!gb->boot_rom_finished)) return; + const GB_cheat_hash_t *hash = gb->cheat_hash[hash_addr(address)]; + if (likely(!hash)) return; + + for (unsigned i = 0; i < hash->size; i++) { + GB_cheat_t *cheat = hash->cheats[i]; + if (cheat->address == address && cheat->enabled && (!cheat->use_old_value || cheat->old_value == *value)) { + if (cheat->bank == GB_CHEAT_ANY_BANK || cheat->bank == bank_for_addr(gb, address)) { + *value = cheat->value; + break; + } + } + } +} + void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value) { if (likely(!gb->cheat_enabled)) return; if (likely(gb->cheat_count == 0)) return; // Optimization - if (unlikely(!gb->boot_rom_finished)) return; - const GB_cheat_hash_t *hash = gb->cheat_hash[hash_addr(address)]; - if (unlikely(hash)) { - for (unsigned i = 0; i < hash->size; i++) { - GB_cheat_t *cheat = hash->cheats[i]; - if (cheat->address == address && cheat->enabled && (!cheat->use_old_value || cheat->old_value == *value)) { - if (cheat->bank == GB_CHEAT_ANY_BANK || cheat->bank == bank_for_addr(gb, address)) { - *value = cheat->value; - break; - } - } - } - } + apply_cheat(gb, address, value); } bool GB_cheats_enabled(GB_gameboy_t *gb) @@ -59,8 +66,10 @@ void GB_set_cheats_enabled(GB_gameboy_t *gb, bool enabled) gb->cheat_enabled = enabled; } -void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled) +const GB_cheat_t *GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + GB_cheat_t *cheat = malloc(sizeof(*cheat)); cheat->address = address; cheat->bank = bank; @@ -84,15 +93,22 @@ void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, u *hash = realloc(*hash, sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); (*hash)->cheats[(*hash)->size - 1] = cheat; } + + return cheat; } const GB_cheat_t *const *GB_get_cheats(GB_gameboy_t *gb, size_t *size) { - *size = gb->cheat_count; + if (size) { + *size = gb->cheat_count; + } return (void *)gb->cheats; } + void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + for (unsigned i = 0; i < gb->cheat_count; i++) { if (gb->cheats[i] == cheat) { gb->cheats[i] = gb->cheats[--gb->cheat_count]; @@ -116,7 +132,7 @@ void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat) *hash = NULL; } else { - *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + *hash = realloc(*hash, sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); } break; } @@ -125,11 +141,13 @@ void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat) free((void *)cheat); } -bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *description, bool enabled) +const GB_cheat_t *GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *description, bool enabled) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + uint8_t dummy; /* GameShark */ - { + if (strlen(cheat) == 8) { uint8_t bank; uint8_t value; uint16_t address; @@ -137,8 +155,8 @@ bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *descriptio if (bank >= 0x80) { bank &= 0xF; } - GB_add_cheat(gb, description, address, bank, value, 0, false, enabled); - return true; + address = __builtin_bswap16(address); + return GB_add_cheat(gb, description, address, bank, value, 0, false, enabled); } } @@ -159,6 +177,9 @@ bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *descriptio uint8_t old_value; uint8_t value; uint16_t address; + if (strlen(stripped_cheat) != 8 && strlen(stripped_cheat) != 6) { + return NULL; + } if (sscanf(stripped_cheat, "%02hhx%04hx%02hhx%c", &value, &address, &old_value, &dummy) == 3) { address = (uint16_t)(address >> 4) | (uint16_t)(address << 12); address ^= 0xF000; @@ -167,8 +188,7 @@ bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *descriptio } old_value = (uint8_t)(old_value >> 2) | (uint8_t)(old_value << 6); old_value ^= 0xBA; - GB_add_cheat(gb, description, address, GB_CHEAT_ANY_BANK, value, old_value, true, enabled); - return true; + return GB_add_cheat(gb, description, address, GB_CHEAT_ANY_BANK, value, old_value, true, enabled); } if (sscanf(stripped_cheat, "%02hhx%04hx%c", &value, &address, &dummy) == 2) { @@ -177,15 +197,16 @@ bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *descriptio if (address > 0x7FFF) { return false; } - GB_add_cheat(gb, description, address, GB_CHEAT_ANY_BANK, value, false, true, enabled); - return true; + return GB_add_cheat(gb, description, address, GB_CHEAT_ANY_BANK, value, false, true, enabled); } } - return false; + return NULL; } void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + GB_cheat_t *cheat = NULL; for (unsigned i = 0; i < gb->cheat_count; i++) { if (gb->cheats[i] == _cheat) { @@ -207,7 +228,7 @@ void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *des *hash = NULL; } else { - *hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); + *hash = realloc(*hash, sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); } break; } @@ -242,6 +263,8 @@ void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *des void GB_load_cheats(GB_gameboy_t *gb, const char *path) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + FILE *f = fopen(path, "rb"); if (!f) { return; diff --git a/Core/cheats.h b/Core/cheats.h index f9c076c..321d858 100644 --- a/Core/cheats.h +++ b/Core/cheats.h @@ -1,14 +1,14 @@ -#ifndef cheats_h -#define cheats_h +#pragma once +#ifndef GB_DISABLE_CHEATS #include "defs.h" #define GB_CHEAT_ANY_BANK 0xFFFF typedef struct GB_cheat_s GB_cheat_t; -void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled); +const GB_cheat_t *GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled); void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat, const char *description, uint16_t address, uint16_t bank, uint8_t value, uint8_t old_value, bool use_old_value, bool enabled); -bool GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *description, bool enabled); +const GB_cheat_t *GB_import_cheat(GB_gameboy_t *gb, const char *cheat, const char *description, bool enabled); const GB_cheat_t *const *GB_get_cheats(GB_gameboy_t *gb, size_t *size); void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat); bool GB_cheats_enabled(GB_gameboy_t *gb); @@ -17,12 +17,8 @@ void GB_load_cheats(GB_gameboy_t *gb, const char *path); int GB_save_cheats(GB_gameboy_t *gb, const char *path); #ifdef GB_INTERNAL -#ifdef GB_DISABLE_CHEATS -#define GB_apply_cheat(...) -#else internal void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value); #endif -#endif typedef struct { size_t size; @@ -38,5 +34,8 @@ struct GB_cheat_s { bool enabled; char description[128]; }; - -#endif +#else +#ifdef GB_INTERNAL +#define GB_apply_cheat(...) +#endif // GB_INTERNAL +#endif // GB_DISABLE_CHEATS diff --git a/Core/debugger.c b/Core/debugger.c index 626ac63..7c9634c 100644 --- a/Core/debugger.c +++ b/Core/debugger.c @@ -26,23 +26,27 @@ typedef struct { #define VALUE_16(x) ((value_t){false, 0, (x)}) struct GB_breakpoint_s { + unsigned id; union { struct { - uint16_t addr; - uint16_t bank; /* -1 = any bank*/ + uint16_t addr; + uint16_t bank; /* -1 = any bank*/ }; uint32_t key; /* For sorting and comparing */ }; char *condition; bool is_jump_to; + uint16_t length; + bool inclusive; }; #define BP_KEY(x) (((struct GB_breakpoint_s){.addr = ((x).value), .bank = (x).has_bank? (x).bank : -1 }).key) -#define GB_WATCHPOINT_R (1) -#define GB_WATCHPOINT_W (2) +#define WATCHPOINT_READ (1) +#define WATCHPOINT_WRITE (2) struct GB_watchpoint_s { + unsigned id; union { struct { uint16_t addr; @@ -52,6 +56,8 @@ struct GB_watchpoint_s { }; char *condition; uint8_t flags; + uint16_t length; + bool inclusive; }; #define WP_KEY(x) (((struct GB_watchpoint_s){.addr = ((x).value), .bank = (x).has_bank? (x).bank : -1 }).key) @@ -122,10 +128,10 @@ static inline void switch_banking_state(GB_gameboy_t *gb, uint16_t bank) } } -static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer_name) +static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer_name, bool prefer_local) { static __thread char output[256]; - const GB_bank_symbol_t *symbol = GB_debugger_find_symbol(gb, value); + const GB_bank_symbol_t *symbol = GB_debugger_find_symbol(gb, value, prefer_local); if (symbol && (value - symbol->addr > 0x1000 || symbol->addr == 0) ) { symbol = NULL; @@ -155,12 +161,20 @@ static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer return output; } -static const char *debugger_value_to_string(GB_gameboy_t *gb, value_t value, bool prefer_name) +static GB_symbol_map_t *get_symbol_map(GB_gameboy_t *gb, uint16_t bank) { - if (!value.has_bank) return value_to_string(gb, value.value, prefer_name); + if (bank >= gb->n_symbol_maps) { + return NULL; + } + return gb->bank_symbols[bank]; +} + +static const char *debugger_value_to_string(GB_gameboy_t *gb, value_t value, bool prefer_name, bool prefer_local) +{ + if (!value.has_bank) return value_to_string(gb, value.value, prefer_name, prefer_local); static __thread char output[256]; - const GB_bank_symbol_t *symbol = GB_map_find_symbol(gb->bank_symbols[value.bank], value.value); + const GB_bank_symbol_t *symbol = GB_map_find_symbol(get_symbol_map(gb, value.bank), value.value, prefer_local); if (symbol && (value.value - symbol->addr > 0x1000 || symbol->addr == 0) ) { symbol = NULL; @@ -677,6 +691,11 @@ exit: return ret; } +static void update_debug_active(GB_gameboy_t *gb) +{ + gb->debug_active = !gb->debug_disable && (gb->debug_stopped || gb->debug_fin_command || gb->debug_next_command || gb->breakpoints); +} + struct debugger_command_s; typedef bool debugger_command_imp_t(GB_gameboy_t *gb, char *arguments, char *modifiers, const struct debugger_command_s *command); typedef char *debugger_completer_imp_t(GB_gameboy_t *gb, const char *string, uintptr_t *context); @@ -702,7 +721,7 @@ static const char *lstrip(const char *str) #define STOPPED_ONLY \ if (!gb->debug_stopped) { \ -GB_log(gb, "Program is running. \n"); \ +GB_log(gb, "Program is running, use 'interrupt' to stop execution.\n"); \ return false; \ } @@ -741,6 +760,79 @@ static bool cont(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug return false; } +static bool interrupt(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + if (gb->debug_stopped) { + GB_log(gb, "Program already stopped.\n"); + return true; + } + + GB_debugger_break(gb); + return true; +} + +static char *reset_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"quick", "reload"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (strncmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + +static bool reset(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + + const char *stripped_argument = lstrip(arguments); + + if (stripped_argument[0] == 0) { + GB_reset(gb); + if (gb->debug_stopped) { + GB_cpu_disassemble(gb, gb->pc, 5); + } + return true; + } + + if (strcmp(stripped_argument, "quick") == 0) { + GB_quick_reset(gb); + if (gb->debug_stopped) { + GB_cpu_disassemble(gb, gb->pc, 5); + } + return true; + } + + if (strcmp(stripped_argument, "reload") == 0) { + if (gb->debugger_reload_callback) { + gb->debugger_reload_callback(gb); + if (gb->undo_state) { + free(gb->undo_state); + gb->undo_state = NULL; + } + if (gb->debug_stopped) { + GB_cpu_disassemble(gb, gb->pc, 5); + } + return true; + } + GB_log(gb, "ROM reloading via the debugger is not supported in this frontend.\n"); + return true; + } + + print_usage(gb, command); + return true; +} + static bool next(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { NO_MODIFIERS @@ -786,22 +878,6 @@ static bool finish(GB_gameboy_t *gb, char *arguments, char *modifiers, const deb return false; } -static bool stack_leak_detection(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) -{ - NO_MODIFIERS - STOPPED_ONLY - - if (strlen(lstrip(arguments))) { - print_usage(gb, command); - return true; - } - - gb->debug_stopped = false; - gb->stack_leak_detection = true; - gb->debug_call_depth = 0; - return false; -} - static bool registers(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { NO_MODIFIERS @@ -816,11 +892,11 @@ static bool registers(GB_gameboy_t *gb, char *arguments, char *modifiers, const (gb->f & GB_HALF_CARRY_FLAG)? 'H' : '-', (gb->f & GB_SUBTRACT_FLAG)? 'N' : '-', (gb->f & GB_ZERO_FLAG)? 'Z' : '-'); - GB_log(gb, "BC = %s\n", value_to_string(gb, gb->bc, false)); - GB_log(gb, "DE = %s\n", value_to_string(gb, gb->de, false)); - GB_log(gb, "HL = %s\n", value_to_string(gb, gb->hl, false)); - GB_log(gb, "SP = %s\n", value_to_string(gb, gb->sp, false)); - GB_log(gb, "PC = %s\n", value_to_string(gb, gb->pc, false)); + GB_log(gb, "BC = %s\n", value_to_string(gb, gb->bc, false, false)); + GB_log(gb, "DE = %s\n", value_to_string(gb, gb->de, false, false)); + GB_log(gb, "HL = %s\n", value_to_string(gb, gb->hl, false, false)); + GB_log(gb, "SP = %s\n", value_to_string(gb, gb->sp, false, false)); + GB_log(gb, "PC = %s\n", value_to_string(gb, gb->pc, false, false)); GB_log(gb, "IME = %s\n", gb->ime? "Enabled" : "Disabled"); return true; } @@ -830,7 +906,7 @@ static char *on_off_completer(GB_gameboy_t *gb, const char *string, uintptr_t *c size_t length = strlen(string); const char *suggestions[] = {"on", "off"}; while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { - if (memcmp(string, suggestions[*context], length) == 0) { + if (strncmp(string, suggestions[*context], length) == 0) { return strdup(suggestions[(*context)++] + length); } (*context)++; @@ -855,30 +931,6 @@ static bool softbreak(GB_gameboy_t *gb, char *arguments, char *modifiers, const return true; } -/* Find the index of the closest breakpoint equal or greater to addr */ -static uint16_t find_breakpoint(GB_gameboy_t *gb, value_t addr) -{ - if (!gb->breakpoints) { - return 0; - } - - uint32_t key = BP_KEY(addr); - - unsigned min = 0; - unsigned max = gb->n_breakpoints; - while (min < max) { - uint16_t pivot = (min + max) / 2; - if (gb->breakpoints[pivot].key == key) return pivot; - if (gb->breakpoints[pivot].key > key) { - max = pivot; - } - else { - min = pivot + 1; - } - } - return (uint16_t) min; -} - static inline bool is_legal_symbol_char(char c) { if (c >= '0' && c <= '9') return true; @@ -911,14 +963,15 @@ static char *symbol_completer(GB_gameboy_t *gb, const char *string, uintptr_t *_ size_t length = strlen(symbol_prefix); while (context->bank < 0x200) { - if (gb->bank_symbols[context->bank] == NULL || - context->symbol >= gb->bank_symbols[context->bank]->n_symbols) { + GB_symbol_map_t *map = get_symbol_map(gb, context->bank); + if (map == NULL || + context->symbol >= map->n_symbols) { context->bank++; context->symbol = 0; continue; } - const char *candidate = gb->bank_symbols[context->bank]->symbols[context->symbol++].name; - if (memcmp(symbol_prefix, candidate, length) == 0) { + const char *candidate = map->symbols[context->symbol++].name; + if (strncmp(symbol_prefix, candidate, length) == 0) { return strdup(candidate + length); } } @@ -930,7 +983,7 @@ static char *j_completer(GB_gameboy_t *gb, const char *string, uintptr_t *contex size_t length = strlen(string); const char *suggestions[] = {"j"}; while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { - if (memcmp(string, suggestions[*context], length) == 0) { + if (strncmp(string, suggestions[*context], length) == 0) { return strdup(suggestions[(*context)++] + length); } (*context)++; @@ -938,6 +991,24 @@ static char *j_completer(GB_gameboy_t *gb, const char *string, uintptr_t *contex return NULL; } +static bool check_inclusive(char *to) +{ + size_t length = strlen(to); + while (length > strlen("inclusive")) { + if (to[length - 1] == ' ') { + to[length - 1] = 0; + length--; + continue; + } + if (strcmp(to + length - strlen("inclusive"), "inclusive") == 0) { + to[length - strlen("inclusive")] = 0; + return true; + } + return false; + } + return false; +} + static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { bool is_jump_to = true; @@ -969,52 +1040,63 @@ static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const if (error) return true; } + + char *to = NULL; + bool inclusive = false; + if ((to = strstr(arguments, " to "))) { + *to = 0; + to += strlen(" to "); + inclusive = check_inclusive(to); + } bool error; value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); - uint32_t key = BP_KEY(result); - if (error) return true; - uint16_t index = find_breakpoint(gb, result); - if (index < gb->n_breakpoints && gb->breakpoints[index].key == key) { - GB_log(gb, "Breakpoint already set at %s\n", debugger_value_to_string(gb, result, true)); - if (!gb->breakpoints[index].condition && condition) { - GB_log(gb, "Added condition to breakpoint\n"); - gb->breakpoints[index].condition = strdup(condition); + uint16_t length = 0; + value_t end = result; + if (to) { + end = debugger_evaluate(gb, to, (unsigned)strlen(to), &error, NULL, NULL); + if (error) return true; + if (end.has_bank && result.has_bank && end.bank != result.bank) { + GB_log(gb, "Breakpoint range start and end points have different banks\n"); + return true; } - else if (gb->breakpoints[index].condition && condition) { - GB_log(gb, "Replaced breakpoint condition\n"); - free(gb->breakpoints[index].condition); - gb->breakpoints[index].condition = strdup(condition); + if (end.value <= result.value) { + GB_log(gb, "Breakpoint range end point must be grater than the start point\n"); + return true; } - else if (gb->breakpoints[index].condition && !condition) { - GB_log(gb, "Removed breakpoint condition\n"); - free(gb->breakpoints[index].condition); - gb->breakpoints[index].condition = NULL; - } - return true; + length = end.value - result.value - 1; } + uint32_t key = BP_KEY(result); + gb->breakpoints = realloc(gb->breakpoints, (gb->n_breakpoints + 1) * sizeof(gb->breakpoints[0])); - memmove(&gb->breakpoints[index + 1], &gb->breakpoints[index], (gb->n_breakpoints - index) * sizeof(gb->breakpoints[0])); - gb->breakpoints[index].key = key; - - if (condition) { - gb->breakpoints[index].condition = strdup(condition); + unsigned id = 1; + if (gb->n_breakpoints) { + id = gb->breakpoints[gb->n_breakpoints - 1].id + 1; } - else { - gb->breakpoints[index].condition = NULL; - } - gb->n_breakpoints++; - - gb->breakpoints[index].is_jump_to = is_jump_to; + + gb->breakpoints[gb->n_breakpoints++] = (struct GB_breakpoint_s){ + .id = id, + .key = key, + .condition = condition? strdup(condition) : NULL, + .is_jump_to = is_jump_to, + .length = length, + .inclusive = inclusive, + }; if (is_jump_to) { gb->has_jump_to_breakpoints = true; } - GB_log(gb, "Breakpoint set at %s\n", debugger_value_to_string(gb, result, true)); + GB_log(gb, "Breakpoint %u set at %s", id, debugger_value_to_string(gb, result, true, false)); + if (length) { + GB_log(gb, " - %s%s\n", debugger_value_to_string(gb, end, true, true), inclusive? " (inclusive)" : ""); + } + else { + GB_log(gb, "\n"); + } return true; } @@ -1030,87 +1112,55 @@ static bool delete(GB_gameboy_t *gb, char *arguments, char *modifiers, const deb free(gb->breakpoints); gb->breakpoints = NULL; gb->n_breakpoints = 0; - return true; - } - - bool error; - value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); - uint32_t key = BP_KEY(result); - - if (error) return true; - - uint16_t index = 0; - for (unsigned i = 0; i < gb->n_breakpoints; i++) { - if (gb->breakpoints[i].key == key) { - /* Full match */ - index = i; - break; - } - if (gb->breakpoints[i].addr == result.value && result.has_bank != (gb->breakpoints[i].bank != (uint16_t) -1)) { - /* Partial match */ - index = i; - } - } - - if (index >= gb->n_breakpoints) { - GB_log(gb, "No breakpoint set at %s\n", debugger_value_to_string(gb, result, true)); - return true; - } - - result.bank = gb->breakpoints[index].bank; - result.has_bank = gb->breakpoints[index].bank != (uint16_t) -1; - - if (gb->breakpoints[index].condition) { - free(gb->breakpoints[index].condition); - } - - if (gb->breakpoints[index].is_jump_to) { gb->has_jump_to_breakpoints = false; - for (unsigned i = 0; i < gb->n_breakpoints; i++) { - if (i == index) continue; - if (gb->breakpoints[i].is_jump_to) { - gb->has_jump_to_breakpoints = true; - break; + return true; + } + + char *end; + unsigned id = strtol(arguments, &end, 10); + if (*end) { + print_usage(gb, command); + return true; + } + + for (unsigned i = 0; i < gb->n_breakpoints; i++) { + if (gb->breakpoints[i].id != id) continue; + + if (gb->breakpoints[i].condition) { + free(gb->breakpoints[i].condition); + } + + if (gb->breakpoints[i].is_jump_to) { + gb->has_jump_to_breakpoints = false; + for (unsigned j = 0; j < gb->n_breakpoints; j++) { + if (j == i) continue; + if (gb->breakpoints[j].is_jump_to) { + gb->has_jump_to_breakpoints = true; + break; + } } } + + memmove(&gb->breakpoints[i], &gb->breakpoints[i + 1], (gb->n_breakpoints - i - 1) * sizeof(gb->breakpoints[0])); + gb->n_breakpoints--; + gb->breakpoints = realloc(gb->breakpoints, gb->n_breakpoints * sizeof(gb->breakpoints[0])); + + value_t addr = (value_t){gb->breakpoints[i].bank != (uint16_t)-1, gb->breakpoints[i].bank, gb->breakpoints[i].addr}; + + GB_log(gb, "Breakpoint %u removed from %s\n", id, debugger_value_to_string(gb, addr, addr.has_bank, false)); + return true; } - memmove(&gb->breakpoints[index], &gb->breakpoints[index + 1], (gb->n_breakpoints - index - 1) * sizeof(gb->breakpoints[0])); - gb->n_breakpoints--; - gb->breakpoints = realloc(gb->breakpoints, gb->n_breakpoints * sizeof(gb->breakpoints[0])); - - GB_log(gb, "Breakpoint removed from %s\n", debugger_value_to_string(gb, result, true)); + GB_log(gb, "Breakpoint %u was not found\n", id); return true; } -/* Find the index of the closest watchpoint equal or greater to addr */ -static uint16_t find_watchpoint(GB_gameboy_t *gb, value_t addr) -{ - if (!gb->watchpoints) { - return 0; - } - uint32_t key = WP_KEY(addr); - unsigned min = 0; - unsigned max = gb->n_watchpoints; - while (min < max) { - uint16_t pivot = (min + max) / 2; - if (gb->watchpoints[pivot].key == key) return pivot; - if (gb->watchpoints[pivot].key > key) { - max = pivot; - } - else { - min = pivot + 1; - } - } - return (uint16_t) min; -} - static char *rw_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) { size_t length = strlen(string); const char *suggestions[] = {"r", "rw", "w"}; while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { - if (memcmp(string, suggestions[*context], length) == 0) { + if (strncmp(string, suggestions[*context], length) == 0) { return strdup(suggestions[(*context)++] + length); } (*context)++; @@ -1121,7 +1171,6 @@ static char *rw_completer(GB_gameboy_t *gb, const char *string, uintptr_t *conte static bool watch(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { if (strlen(lstrip(arguments)) == 0) { -print_usage: print_usage(gb, command); return true; } @@ -1139,19 +1188,21 @@ print_usage: while (*modifiers) { switch (*modifiers) { case 'r': - flags |= GB_WATCHPOINT_R; + flags |= WATCHPOINT_READ; break; case 'w': - flags |= GB_WATCHPOINT_W; + flags |= WATCHPOINT_WRITE; break; default: - goto print_usage; + print_usage(gb, command); + return true; } modifiers++; } if (!flags) { - goto print_usage; + print_usage(gb, command); + return true; } char *condition = NULL; @@ -1160,57 +1211,67 @@ print_usage: condition += strlen(" if "); /* Verify condition is sane (Todo: This might have side effects!) */ bool error; - /* To make $new and $old legal */ + /* To make new and old legal */ uint16_t dummy = 0; uint8_t dummy2 = 0; debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, &dummy, &dummy2); if (error) return true; - + } + + char *to = NULL; + bool inclusive = false; + if ((to = strstr(arguments, " to "))) { + *to = 0; + to += strlen(" to "); + inclusive = check_inclusive(to); } bool error; value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); uint32_t key = WP_KEY(result); + + uint16_t length = 0; + value_t end = result; + if (to) { + end = debugger_evaluate(gb, to, (unsigned)strlen(to), &error, NULL, NULL); + if (error) return true; + if (end.has_bank && result.has_bank && end.bank != result.bank) { + GB_log(gb, "Watchpoint range start and end points have different banks\n"); + return true; + } + if (end.value <= result.value) { + GB_log(gb, "Watchpoint range end point must be grater than the start point\n"); + return true; + } + length = end.value - result.value - 1; + } if (error) return true; - - uint16_t index = find_watchpoint(gb, result); - if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) { - GB_log(gb, "Watchpoint already set at %s\n", debugger_value_to_string(gb, result, true)); - if (gb->watchpoints[index].flags != flags) { - GB_log(gb, "Modified watchpoint type\n"); - gb->watchpoints[index].flags = flags; - } - if (!gb->watchpoints[index].condition && condition) { - GB_log(gb, "Added condition to watchpoint\n"); - gb->watchpoints[index].condition = strdup(condition); - } - else if (gb->watchpoints[index].condition && condition) { - GB_log(gb, "Replaced watchpoint condition\n"); - free(gb->watchpoints[index].condition); - gb->watchpoints[index].condition = strdup(condition); - } - else if (gb->watchpoints[index].condition && !condition) { - GB_log(gb, "Removed watchpoint condition\n"); - free(gb->watchpoints[index].condition); - gb->watchpoints[index].condition = NULL; - } - return true; + + unsigned id = 1; + if (gb->n_watchpoints) { + id = gb->watchpoints[gb->n_watchpoints - 1].id + 1; } + gb->watchpoints = realloc(gb->watchpoints, (gb->n_watchpoints + 1) * sizeof(gb->watchpoints[0])); - memmove(&gb->watchpoints[index + 1], &gb->watchpoints[index], (gb->n_watchpoints - index) * sizeof(gb->watchpoints[0])); - gb->watchpoints[index].key = key; - gb->watchpoints[index].flags = flags; - if (condition) { - gb->watchpoints[index].condition = strdup(condition); + + gb->watchpoints[gb->n_watchpoints++] = (struct GB_watchpoint_s){ + .id = id, + .key = key, + .condition = condition? strdup(condition) : NULL, + .flags = flags, + .length = length, + .inclusive = inclusive, + }; + + GB_log(gb, "Watchpoint %u set at %s", id, debugger_value_to_string(gb, result, true, false)); + if (length) { + GB_log(gb, " - %s%s\n", debugger_value_to_string(gb, end, true, true), inclusive? " (inclusive)" : ""); } else { - gb->watchpoints[index].condition = NULL; + GB_log(gb, "\n"); } - gb->n_watchpoints++; - - GB_log(gb, "Watchpoint set at %s\n", debugger_value_to_string(gb, result, true)); return true; } @@ -1228,43 +1289,32 @@ static bool unwatch(GB_gameboy_t *gb, char *arguments, char *modifiers, const de gb->n_watchpoints = 0; return true; } - - bool error; - value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); - uint32_t key = WP_KEY(result); - - if (error) return true; - - uint16_t index = 0; - for (unsigned i = 0; i < gb->n_watchpoints; i++) { - if (gb->watchpoints[i].key == key) { - /* Full match */ - index = i; - break; - } - if (gb->watchpoints[i].addr == result.value && result.has_bank != (gb->watchpoints[i].bank != (uint16_t) -1)) { - /* Partial match */ - index = i; - } - } - - if (index >= gb->n_watchpoints) { - GB_log(gb, "No watchpoint set at %s\n", debugger_value_to_string(gb, result, true)); + + char *end; + unsigned id = strtol(arguments, &end, 10); + if (*end) { + print_usage(gb, command); return true; } - - result.bank = gb->watchpoints[index].bank; - result.has_bank = gb->watchpoints[index].bank != (uint16_t) -1; - - if (gb->watchpoints[index].condition) { - free(gb->watchpoints[index].condition); + + for (unsigned i = 0; i < gb->n_watchpoints; i++) { + if (gb->watchpoints[i].id != id) continue; + + if (gb->watchpoints[i].condition) { + free(gb->watchpoints[i].condition); + } + + memmove(&gb->watchpoints[i], &gb->watchpoints[i + 1], (gb->n_watchpoints - i - 1) * sizeof(gb->watchpoints[0])); + gb->n_watchpoints--; + gb->watchpoints = realloc(gb->watchpoints, gb->n_watchpoints * sizeof(gb->watchpoints[0])); + + value_t addr = (value_t){gb->watchpoints[i].bank != (uint16_t)-1, gb->watchpoints[i].bank, gb->watchpoints[i].addr}; + + GB_log(gb, "Watchpoint %u removed from %s\n", id, debugger_value_to_string(gb, addr, addr.has_bank, false)); + return true; } - - memmove(&gb->watchpoints[index], &gb->watchpoints[index + 1], (gb->n_watchpoints - index - 1) * sizeof(gb->watchpoints[0])); - gb->n_watchpoints--; - gb->watchpoints = realloc(gb->watchpoints, gb->n_watchpoints *sizeof(gb->watchpoints[0])); - - GB_log(gb, "Watchpoint removed from %s\n", debugger_value_to_string(gb, result, true)); + + GB_log(gb, "Watchpoint %u was not found\n", id); return true; } @@ -1283,17 +1333,32 @@ static bool list(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug GB_log(gb, "%d breakpoint(s) set:\n", gb->n_breakpoints); for (uint16_t i = 0; i < gb->n_breakpoints; i++) { value_t addr = (value_t){gb->breakpoints[i].bank != (uint16_t)-1, gb->breakpoints[i].bank, gb->breakpoints[i].addr}; + char *end_string = NULL; + if (gb->breakpoints[i].length) { + value_t end = addr; + end.value += gb->breakpoints[i].length + 1; + end_string = strdup(debugger_value_to_string(gb, end, addr.has_bank, true)); + } if (gb->breakpoints[i].condition) { - GB_log(gb, " %d. %s (%sCondition: %s)\n", i + 1, - debugger_value_to_string(gb, addr, addr.has_bank), + GB_log(gb, " %d. %s%s%s%s (%sCondition: %s)\n", gb->breakpoints[i].id, + debugger_value_to_string(gb, addr, addr.has_bank, false), + end_string? " - " : "", + end_string ?: "", + gb->breakpoints[i].inclusive? " (inclusive)" : "", gb->breakpoints[i].is_jump_to? "Jump to, ": "", gb->breakpoints[i].condition); } else { - GB_log(gb, " %d. %s%s\n", i + 1, - debugger_value_to_string(gb, addr, addr.has_bank), + GB_log(gb, " %d. %s%s%s%s%s\n", gb->breakpoints[i].id, + debugger_value_to_string(gb, addr, addr.has_bank, false), + end_string? " - " : "", + end_string ?: "", + gb->breakpoints[i].inclusive? " (inclusive)" : "", gb->breakpoints[i].is_jump_to? " (Jump to)" : ""); } + if (end_string) { + free(end_string); + } } } @@ -1304,16 +1369,26 @@ static bool list(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug GB_log(gb, "%d watchpoint(s) set:\n", gb->n_watchpoints); for (uint16_t i = 0; i < gb->n_watchpoints; i++) { value_t addr = (value_t){gb->watchpoints[i].bank != (uint16_t)-1, gb->watchpoints[i].bank, gb->watchpoints[i].addr}; + char *end_string = NULL; + if (gb->watchpoints[i].length) { + value_t end = addr; + end.value += gb->watchpoints[i].length + 1; + end_string = strdup(debugger_value_to_string(gb, end, addr.has_bank, true)); + } if (gb->watchpoints[i].condition) { - GB_log(gb, " %d. %s (%c%c, Condition: %s)\n", i + 1, debugger_value_to_string(gb, addr, addr.has_bank), - (gb->watchpoints[i].flags & GB_WATCHPOINT_R)? 'r' : '-', - (gb->watchpoints[i].flags & GB_WATCHPOINT_W)? 'w' : '-', - gb->watchpoints[i].condition); + GB_log(gb, " %d. %s%s%s%s (%c%c, Condition: %s)\n", gb->watchpoints[i].id, debugger_value_to_string(gb, addr, addr.has_bank, false), + end_string? " - " : "", end_string ?: "", + gb->watchpoints[i].inclusive? " (inclusive)" : "", + (gb->watchpoints[i].flags & WATCHPOINT_READ)? 'r' : '-', + (gb->watchpoints[i].flags & WATCHPOINT_WRITE)? 'w' : '-', + gb->watchpoints[i].condition); } else { - GB_log(gb, " %d. %s (%c%c)\n", i + 1, debugger_value_to_string(gb, addr, addr.has_bank), - (gb->watchpoints[i].flags & GB_WATCHPOINT_R)? 'r' : '-', - (gb->watchpoints[i].flags & GB_WATCHPOINT_W)? 'w' : '-'); + GB_log(gb, " %d. %s%s%s%s (%c%c)\n", gb->watchpoints[i].id, debugger_value_to_string(gb, addr, addr.has_bank, false), + end_string? " - " : "", end_string ?: "", + gb->watchpoints[i].inclusive? " (inclusive)" : "", + (gb->watchpoints[i].flags & WATCHPOINT_READ)? 'r' : '-', + (gb->watchpoints[i].flags & WATCHPOINT_WRITE)? 'w' : '-'); } } } @@ -1321,38 +1396,33 @@ static bool list(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug return true; } -static bool _should_break(GB_gameboy_t *gb, value_t addr, bool jump_to) +// Returns the id or 0 +static unsigned should_break(GB_gameboy_t *gb, uint16_t addr, bool jump_to) { - uint16_t index = find_breakpoint(gb, addr); - uint32_t key = BP_KEY(addr); - - if (index < gb->n_breakpoints && gb->breakpoints[index].key == key && gb->breakpoints[index].is_jump_to == jump_to) { - if (!gb->breakpoints[index].condition) { - return true; + if (unlikely(gb->backstep_instructions)) return false; + uint16_t bank = bank_for_addr(gb, addr); + for (unsigned i = 0; i < gb->n_breakpoints; i++) { + struct GB_breakpoint_s *breakpoint = &gb->breakpoints[i]; + if (breakpoint->bank != (uint16_t)-1) { + if (breakpoint->bank != bank) continue; + if (!gb->boot_rom_finished) continue; } + if (breakpoint->is_jump_to != jump_to) continue; + if (addr < breakpoint->addr) continue; + if (addr > (uint32_t)breakpoint->addr + breakpoint->length + breakpoint->inclusive) continue; + if (!breakpoint->condition) return breakpoint->id; bool error; - bool condition = debugger_evaluate(gb, gb->breakpoints[index].condition, - (unsigned)strlen(gb->breakpoints[index].condition), &error, NULL, NULL).value; + bool condition = debugger_evaluate(gb, breakpoint->condition, + (unsigned)strlen(breakpoint->condition), + &error, NULL, NULL).value; if (error) { - /* Should never happen */ - GB_log(gb, "An internal error has occured\n"); - return true; + GB_log(gb, "The condition for breakpoint %u is no longer a valid expression\n", breakpoint->id); + return breakpoint->id; } - return condition; + if (condition) return breakpoint->id; } - return false; -} - -static bool should_break(GB_gameboy_t *gb, uint16_t addr, bool jump_to) -{ - /* Try any-bank breakpoint */ - value_t full_addr = (VALUE_16(addr)); - if (_should_break(gb, full_addr, jump_to)) return true; - - /* Try bank-specific breakpoint */ - full_addr.has_bank = true; - full_addr.bank = bank_for_addr(gb, addr); - return _should_break(gb, full_addr, jump_to); + + return 0; } static char *format_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) @@ -1360,7 +1430,7 @@ static char *format_completer(GB_gameboy_t *gb, const char *string, uintptr_t *c size_t length = strlen(string); const char *suggestions[] = {"a", "b", "d", "o", "x"}; while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { - if (memcmp(string, suggestions[*context], length) == 0) { + if (strncmp(string, suggestions[*context], length) == 0) { return strdup(suggestions[(*context)++] + length); } (*context)++; @@ -1388,7 +1458,7 @@ static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu if (!error) { switch (modifiers[0]) { case 'a': - GB_log(gb, "=%s\n", debugger_value_to_string(gb, result, false)); + GB_log(gb, "=%s\n", debugger_value_to_string(gb, result, false, false)); break; case 'd': GB_log(gb, "=%d\n", result.value); @@ -1537,16 +1607,22 @@ static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg } else { static const char *const mapper_names[] = { - [GB_MBC1] = "MBC1", - [GB_MBC2] = "MBC2", - [GB_MBC3] = "MBC3", - [GB_MBC5] = "MBC5", - [GB_MBC7] = "MBC7", - [GB_HUC1] = "HUC-1", - [GB_HUC3] = "HUC-3", + [GB_MBC1] = "MBC1", + [GB_MBC2] = "MBC2", + [GB_MBC3] = "MBC3", + [GB_MBC5] = "MBC5", + [GB_MBC7] = "MBC7", + [GB_MMM01] = "MMM01", + [GB_HUC1] = "HUC-1", + [GB_HUC3] = "HUC-3", + [GB_CAMERA] = "MAC-GBD", + }; GB_log(gb, "%s\n", mapper_names[cartridge->mbc_type]); } + if (cartridge->mbc_type == GB_MMM01 || cartridge->mbc_type == GB_MBC1) { + GB_log(gb, "Current mapped ROM0 bank: %x\n", gb->mbc_rom0_bank); + } GB_log(gb, "Current mapped ROM bank: %x\n", gb->mbc_rom_bank); if (cartridge->has_ram) { GB_log(gb, "Current mapped RAM bank: %x\n", gb->mbc_ram_bank); @@ -1589,20 +1665,36 @@ static bool backtrace(GB_gameboy_t *gb, char *arguments, char *modifiers, const return true; } - GB_log(gb, " 1. %s\n", debugger_value_to_string(gb, (value_t){true, bank_for_addr(gb, gb->pc), gb->pc}, true)); + GB_log(gb, " 1. %s\n", debugger_value_to_string(gb, (value_t){true, bank_for_addr(gb, gb->pc), gb->pc}, true, false)); for (unsigned i = gb->backtrace_size; i--;) { - GB_log(gb, "%3d. %s\n", gb->backtrace_size - i + 1, debugger_value_to_string(gb, (value_t){true, gb->backtrace_returns[i].bank, gb->backtrace_returns[i].addr}, true)); + GB_log(gb, "%3d. %s\n", gb->backtrace_size - i + 1, debugger_value_to_string(gb, (value_t){true, gb->backtrace_returns[i].bank, gb->backtrace_returns[i].addr}, true, false)); } return true; } +static char *keep_completer(GB_gameboy_t *gb, const char *string, uintptr_t *context) +{ + size_t length = strlen(string); + const char *suggestions[] = {"keep"}; + while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { + if (strncmp(string, suggestions[*context], length) == 0) { + return strdup(suggestions[(*context)++] + length); + } + (*context)++; + } + return NULL; +} + static bool ticks(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { NO_MODIFIERS STOPPED_ONLY - - if (strlen(lstrip(arguments))) { + bool keep = false; + if (strcmp(lstrip(arguments), "keep") == 0) { + keep = true; + } + else if (lstrip(arguments)[0]) { print_usage(gb, command); return true; } @@ -1610,9 +1702,11 @@ static bool ticks(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu GB_log(gb, "T-cycles: %llu\n", (unsigned long long)gb->debugger_ticks); GB_log(gb, "M-cycles: %llu\n", (unsigned long long)gb->debugger_ticks / 4); GB_log(gb, "Absolute 8MHz ticks: %llu\n", (unsigned long long)gb->absolute_debugger_ticks); - GB_log(gb, "Tick count reset.\n"); - gb->debugger_ticks = 0; - gb->absolute_debugger_ticks = 0; + if (!keep) { + GB_log(gb, "Tick count reset.\n"); + gb->debugger_ticks = 0; + gb->absolute_debugger_ticks = 0; + } return true; } @@ -1681,15 +1775,15 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg return true; } GB_log(gb, "LCDC:\n"); - GB_log(gb, " LCD enabled: %s\n",(gb->io_registers[GB_IO_LCDC] & 128)? "Enabled" : "Disabled"); + GB_log(gb, " LCD enabled: %s\n",(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)? "Enabled" : "Disabled"); GB_log(gb, " %s: %s\n", (gb->cgb_mode? "Object priority flags" : "Background and Window"), - (gb->io_registers[GB_IO_LCDC] & 1)? "Enabled" : "Disabled"); - GB_log(gb, " Objects: %s\n", (gb->io_registers[GB_IO_LCDC] & 2)? "Enabled" : "Disabled"); - GB_log(gb, " Object size: %s\n", (gb->io_registers[GB_IO_LCDC] & 4)? "8x16" : "8x8"); - GB_log(gb, " Background tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & 8)? "$9C00" : "$9800"); - GB_log(gb, " Background and Window Tileset: %s\n", (gb->io_registers[GB_IO_LCDC] & 16)? "$8000" : "$8800"); - GB_log(gb, " Window: %s\n", (gb->io_registers[GB_IO_LCDC] & 32)? "Enabled" : "Disabled"); - GB_log(gb, " Window tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & 64)? "$9C00" : "$9800"); + (gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_EN)? "Enabled" : "Disabled"); + GB_log(gb, " Objects: %s\n", (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_EN)? "Enabled" : "Disabled"); + GB_log(gb, " Object size: %s\n", (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_SIZE)? "8x16" : "8x8"); + GB_log(gb, " Background tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_MAP)? "$9C00" : "$9800"); + GB_log(gb, " Background and Window Tileset: %s\n", (gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL)? "$8000" : "$8800"); + GB_log(gb, " Window: %s\n", (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE)? "Enabled" : "Disabled"); + GB_log(gb, " Window tilemap: %s\n", (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_MAP)? "$9C00" : "$9800"); GB_log(gb, "\nSTAT:\n"); static const char *modes[] = {"Mode 0, H-Blank", "Mode 1, V-Blank", "Mode 2, OAM", "Mode 3, Rendering"}; @@ -1704,7 +1798,7 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg GB_log(gb, "\nCurrent line: %d\n", gb->current_line); GB_log(gb, "Current state: "); - if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) { + if (!(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) { GB_log(gb, "Off\n"); } else if (gb->display_state == 7 || gb->display_state == 8) { @@ -1714,8 +1808,13 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg GB_log(gb, "Glitched line 0 OAM mode (%d cycles to next event)\n", -gb->display_cycles / 2); } else if (gb->mode_for_interrupt == 3) { - signed pixel = gb->position_in_line > 160? (int8_t) gb->position_in_line : gb->position_in_line; - GB_log(gb, "Rendering pixel (%d/160)\n", pixel); + if (((uint8_t)(gb->position_in_line + 16) < 8)) { + GB_log(gb, "Adjusting for scrolling (%d/%d)\n", gb->position_in_line & 7, gb->io_registers[GB_IO_SCX] & 7); + } + else { + signed pixel = gb->position_in_line > 160? (int8_t) gb->position_in_line : gb->position_in_line; + GB_log(gb, "Rendering pixel (%d/160)\n", pixel); + } } else { GB_log(gb, "Sleeping (%d cycles to next event)\n", -gb->display_cycles / 2); @@ -1724,6 +1823,17 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg GB_log(gb, "LYC: %d\n", gb->io_registers[GB_IO_LYC]); GB_log(gb, "Window position: %d, %d\n", (signed) gb->io_registers[GB_IO_WX] - 7, gb->io_registers[GB_IO_WY]); GB_log(gb, "Interrupt line: %s\n", gb->stat_interrupt_line? "On" : "Off"); + GB_log(gb, "Background shifter size: %d\n", gb->bg_fifo.size); + GB_log(gb, "Background fetcher state: %s\n", inline_const(const char *[], { + "Tile (1/2)", + "Tile (2/2)", + "Low data (1/2)", + "Low data (2/2)", + "High data (1/2)", + "High data (2/2)", + "Push (1/2)", + "Push (2/2)", + })[gb->fetcher_state & 7]); return true; } @@ -1731,54 +1841,60 @@ static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { NO_MODIFIERS - if (strlen(lstrip(arguments))) { - print_usage(gb, command); - return true; - } - - - GB_log(gb, "Current state: "); - if (!gb->apu.global_enable) { - GB_log(gb, "Disabled\n"); - } - else { - GB_log(gb, "Enabled\n"); - for (uint8_t channel = 0; channel < GB_N_CHANNELS; channel++) { - GB_log(gb, "CH%u is %s, DAC %s; current sample = 0x%x\n", channel + 1, - gb->apu.is_active[channel] ? "active " : "inactive", - GB_apu_is_DAC_enabled(gb, channel) ? "active " : "inactive", - gb->apu.samples[channel]); + const char *stripped = lstrip(arguments); + if (strlen(stripped)) { + if (stripped[0] != 0 && (stripped[0] < '1' || stripped[0] > '5')) { + print_usage(gb, command); + return true; } } - GB_log(gb, "SO1 (left output): volume %u,", gb->io_registers[GB_IO_NR50] & 0x07); - if (gb->io_registers[GB_IO_NR51] & 0x0f) { - for (uint8_t channel = 0, mask = 0x01; channel < GB_N_CHANNELS; channel++, mask <<= 1) { - if (gb->io_registers[GB_IO_NR51] & mask) { - GB_log(gb, " CH%u", channel + 1); + if (stripped[0] == 0 || stripped[0] == '5') { + GB_log(gb, "Current state: "); + if (!gb->apu.global_enable) { + GB_log(gb, "Disabled\n"); + } + else { + GB_log(gb, "Enabled\n"); + for (uint8_t channel = 0; channel < GB_N_CHANNELS; channel++) { + GB_log(gb, "CH%u is %s, DAC %s; current sample = 0x%x\n", channel + 1, + gb->apu.is_active[channel] ? "active " : "inactive", + GB_apu_is_DAC_enabled(gb, channel) ? "active " : "inactive", + gb->apu.samples[channel]); } } - } - else { - GB_log(gb, " no channels"); - } - GB_log(gb, "%s\n", gb->io_registers[GB_IO_NR50] & 0x80 ? " VIN": ""); - GB_log(gb, "SO2 (right output): volume %u,", gb->io_registers[GB_IO_NR50] & 0x70 >> 4); - if (gb->io_registers[GB_IO_NR51] & 0xf0) { - for (uint8_t channel = 0, mask = 0x10; channel < GB_N_CHANNELS; channel++, mask <<= 1) { - if (gb->io_registers[GB_IO_NR51] & mask) { - GB_log(gb, " CH%u", channel + 1); + GB_log(gb, "SO1 (left output): volume %u,", gb->io_registers[GB_IO_NR50] & 0x07); + if (gb->io_registers[GB_IO_NR51] & 0x0F) { + for (uint8_t channel = 0, mask = 0x01; channel < GB_N_CHANNELS; channel++, mask <<= 1) { + if (gb->io_registers[GB_IO_NR51] & mask) { + GB_log(gb, " CH%u", channel + 1); + } } } + else { + GB_log(gb, " no channels"); + } + GB_log(gb, "%s\n", gb->io_registers[GB_IO_NR50] & 0x80 ? " VIN": ""); + + GB_log(gb, "SO2 (right output): volume %u,", gb->io_registers[GB_IO_NR50] & 0x70 >> 4); + if (gb->io_registers[GB_IO_NR51] & 0xF0) { + for (uint8_t channel = 0, mask = 0x10; channel < GB_N_CHANNELS; channel++, mask <<= 1) { + if (gb->io_registers[GB_IO_NR51] & mask) { + GB_log(gb, " CH%u", channel + 1); + } + } + } + else { + GB_log(gb, " no channels"); + } + GB_log(gb, "%s\n", gb->io_registers[GB_IO_NR50] & 0x80 ? " VIN": ""); } - else { - GB_log(gb, " no channels"); - } - GB_log(gb, "%s\n", gb->io_registers[GB_IO_NR50] & 0x80 ? " VIN": ""); for (uint8_t channel = GB_SQUARE_1; channel <= GB_SQUARE_2; channel++) { + if (stripped[0] != 0 && stripped[0] != ('1') + channel) continue; + GB_log(gb, "\nCH%u:\n", channel + 1); GB_log(gb, " Current volume: %u, current sample length: %u APU ticks (next in %u ticks)\n", gb->apu.square_channels[channel].current_volume, @@ -1793,8 +1909,8 @@ static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg uint8_t duty = gb->io_registers[channel == GB_SQUARE_1? GB_IO_NR11 :GB_IO_NR21] >> 6; GB_log(gb, " Duty cycle %s%% (%s), current index %u/8%s\n", - duty > 3? "" : (const char *[]){"12.5", " 25", " 50", " 75"}[duty], - duty > 3? "" : (const char *[]){"_______-", "-______-", "-____---", "_------_"}[duty], + duty > 3? "" : inline_const(const char *[], {"12.5", " 25", " 50", " 75"})[duty], + duty > 3? "" : inline_const(const char *[], {"_______-", "-______-", "-____---", "_------_"})[duty], gb->apu.square_channels[channel].current_sample_index, gb->apu.square_channels[channel].sample_surpressed ? " (suppressed)" : ""); @@ -1803,8 +1919,8 @@ static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg ((gb->io_registers[GB_IO_NR10] & 0x7) && (gb->io_registers[GB_IO_NR10] & 0x70))? "active" : "inactive", (gb->io_registers[GB_IO_NR10] & 0x8) ? "decreasing" : "increasing"); if (gb->apu.square_sweep_calculate_countdown) { - GB_log(gb, " On going frequency calculation will be ready in %u APU ticks\n", - gb->apu.square_sweep_calculate_countdown); + GB_log(gb, " On-going frequency calculation will be ready in %u APU ticks\n", + gb->apu.square_sweep_calculate_countdown * 2 + 1 - gb->apu.lf_div); } else { GB_log(gb, " Shadow frequency register: 0x%03x\n", gb->apu.shadow_sweep_sample_length); @@ -1818,50 +1934,53 @@ static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg } } + if (stripped[0] == 0 || stripped[0] == '3') { + GB_log(gb, "\nCH3:\n"); + GB_log(gb, " Wave:"); + for (uint8_t i = 0; i < 16; i++) { + GB_log(gb, "%s%X", i % 2? "" : " ", gb->io_registers[GB_IO_WAV_START + i] >> 4); + GB_log(gb, "%X", gb->io_registers[GB_IO_WAV_START + i] & 0xF); + } + GB_log(gb, "\n"); + GB_log(gb, " Current position: %u\n", gb->apu.wave_channel.current_sample_index); - GB_log(gb, "\nCH3:\n"); - GB_log(gb, " Wave:"); - for (uint8_t i = 0; i < 16; i++) { - GB_log(gb, "%s%X", i % 2? "" : " ", gb->io_registers[GB_IO_WAV_START + i] >> 4); - GB_log(gb, "%X", gb->io_registers[GB_IO_WAV_START + i] & 0xF); - } - GB_log(gb, "\n"); - GB_log(gb, " Current position: %u\n", gb->apu.wave_channel.current_sample_index); + GB_log(gb, " Volume %s (right-shifted %u times)\n", + gb->apu.wave_channel.shift > 4? "" : inline_const(const char *[], {"100%", "50%", "25%", "", "muted"})[gb->apu.wave_channel.shift], + gb->apu.wave_channel.shift); - GB_log(gb, " Volume %s (right-shifted %u times)\n", - gb->apu.wave_channel.shift > 4? "" : (const char *[]){"100%", "50%", "25%", "", "muted"}[gb->apu.wave_channel.shift], - gb->apu.wave_channel.shift); + GB_log(gb, " Current sample length: %u APU ticks (next in %u ticks)\n", + gb->apu.wave_channel.sample_length ^ 0x7FF, + gb->apu.wave_channel.sample_countdown); - GB_log(gb, " Current sample length: %u APU ticks (next in %u ticks)\n", - gb->apu.wave_channel.sample_length ^ 0x7ff, - gb->apu.wave_channel.sample_countdown); - - if (gb->apu.wave_channel.length_enabled) { - GB_log(gb, " Channel will end in %u 256 Hz ticks\n", - gb->apu.wave_channel.pulse_length); + if (gb->apu.wave_channel.length_enabled) { + GB_log(gb, " Channel will end in %u 256 Hz ticks\n", + gb->apu.wave_channel.pulse_length); + } } - GB_log(gb, "\nCH4:\n"); - GB_log(gb, " Current volume: %u, current internal counter: 0x%04x (next increase in %u ticks)\n", - gb->apu.noise_channel.current_volume, - gb->apu.noise_channel.counter, - gb->apu.noise_channel.counter_countdown); + if (stripped[0] == 0 || stripped[0] == '4') { + GB_log(gb, "\nCH4:\n"); + GB_log(gb, " Current volume: %u, current internal counter: 0x%04x (next increase in %u ticks)\n", + gb->apu.noise_channel.current_volume, + gb->apu.noise_channel.counter, + gb->apu.noise_channel.counter_countdown); - GB_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n", - gb->apu.noise_channel.volume_countdown, - gb->io_registers[GB_IO_NR42] & 8 ? "in" : "de", - gb->io_registers[GB_IO_NR42] & 7); + GB_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n", + gb->apu.noise_channel.volume_countdown, + gb->io_registers[GB_IO_NR42] & 8 ? "in" : "de", + gb->io_registers[GB_IO_NR42] & 7); - GB_log(gb, " LFSR in %u-step mode, current value ", - gb->apu.noise_channel.narrow? 7 : 15); - for (uint16_t lfsr = gb->apu.noise_channel.lfsr, i = 15; i--; lfsr <<= 1) { - GB_log(gb, "%u%s", (lfsr >> 14) & 1, i%4 ? "" : " "); - } + GB_log(gb, " LFSR in %u-step mode, current value ", + gb->apu.noise_channel.narrow? 7 : 15); + nounroll for (uint16_t lfsr = gb->apu.noise_channel.lfsr, i = 15; i--; lfsr <<= 1) { + GB_log(gb, "%u%s", (lfsr >> 14) & 1, i%4 ? "" : " "); + } - if (gb->apu.noise_channel.length_enabled) { - GB_log(gb, " Channel will end in %u 256 Hz ticks\n", - gb->apu.noise_channel.pulse_length); + if (gb->apu.noise_channel.length_enabled) { + GB_log(gb, " Channel will end in %u 256 Hz ticks\n", + gb->apu.noise_channel.pulse_length); + } } @@ -1875,7 +1994,7 @@ static char *wave_completer(GB_gameboy_t *gb, const char *string, uintptr_t *con size_t length = strlen(string); const char *suggestions[] = {"c", "f", "l"}; while (*context < sizeof(suggestions) / sizeof(suggestions[0])) { - if (memcmp(string, suggestions[*context], length) == 0) { + if (strncmp(string, suggestions[*context], length) == 0) { return strdup(suggestions[(*context)++] + length); } (*context)++; @@ -1901,9 +2020,9 @@ static bool wave(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug break; } } - mask = (0xf << (shift_amount - 1)) & 0xf; + mask = (0xF << (shift_amount - 1)) & 0xF; - for (int8_t cur_val = 0xf & mask; cur_val >= 0; cur_val -= shift_amount) { + for (int8_t cur_val = 0xF & mask; cur_val >= 0; cur_val -= shift_amount) { for (uint8_t i = 0; i < 32; i++) { uint8_t sample = i & 1? (gb->io_registers[GB_IO_WAV_START + i / 2] & 0xF) : @@ -1946,57 +2065,118 @@ static bool undo(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug return true; } +#ifndef DISABLE_REWIND +static bool backstep(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) +{ + NO_MODIFIERS + STOPPED_ONLY + + if (strlen(lstrip(arguments))) { + print_usage(gb, command); + return true; + } + + bool didPop = false; +retry:; + typeof(gb->rewind_sequences[0]) *sequence = &gb->rewind_sequences[gb->rewind_pos]; + if (!gb->rewind_sequences || !sequence->key_state) { + if (gb->rewind_buffer_length == 0) { + GB_log(gb, "Backstepping requires enabling rewinding\n"); + } + else { + GB_log(gb, "Reached the end of the rewind buffer\n"); + if (didPop) { + GB_rewind_push(gb); + sequence = &gb->rewind_sequences[gb->rewind_pos]; + sequence->instruction_count[sequence->pos] = 1; + } + } + return true; + } + + gb->backstep_instructions = sequence->instruction_count[sequence->pos] - 2; + if (gb->backstep_instructions == (uint32_t)-1) { // This frame was just pushed, pop it and try again + GB_rewind_pop(gb); + gb->backstep_instructions = 0; + didPop = true; + goto retry; + } + else if (gb->backstep_instructions > 0x20000) { + GB_log(gb, "Backstepping is currently not available\n"); + gb->backstep_instructions = 0; + return true; + } + GB_rewind_pop(gb); + GB_rewind_push(gb); + sequence = &gb->rewind_sequences[gb->rewind_pos]; + sequence->instruction_count[sequence->pos] = 1; + while (gb->backstep_instructions) { + GB_run(gb); + } + GB_cpu_disassemble(gb, gb->pc, 5); + return true; +} +#endif + static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command); -#define HELP_NEWLINE "\n " /* Commands without implementations are aliases of the previous non-alias commands */ static const debugger_command_t commands[] = { {"continue", 1, cont, "Continue running until next stop"}, + {"interrupt", 1, interrupt, "Interrupt the program execution"}, + {"reset", 3, reset, "Reset the program execution. " + "Add 'quick' as an argument to perform a quick reset that does not reset RAM. " + "Add 'reload' as an argument to reload the ROM and symbols before resetting.", + "[quick|reload]", .argument_completer = reset_completer}, {"next", 1, next, "Run the next instruction, skipping over function calls"}, {"step", 1, step, "Run the next instruction, stepping into function calls"}, {"finish", 1, finish, "Run until the current function returns"}, - {"undo", 1, undo, "Reverts the last command"}, - {"backtrace", 2, backtrace, "Displays the current call stack"}, - {"bt", 2, }, /* Alias */ - {"sld", 3, stack_leak_detection, "Like finish, but stops if a stack leak is detected"}, - {"ticks", 2, ticks, "Displays the number of CPU ticks since the last time 'ticks' was" HELP_NEWLINE - "used"}, +#ifndef DISABLE_REWIND + {"backstep", 5, backstep, "Step one instruction backwards, assuming constant inputs"}, + {"bs", 2, }, /* Alias */ +#endif + {"undo", 1, undo, "Revert the last command"}, {"registers", 1, registers, "Print values of processor registers and other important registers"}, - {"cartridge", 2, mbc, "Displays information about the MBC and cartridge"}, - {"mbc", 3, }, /* Alias */ - {"apu", 3, apu, "Displays information about the current state of the audio chip"}, - {"wave", 3, wave, "Prints a visual representation of the wave RAM." HELP_NEWLINE - "Modifiers can be used for a (f)ull print (the default)," HELP_NEWLINE - "a more (c)ompact one, or a one-(l)iner", "", "(f|c|l)", .modifiers_completer = wave_completer}, - {"lcd", 3, lcd, "Displays information about the current state of the LCD controller"}, - {"palettes", 3, palettes, "Displays the current CGB palettes"}, - {"dma", 3, dma, "Displays the current OAM DMA status"}, - {"softbreak", 2, softbreak, "Enables or disables software breakpoints", "(on|off)", .argument_completer = on_off_completer}, - {"breakpoint", 1, breakpoint, "Add a new breakpoint at the specified address/expression" HELP_NEWLINE - "Can also modify the condition of existing breakpoints." HELP_NEWLINE - "If the j modifier is used, the breakpoint will occur just before" HELP_NEWLINE - "jumping to the target.", - "[ if ]", "j", - .argument_completer = symbol_completer, .modifiers_completer = j_completer}, - {"delete", 2, delete, "Delete a breakpoint by its address, or all breakpoints", "[]", .argument_completer = symbol_completer}, - {"watch", 1, watch, "Add a new watchpoint at the specified address/expression." HELP_NEWLINE - "Can also modify the condition and type of existing watchpoints." HELP_NEWLINE - "Default watchpoint type is write-only.", - "[ if ]", "(r|w|rw)", - .argument_completer = symbol_completer, .modifiers_completer = rw_completer - }, - {"unwatch", 3, unwatch, "Delete a watchpoint by its address, or all watchpoints", "[]", .argument_completer = symbol_completer}, - {"list", 1, list, "List all set breakpoints and watchpoints"}, - {"print", 1, print, "Evaluate and print an expression" HELP_NEWLINE - "Use modifier to format as an address (a, default) or as a number in" HELP_NEWLINE + {"backtrace", 2, backtrace, "Display the current call stack"}, + {"bt", 2, }, /* Alias */ + {"print", 1, print, "Evaluate and print an expression. " + "Use modifier to format as an address (a, default) or as a number in " "decimal (d), hexadecimal (x), octal (o) or binary (b).", "", "format", .argument_completer = symbol_completer, .modifiers_completer = format_completer}, {"eval", 2, }, /* Alias */ {"examine", 2, examine, "Examine values at address", "", "count", .argument_completer = symbol_completer}, {"x", 1, }, /* Alias */ {"disassemble", 1, disassemble, "Disassemble instructions at address", "", "count", .argument_completer = symbol_completer}, - + {"breakpoint", 1, breakpoint, "Add a new breakpoint at the specified address/expression or range. " + "Ranges are exclusive by default, unless \"inclusive\" is used. " + "If the j modifier is used, the breakpoint will occur just before " + "jumping to the target.", + " [to [inclusive]] [if ]", "j", + .argument_completer = symbol_completer, .modifiers_completer = j_completer}, + {"delete", 2, delete, "Delete a breakpoint by its identifier, or all breakpoints", "[]"}, + {"watch", 1, watch, "Add a new watchpoint at the specified address/expression or range. " + "Ranges are exclusive by default, unless \"inclusive\" is used. " + "The default watchpoint type is write-only.", + " [to [inclusive]] [if ]", "(r|w|rw)", + .argument_completer = symbol_completer, .modifiers_completer = rw_completer + }, + {"unwatch", 3, unwatch, "Delete a watchpoint by its identifier, or all watchpoints", "[]"}, + {"softbreak", 2, softbreak, "Enable or disable software breakpoints ('ld b, b' opcodes)", "(on|off)", .argument_completer = on_off_completer}, + {"list", 1, list, "List all set breakpoints and watchpoints"}, + {"ticks", 2, ticks, "Display the number of CPU ticks since the last time 'ticks' was " + "used. Use 'keep' as an argument to display ticks without reseeting " + "the count.", "(keep)", .argument_completer = keep_completer}, + {"cartridge", 2, mbc, "Display information about the MBC and cartridge"}, + {"mbc", 3, }, /* Alias */ + {"apu", 3, apu, "Display information about the current state of the audio processing unit", + "[channel (1-4, 5 for NR5x)]"}, + {"wave", 3, wave, "Print a visual representation of the wave RAM. " + "Modifiers can be used for a (f)ull print (the default), " + "a more (c)ompact one, or a one-(l)iner", "", "(f|c|l)", .modifiers_completer = wave_completer}, + {"lcd", 3, lcd, "Display information about the current state of the LCD controller"}, + {"palettes", 3, palettes, "Display the current CGB palettes"}, + {"dma", 3, dma, "Display the current OAM DMA status"}, {"help", 1, help, "List available commands or show help for the specified command", "[]"}, {NULL,}, /* Null terminator */ @@ -2007,7 +2187,7 @@ static const debugger_command_t *find_command(const char *string) size_t length = strlen(string); for (const debugger_command_t *command = commands; command->command; command++) { if (command->min_length > length) continue; - if (memcmp(command->command, string, length) == 0) { /* Is a substring? */ + if (strncmp(command->command, string, length) == 0) { /* Is a substring? */ /* Aliases */ while (!command->implementation) { command--; @@ -2029,7 +2209,24 @@ static void print_command_description(GB_gameboy_t *gb, const debugger_command_t { print_command_shortcut(gb, command); GB_log(gb, ": "); - GB_log(gb, (const char *)&" %s\n" + strlen(command->command), command->help_string); + GB_log(gb, "%s", (const char *)&" " + strlen(command->command)); + + const char *string = command->help_string; + const unsigned width = 80 - 13; + nounroll while (strlen(string) > width) { + const char *space = string + width; + nounroll while (*space != ' ') { + space--; + if (space == string) { + // This help string has some extra long word? Abort line-breaking, it's going to break anyway. + GB_log(gb, "%s\n", string); + return; + } + } + GB_log(gb, "%.*s\n ", (unsigned)(space - string), string); + string = space + 1; + } + GB_log(gb, "%s\n", string); } static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *ignored) @@ -2052,7 +2249,7 @@ static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug } return true; } - for (command = commands; command->command; command++) { + nounroll for (command = commands; command->command; command++) { if (command->help_string) { print_command_description(gb, command); } @@ -2064,22 +2261,11 @@ void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr) { /* Called just after the CPU calls a function/enters an interrupt/etc... */ - if (gb->stack_leak_detection) { - if (gb->debug_call_depth >= sizeof(gb->sp_for_call_depth) / sizeof(gb->sp_for_call_depth[0])) { - GB_log(gb, "Potential stack overflow detected (Functions nest too much). \n"); - gb->debug_stopped = true; - } - else { - gb->sp_for_call_depth[gb->debug_call_depth] = gb->sp; - gb->addr_for_call_depth[gb->debug_call_depth] = gb->pc; - } - } - if (gb->backtrace_size < sizeof(gb->backtrace_sps) / sizeof(gb->backtrace_sps[0])) { - while (gb->backtrace_size) { - if (gb->backtrace_sps[gb->backtrace_size - 1] < gb->sp) { + if (gb->backtrace_sps[gb->backtrace_size - 1] <= gb->sp) { gb->backtrace_size--; + gb->debug_call_depth--; } else { break; @@ -2090,35 +2276,18 @@ void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr) gb->backtrace_returns[gb->backtrace_size].bank = bank_for_addr(gb, call_addr); gb->backtrace_returns[gb->backtrace_size].addr = call_addr; gb->backtrace_size++; + gb->debug_call_depth++; } - - gb->debug_call_depth++; } void GB_debugger_ret_hook(GB_gameboy_t *gb) { /* Called just before the CPU runs ret/reti */ - gb->debug_call_depth--; - - if (gb->stack_leak_detection) { - if (gb->debug_call_depth < 0) { - GB_log(gb, "Function finished without a stack leak.\n"); - gb->debug_stopped = true; - } - else { - if (gb->sp != gb->sp_for_call_depth[gb->debug_call_depth]) { - GB_log(gb, "Stack leak detected for function %s!\n", value_to_string(gb, gb->addr_for_call_depth[gb->debug_call_depth], true)); - GB_log(gb, "SP is $%04x, should be $%04x.\n", gb->sp, - gb->sp_for_call_depth[gb->debug_call_depth]); - gb->debug_stopped = true; - } - } - } - while (gb->backtrace_size) { if (gb->backtrace_sps[gb->backtrace_size - 1] <= gb->sp) { gb->backtrace_size--; + gb->debug_call_depth--; } else { break; @@ -2126,99 +2295,66 @@ void GB_debugger_ret_hook(GB_gameboy_t *gb) } } -static bool _GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, value_t addr, uint8_t value) -{ - uint16_t index = find_watchpoint(gb, addr); - uint32_t key = WP_KEY(addr); - if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) { - if (!(gb->watchpoints[index].flags & GB_WATCHPOINT_W)) { - return false; +// Returns the id or 0 +static void test_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t flags, uint8_t value) +{ + if (unlikely(gb->backstep_instructions)) return; + uint16_t bank = bank_for_addr(gb, addr); + for (unsigned i = 0; i < gb->n_watchpoints; i++) { + struct GB_watchpoint_s *watchpoint = &gb->watchpoints[i]; + if (watchpoint->bank != (uint16_t)-1) { + if (watchpoint->bank != bank) continue; } - if (!gb->watchpoints[index].condition) { - gb->debug_stopped = true; - GB_log(gb, "Watchpoint: [%s] = $%02x\n", debugger_value_to_string(gb, addr, true), value); - return true; + if (!(watchpoint->flags & flags)) continue; + if (addr < watchpoint->addr) continue; + if (addr > (uint32_t)watchpoint->addr + watchpoint->length + watchpoint->inclusive) continue; + if (!watchpoint->condition) { + condition_ok: + GB_debugger_break(gb); + if (flags == WATCHPOINT_READ) { + GB_log(gb, "Watchpoint %u: [%s]\n", watchpoint->id, value_to_string(gb, addr, true, false)); + } + else { + GB_log(gb, "Watchpoint %u: [%s] = $%02x\n", watchpoint->id, value_to_string(gb, addr, true, false), value); + } + return; } bool error; - bool condition = debugger_evaluate(gb, gb->watchpoints[index].condition, - (unsigned)strlen(gb->watchpoints[index].condition), &error, &addr.value, &value).value; + bool condition = debugger_evaluate(gb, watchpoint->condition, + (unsigned)strlen(watchpoint->condition), + &error, &addr, flags == WATCHPOINT_WRITE? &value : NULL).value; if (error) { - /* Should never happen */ - GB_log(gb, "An internal error has occured\n"); - return false; + GB_log(gb, "The condition for watchpoint %u is no longer a valid expression\n", watchpoint->id); + GB_debugger_break(gb); } if (condition) { - gb->debug_stopped = true; - GB_log(gb, "Watchpoint: [%s] = $%02x\n", debugger_value_to_string(gb, addr, true), value); - return true; + goto condition_ok; } } - return false; } void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { if (gb->debug_stopped) return; - - /* Try any-bank breakpoint */ - value_t full_addr = (VALUE_16(addr)); - if (_GB_debugger_test_write_watchpoint(gb, full_addr, value)) return; - - /* Try bank-specific breakpoint */ - full_addr.has_bank = true; - full_addr.bank = bank_for_addr(gb, addr); - _GB_debugger_test_write_watchpoint(gb, full_addr, value); + test_watchpoint(gb, addr, WATCHPOINT_WRITE, value); } -static bool _GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, value_t addr) -{ - uint16_t index = find_watchpoint(gb, addr); - uint32_t key = WP_KEY(addr); - - if (index < gb->n_watchpoints && gb->watchpoints[index].key == key) { - if (!(gb->watchpoints[index].flags & GB_WATCHPOINT_R)) { - return false; - } - if (!gb->watchpoints[index].condition) { - gb->debug_stopped = true; - GB_log(gb, "Watchpoint: [%s]\n", debugger_value_to_string(gb, addr, true)); - return true; - } - bool error; - bool condition = debugger_evaluate(gb, gb->watchpoints[index].condition, - (unsigned)strlen(gb->watchpoints[index].condition), &error, &addr.value, NULL).value; - if (error) { - /* Should never happen */ - GB_log(gb, "An internal error has occured\n"); - return false; - } - if (condition) { - gb->debug_stopped = true; - GB_log(gb, "Watchpoint: [%s]\n", debugger_value_to_string(gb, addr, true)); - return true; - } - } - return false; -} void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr) { if (gb->debug_stopped) return; - - /* Try any-bank breakpoint */ - value_t full_addr = (VALUE_16(addr)); - if (_GB_debugger_test_read_watchpoint(gb, full_addr)) return; - - /* Try bank-specific breakpoint */ - full_addr.has_bank = true; - full_addr.bank = bank_for_addr(gb, addr); - _GB_debugger_test_read_watchpoint(gb, full_addr); + test_watchpoint(gb, addr, WATCHPOINT_READ, 0); } /* Returns true if debugger waits for more commands */ bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + + while (*input == ' ') { + input++; + } if (!input[0]) { return true; } @@ -2244,6 +2380,7 @@ bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input) modifiers++; } + gb->help_shown = true; const debugger_command_t *command = find_command(command_string); if (command) { uint8_t *old_state = malloc(GB_get_save_state_size_no_bess(gb)); @@ -2272,7 +2409,7 @@ bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input) return ret; } else { - GB_log(gb, "%s: no such command.\n", command_string); + GB_log(gb, "%s: no such command. Type 'help' to list the available debugger commands.\n", command_string); return true; } } @@ -2308,7 +2445,7 @@ char *GB_debugger_complete_substring(GB_gameboy_t *gb, char *input, uintptr_t *c } for (const debugger_command_t *command = &commands[*context]; command->command; command++) { (*context)++; - if (memcmp(command->command, command_string, length) == 0) { /* Is a substring? */ + if (strncmp(command->command, command_string, length) == 0) { /* Is a substring? */ return strdup(command->command + length); } } @@ -2339,12 +2476,10 @@ typedef enum { JUMP_TO_NONTRIVIAL, } jump_to_return_t; -static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *address); +static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *address, unsigned *breakpoint_id); -void GB_debugger_run(GB_gameboy_t *gb) +static void noinline debugger_run(GB_gameboy_t *gb) { - if (gb->debug_disable) return; - if (!gb->undo_state) { gb->undo_state = malloc(GB_get_save_state_size_no_bess(gb)); GB_save_state_to_buffer_no_bess(gb, gb->undo_state); @@ -2352,47 +2487,53 @@ void GB_debugger_run(GB_gameboy_t *gb) char *input = NULL; if (gb->debug_next_command && gb->debug_call_depth <= 0 && !gb->halted) { - gb->debug_stopped = true; + GB_debugger_break(gb); } - if (gb->debug_fin_command && gb->debug_call_depth == -1) { - gb->debug_stopped = true; + if (gb->debug_fin_command && gb->debug_call_depth <= -1) { + GB_debugger_break(gb); } if (gb->debug_stopped) { + if (!gb->help_shown) { + gb->help_shown = true; + GB_log(gb, "Type 'help' to list the available debugger commands.\n"); + } GB_cpu_disassemble(gb, gb->pc, 5); } next_command: if (input) { free(input); } - if (gb->breakpoints && !gb->debug_stopped && should_break(gb, gb->pc, false)) { - gb->debug_stopped = true; - GB_log(gb, "Breakpoint: PC = %s\n", value_to_string(gb, gb->pc, true)); + unsigned breakpoint_id = 0; + if (gb->breakpoints && !gb->debug_stopped && (breakpoint_id = should_break(gb, gb->pc, false))) { + GB_debugger_break(gb); + GB_log(gb, "Breakpoint %u: PC = %s\n", breakpoint_id, value_to_string(gb, gb->pc, true, false)); GB_cpu_disassemble(gb, gb->pc, 5); } if (gb->breakpoints && !gb->debug_stopped) { uint16_t address = 0; - jump_to_return_t jump_to_result = test_jump_to_breakpoints(gb, &address); + jump_to_return_t jump_to_result = test_jump_to_breakpoints(gb, &address, &breakpoint_id); bool should_delete_state = true; - if (gb->nontrivial_jump_state && should_break(gb, gb->pc, true)) { + if (jump_to_result == JUMP_TO_BREAK) { + GB_debugger_break(gb); + GB_log(gb, "Jumping to breakpoint %u: %s\n", breakpoint_id, value_to_string(gb, address, true, false)); + GB_cpu_disassemble(gb, gb->pc, 5); + gb->non_trivial_jump_breakpoint_occured = false; + } + else if (gb->nontrivial_jump_state && (breakpoint_id = should_break(gb, gb->pc, true))) { if (gb->non_trivial_jump_breakpoint_occured) { gb->non_trivial_jump_breakpoint_occured = false; } else { gb->non_trivial_jump_breakpoint_occured = true; - GB_log(gb, "Jumping to breakpoint: PC = %s\n", value_to_string(gb, gb->pc, true)); - GB_cpu_disassemble(gb, gb->pc, 5); + GB_log(gb, "Jumping to breakpoint %u: %s\n", breakpoint_id, value_to_string(gb, gb->pc, true, false)); GB_load_state_from_buffer(gb, gb->nontrivial_jump_state, -1); - gb->debug_stopped = true; + GB_rewind_push(gb); + GB_cpu_disassemble(gb, gb->pc, 5); + GB_debugger_break(gb); } } - else if (jump_to_result == JUMP_TO_BREAK) { - gb->debug_stopped = true; - GB_log(gb, "Jumping to breakpoint: PC = %s\n", value_to_string(gb, address, true)); - GB_cpu_disassemble(gb, gb->pc, 5); - gb->non_trivial_jump_breakpoint_occured = false; - } else if (jump_to_result == JUMP_TO_NONTRIVIAL) { if (!gb->nontrivial_jump_state) { gb->nontrivial_jump_state = malloc(GB_get_save_state_size_no_bess(gb)); @@ -2416,12 +2557,12 @@ next_command: if (gb->debug_stopped && !gb->debug_disable) { gb->debug_next_command = false; gb->debug_fin_command = false; - gb->stack_leak_detection = false; input = gb->input_callback(gb); if (input == NULL) { /* Debugging is no currently available, continue running */ gb->debug_stopped = false; + update_debug_active(gb); return; } @@ -2431,6 +2572,22 @@ next_command: free(input); } + update_debug_active(gb); +} +void GB_debugger_run(GB_gameboy_t *gb) +{ +#ifndef DISABLE_REWIND + if (gb->rewind_sequences && gb->rewind_sequences[gb->rewind_pos].key_state) { + typeof(gb->rewind_sequences[0]) *sequence = &gb->rewind_sequences[gb->rewind_pos]; + sequence->instruction_count[sequence->pos]++; + } + if (unlikely(gb->backstep_instructions)) { + gb->backstep_instructions--; + return; + } +#endif + if (likely(!gb->debug_active)) return; + debugger_run(gb); } void GB_debugger_handle_async_commands(GB_gameboy_t *gb) @@ -2439,13 +2596,19 @@ void GB_debugger_handle_async_commands(GB_gameboy_t *gb) while (gb->async_input_callback && (input = gb->async_input_callback(gb))) { GB_debugger_execute_command(gb, input); + update_debug_active(gb); free(input); } } void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol) { - bank &= 0x1FF; + if (bank >= gb->n_symbol_maps) { + gb->bank_symbols = realloc(gb->bank_symbols, (bank + 1) * sizeof(*gb->bank_symbols)); + while (bank >= gb->n_symbol_maps) { + gb->bank_symbols[gb->n_symbol_maps++] = NULL; + } + } if (!gb->bank_symbols[bank]) { gb->bank_symbols[bank] = GB_map_alloc(); @@ -2487,7 +2650,7 @@ void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path) void GB_debugger_clear_symbols(GB_gameboy_t *gb) { - for (unsigned i = sizeof(gb->bank_symbols) / sizeof(gb->bank_symbols[0]); i--;) { + for (unsigned i = gb->n_symbol_maps; i--;) { if (gb->bank_symbols[i]) { GB_map_free(gb->bank_symbols[i]); gb->bank_symbols[i] = 0; @@ -2500,36 +2663,64 @@ void GB_debugger_clear_symbols(GB_gameboy_t *gb) gb->reversed_symbol_map.buckets[i] = next; } } + gb->n_symbol_maps = 0; + if (gb->bank_symbols) { + free(gb->bank_symbols); + gb->bank_symbols = NULL; + } } -const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr) +const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr, bool prefer_local) { uint16_t bank = bank_for_addr(gb, addr); - const GB_bank_symbol_t *symbol = GB_map_find_symbol(gb->bank_symbols[bank], addr); + const GB_bank_symbol_t *symbol = GB_map_find_symbol(get_symbol_map(gb, bank), addr, prefer_local); if (symbol) return symbol; - if (bank != 0) return GB_map_find_symbol(gb->bank_symbols[0], addr); /* Maybe the symbol incorrectly uses bank 0? */ + if (bank != 0) return GB_map_find_symbol(get_symbol_map(gb, 0), addr, prefer_local); /* Maybe the symbol incorrectly uses bank 0? */ return NULL; } const char *GB_debugger_name_for_address(GB_gameboy_t *gb, uint16_t addr) { - const GB_bank_symbol_t *symbol = GB_debugger_find_symbol(gb, addr); - if (symbol && symbol->addr == addr) return symbol->name; - return NULL; + return GB_debugger_describe_address(gb, addr, -1, true, false); +} + +const char *GB_debugger_describe_address(GB_gameboy_t *gb, + uint16_t addr, uint16_t bank, + bool exact_match, bool prefer_local) +{ + if (bank == (uint16_t)-1) { + bank = bank_for_addr(gb, addr); + } + if (exact_match) { + const GB_bank_symbol_t *symbol = GB_map_find_symbol(get_symbol_map(gb, bank), addr, prefer_local); + if (symbol && symbol->addr == addr) return symbol->name; + if (bank != 0) symbol = GB_map_find_symbol(get_symbol_map(gb, 0), addr, prefer_local); /* Maybe the symbol incorrectly uses bank 0? */ + if (symbol && symbol->addr == addr) return symbol->name; + + return NULL; + } + + return debugger_value_to_string(gb, (value_t){ + .value = addr, + .bank = bank, + .has_bank = true, + }, true, prefer_local); } /* The public version of debugger_evaluate */ bool GB_debugger_evaluate(GB_gameboy_t *gb, const char *string, uint16_t *result, uint16_t *result_bank) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + bool error = false; value_t value = debugger_evaluate(gb, string, strlen(string), &error, NULL, NULL); if (result) { *result = value.value; } if (result_bank) { - *result_bank = value.has_bank? value.value : -1; + *result_bank = value.has_bank? value.bank : -1; } return error; } @@ -2537,6 +2728,7 @@ bool GB_debugger_evaluate(GB_gameboy_t *gb, const char *string, uint16_t *result void GB_debugger_break(GB_gameboy_t *gb) { gb->debug_stopped = true; + update_debug_active(gb); } bool GB_debugger_is_stopped(GB_gameboy_t *gb) @@ -2547,6 +2739,7 @@ bool GB_debugger_is_stopped(GB_gameboy_t *gb) void GB_debugger_set_disabled(GB_gameboy_t *gb, bool disabled) { gb->debug_disable = disabled; + update_debug_active(gb); } /* Jump-to breakpoints */ @@ -2573,17 +2766,17 @@ static bool is_in_trivial_memory(uint16_t addr) typedef uint16_t opcode_address_getter_t(GB_gameboy_t *gb, uint8_t opcode); -uint16_t trivial_1(GB_gameboy_t *gb, uint8_t opcode) +static uint16_t trivial_1(GB_gameboy_t *gb, uint8_t opcode) { return gb->pc + 1; } -uint16_t trivial_2(GB_gameboy_t *gb, uint8_t opcode) +static uint16_t trivial_2(GB_gameboy_t *gb, uint8_t opcode) { return gb->pc + 2; } -uint16_t trivial_3(GB_gameboy_t *gb, uint8_t opcode) +static uint16_t trivial_3(GB_gameboy_t *gb, uint8_t opcode) { return gb->pc + 3; } @@ -2698,12 +2891,12 @@ static opcode_address_getter_t *opcodes[256] = { trivial_2, trivial_1, trivial_3, trivial_1, NULL, NULL, trivial_2, rst, }; -static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *address) +static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *address, unsigned *breakpoint_id) { if (!gb->has_jump_to_breakpoints) return JUMP_TO_NONE; if (!is_in_trivial_memory(gb->pc) || !is_in_trivial_memory(gb->pc + 2) || - !is_in_trivial_memory(gb->sp) || !is_in_trivial_memory(gb->sp + 1)) { + !is_in_trivial_memory(gb->sp) || !is_in_trivial_memory(gb->sp - 1)) { return JUMP_TO_NONTRIVIAL; } @@ -2711,7 +2904,7 @@ static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *add if (gb->ime) { for (unsigned i = 0; i < 5; i++) { if ((gb->interrupt_enable & (1 << i)) && (gb->io_registers[GB_IO_IF] & (1 << i))) { - if (should_break(gb, 0x40 + i * 8, true)) { + if ((*breakpoint_id = should_break(gb, 0x40 + i * 8, true))) { if (address) { *address = 0x40 + i * 8; } @@ -2753,5 +2946,5 @@ static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *add *address = new_pc; } - return should_break(gb, new_pc, true) ? JUMP_TO_BREAK : JUMP_TO_NONE; + return (*breakpoint_id = should_break(gb, new_pc, true)) ? JUMP_TO_BREAK : JUMP_TO_NONE; } diff --git a/Core/debugger.h b/Core/debugger.h index 3d77b7a..8b46b70 100644 --- a/Core/debugger.h +++ b/Core/debugger.h @@ -1,33 +1,11 @@ -#ifndef debugger_h -#define debugger_h +#pragma once +#ifndef GB_DISABLE_DEBUGGER #include #include #include "defs.h" #include "symbol_hash.h" - -#ifdef GB_INTERNAL -#ifdef GB_DISABLE_DEBUGGER -#define GB_debugger_run(gb) (void)0 -#define GB_debugger_handle_async_commands(gb) (void)0 -#define GB_debugger_ret_hook(gb) (void)0 -#define GB_debugger_call_hook(gb, addr) (void)addr -#define GB_debugger_test_write_watchpoint(gb, addr, value) ((void)addr, (void)value) -#define GB_debugger_test_read_watchpoint(gb, addr) (void)addr -#define GB_debugger_add_symbol(gb, bank, address, symbol) ((void)bank, (void)address, (void)symbol) - -#else -internal void GB_debugger_run(GB_gameboy_t *gb); -internal void GB_debugger_handle_async_commands(GB_gameboy_t *gb); -internal void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr); -internal void GB_debugger_ret_hook(GB_gameboy_t *gb); -internal void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value); -internal void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr); -internal const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr); -internal void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol); -#endif /* GB_DISABLE_DEBUGGER */ -#endif - +void GB_debugger_break(GB_gameboy_t *gb); #ifdef GB_INTERNAL bool /* Returns true if debugger waits for more commands. Not relevant for non-GB_INTERNAL */ #else @@ -35,12 +13,36 @@ void #endif GB_debugger_execute_command(GB_gameboy_t *gb, char *input); /* Destroys input. */ char *GB_debugger_complete_substring(GB_gameboy_t *gb, char *input, uintptr_t *context); /* Destroys input, result requires free */ - void GB_debugger_load_symbol_file(GB_gameboy_t *gb, const char *path); const char *GB_debugger_name_for_address(GB_gameboy_t *gb, uint16_t addr); +/* Use -1 for bank to use the currently mapped bank */ +const char *GB_debugger_describe_address(GB_gameboy_t *gb, uint16_t addr, uint16_t bank, bool exact_match, bool prefer_local); bool GB_debugger_evaluate(GB_gameboy_t *gb, const char *string, uint16_t *result, uint16_t *result_bank); /* result_bank is -1 if unused. */ -void GB_debugger_break(GB_gameboy_t *gb); bool GB_debugger_is_stopped(GB_gameboy_t *gb); void GB_debugger_set_disabled(GB_gameboy_t *gb, bool disabled); void GB_debugger_clear_symbols(GB_gameboy_t *gb); -#endif /* debugger_h */ + +#ifdef GB_INTERNAL +internal void GB_debugger_run(GB_gameboy_t *gb); +internal void GB_debugger_handle_async_commands(GB_gameboy_t *gb); +internal void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr); +internal void GB_debugger_ret_hook(GB_gameboy_t *gb); +internal void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value); +internal void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr); +internal const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr, bool prefer_local); +internal void GB_debugger_add_symbol(GB_gameboy_t *gb, uint16_t bank, uint16_t address, const char *symbol); +#endif + +#else // GB_DISABLE_DEBUGGER +#ifdef GB_INTERNAL +#define GB_debugger_run(gb) (void)0 +#define GB_debugger_handle_async_commands(gb) (void)0 +#define GB_debugger_ret_hook(gb) (void)0 +#define GB_debugger_call_hook(gb, addr) (void)addr +#define GB_debugger_test_write_watchpoint(gb, addr, value) ((void)addr, (void)value) +#define GB_debugger_test_read_watchpoint(gb, addr) (void)addr +#define GB_debugger_add_symbol(gb, bank, address, symbol) ((void)bank, (void)address, (void)symbol) +#define GB_debugger_break(gb) (void)0 +#endif // GB_INTERNAL + +#endif // GB_DISABLE_DEBUGGER diff --git a/Core/defs.h b/Core/defs.h index b512986..29fb5dd 100644 --- a/Core/defs.h +++ b/Core/defs.h @@ -1,20 +1,41 @@ -#ifndef defs_h -#define defs_h +#pragma once + +#define GB_likely(x) __builtin_expect((bool)(x), 1) +#define GB_unlikely(x) __builtin_expect((bool)(x), 0) +#define GB_inline_const(type, ...) (*({static const typeof(type) _= __VA_ARGS__; &_;})) #ifdef GB_INTERNAL // "Keyword" definitions -#define likely(x) __builtin_expect((bool)(x), 1) -#define unlikely(x) __builtin_expect((bool)(x), 0) +#define likely(x) GB_likely(x) +#define unlikely(x) GB_unlikely(x) +#define inline_const GB_inline_const +#define typeof __typeof__ -#define internal __attribute__((visibility("internal"))) +#if !defined(MIN) +#define MIN(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) +#endif + +#if !defined(MAX) +#define MAX(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) +#endif + +#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 8) +#define __builtin_bswap16(x) ({ typeof(x) _x = (x); _x >> 8 | _x << 8; }) +#endif + +#define internal __attribute__((visibility("hidden"))) +#define noinline __attribute__((noinline)) #if __clang__ #define unrolled _Pragma("unroll") +#define nounroll _Pragma("clang loop unroll(disable)") #elif __GNUC__ >= 8 #define unrolled _Pragma("GCC unroll 8") +#define nounroll _Pragma("GCC unroll 0") #else #define unrolled +#define nounroll #endif #define unreachable() __builtin_unreachable(); @@ -37,10 +58,5 @@ #endif #endif -#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 8) -#define __builtin_bswap16(x) ({ typeof(x) _x = (x); _x >> 8 | _x << 8; }) -#endif - struct GB_gameboy_s; typedef struct GB_gameboy_s GB_gameboy_t; -#endif diff --git a/Core/display.c b/Core/display.c index 35ac8df..e2a70d0 100644 --- a/Core/display.c +++ b/Core/display.c @@ -9,27 +9,32 @@ static inline unsigned fifo_size(GB_fifo_t *fifo) { - return (fifo->write_end - fifo->read_end) & (GB_FIFO_LENGTH - 1); + return fifo->size; } static void fifo_clear(GB_fifo_t *fifo) { - fifo->read_end = fifo->write_end = 0; + fifo->read_end = fifo->size = 0; } -static GB_fifo_item_t *fifo_pop(GB_fifo_t *fifo) +static const GB_fifo_item_t *fifo_pop(GB_fifo_t *fifo) { + assert(fifo->size); + assert(fifo->size <= 8); GB_fifo_item_t *ret = &fifo->fifo[fifo->read_end]; fifo->read_end++; fifo->read_end &= (GB_FIFO_LENGTH - 1); + fifo->size--; return ret; } static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint8_t palette, bool bg_priority, bool flip_x) { + assert(fifo->size == 0); + fifo->size = 8; if (!flip_x) { - unrolled for (unsigned i = 8; i--;) { - fifo->fifo[fifo->write_end] = (GB_fifo_item_t) { + unrolled for (unsigned i = 0; i < 8; i++) { + fifo->fifo[i] = (GB_fifo_item_t) { (lower >> 7) | ((upper >> 7) << 1), palette, 0, @@ -37,14 +42,11 @@ static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint }; lower <<= 1; upper <<= 1; - - fifo->write_end++; - fifo->write_end &= (GB_FIFO_LENGTH - 1); } } else { - unrolled for (unsigned i = 8; i--;) { - fifo->fifo[fifo->write_end] = (GB_fifo_item_t) { + unrolled for (unsigned i = 0; i < 8; i++) { + fifo->fifo[i] = (GB_fifo_item_t) { (lower & 1) | ((upper & 1) << 1), palette, 0, @@ -52,19 +54,15 @@ static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint }; lower >>= 1; upper >>= 1; - - fifo->write_end++; - fifo->write_end &= (GB_FIFO_LENGTH - 1); } } } static void fifo_overlay_object_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint8_t palette, bool bg_priority, uint8_t priority, bool flip_x) { - while (fifo_size(fifo) < 8) { - fifo->fifo[fifo->write_end] = (GB_fifo_item_t) {0,}; - fifo->write_end++; - fifo->write_end &= (GB_FIFO_LENGTH - 1); + while (fifo->size < GB_FIFO_LENGTH) { + fifo->fifo[(fifo->read_end + fifo->size) & (GB_FIFO_LENGTH - 1)] = (GB_fifo_item_t) {0,}; + fifo->size++; } uint8_t flip_xor = flip_x? 0: 0x7; @@ -99,8 +97,7 @@ static void fifo_overlay_object_row(GB_fifo_t *fifo, uint8_t lower, uint8_t uppe #define WIDTH (160) #define BORDERED_WIDTH 256 #define BORDERED_HEIGHT 224 -#define FRAME_LENGTH (LCDC_PERIOD) -#define VIRTUAL_LINES (FRAME_LENGTH / LINE_LENGTH) // = 154 +#define VIRTUAL_LINES (LCDC_PERIOD / LINE_LENGTH) // = 154 typedef struct __attribute__((packed)) { uint8_t y; @@ -109,8 +106,8 @@ typedef struct __attribute__((packed)) { uint8_t flags; } object_t; -void GB_display_vblank(GB_gameboy_t *gb) -{ +void GB_display_vblank(GB_gameboy_t *gb, GB_vblank_type_t type) +{ gb->vblank_just_occured = true; gb->cycles_since_vblank_callback = 0; gb->lcd_disabled_outside_of_vblank = false; @@ -126,9 +123,19 @@ void GB_display_vblank(GB_gameboy_t *gb) } } - bool is_ppu_stopped = !GB_is_cgb(gb) && gb->stopped && gb->io_registers[GB_IO_LCDC] & 0x80; + if (GB_is_cgb(gb) && type == GB_VBLANK_TYPE_NORMAL_FRAME && gb->frame_repeat_countdown > 0 && gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON) { + GB_handle_rumble(gb); + + if (gb->vblank_callback) { + gb->vblank_callback(gb, GB_VBLANK_TYPE_REPEAT); + } + GB_timing_sync(gb); + return; + } - if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & 0x80) || is_ppu_stopped) || gb->cgb_repeated_a_frame || gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON)) { + bool is_ppu_stopped = !GB_is_cgb(gb) && gb->stopped && gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE; + + if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE) || is_ppu_stopped) || gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON)) { /* LCD is off, set screen to white or black (if LCD is on in stop mode) */ if (!GB_is_sgb(gb)) { uint32_t color = 0; @@ -165,7 +172,7 @@ void GB_display_vblank(GB_gameboy_t *gb) 0x30DA, 0x69AD, 0x2B57, 0x2B5D, 0x632C, 0x1050, 0x3C84, 0x0E07, 0x0E18, 0x2964, }; - unsigned index = gb->rom? gb->rom[0x14e] % 5 : 0; + unsigned index = gb->rom? gb->rom[0x14E] % 5 : 0; if (gb->model == GB_MODEL_CGB_0) { index = 1; // CGB 0 was only available in indigo! } @@ -214,7 +221,7 @@ void GB_display_vblank(GB_gameboy_t *gb) GB_handle_rumble(gb); if (gb->vblank_callback) { - gb->vblank_callback(gb); + gb->vblank_callback(gb, type); } GB_timing_sync(gb); } @@ -228,7 +235,7 @@ static inline void temperature_tint(double temperature, double *r, double *g, do *b = 0; } else { - *b = sqrt(0.75 - temperature); + *b = sqrt(0.75 - temperature) / sqrt(0.75); } } else { @@ -246,17 +253,17 @@ static inline uint8_t scale_channel(uint8_t x) static inline uint8_t scale_channel_with_curve(uint8_t x) { - return (uint8_t[]){0,6,12,20,28,36,45,56,66,76,88,100,113,125,137,149,161,172,182,192,202,210,218,225,232,238,243,247,250,252,254,255}[x]; + return inline_const(uint8_t[], {0,6,12,20,28,36,45,56,66,76,88,100,113,125,137,149,161,172,182,192,202,210,218,225,232,238,243,247,250,252,254,255})[x]; } static inline uint8_t scale_channel_with_curve_agb(uint8_t x) { - return (uint8_t[]){0,3,8,14,20,26,33,40,47,54,62,70,78,86,94,103,112,120,129,138,147,157,166,176,185,195,205,215,225,235,245,255}[x]; + return inline_const(uint8_t[], {0,3,8,14,20,26,33,40,47,54,62,70,78,86,94,103,112,120,129,138,147,157,166,176,185,195,205,215,225,235,245,255})[x]; } static inline uint8_t scale_channel_with_curve_sgb(uint8_t x) { - return (uint8_t[]){0,2,5,9,15,20,27,34,42,50,58,67,76,85,94,104,114,123,133,143,153,163,173,182,192,202,211,220,229,238,247,255}[x]; + return inline_const(uint8_t[], {0,2,5,9,15,20,27,34,42,50,58,67,76,85,94,104,114,123,133,143,153,163,173,182,192,202,211,220,229,238,247,255})[x]; } @@ -284,41 +291,69 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border) if (gb->color_correction_mode != GB_COLOR_CORRECTION_CORRECT_CURVES) { uint8_t new_r, new_g, new_b; - if (agb) { - new_g = (g * 6 + b * 1) / 7; + if (g != b) { // Minor optimization + double gamma = 2.2; + if (gb->color_correction_mode < GB_COLOR_CORRECTION_REDUCE_CONTRAST) { + /* Don't use absolutely gamma-correct mixing for the high-contrast + modes, to prevent the blue hues from being too washed out */ + gamma = 1.6; + } + + // TODO: Optimze pow out using a LUT + if (agb) { + new_g = round(pow((pow(g / 255.0, gamma) * 5 + pow(b / 255.0, gamma)) / 6, 1 / gamma) * 255); + } + else { + new_g = round(pow((pow(g / 255.0, gamma) * 3 + pow(b / 255.0, gamma)) / 4, 1 / gamma) * 255); + } } else { - new_g = (g * 3 + b) / 4; + new_g = g; } + new_r = r; new_b = b; if (gb->color_correction_mode == GB_COLOR_CORRECTION_REDUCE_CONTRAST) { r = new_r; - g = new_r; - b = new_r; + g = new_g; + b = new_b; - new_r = new_r * 7 / 8 + ( g + b) / 16; - new_g = new_g * 7 / 8 + (r + b) / 16; - new_b = new_b * 7 / 8 + (r + g ) / 16; + new_r = new_r * 15 / 16 + ( g + b) / 32; + new_g = new_g * 15 / 16 + (r + b) / 32; + new_b = new_b * 15 / 16 + (r + g ) / 32; - new_r = new_r * (224 - 32) / 255 + 32; - new_g = new_g * (220 - 36) / 255 + 36; - new_b = new_b * (216 - 40) / 255 + 40; + if (agb) { + new_r = new_r * (224 - 20) / 255 + 20; + new_g = new_g * (220 - 18) / 255 + 18; + new_b = new_b * (216 - 16) / 255 + 16; + } + else { + new_r = new_r * (220 - 40) / 255 + 40; + new_g = new_g * (224 - 36) / 255 + 36; + new_b = new_b * (216 - 32) / 255 + 32; + } } else if (gb->color_correction_mode == GB_COLOR_CORRECTION_LOW_CONTRAST) { r = new_r; - g = new_r; - b = new_r; + g = new_g; + b = new_b; + + new_r = new_r * 15 / 16 + ( g + b) / 32; + new_g = new_g * 15 / 16 + (r + b) / 32; + new_b = new_b * 15 / 16 + (r + g ) / 32; - new_r = new_r * 7 / 8 + ( g + b) / 16; - new_g = new_g * 7 / 8 + (r + b) / 16; - new_b = new_b * 7 / 8 + (r + g ) / 16; - - new_r = new_r * (162 - 67) / 255 + 67; - new_g = new_g * (167 - 62) / 255 + 62; - new_b = new_b * (157 - 58) / 255 + 58; + if (agb) { + new_r = new_r * (167 - 27) / 255 + 27; + new_g = new_g * (165 - 24) / 255 + 24; + new_b = new_b * (157 - 22) / 255 + 22; + } + else { + new_r = new_r * (162 - 45) / 255 + 45; + new_g = new_g * (167 - 41) / 255 + 41; + new_b = new_b * (157 - 38) / 255 + 38; + } } - else if (gb->color_correction_mode == GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS) { + else if (gb->color_correction_mode == GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST) { uint8_t old_max = MAX(r, MAX(g, b)); uint8_t new_max = MAX(new_r, MAX(new_g, new_b)); @@ -331,10 +366,10 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border) uint8_t old_min = MIN(r, MIN(g, b)); uint8_t new_min = MIN(new_r, MIN(new_g, new_b)); - if (new_min != 0xff) { - new_r = 0xff - (0xff - new_r) * (0xff - old_min) / (0xff - new_min); - new_g = 0xff - (0xff - new_g) * (0xff - old_min) / (0xff - new_min); - new_b = 0xff - (0xff - new_b) * (0xff - old_min) / (0xff - new_min); + if (new_min != 0xFF) { + new_r = 0xFF - (0xFF - new_r) * (0xFF - old_min) / (0xFF - new_min); + new_g = 0xFF - (0xFF - new_g) * (0xFF - old_min) / (0xFF - new_min); + new_b = 0xFF - (0xFF - new_b) * (0xFF - old_min) / (0xFF - new_min); } } r = new_r; @@ -367,7 +402,7 @@ void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t m { gb->color_correction_mode = mode; if (GB_is_cgb(gb)) { - for (unsigned i = 0; i < 32; i++) { + nounroll for (unsigned i = 0; i < 32; i++) { GB_palette_changed(gb, false, i * 2); GB_palette_changed(gb, true, i * 2); } @@ -378,25 +413,19 @@ void GB_set_light_temperature(GB_gameboy_t *gb, double temperature) { gb->light_temperature = temperature; if (GB_is_cgb(gb)) { - for (unsigned i = 0; i < 32; i++) { + nounroll for (unsigned i = 0; i < 32; i++) { GB_palette_changed(gb, false, i * 2); GB_palette_changed(gb, true, i * 2); } } } -/* - STAT interrupt is implemented based on this finding: - http://board.byuu.org/phpbb3/viewtopic.php?p=25527#p25531 - - General timing is based on GiiBiiAdvance's documents: - https://github.com/AntonioND/giibiiadvance - - */ - void GB_STAT_update(GB_gameboy_t *gb) { - if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) return; + if (!(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) return; + if (GB_is_dma_active(gb) && (gb->io_registers[GB_IO_STAT] & 3) == 2) { + gb->io_registers[GB_IO_STAT] &= ~3; + } bool previous_interrupt_line = gb->stat_interrupt_line; /* Set LY=LYC bit */ @@ -433,6 +462,7 @@ void GB_STAT_update(GB_gameboy_t *gb) void GB_lcd_off(GB_gameboy_t *gb) { + gb->cycles_for_line = 0; gb->display_state = 0; gb->display_cycles = 0; /* When the LCD is disabled, state is constant */ @@ -468,25 +498,32 @@ static inline uint8_t oam_read(GB_gameboy_t *gb, uint8_t addr) if (unlikely(gb->oam_ppu_blocked)) { return 0xFF; } - if (unlikely(gb->dma_current_dest <= 0xa0 && gb->dma_current_dest > 0)) { // TODO: what happens in the last and first M cycles? - return gb->oam[((gb->dma_current_dest - 1 + (gb->halted || gb->stopped)) & ~1) | (addr & 1)]; + if (unlikely(gb->dma_current_dest <= 0xA0 && gb->dma_current_dest > 0)) { // TODO: what happens in the last and first M cycles? + if (gb->hdma_in_progress) { + return GB_read_oam(gb, (gb->hdma_current_src & ~1) | (addr & 1)); + } + if (gb->dma_current_dest != 0xA0) { + return gb->oam[(gb->dma_current_dest & ~1) | (addr & 1)]; + } } return gb->oam[addr]; } static void add_object_from_index(GB_gameboy_t *gb, unsigned index) { + if (likely(!GB_is_dma_active(gb) || gb->halted || gb->stopped)) { + gb->mode2_y_bus = oam_read(gb, index * 4); + gb->mode2_x_bus = oam_read(gb, index * 4 + 1); + } + if (gb->n_visible_objs == 10) return; /* TODO: It appears that DMA blocks PPU access to OAM, but it needs verification. */ - if (unlikely(GB_is_dma_active(gb))) { - if (!gb->halted && !gb->stopped) { - return; - } + if (unlikely(GB_is_dma_active(gb) && (gb->halted || gb->stopped))) { if (gb->model < GB_MODEL_CGB_E) { return; } - /* CGB-0 to CGB-D: Halted DMA still blocks Mode 2; + /* CGB-0 to CGB-D: Halted DMA blocks Mode 2; Pre-CGB: Unit specific behavior, some units read FFs, some units read using several different corruption pattterns. For simplicity, we emulate FFs. */ @@ -495,23 +532,21 @@ static void add_object_from_index(GB_gameboy_t *gb, unsigned index) if (unlikely(gb->oam_ppu_blocked)) { return; } - + + bool height_16 = (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_SIZE) != 0; + signed y = gb->mode2_y_bus - 16; /* This reverse sorts the visible objects by location and priority */ - uint8_t oam_y = oam_read(gb, index * 4); - uint8_t oam_x = oam_read(gb, index * 4 + 1); - bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; - signed y = oam_y - 16; if (y <= gb->current_line && y + (height_16? 16 : 8) > gb->current_line) { unsigned j = 0; for (; j < gb->n_visible_objs; j++) { - if (gb->objects_x[j] <= oam_x) break; + if (gb->objects_x[j] <= gb->mode2_x_bus) break; } memmove(gb->visible_objs + j + 1, gb->visible_objs + j, gb->n_visible_objs - j); memmove(gb->objects_x + j + 1, gb->objects_x + j, gb->n_visible_objs - j); memmove(gb->objects_y + j + 1, gb->objects_y + j, gb->n_visible_objs - j); gb->visible_objs[j] = index; - gb->objects_x[j] = oam_x; - gb->objects_y[j] = oam_y; + gb->objects_x[j] = gb->mode2_x_bus; + gb->objects_y[j] = gb->mode2_y_bus; gb->n_visible_objs++; } } @@ -525,16 +560,15 @@ static uint8_t data_for_tile_sel_glitch(GB_gameboy_t *gb, bool *should_use, bool *should_use = true; *cgb_d_glitch = false; - if (gb->io_registers[GB_IO_LCDC] & 0x10) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL) { if (gb->model != GB_MODEL_CGB_D) { *should_use = !(gb->current_tile & 0x80); return gb->current_tile; } *cgb_d_glitch = true; *should_use = false; - gb->io_registers[GB_IO_LCDC] &= ~0x10; + gb->io_registers[GB_IO_LCDC] &= ~GB_LCDC_TILE_SEL; if (gb->fetcher_state == 3) { - *should_use = false; *cgb_d_glitch = true; return 0; } @@ -550,22 +584,43 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) const GB_fifo_item_t *oam_fifo_item = NULL; bool draw_oam = false; bool bg_enabled = true, bg_priority = false; + + // Rendering (including scrolling adjustment) does not occur as long as an object at x=0 is pending + if (gb->n_visible_objs != 0 && + (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_EN || GB_is_cgb(gb)) && + gb->objects_x[gb->n_visible_objs - 1] == 0) { + return; + } - if (fifo_size(&gb->bg_fifo)) { + if (unlikely(!fifo_size(&gb->bg_fifo))) return; + + if (unlikely(gb->insert_bg_pixel)) { + gb->insert_bg_pixel = false; + fifo_item = ({static const GB_fifo_item_t empty_item = {0,}; &empty_item;}); + } + else { fifo_item = fifo_pop(&gb->bg_fifo); - bg_priority = fifo_item->bg_priority; - - if (fifo_size(&gb->oam_fifo)) { - oam_fifo_item = fifo_pop(&gb->oam_fifo); - if (oam_fifo_item->pixel && (gb->io_registers[GB_IO_LCDC] & 2) && unlikely(!gb->objects_disabled)) { - draw_oam = true; - bg_priority |= oam_fifo_item->bg_priority; - } + } + bg_priority = fifo_item->bg_priority; + + if (fifo_size(&gb->oam_fifo)) { + oam_fifo_item = fifo_pop(&gb->oam_fifo); + if (oam_fifo_item->pixel && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_EN) && unlikely(!gb->objects_disabled)) { + draw_oam = true; + bg_priority |= oam_fifo_item->bg_priority; } } - - if (!fifo_item) return; + // (gb->position_in_line + 16 < 8) is (gb->position_in_line < -8) in unsigned logic + if (((uint8_t)(gb->position_in_line + 16) < 8)) { + if ((gb->position_in_line & 7) == (gb->io_registers[GB_IO_SCX] & 7)) { + gb->position_in_line = -8; + } + else if (gb->position_in_line == (uint8_t) -9) { + gb->position_in_line = -16; + return; + } + } /* Drop pixels for scrollings */ if (gb->position_in_line >= 160 || (gb->disable_rendering && !gb->sgb)) { @@ -575,7 +630,7 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) /* Mixing */ - if ((gb->io_registers[GB_IO_LCDC] & 0x1) == 0) { + if ((gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_EN) == 0) { if (gb->cgb_mode) { bg_priority = false; } @@ -699,7 +754,7 @@ static inline uint8_t vram_read(GB_gameboy_t *gb, uint16_t addr) return 0; } // TODO: what if both? - else if (unlikely(gb->dma_current_dest <= 0xa0 && gb->dma_current_dest > 0 && (gb->dma_current_src & 0xE000) == 0x8000)) { // TODO: what happens in the last and first M cycles? + else if (unlikely(gb->dma_current_dest <= 0xA0 && gb->dma_current_dest > 0 && (gb->dma_current_src & 0xE000) == 0x8000)) { // TODO: what happens in the last and first M cycles? // DMAing from VRAM! /* TODO: AGS has its own, very different pattern, but AGS is not currently a supported model */ /* TODO: Research this when researching odd modes */ @@ -752,16 +807,16 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) dma_sync(gb, cycles); uint16_t map = 0x1800; - if (!(gb->io_registers[GB_IO_LCDC] & 0x20)) { + if (!(gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE)) { gb->wx_triggered = false; gb->wx166_glitch = false; } /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ - if (gb->io_registers[GB_IO_LCDC] & 0x08 && !gb->wx_triggered) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_MAP && !gb->wx_triggered) { map = 0x1C00; } - else if (gb->io_registers[GB_IO_LCDC] & 0x40 && gb->wx_triggered) { + else if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_MAP && gb->wx_triggered) { map = 0x1C00; } @@ -771,6 +826,9 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) if (gb->wx_triggered) { x = gb->window_tile_x; } + else if ((uint8_t)(gb->position_in_line + 16) < 8) { + x = gb->io_registers[GB_IO_SCX] >> 3; + } else { /* TODO: There is some CGB timing error around here. Adjusting SCX by 7 or less shouldn't have an effect on a CGB, @@ -804,7 +862,7 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb); /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ - if (gb->io_registers[GB_IO_LCDC] & 0x10) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL) { tile_address = gb->current_tile * 0x10; } else { @@ -821,7 +879,7 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) vram_read(gb, tile_address + ((y & 7) ^ y_flip) * 2); } - if ((gb->io_registers[GB_IO_LCDC] & 0x10) && gb->tile_sel_glitch) { + if ((gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL) && gb->tile_sel_glitch) { gb->data_for_sel_glitch = vram_read(gb, tile_address + ((y & 7) ^ y_flip) * 2); } @@ -845,7 +903,7 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) uint16_t tile_address = 0; uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb); - if (gb->io_registers[GB_IO_LCDC] & 0x10) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL) { tile_address = gb->current_tile * 0x10; } else { @@ -860,10 +918,10 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) } gb->last_tile_data_address = tile_address + ((y & 7) ^ y_flip) * 2 + 1 - cgb_d_glitch; if (!use_glitched) { - gb->current_tile_data[1] = + gb->data_for_sel_glitch = gb->current_tile_data[1] = vram_read(gb, gb->last_tile_data_address); } - if ((gb->io_registers[GB_IO_LCDC] & 0x10) && gb->tile_sel_glitch) { + if ((gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL) && gb->tile_sel_glitch) { gb->data_for_sel_glitch = vram_read(gb, gb->last_tile_data_address); } @@ -874,7 +932,7 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) } if (gb->wx_triggered) { gb->window_tile_x++; - gb->window_tile_x &= 0x1f; + gb->window_tile_x &= 0x1F; } // fallthrough @@ -884,6 +942,21 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) } if (fifo_size(&gb->bg_fifo) > 0) break; + if (unlikely(gb->wy_triggered && !(gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE) && !GB_is_cgb(gb) && !gb->disable_window_pixel_insertion_glitch)) { + /* See https://github.com/LIJI32/SameBoy/issues/278 for documentation */ + uint8_t logical_position = gb->position_in_line + 7; + if (logical_position > 167) { + logical_position = 0; + } + if (gb->io_registers[GB_IO_WX] == logical_position) { + gb->bg_fifo.read_end--; + gb->bg_fifo.read_end &= GB_FIFO_LENGTH - 1; + gb->bg_fifo.fifo[gb->bg_fifo.read_end] = (GB_fifo_item_t){0,}; + gb->bg_fifo.size = 1; + break; + } + } + fifo_push_bg_row(&gb->bg_fifo, gb->current_tile_data[0], gb->current_tile_data[1], gb->current_tile_attributes & 7, gb->current_tile_attributes & 0x80, gb->current_tile_attributes & 0x20); gb->fetcher_state = 0; @@ -902,7 +975,7 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) static uint16_t get_object_line_address(GB_gameboy_t *gb, uint8_t y, uint8_t tile, uint8_t flags) { - bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; /* Todo: Which T-cycle actually reads this? */ + bool height_16 = (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_SIZE) != 0; /* Todo: Which T-cycle actually reads this? */ uint8_t tile_y = (gb->current_line - y) & (height_16? 0xF : 7); if (flags & 0x40) { /* Flip Y */ @@ -934,7 +1007,7 @@ static inline void get_tile_data(const GB_gameboy_t *gb, uint8_t tile_x, uint8_t uint16_t tile_address = 0; /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ - if (gb->io_registers[GB_IO_LCDC] & 0x10) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL) { tile_address = current_tile * 0x10; } else { @@ -973,14 +1046,14 @@ static void render_line(GB_gameboy_t *gb) static const uint8_t empty_object_buffer[sizeof(_object_buffer)]; const typeof(_object_buffer[0]) *object_buffer; - if (gb->n_visible_objs && !gb->objects_disabled && (gb->io_registers[GB_IO_LCDC] & 2)) { + if (gb->n_visible_objs && !gb->objects_disabled && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_EN)) { object_buffer = &_object_buffer[0]; object_t *objects = (object_t *) &gb->oam; memset(_object_buffer, 0, sizeof(_object_buffer)); while (gb->n_visible_objs) { unsigned object_index = gb->visible_objs[gb->n_visible_objs - 1]; - unsigned priority = gb->object_priority == GB_OBJECT_PRIORITY_X? 0 : object_index; + unsigned priority = (gb->object_priority == GB_OBJECT_PRIORITY_X)? 0 : object_index; const object_t *object = &objects[object_index]; gb->n_visible_objs--; @@ -1034,7 +1107,7 @@ static void render_line(GB_gameboy_t *gb) p += WIDTH * gb->current_line; } - if (unlikely(gb->background_disabled) || (!gb->cgb_mode && !(gb->io_registers[GB_IO_LCDC] & 1))) { + if (unlikely(gb->background_disabled) || (!gb->cgb_mode && !(gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_EN))) { uint32_t bg = gb->background_palettes_rgb[gb->cgb_mode? 0 : (gb->io_registers[GB_IO_BGP] & 3)]; for (unsigned i = 160; i--;) { if (unlikely(object_buffer_pointer->pixel)) { @@ -1056,7 +1129,7 @@ static void render_line(GB_gameboy_t *gb) uint8_t tile_x = gb->io_registers[GB_IO_SCX] / 8; unsigned fractional_scroll = gb->io_registers[GB_IO_SCX] & 7; uint16_t map = 0x1800; - if (gb->io_registers[GB_IO_LCDC] & 0x08) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_MAP) { map = 0x1C00; } uint8_t y = gb->current_line + gb->io_registers[GB_IO_SCY]; @@ -1069,7 +1142,7 @@ uint8_t pixel = (data0 >> 7) | ((data1 >> 7) << 1);\ data0 <<= 1;\ data1 <<= 1;\ \ -if (unlikely(object_buffer_pointer->pixel) && (pixel == 0 || !(object_buffer_pointer->bg_priority || (attributes & 0x80)) || !(gb->io_registers[GB_IO_LCDC] & 1))) {\ +if (unlikely(object_buffer_pointer->pixel) && (pixel == 0 || !(object_buffer_pointer->bg_priority || (attributes & 0x80)) || !(gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_EN))) {\ pixel = object_buffer_pointer->pixel;\ if (!gb->cgb_mode) {\ pixel = ((gb->io_registers[GB_IO_OBP0 + object_buffer_pointer->palette] >> (pixel << 1)) & 3);\ @@ -1088,12 +1161,12 @@ object_buffer_pointer++\ // First 1-8 pixels data0 <<= fractional_scroll; data1 <<= fractional_scroll; - bool check_window = gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20); - for (unsigned i = fractional_scroll; i < 8; i++) { + bool check_window = gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE); + nounroll for (unsigned i = fractional_scroll; i < 8; i++) { if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { activate_window: check_window = false; - map = gb->io_registers[GB_IO_LCDC] & 0x40? 0x1C00 : 0x1800; + map = gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_MAP? 0x1C00 : 0x1800; tile_x = -1; y = ++gb->window_y; break; @@ -1104,7 +1177,7 @@ activate_window: while (pixels < 160 - 8) { get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); - for (unsigned i = 0; i < 8; i++) { + nounroll for (unsigned i = 0; i < 8; i++) { if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { goto activate_window; } @@ -1139,7 +1212,7 @@ static void render_line_sgb(GB_gameboy_t *gb) static const uint8_t empty_object_buffer[sizeof(_object_buffer)]; const typeof(_object_buffer[0]) *object_buffer; - if (gb->n_visible_objs && !gb->objects_disabled && (gb->io_registers[GB_IO_LCDC] & 2)) { + if (gb->n_visible_objs && !gb->objects_disabled && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_EN)) { object_buffer = &_object_buffer[0]; object_t *objects = (object_t *) &gb->oam; memset(_object_buffer, 0, sizeof(_object_buffer)); @@ -1182,7 +1255,7 @@ static void render_line_sgb(GB_gameboy_t *gb) typeof(object_buffer[0]) *object_buffer_pointer = object_buffer + 8; p += WIDTH * gb->current_line; - if (unlikely(gb->background_disabled) || (!gb->cgb_mode && !(gb->io_registers[GB_IO_LCDC] & 1))) { + if (unlikely(gb->background_disabled) || (!gb->cgb_mode && !(gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_EN))) { for (unsigned i = 160; i--;) { if (unlikely(object_buffer_pointer->pixel)) { uint8_t pixel = object_buffer_pointer->pixel; @@ -1201,7 +1274,7 @@ static void render_line_sgb(GB_gameboy_t *gb) uint8_t tile_x = gb->io_registers[GB_IO_SCX] / 8; unsigned fractional_scroll = gb->io_registers[GB_IO_SCX] & 7; uint16_t map = 0x1800; - if (gb->io_registers[GB_IO_LCDC] & 0x08) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_MAP) { map = 0x1C00; } uint8_t y = gb->current_line + gb->io_registers[GB_IO_SCY]; @@ -1214,7 +1287,7 @@ uint8_t pixel = (data0 >> 7) | ((data1 >> 7) << 1);\ data0 <<= 1;\ data1 <<= 1;\ \ -if (unlikely(object_buffer_pointer->pixel) && (pixel == 0 || !object_buffer_pointer->bg_priority || !(gb->io_registers[GB_IO_LCDC] & 1))) {\ +if (unlikely(object_buffer_pointer->pixel) && (pixel == 0 || !object_buffer_pointer->bg_priority || !(gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_EN))) {\ pixel = object_buffer_pointer->pixel;\ pixel = ((gb->io_registers[GB_IO_OBP0 + object_buffer_pointer->palette] >> (pixel << 1)) & 3);\ *(p++) = pixel;\ @@ -1229,12 +1302,12 @@ object_buffer_pointer++\ // First 1-8 pixels data0 <<= fractional_scroll; data1 <<= fractional_scroll; - bool check_window = gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20); - for (unsigned i = fractional_scroll; i < 8; i++) { + bool check_window = gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE); + nounroll for (unsigned i = fractional_scroll; i < 8; i++) { if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { activate_window: check_window = false; - map = gb->io_registers[GB_IO_LCDC] & 0x40? 0x1C00 : 0x1800; + map = gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_MAP? 0x1C00 : 0x1800; tile_x = -1; y = ++gb->window_y; break; @@ -1245,7 +1318,7 @@ object_buffer_pointer++\ while (pixels < 160 - 8) { get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); - for (unsigned i = 0; i < 8; i++) { + nounroll for (unsigned i = 0; i < 8; i++) { if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { goto activate_window; } @@ -1265,15 +1338,26 @@ object_buffer_pointer++\ static inline uint16_t mode3_batching_length(GB_gameboy_t *gb) { + if (gb->position_in_line != (uint8_t)-16) return 0; if (gb->model & GB_MODEL_NO_SFC_BIT) return 0; if (gb->hdma_on) return 0; + if (gb->stopped) return 0; if (GB_is_dma_active(gb)) return 0; - if (gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20) && (gb->io_registers[GB_IO_WX] < 8 || gb->io_registers[GB_IO_WX] == 166)) { - return 0; + if (gb->wy_triggered) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE) { + if ((gb->io_registers[GB_IO_WX] < 8 || gb->io_registers[GB_IO_WX] == 166)) { + return 0; + } + } + else { + if (gb->io_registers[GB_IO_WX] < 167 && !GB_is_cgb(gb)) { + return 0; + } + } } // No objects or window, timing is trivial - if (gb->n_visible_objs == 0 && !(gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20))) return 167 + (gb->io_registers[GB_IO_SCX] & 7); + if (gb->n_visible_objs == 0 && !(gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE))) return 167 + (gb->io_registers[GB_IO_SCX] & 7); if (gb->hdma_on_hblank) return 0; @@ -1288,22 +1372,70 @@ static inline uint16_t mode3_batching_length(GB_gameboy_t *gb) return 0; } +static inline uint8_t x_for_object_match(GB_gameboy_t *gb) +{ + uint8_t ret = gb->position_in_line + 8; + if (ret > (uint8_t)-16) return 0; + return ret; +} + +static void update_frame_parity(GB_gameboy_t *gb) +{ + if (gb->model <= GB_MODEL_CGB_E) { + gb->is_odd_frame ^= true; + } + else { + // Faster than division, it's normally only once + while (gb->frame_parity_ticks > LCDC_PERIOD * 2) { + gb->frame_parity_ticks -= LCDC_PERIOD * 2; + gb->is_odd_frame ^= true; + } + } +} + /* TODO: It seems that the STAT register's mode bits are always "late" by 4 T-cycles. The PPU logic can be greatly simplified if that delay is simply emulated. */ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) { + gb->frame_parity_ticks += cycles; + + if (unlikely((gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE) && (signed)(gb->cycles_for_line * 2 + cycles + gb->display_cycles) > LINE_LENGTH * 2)) { + unsigned first_batch = (LINE_LENGTH * 2 - gb->cycles_for_line * 2 + gb->display_cycles); + GB_display_run(gb, first_batch, force); + cycles -= first_batch; + if (gb->display_state == 22) { + gb->io_registers[GB_IO_STAT] &= ~3; + gb->mode_for_interrupt = 0; + GB_STAT_update(gb); + } + gb->display_state = 9; + gb->display_cycles = 0; + } + if (unlikely(gb->delayed_glitch_hblank_interrupt && cycles && gb->current_line < LINES)) { + gb->delayed_glitch_hblank_interrupt = false; + gb->mode_for_interrupt = 0; + GB_STAT_update(gb); + gb->mode_for_interrupt = 3; + } gb->cycles_since_vblank_callback += cycles / 2; + + if (cycles < gb->frame_repeat_countdown) { + gb->frame_repeat_countdown -= cycles; + } + else { + gb->frame_repeat_countdown = 0; + } /* The PPU does not advance while in STOP mode on the DMG */ if (gb->stopped && !GB_is_cgb(gb)) { if (gb->cycles_since_vblank_callback >= LCDC_PERIOD) { - GB_display_vblank(gb); + GB_display_vblank(gb, GB_VBLANK_TYPE_ARTIFICIAL); } return; } - + GB_BATCHABLE_STATE_MACHINE(gb, display, cycles, 2, !force) { GB_STATE(gb, display, 1); GB_STATE(gb, display, 2); @@ -1313,7 +1445,7 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) GB_STATE(gb, display, 6); GB_STATE(gb, display, 7); GB_STATE(gb, display, 8); - // GB_STATE(gb, display, 9); + GB_STATE(gb, display, 9); GB_STATE(gb, display, 10); GB_STATE(gb, display, 11); GB_STATE(gb, display, 12); @@ -1345,21 +1477,20 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) GB_STATE(gb, display, 40); GB_STATE(gb, display, 41); GB_STATE(gb, display, 42); + GB_STATE(gb, display, 43); } - if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) { + if (!(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) { while (true) { if (gb->cycles_since_vblank_callback < LCDC_PERIOD) { GB_SLEEP(gb, display, 1, LCDC_PERIOD - gb->cycles_since_vblank_callback); } - GB_display_vblank(gb); - gb->cgb_repeated_a_frame = true; + update_frame_parity(gb); // TODO: test actual timing + GB_display_vblank(gb, GB_VBLANK_TYPE_LCD_OFF); } return; } - - gb->is_odd_frame = false; - + if (!GB_is_cgb(gb)) { GB_SLEEP(gb, display, 23, 1); } @@ -1368,6 +1499,7 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) gb->current_line = 0; gb->window_y = -1; gb->wy_triggered = false; + gb->position_in_line = -16; gb->ly_for_comparison = 0; gb->io_registers[GB_IO_STAT] &= ~3; @@ -1387,6 +1519,7 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) GB_SLEEP(gb, display, 34, 2); gb->n_visible_objs = 0; + gb->orig_n_visible_objs = 0; gb->cycles_for_line += 8; // Mode 0 is shorter on the first line 0, so we augment cycles_for_line by 8 extra cycles. gb->io_registers[GB_IO_STAT] &= ~3; @@ -1413,7 +1546,47 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) gb->wx_triggered = false; gb->wx166_glitch = false; goto mode_3_start; - + + // Mode 3 abort, state 9 + display9: { + // TODO: Timing of things in this scenario is almost completely untested + if (gb->current_line < LINES && !GB_is_sgb(gb) && !gb->disable_rendering) { + GB_log(gb, "The ROM is preventing line %d from fully rendering, this could damage a real device's LCD display.\n", gb->current_line); + uint32_t *dest = NULL; + if (gb->border_mode != GB_BORDER_ALWAYS) { + dest = gb->screen + gb->lcd_x + gb->current_line * WIDTH; + } + else { + dest = gb->screen + gb->lcd_x + gb->current_line * BORDERED_WIDTH + (BORDERED_WIDTH - WIDTH) / 2 + (BORDERED_HEIGHT - LINES) / 2 * BORDERED_WIDTH; + } + uint32_t color = GB_is_cgb(gb)? GB_convert_rgb15(gb, 0x7FFF, false) : gb->background_palettes_rgb[4]; + while (gb->lcd_x < 160) { + *(dest++) = color; + gb->lcd_x++; + } + } + gb->n_visible_objs = gb->orig_n_visible_objs; + gb->current_line++; + gb->cycles_for_line = 0; + if (gb->current_line != LINES) { + gb->cycles_for_line = 2; + GB_SLEEP(gb, display, 28, 2); + gb->io_registers[GB_IO_LY] = gb->current_line; + if (gb->position_in_line >= 156 && gb->position_in_line < (uint8_t)-16) { + gb->delayed_glitch_hblank_interrupt = true; + } + GB_STAT_update(gb); + gb->position_in_line = -15; + goto mode_3_start; + } + else { + if (gb->position_in_line >= 156 && gb->position_in_line < (uint8_t)-16) { + gb->delayed_glitch_hblank_interrupt = true; + } + gb->position_in_line = -16; + } + } + while (true) { /* Lines 0 - 143 */ gb->window_y = -1; @@ -1455,6 +1628,7 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) gb->mode_for_interrupt = -1; GB_STAT_update(gb); gb->n_visible_objs = 0; + gb->orig_n_visible_objs = 0; if (!GB_is_dma_active(gb) && !gb->oam_ppu_blocked) { GB_BATCHPOINT(gb, display, 5, 80); @@ -1477,7 +1651,7 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) } } gb->cycles_for_line = MODE2_LENGTH + 4; - + gb->orig_n_visible_objs = gb->n_visible_objs; gb->accessed_oam_row = -1; gb->io_registers[GB_IO_STAT] &= ~3; gb->io_registers[GB_IO_STAT] |= 3; @@ -1502,8 +1676,9 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) gb->cycles_for_line += 2; GB_SLEEP(gb, display, 32, 2); mode_3_start: + gb->disable_window_pixel_insertion_glitch = false; /* TODO: Timing seems incorrect, might need an access conflict handling. */ - if ((gb->io_registers[GB_IO_LCDC] & 0x20) && + if ((gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE) && gb->io_registers[GB_IO_WY] == gb->current_line) { gb->wy_triggered = true; } @@ -1512,11 +1687,7 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) fifo_clear(&gb->oam_fifo); /* Fill the FIFO with 8 pixels of "junk", it's going to be dropped anyway. */ fifo_push_bg_row(&gb->bg_fifo, 0, 0, 0, false, false); - /* Todo: find out actual access time of SCX */ - gb->position_in_line = - (gb->io_registers[GB_IO_SCX] & 7) - 8; gb->lcd_x = 0; - - gb->extra_penalty_for_object_at_0 = MIN((gb->io_registers[GB_IO_SCX] & 7), 5); /* The actual rendering cycle */ gb->fetcher_state = 0; @@ -1544,16 +1715,16 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) that point. The code should be updated to represent this, and this will fix the time travel hack in WX's access conflict code. */ - if (!gb->wx_triggered && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20)) { + if (!gb->wx_triggered && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE)) { bool should_activate_window = false; if (gb->io_registers[GB_IO_WX] == 0) { - static const uint8_t scx_to_wx0_comparisons[] = {-7, -9, -10, -11, -12, -13, -14, -14}; + static const uint8_t scx_to_wx0_comparisons[] = {-7, -1, -2, -3, -4, -5, -6, -6}; if (gb->position_in_line == scx_to_wx0_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { should_activate_window = true; } } else if (gb->wx166_glitch) { - static const uint8_t scx_to_wx166_comparisons[] = {-8, -9, -10, -11, -12, -13, -14, -15}; + static const uint8_t scx_to_wx166_comparisons[] = {-16, -1, -2, -3, -4, -5, -6, -7}; if (gb->position_in_line == scx_to_wx166_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { should_activate_window = true; } @@ -1591,31 +1762,27 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) gb->window_y++; } } - - /* TODO: What happens when WX=0? */ - if (!GB_is_cgb(gb) && gb->wx_triggered && !gb->window_is_being_fetched && - gb->fetcher_state == 0 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7) ) { - // Insert a pixel right at the FIFO's end - gb->bg_fifo.read_end--; - gb->bg_fifo.read_end &= GB_FIFO_LENGTH - 1; - gb->bg_fifo.fifo[gb->bg_fifo.read_end] = (GB_fifo_item_t){0,}; - gb->window_is_being_fetched = false; - } + /* TODO: What happens when WX=0?*/ + if (!GB_is_cgb(gb) && gb->wx_triggered && !gb->window_is_being_fetched && + gb->fetcher_state == 0 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7) && gb->bg_fifo.size == 8) { + // Insert a pixel right at the FIFO's end + gb->insert_bg_pixel = true; + } + /* Handle objects */ /* When the object enabled bit is off, this proccess is skipped entirely on the DMG, but not on the CGB. On the CGB, this bit is checked only when the pixel is actually popped from the FIFO. */ - + while (gb->n_visible_objs != 0 && - (gb->position_in_line < 160 || gb->position_in_line >= (uint8_t)(-8)) && - gb->objects_x[gb->n_visible_objs - 1] < (uint8_t)(gb->position_in_line + 8)) { + gb->objects_x[gb->n_visible_objs - 1] < x_for_object_match(gb)) { gb->n_visible_objs--; } gb->during_object_fetch = true; while (gb->n_visible_objs != 0 && - (gb->io_registers[GB_IO_LCDC] & 2 || GB_is_cgb(gb)) && - gb->objects_x[gb->n_visible_objs - 1] == (uint8_t)(gb->position_in_line + 8)) { + (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_EN || GB_is_cgb(gb)) && + gb->objects_x[gb->n_visible_objs - 1] == x_for_object_match(gb)) { while (gb->fetcher_state < 5 || fifo_size(&gb->bg_fifo) == 0) { advance_fetcher_state_machine(gb, &cycles); @@ -1626,18 +1793,6 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) } } - /* Todo: Measure if penalty occurs before or after waiting for the fetcher. */ - if (gb->extra_penalty_for_object_at_0 != 0) { - if (gb->objects_x[gb->n_visible_objs - 1] == 0) { - gb->cycles_for_line += gb->extra_penalty_for_object_at_0; - GB_SLEEP(gb, display, 28, gb->extra_penalty_for_object_at_0); - gb->extra_penalty_for_object_at_0 = 0; - if (gb->object_fetch_aborted) { - goto abort_fetching_object; - } - } - } - /* TODO: Can this be deleted? { */ advance_fetcher_state_machine(gb, &cycles); gb->cycles_for_line++; @@ -1649,12 +1804,9 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) advance_fetcher_state_machine(gb, &cycles); dma_sync(gb, &cycles); - gb->object_low_line_address = get_object_line_address(gb, - gb->objects_y[gb->n_visible_objs - 1], - oam_read(gb, gb->visible_objs[gb->n_visible_objs - 1] * 4 + 2), - gb->object_flags = oam_read(gb, gb->visible_objs[gb->n_visible_objs - 1] * 4 + 3) - ); - + gb->mode2_y_bus = oam_read(gb, gb->visible_objs[gb->n_visible_objs - 1] * 4 + 2); + gb->object_flags = oam_read(gb, gb->visible_objs[gb->n_visible_objs - 1] * 4 + 3); + gb->cycles_for_line += 2; GB_SLEEP(gb, display, 20, 2); if (gb->object_fetch_aborted) { @@ -1663,6 +1815,10 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) /* TODO: timing not verified */ dma_sync(gb, &cycles); + gb->object_low_line_address = get_object_line_address(gb, + gb->objects_y[gb->n_visible_objs - 1], + gb->mode2_y_bus, + gb->object_flags); gb->object_tile_data[0] = vram_read(gb, gb->object_low_line_address); @@ -1672,13 +1828,15 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) goto abort_fetching_object; } - /* TODO: timing not verified */ - dma_sync(gb, &cycles); - gb->object_tile_data[1] = vram_read(gb, gb->object_low_line_address + 1); - - gb->during_object_fetch = false; gb->cycles_for_line++; + gb->object_low_line_address = get_object_line_address(gb, + gb->objects_y[gb->n_visible_objs - 1], + gb->mode2_y_bus, + gb->object_flags); + + dma_sync(gb, &cycles); + gb->object_tile_data[1] = vram_read(gb, gb->object_low_line_address + 1); GB_SLEEP(gb, display, 40, 1); @@ -1710,7 +1868,8 @@ abort_fetching_object: GB_SLEEP(gb, display, 21, 1); } skip_slow_mode_3: - + gb->position_in_line = -16; + /* TODO: This seems incorrect (glitches Tesserae), verify further */ /* if (gb->fetcher_state == 4 || gb->fetcher_state == 5) { @@ -1721,7 +1880,7 @@ skip_slow_mode_3: } */ while (gb->lcd_x != 160 && !gb->disable_rendering && gb->screen && !gb->sgb) { - /* Oh no! The PPU and LCD desynced! Fill the rest of the line whith white. */ + /* Oh no! The PPU and LCD desynced! Fill the rest of the line with the last color. */ uint32_t *dest = NULL; if (gb->border_mode != GB_BORDER_ALWAYS) { dest = gb->screen + gb->lcd_x + gb->current_line * WIDTH; @@ -1729,13 +1888,13 @@ skip_slow_mode_3: else { dest = gb->screen + gb->lcd_x + gb->current_line * BORDERED_WIDTH + (BORDERED_WIDTH - WIDTH) / 2 + (BORDERED_HEIGHT - LINES) / 2 * BORDERED_WIDTH; } - *dest = gb->background_palettes_rgb[0]; + *dest = (gb->lcd_x == 0)? gb->background_palettes_rgb[0] : dest[-1]; gb->lcd_x++; } /* TODO: Verify timing */ - if (!GB_is_cgb(gb) && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20) && gb->io_registers[GB_IO_WX] == 166) { + if (!GB_is_cgb(gb) && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE) && gb->io_registers[GB_IO_WX] == 166) { gb->wx166_glitch = true; } else { @@ -1781,15 +1940,26 @@ skip_slow_mode_3: GB_SLEEP(gb, display, 36, 2); gb->cgb_palettes_blocked = false; - GB_SLEEP(gb, display, 11, LINE_LENGTH - gb->cycles_for_line - 2); + if (gb->cycles_for_line > LINE_LENGTH - 2) { + gb->cycles_for_line = 0; + GB_SLEEP(gb, display, 43, LINE_LENGTH - gb->cycles_for_line); + goto display9; + } + + { + uint16_t cycles_for_line = gb->cycles_for_line; + gb->cycles_for_line = 0; + GB_SLEEP(gb, display, 11, LINE_LENGTH - cycles_for_line - 2); + } /* TODO: Verify double speed timing TODO: Timing differs on a DMG */ - if ((gb->io_registers[GB_IO_LCDC] & 0x20) && + if ((gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE) && (gb->io_registers[GB_IO_WY] == gb->current_line)) { gb->wy_triggered = true; } + gb->cycles_for_line = 0; GB_SLEEP(gb, display, 31, 2); if (gb->current_line != LINES - 1) { gb->mode_for_interrupt = 2; @@ -1798,7 +1968,7 @@ skip_slow_mode_3: // Todo: unverified timing gb->current_lcd_line++; if (gb->current_lcd_line == LINES && GB_is_sgb(gb)) { - GB_display_vblank(gb); + GB_display_vblank(gb, GB_VBLANK_TYPE_NORMAL_FRAME); } if (gb->icd_hreset_callback) { @@ -1819,6 +1989,10 @@ skip_slow_mode_3: gb->io_registers[GB_IO_IF] |= 2; } GB_SLEEP(gb, display, 12, 2); + if (gb->delayed_glitch_hblank_interrupt) { + gb->delayed_glitch_hblank_interrupt = false; + gb->mode_for_interrupt = 0; + } gb->ly_for_comparison = gb->current_line; GB_STAT_update(gb); GB_SLEEP(gb, display, 24, 1); @@ -1836,36 +2010,38 @@ skip_slow_mode_3: if (gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON) { if (GB_is_cgb(gb)) { - GB_display_vblank(gb); - gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_SKIPPED; + GB_display_vblank(gb, GB_VBLANK_TYPE_NORMAL_FRAME); + gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_RENDERED; } else { if (!GB_is_sgb(gb) || gb->current_lcd_line < LINES) { - gb->is_odd_frame ^= true; - GB_display_vblank(gb); + update_frame_parity(gb); // TODO: test actual timing + GB_display_vblank(gb, GB_VBLANK_TYPE_NORMAL_FRAME); } - gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; + gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_RENDERED; } } else { if (!GB_is_sgb(gb) || gb->current_lcd_line < LINES) { - gb->is_odd_frame ^= true; - GB_display_vblank(gb); - } - if (gb->frame_skip_state == GB_FRAMESKIP_FIRST_FRAME_SKIPPED) { - gb->cgb_repeated_a_frame = true; - gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; - } - else { - gb->cgb_repeated_a_frame = false; + update_frame_parity(gb); // TODO: test actual timing + GB_display_vblank(gb, GB_VBLANK_TYPE_NORMAL_FRAME); } } } + /* 3640 is just a few cycles less than 4 lines, no clue where the + AGB constant comes from (These are measured and confirmed) */ + gb->frame_repeat_countdown = LINES * LINE_LENGTH * 2 + (gb->model > GB_MODEL_CGB_E? 5982 : 3640); // 8MHz units + if (gb->display_cycles < gb->frame_repeat_countdown) { + gb->frame_repeat_countdown -= gb->display_cycles; + } + else { + gb->frame_repeat_countdown = 0; + } + GB_SLEEP(gb, display, 13, LINE_LENGTH - 5); } - /* TODO: Verified on SGB2 and CGB-E. Actual interrupt timings not tested. */ /* Lines 153 */ gb->ly_for_comparison = -1; GB_STAT_update(gb); @@ -1976,12 +2152,12 @@ void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette break; } - if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && gb->io_registers[GB_IO_LCDC] & 0x08)) { - map = 0x1c00; + if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && gb->io_registers[GB_IO_LCDC] & GB_LCDC_BG_MAP)) { + map = 0x1C00; } if (tileset_type == GB_TILESET_AUTO) { - tileset_type = (gb->io_registers[GB_IO_LCDC] & 0x10)? GB_TILESET_8800 : GB_TILESET_8000; + tileset_type = (gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL)? GB_TILESET_8800 : GB_TILESET_8000; } for (unsigned y = 0; y < 256; y++) { @@ -2025,14 +2201,14 @@ void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *object_height) { uint8_t count = 0; - *object_height = (gb->io_registers[GB_IO_LCDC] & 4) ? 16:8; + *object_height = (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_SIZE) ? 16:8; uint8_t oam_to_dest_index[40] = {0,}; for (signed y = 0; y < LINES; y++) { object_t *object = (object_t *) &gb->oam; uint8_t objects_in_line = 0; + bool obscured = false; for (uint8_t i = 0; i < 40; i++, object++) { signed object_y = object->y - 16; - bool obscured = false; // Is object not in this line? if (object_y > y || object_y + *object_height <= y) continue; if (++objects_in_line == 11) obscured = true; diff --git a/Core/display.h b/Core/display.h index d50dc18..c160182 100644 --- a/Core/display.h +++ b/Core/display.h @@ -1,16 +1,22 @@ -#ifndef display_h -#define display_h +#pragma once #include "gb.h" #include #include +typedef enum { + GB_VBLANK_TYPE_NORMAL_FRAME, // An actual Vblank-triggered frame + GB_VBLANK_TYPE_LCD_OFF, // An artificial frame pushed while the LCD was off + GB_VBLANK_TYPE_ARTIFICIAL, // An artificial frame pushed for some other reason + GB_VBLANK_TYPE_REPEAT, // A frame that would not render on actual hardware, but the screen should retain the previous frame +} GB_vblank_type_t; + #ifdef GB_INTERNAL internal void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force); internal void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index); internal void GB_STAT_update(GB_gameboy_t *gb); internal void GB_lcd_off(GB_gameboy_t *gb); -internal void GB_display_vblank(GB_gameboy_t *gb); +internal void GB_display_vblank(GB_gameboy_t *gb, GB_vblank_type_t type); #define GB_display_sync(gb) GB_display_run(gb, 0, true) enum { @@ -49,12 +55,16 @@ typedef struct { typedef enum { GB_COLOR_CORRECTION_DISABLED, GB_COLOR_CORRECTION_CORRECT_CURVES, - GB_COLOR_CORRECTION_EMULATE_HARDWARE, - GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS, + GB_COLOR_CORRECTION_MODERN_BALANCED, + GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST, GB_COLOR_CORRECTION_REDUCE_CONTRAST, GB_COLOR_CORRECTION_LOW_CONTRAST, + GB_COLOR_CORRECTION_MODERN_ACCURATE, } GB_color_correction_mode_t; +static const GB_color_correction_mode_t __attribute__((deprecated("Use GB_COLOR_CORRECTION_MODERN_BALANCED instead"))) GB_COLOR_CORRECTION_EMULATE_HARDWARE = GB_COLOR_CORRECTION_MODERN_BALANCED; +static const GB_color_correction_mode_t __attribute__((deprecated("Use GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST instead"))) GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS = GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST; + void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index); void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index, GB_map_type_t map_type, GB_tileset_type_t tileset_type); uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *object_height); @@ -67,6 +77,3 @@ void GB_set_object_rendering_disabled(GB_gameboy_t *gb, bool disabled); void GB_set_background_rendering_disabled(GB_gameboy_t *gb, bool disabled); bool GB_is_object_rendering_disabled(GB_gameboy_t *gb); bool GB_is_background_rendering_disabled(GB_gameboy_t *gb); - - -#endif /* display_h */ diff --git a/Core/gb.c b/Core/gb.c index a73d870..398ff24 100644 --- a/Core/gb.c +++ b/Core/gb.c @@ -13,12 +13,6 @@ #include "gb.h" -#ifdef GB_DISABLE_REWIND -#define GB_rewind_free(...) -#define GB_rewind_push(...) -#endif - - void GB_attributed_logv(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, va_list args) { char *string = NULL; @@ -76,7 +70,7 @@ static char *default_input_callback(GB_gameboy_t *gb) } if (expression[0] == '\x03') { - gb->debug_stopped = true; + GB_debugger_break(gb); free(expression); return strdup(""); } @@ -146,7 +140,19 @@ static void load_default_border(GB_gameboy_t *gb) } } -void GB_init(GB_gameboy_t *gb, GB_model_t model) +size_t GB_allocation_size(void) +{ + return sizeof(GB_gameboy_t); +} + +GB_gameboy_t *GB_alloc(void) +{ + GB_gameboy_t *ret = malloc(sizeof(*ret)); + ret->magic = 0; + return ret; +} + +GB_gameboy_t *GB_init(GB_gameboy_t *gb, GB_model_t model) { memset(gb, 0, sizeof(*gb)); gb->model = model; @@ -165,14 +171,18 @@ void GB_init(GB_gameboy_t *gb, GB_model_t model) #endif gb->cartridge_type = &GB_cart_defs[0]; // Default cartridge type gb->clock_multiplier = 1.0; + gb->apu_output.max_cycles_per_sample = 0x400; if (model & GB_MODEL_NO_SFC_BIT) { /* Disable time syncing. Timing should be done by the SFC emulator. */ gb->turbo = true; } + gb->data_bus_decay = 12; + GB_reset(gb); load_default_border(gb); + return gb; } GB_model_t GB_get_model(GB_gameboy_t *gb) @@ -182,6 +192,7 @@ GB_model_t GB_get_model(GB_gameboy_t *gb) void GB_free(GB_gameboy_t *gb) { + GB_ASSERT_NOT_RUNNING(gb) gb->magic = 0; if (gb->ram) { free(gb->ram); @@ -195,11 +206,16 @@ void GB_free(GB_gameboy_t *gb) if (gb->rom) { free(gb->rom); } + if (gb->sgb) { + free(gb->sgb); + } +#ifndef GB_DISABLE_DEBUGGER + GB_debugger_clear_symbols(gb); if (gb->breakpoints) { free(gb->breakpoints); } - if (gb->sgb) { - free(gb->sgb); + if (gb->watchpoints) { + free(gb->watchpoints); } if (gb->nontrivial_jump_state) { free(gb->nontrivial_jump_state); @@ -207,16 +223,23 @@ void GB_free(GB_gameboy_t *gb) if (gb->undo_state) { free(gb->undo_state); } -#ifndef GB_DISABLE_DEBUGGER - GB_debugger_clear_symbols(gb); #endif - GB_rewind_free(gb); + GB_rewind_reset(gb); #ifndef GB_DISABLE_CHEATS while (gb->cheats) { GB_remove_cheat(gb, gb->cheats[0]); } #endif - memset(gb, 0, sizeof(*gb)); + GB_stop_audio_recording(gb); + memset(gb, 0, sizeof(*gb)); +} + +void GB_dealloc(GB_gameboy_t *gb) +{ + if (GB_is_inited(gb)) { + GB_free(gb); + } + free(gb); } int GB_load_boot_rom(GB_gameboy_t *gb, const char *path) @@ -280,24 +303,32 @@ void GB_borrow_sgb_border(GB_gameboy_t *gb) GB_free(&sgb); } +static size_t rounded_rom_size(size_t size) +{ + size = (size + 0x3FFF) & ~0x3FFF; /* Round to bank */ + /* And then round to a power of two */ + while (size & (size - 1)) { + /* I promise this works. */ + size |= size >> 1; + size++; + } + if (size < 0x8000) { + size = 0x8000; + } + return size; +} + int GB_load_rom(GB_gameboy_t *gb, const char *path) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + FILE *f = fopen(path, "rb"); if (!f) { GB_log(gb, "Could not open ROM: %s.\n", strerror(errno)); return errno; } fseek(f, 0, SEEK_END); - gb->rom_size = (ftell(f) + 0x3FFF) & ~0x3FFF; /* Round to bank */ - /* And then round to a power of two */ - while (gb->rom_size & (gb->rom_size - 1)) { - /* I promise this works. */ - gb->rom_size |= gb->rom_size >> 1; - gb->rom_size++; - } - if (gb->rom_size < 0x8000) { - gb->rom_size = 0x8000; - } + gb->rom_size = rounded_rom_size(ftell(f)); fseek(f, 0, SEEK_SET); if (gb->rom) { free(gb->rom); @@ -338,7 +369,7 @@ static void generate_gbs_entry(GB_gameboy_t *gb, uint8_t *data) void GB_gbs_switch_track(GB_gameboy_t *gb, uint8_t track) { GB_reset(gb); - GB_write_memory(gb, 0xFF00 + GB_IO_LCDC, 0x80); + GB_write_memory(gb, 0xFF00 + GB_IO_LCDC, GB_LCDC_ENABLE); GB_write_memory(gb, 0xFF00 + GB_IO_TAC, gb->gbs_header.TAC); GB_write_memory(gb, 0xFF00 + GB_IO_TMA, gb->gbs_header.TMA); GB_write_memory(gb, 0xFF00 + GB_IO_NR52, 0x80); @@ -375,13 +406,18 @@ void GB_gbs_switch_track(GB_gameboy_t *gb, uint8_t track) gb->sgb->intro_animation = GB_SGB_INTRO_ANIMATION_LENGTH; gb->sgb->disable_commands = true; } - if (gb->gbs_header.TAC & 0x40) { - gb->interrupt_enable = true; + if (gb->gbs_header.TAC & 0x4) { + gb->interrupt_enable = 4; + } + else { + gb->interrupt_enable = 1; } } int GB_load_gbs_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size, GB_gbs_info_t *info) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + if (size < sizeof(gb->gbs_header)) { GB_log(gb, "Not a valid GBS file.\n"); return -1; @@ -399,18 +435,8 @@ int GB_load_gbs_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size size_t data_size = size - sizeof(gb->gbs_header); - gb->rom_size = (data_size + LE16(gb->gbs_header.load_address) + 0x3FFF) & ~0x3FFF; /* Round to bank */ - /* And then round to a power of two */ - while (gb->rom_size & (gb->rom_size - 1)) { - /* I promise this works. */ - gb->rom_size |= gb->rom_size >> 1; - gb->rom_size++; - } + gb->rom_size = rounded_rom_size(data_size + LE16(gb->gbs_header.load_address)); - if (gb->rom_size < 0x8000) { - gb->rom_size = 0x8000; - } - if (gb->rom) { free(gb->rom); } @@ -437,12 +463,12 @@ int GB_load_gbs_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size if (gb->gbs_header.load_address) { // Generate interrupt handlers for (unsigned i = 0; i <= (has_interrupts? 0x50 : 0x38); i += 8) { - gb->rom[i] = 0xc3; // jp $XXXX + gb->rom[i] = 0xC3; // jp $XXXX gb->rom[i + 1] = (LE16(gb->gbs_header.load_address) + i); gb->rom[i + 2] = (LE16(gb->gbs_header.load_address) + i) >> 8; } for (unsigned i = has_interrupts? 0x58 : 0x40; i <= 0x60; i += 8) { - gb->rom[i] = 0xc9; // ret + gb->rom[i] = 0xC9; // ret } // Generate entry @@ -468,6 +494,8 @@ int GB_load_gbs_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size int GB_load_gbs(GB_gameboy_t *gb, const char *path, GB_gbs_info_t *info) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + FILE *f = fopen(path, "rb"); if (!f) { GB_log(gb, "Could not open GBS: %s.\n", strerror(errno)); @@ -487,6 +515,8 @@ int GB_load_gbs(GB_gameboy_t *gb, const char *path, GB_gbs_info_t *info) int GB_load_isx(GB_gameboy_t *gb, const char *path) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + FILE *f = fopen(path, "rb"); if (!f) { GB_log(gb, "Could not open ISX file: %s.\n", strerror(errno)); @@ -520,7 +550,10 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) bank = byte; if (byte >= 0x80) { READ(byte); - bank |= byte << 8; + /* TODO: This is just a guess, the docs don't elaborator on how banks > 0xFF are saved, + other than the fact that banks >= 80 requires two bytes to store them, and I haven't + encountered an ISX file for a ROM larger than 4MBs yet. */ + bank += byte << 7; } READ(address); @@ -572,9 +605,8 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) uint8_t length; char name[257]; uint8_t flag; - uint16_t bank; + uint8_t bank; uint16_t address; - uint8_t byte; READ(count); count = LE16(count); while (count--) { @@ -583,12 +615,7 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) name[length] = 0; READ(flag); // unused - READ(byte); - bank = byte; - if (byte >= 0x80) { - READ(byte); - bank |= byte << 8; - } + READ(bank); READ(address); address = LE16(address); @@ -703,19 +730,14 @@ error: void GB_load_rom_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size) { - gb->rom_size = (size + 0x3fff) & ~0x3fff; - while (gb->rom_size & (gb->rom_size - 1)) { - gb->rom_size |= gb->rom_size >> 1; - gb->rom_size++; - } - if (gb->rom_size == 0) { - gb->rom_size = 0x8000; - } + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + + gb->rom_size = rounded_rom_size(size); if (gb->rom) { free(gb->rom); } gb->rom = malloc(gb->rom_size); - memset(gb->rom, 0xff, gb->rom_size); + memset(gb->rom, 0xFF, gb->rom_size); memcpy(gb->rom, buffer, size); GB_configure_cart(gb); gb->tried_loading_sgb_border = false; @@ -844,6 +866,8 @@ int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size) int GB_save_battery(GB_gameboy_t *gb, const char *path) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + if (!gb->cartridge_type->has_battery) return 0; // Nothing to save. if (gb->cartridge_type->mbc_type == GB_TPP1 && !(gb->rom[0x153] & 8)) return 0; // Nothing to save. if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */ @@ -916,6 +940,8 @@ static void load_tpp1_save_data(GB_gameboy_t *gb, const tpp1_rtc_save_t *data) void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + memcpy(gb->mbc_ram, buffer, MIN(gb->mbc_ram_size, size)); if (size <= gb->mbc_ram_size) { goto reset_rtc; @@ -1006,7 +1032,9 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t really RTC data. */ goto reset_rtc; } + GB_rtc_set_time(gb, time(NULL)); goto exit; + reset_rtc: gb->last_rtc_second = time(NULL); gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */ @@ -1022,6 +1050,8 @@ exit: /* Loading will silently stop if the format is incomplete */ void GB_load_battery(GB_gameboy_t *gb, const char *path) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + FILE *f = fopen(path, "rb"); if (!f) { return; @@ -1114,7 +1144,9 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) really RTC data. */ goto reset_rtc; } + GB_rtc_set_time(gb, time(NULL)); goto exit; + reset_rtc: gb->last_rtc_second = time(NULL); gb->rtc_real.high |= 0x80; /* This gives the game a hint that the clock should be reset. */ @@ -1130,26 +1162,34 @@ exit: unsigned GB_run(GB_gameboy_t *gb) { + GB_ASSERT_NOT_RUNNING(gb) gb->vblank_just_occured = false; - if (gb->sgb && gb->sgb->intro_animation < 96) { + if (unlikely(gb->sgb && gb->sgb->intro_animation < 96)) { /* On the SGB, the GB is halted after finishing the boot ROM. Then, after the boot animation is almost done, it's reset. Since the SGB HLE does not perform any header validity checks, we just halt the CPU (with hacky code) until the correct time. This ensures the Nintendo logo doesn't flash on screen, and the game does "run in background" while the animation is playing. */ + + GB_set_running_thread(gb); GB_display_run(gb, 228, true); + GB_clear_running_thread(gb); gb->cycles_since_last_sync += 228; return 228; } GB_debugger_run(gb); gb->cycles_since_run = 0; + GB_set_running_thread(gb); GB_cpu_run(gb); - if (gb->vblank_just_occured) { + GB_clear_running_thread(gb); + if (unlikely(gb->vblank_just_occured)) { GB_debugger_handle_async_commands(gb); + GB_set_running_thread(gb); GB_rewind_push(gb); + GB_clear_running_thread(gb); } if (!(gb->io_registers[GB_IO_IF] & 0x10) && (gb->io_registers[GB_IO_JOYP] & 0x30) != 0x30) { gb->joyp_accessed = true; @@ -1179,9 +1219,15 @@ uint64_t GB_run_frame(GB_gameboy_t *gb) void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) gb->screen = output; } +uint32_t *GB_get_pixels_output(GB_gameboy_t *gb) +{ + return gb->screen; +} + void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback) { gb->vblank_callback = callback; @@ -1209,6 +1255,11 @@ void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) #endif } +void GB_set_debugger_reload_callback(GB_gameboy_t *gb, GB_debugger_reload_callback_t callback) +{ + gb->debugger_reload_callback = callback; +} + void GB_set_execution_callback(GB_gameboy_t *gb, GB_execution_callback_t callback) { gb->execution_callback = callback; @@ -1219,10 +1270,15 @@ void GB_set_lcd_line_callback(GB_gameboy_t *gb, GB_lcd_line_callback_t callback) gb->lcd_line_callback = callback; } -const GB_palette_t GB_PALETTE_GREY = {{{0x00, 0x00, 0x00}, {0x55, 0x55, 0x55}, {0xaa, 0xaa, 0xaa}, {0xff, 0xff, 0xff}, {0xff, 0xff, 0xff}}}; -const GB_palette_t GB_PALETTE_DMG = {{{0x08, 0x18, 0x10}, {0x39, 0x61, 0x39}, {0x84, 0xa5, 0x63}, {0xc6, 0xde, 0x8c}, {0xd2, 0xe6, 0xa6}}}; -const GB_palette_t GB_PALETTE_MGB = {{{0x07, 0x10, 0x0e}, {0x3a, 0x4c, 0x3a}, {0x81, 0x8d, 0x66}, {0xc2, 0xce, 0x93}, {0xcf, 0xda, 0xac}}}; -const GB_palette_t GB_PALETTE_GBL = {{{0x0a, 0x1c, 0x15}, {0x35, 0x78, 0x62}, {0x56, 0xb4, 0x95}, {0x7f, 0xe2, 0xc3}, {0x91, 0xea, 0xd0}}}; +void GB_set_lcd_status_callback(GB_gameboy_t *gb, GB_lcd_status_callback_t callback) +{ + gb->lcd_status_callback = callback; +} + +const GB_palette_t GB_PALETTE_GREY = {{{0x00, 0x00, 0x00}, {0x55, 0x55, 0x55}, {0xAA, 0xAA, 0xAA}, {0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF}}}; +const GB_palette_t GB_PALETTE_DMG = {{{0x08, 0x18, 0x10}, {0x39, 0x61, 0x39}, {0x84, 0xA5, 0x63}, {0xC6, 0xDE, 0x8C}, {0xD2, 0xE6, 0xA6}}}; +const GB_palette_t GB_PALETTE_MGB = {{{0x07, 0x10, 0x0E}, {0x3A, 0x4C, 0x3A}, {0x81, 0x8D, 0x66}, {0xC2, 0xCE, 0x93}, {0xCF, 0xDA, 0xAC}}}; +const GB_palette_t GB_PALETTE_GBL = {{{0x0A, 0x1C, 0x15}, {0x35, 0x78, 0x62}, {0x56, 0xB4, 0x95}, {0x7F, 0xE2, 0xC3}, {0x91, 0xEA, 0xD0}}}; static void update_dmg_palette(GB_gameboy_t *gb) { @@ -1293,26 +1349,38 @@ void GB_set_serial_transfer_bit_end_callback(GB_gameboy_t *gb, GB_serial_transfe bool GB_serial_get_data_bit(GB_gameboy_t *gb) { + if (!(gb->io_registers[GB_IO_SC] & 0x80)) { + /* Disabled serial returns 0 bits */ + return false; + } + if (gb->io_registers[GB_IO_SC] & 1) { /* Internal Clock */ GB_log(gb, "Serial read request while using internal clock. \n"); - return 0xFF; + return true; } return gb->io_registers[GB_IO_SB] & 0x80; } void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data) { + if (!(gb->io_registers[GB_IO_SC] & 0x80)) { + /* Serial disabled */ + return; + } + if (gb->io_registers[GB_IO_SC] & 1) { /* Internal Clock */ GB_log(gb, "Serial write request while using internal clock. \n"); return; } + gb->io_registers[GB_IO_SB] <<= 1; gb->io_registers[GB_IO_SB] |= data; gb->serial_count++; if (gb->serial_count == 8) { gb->io_registers[GB_IO_IF] |= 8; + gb->io_registers[GB_IO_SC] &= ~0x80; gb->serial_count = 0; } } @@ -1323,13 +1391,17 @@ void GB_disconnect_serial(GB_gameboy_t *gb) gb->serial_transfer_bit_end_callback = NULL; /* Reset any internally-emulated device. */ - memset(&gb->printer, 0, sizeof(gb->printer)); - memset(&gb->workboy, 0, sizeof(gb->workboy)); + memset(GB_GET_SECTION(gb, accessory), 0, GB_SECTION_SIZE(accessory)); +} + +GB_accessory_t GB_get_built_in_accessory(GB_gameboy_t *gb) +{ + return gb->accessory; } bool GB_is_inited(GB_gameboy_t *gb) { - return gb->magic == state_magic(); + return gb->magic == GB_state_magic(); } bool GB_is_cgb(const GB_gameboy_t *gb) @@ -1379,6 +1451,7 @@ static void reset_ram(GB_gameboy_t *gb) case GB_MODEL_MGB: case GB_MODEL_CGB_E: case GB_MODEL_AGB_A: /* Unverified */ + case GB_MODEL_GBP_A: for (unsigned i = 0; i < gb->ram_size; i++) { gb->ram[i] = GB_random(); } @@ -1443,7 +1516,8 @@ static void reset_ram(GB_gameboy_t *gb) case GB_MODEL_CGB_D: case GB_MODEL_CGB_E: case GB_MODEL_AGB_A: - for (unsigned i = 0; i < sizeof(gb->hram); i++) { + case GB_MODEL_GBP_A: + nounroll for (unsigned i = 0; i < sizeof(gb->hram); i++) { gb->hram[i] = GB_random(); } break; @@ -1456,7 +1530,7 @@ static void reset_ram(GB_gameboy_t *gb) case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ case GB_MODEL_SGB2: case GB_MODEL_SGB2_NO_SFC: - for (unsigned i = 0; i < sizeof(gb->hram); i++) { + nounroll for (unsigned i = 0; i < sizeof(gb->hram); i++) { if (i & 1) { gb->hram[i] = GB_random() | GB_random() | GB_random(); } @@ -1476,6 +1550,7 @@ static void reset_ram(GB_gameboy_t *gb) case GB_MODEL_CGB_D: case GB_MODEL_CGB_E: case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: /* Zero'd out by boot ROM anyway */ break; @@ -1495,7 +1570,7 @@ static void reset_ram(GB_gameboy_t *gb) gb->oam[i] = GB_random() | GB_random() | GB_random(); } } - for (unsigned i = 8; i < sizeof(gb->oam); i++) { + nounroll for (unsigned i = 8; i < sizeof(gb->oam); i++) { gb->oam[i] = gb->oam[i - 8]; } break; @@ -1510,10 +1585,11 @@ static void reset_ram(GB_gameboy_t *gb) case GB_MODEL_CGB_D: case GB_MODEL_CGB_E: case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: /* Initialized by CGB-A and newer, 0s in CGB-0 */ break; case GB_MODEL_MGB: { - for (unsigned i = 0; i < GB_IO_WAV_END - GB_IO_WAV_START; i++) { + nounroll for (unsigned i = 0; i < GB_IO_WAV_END - GB_IO_WAV_START; i++) { if (i & 1) { gb->io_registers[GB_IO_WAV_START + i] = GB_random() & GB_random(); } @@ -1530,7 +1606,7 @@ static void reset_ram(GB_gameboy_t *gb) case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ case GB_MODEL_SGB2: case GB_MODEL_SGB2_NO_SFC: { - for (unsigned i = 0; i < GB_IO_WAV_END - GB_IO_WAV_START; i++) { + nounroll for (unsigned i = 0; i < GB_IO_WAV_END - GB_IO_WAV_START; i++) { if (i & 1) { gb->io_registers[GB_IO_WAV_START + i] = GB_random() & GB_random() & GB_random(); } @@ -1556,6 +1632,10 @@ static void reset_ram(GB_gameboy_t *gb) GB_palette_changed(gb, false, i * 2); } } + + if (!gb->cartridge_type->has_battery) { + memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size); + } } static void request_boot_rom(GB_gameboy_t *gb) @@ -1586,10 +1666,13 @@ static void request_boot_rom(GB_gameboy_t *gb) case GB_MODEL_CGB_B: case GB_MODEL_CGB_C: case GB_MODEL_CGB_D: - case GB_MODEL_CGB_E: type = GB_BOOT_ROM_CGB; break; + case GB_MODEL_CGB_E: + type = GB_BOOT_ROM_CGB_E; + break; case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: type = GB_BOOT_ROM_AGB; break; } @@ -1597,19 +1680,41 @@ static void request_boot_rom(GB_gameboy_t *gb) } } -void GB_reset(GB_gameboy_t *gb) +static void GB_reset_internal(GB_gameboy_t *gb, bool quick) { + struct { + uint8_t hram[sizeof(gb->hram)]; + uint8_t background_palettes_data[sizeof(gb->background_palettes_data)]; + uint8_t object_palettes_data[sizeof(gb->object_palettes_data)]; + uint8_t oam[sizeof(gb->oam)]; + uint8_t extra_oam[sizeof(gb->extra_oam)]; + uint8_t dma, obp0, obp1; + } *preserved_state = NULL; + + if (quick) { + preserved_state = alloca(sizeof(*preserved_state)); + memcpy(preserved_state->hram, gb->hram, sizeof(gb->hram)); + memcpy(preserved_state->background_palettes_data, gb->background_palettes_data, sizeof(gb->background_palettes_data)); + memcpy(preserved_state->object_palettes_data, gb->object_palettes_data, sizeof(gb->object_palettes_data)); + memcpy(preserved_state->oam, gb->oam, sizeof(gb->oam)); + memcpy(preserved_state->extra_oam, gb->extra_oam, sizeof(gb->extra_oam)); + preserved_state->dma = gb->io_registers[GB_IO_DMA]; + preserved_state->obp0 = gb->io_registers[GB_IO_OBP0]; + preserved_state->obp1 = gb->io_registers[GB_IO_OBP1]; + } + uint32_t mbc_ram_size = gb->mbc_ram_size; GB_model_t model = gb->model; GB_update_clock_rate(gb); uint8_t rtc_section[GB_SECTION_SIZE(rtc)]; memcpy(rtc_section, GB_GET_SECTION(gb, rtc), sizeof(rtc_section)); - memset(gb, 0, (size_t)GB_GET_SECTION((GB_gameboy_t *) 0, unsaved)); + memset(gb, 0, GB_SECTION_OFFSET(unsaved)); memcpy(GB_GET_SECTION(gb, rtc), rtc_section, sizeof(rtc_section)); gb->model = model; gb->version = GB_STRUCT_VERSION; - gb->mbc_rom_bank = 1; + GB_reset_mbc(gb); + gb->last_rtc_second = time(NULL); gb->cgb_ram_bank = 1; gb->io_registers[GB_IO_JOYP] = 0xCF; @@ -1629,17 +1734,11 @@ void GB_reset(GB_gameboy_t *gb) update_dmg_palette(gb); } - reset_ram(gb); - /* The serial interrupt always occur on the 0xF7th cycle of every 0x100 cycle since boot. */ - gb->serial_cycles = 0x100-0xF7; + gb->serial_mask = 0x80; gb->io_registers[GB_IO_SC] = 0x7E; - - /* These are not deterministic, but 00 (CGB) and FF (DMG) are the most common initial values by far */ - gb->io_registers[GB_IO_DMA] = gb->io_registers[GB_IO_OBP0] = gb->io_registers[GB_IO_OBP1] = GB_is_cgb(gb)? 0x00 : 0xFF; - gb->accessed_oam_row = -1; - gb->dma_current_dest = 0xa1; + gb->dma_current_dest = 0xA1; if (GB_is_hle_sgb(gb)) { if (!gb->sgb) { @@ -1663,18 +1762,54 @@ void GB_reset(GB_gameboy_t *gb) } GB_set_internal_div_counter(gb, 8); + /* TODO: AGS-101 is inverted in comparison to AGS-001 and AGB */ + gb->is_odd_frame = gb->model > GB_MODEL_CGB_E; +#ifndef GB_DISABLE_DEBUGGER if (gb->nontrivial_jump_state) { free(gb->nontrivial_jump_state); gb->nontrivial_jump_state = NULL; } +#endif - gb->magic = state_magic(); + if (!quick) { + reset_ram(gb); + /* These are not deterministic, but 00 (CGB) and FF (DMG) are the most common initial values by far. + The retain their previous values on quick resets */ + gb->io_registers[GB_IO_DMA] = gb->io_registers[GB_IO_OBP0] = gb->io_registers[GB_IO_OBP1] = GB_is_cgb(gb)? 0x00 : 0xFF; + } + else { + memcpy(gb->hram, preserved_state->hram, sizeof(gb->hram)); + memcpy(gb->background_palettes_data, preserved_state->background_palettes_data, sizeof(gb->background_palettes_data)); + memcpy(gb->object_palettes_data, preserved_state->object_palettes_data, sizeof(gb->object_palettes_data)); + memcpy(gb->oam, preserved_state->oam, sizeof(gb->oam)); + memcpy(gb->extra_oam, preserved_state->extra_oam, sizeof(gb->extra_oam)); + gb->io_registers[GB_IO_DMA] = preserved_state->dma; + gb->io_registers[GB_IO_OBP0] = preserved_state->obp0; + gb->io_registers[GB_IO_OBP1] = preserved_state->obp1; + } + gb->apu.apu_cycles_in_2mhz = true; + + gb->magic = GB_state_magic(); request_boot_rom(gb); + GB_rewind_push(gb); +} + +void GB_reset(GB_gameboy_t *gb) +{ + GB_ASSERT_NOT_RUNNING(gb) + GB_reset_internal(gb, false); +} + +void GB_quick_reset(GB_gameboy_t *gb) +{ + GB_ASSERT_NOT_RUNNING(gb) + GB_reset_internal(gb, true); } void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model) { + GB_ASSERT_NOT_RUNNING(gb) gb->model = model; if (GB_is_cgb(gb)) { gb->ram = realloc(gb->ram, gb->ram_size = 0x1000 * 8); @@ -1684,11 +1819,13 @@ void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model) gb->ram = realloc(gb->ram, gb->ram_size = 0x2000); gb->vram = realloc(gb->vram, gb->vram_size = 0x2000); } +#ifndef GB_DISABLE_DEBUGGER if (gb->undo_state) { free(gb->undo_state); gb->undo_state = NULL; } - GB_rewind_free(gb); +#endif + GB_rewind_reset(gb); GB_reset(gb); load_default_border(gb); } @@ -1710,7 +1847,11 @@ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t * switch (access) { case GB_DIRECT_ACCESS_ROM: *size = gb->rom_size; - *bank = gb->mbc_rom_bank; + *bank = gb->mbc_rom_bank & (gb->rom_size / 0x4000 - 1); + return gb->rom; + case GB_DIRECT_ACCESS_ROM0: + *size = gb->rom_size; + *bank = gb->mbc_rom0_bank & (gb->rom_size / 0x4000 - 1); return gb->rom; case GB_DIRECT_ACCESS_RAM: *size = gb->ram_size; @@ -1718,7 +1859,7 @@ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t * return gb->ram; case GB_DIRECT_ACCESS_CART_RAM: *size = gb->mbc_ram_size; - *bank = gb->mbc_ram_bank; + *bank = gb->mbc_ram_bank & (gb->mbc_ram_size / 0x2000 - 1); return gb->mbc_ram; case GB_DIRECT_ACCESS_VRAM: *size = gb->vram_size; @@ -1766,8 +1907,10 @@ GB_registers_t *GB_get_registers(GB_gameboy_t *gb) void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier) { - gb->clock_multiplier = multiplier; - GB_update_clock_rate(gb); + if (multiplier != gb->clock_multiplier) { + gb->clock_multiplier = multiplier; + GB_update_clock_rate(gb); + } } uint32_t GB_get_clock_rate(GB_gameboy_t *gb) @@ -1793,6 +1936,7 @@ void GB_update_clock_rate(GB_gameboy_t *gb) } gb->clock_rate = gb->unmultiplied_clock_rate * gb->clock_multiplier; + GB_set_sample_rate(gb, gb->apu_output.sample_rate); } void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode) @@ -1880,6 +2024,11 @@ unsigned GB_time_to_alarm(GB_gameboy_t *gb) return alarm_time - current_time; } +bool GB_rom_supports_alarms(GB_gameboy_t *gb) +{ + return gb->cartridge_type->mbc_type == GB_HUC3; +} + bool GB_has_accelerometer(GB_gameboy_t *gb) { return gb->cartridge_type->mbc_type == GB_MBC7; @@ -1891,6 +2040,11 @@ void GB_set_accelerometer_values(GB_gameboy_t *gb, double x, double y) gb->accelerometer_y = y; } +void GB_set_open_bus_decay_time(GB_gameboy_t *gb, uint32_t decay) +{ + gb->data_bus_decay = decay; +} + void GB_get_rom_title(GB_gameboy_t *gb, char *title) { memset(title, 0, 17); @@ -1905,49 +2059,49 @@ void GB_get_rom_title(GB_gameboy_t *gb, char *title) uint32_t GB_get_rom_crc32(GB_gameboy_t *gb) { static const uint32_t table[] = { - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, - 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, - 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, - 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, - 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, - 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, - 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, - 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, - 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, - 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, - 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, - 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, - 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, - 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, - 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, - 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, - 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, - 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, - 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, - 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, - 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, - 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, - 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, - 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, - 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, - 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, - 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, - 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, - 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, - 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, - 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, - 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, - 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, - 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, - 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, - 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, - 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, - 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, - 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, - 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, - 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, - 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, + 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, + 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, + 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, + 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, + 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, + 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, + 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, + 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, + 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, + 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, + 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, + 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, + 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, + 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D }; const uint8_t *byte = gb->rom; @@ -1958,3 +2112,24 @@ uint32_t GB_get_rom_crc32(GB_gameboy_t *gb) } return ~ret; } + + +#ifdef GB_CONTEXT_SAFETY +void *GB_get_thread_id(void) +{ + // POSIX requires errno to be thread local, making errno's address unique per thread + return &errno; +} + +void GB_set_running_thread(GB_gameboy_t *gb) +{ + GB_ASSERT_NOT_RUNNING(gb) + gb->running_thread_id = GB_get_thread_id(); +} + +void GB_clear_running_thread(GB_gameboy_t *gb) +{ + assert(gb->running_thread_id == GB_get_thread_id()); + gb->running_thread_id = NULL; +} +#endif diff --git a/Core/gb.h b/Core/gb.h index 5827533..65372dd 100644 --- a/Core/gb.h +++ b/Core/gb.h @@ -1,11 +1,15 @@ -#ifndef GB_h -#define GB_h -#define typeof __typeof__ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include #include +#include "model.h" #include "defs.h" #include "save_state.h" @@ -27,14 +31,7 @@ #include "workboy.h" #include "random.h" -#define GB_STRUCT_VERSION 14 - -#define GB_MODEL_FAMILY_MASK 0xF00 -#define GB_MODEL_DMG_FAMILY 0x000 -#define GB_MODEL_MGB_FAMILY 0x100 -#define GB_MODEL_CGB_FAMILY 0x200 -#define GB_MODEL_PAL_BIT 0x40 -#define GB_MODEL_NO_SFC_BIT 0x80 +#define GB_STRUCT_VERSION 15 #define GB_REWIND_FRAMES_PER_KEY 255 @@ -95,32 +92,6 @@ typedef struct __attribute__((packed)) { uint8_t alarm_enabled; } GB_huc3_rtc_time_t; -typedef enum { - // GB_MODEL_DMG_0 = 0x000, - // GB_MODEL_DMG_A = 0x001, - GB_MODEL_DMG_B = 0x002, - // GB_MODEL_DMG_C = 0x003, - GB_MODEL_SGB = 0x004, - GB_MODEL_SGB_NTSC = GB_MODEL_SGB, - GB_MODEL_SGB_PAL = GB_MODEL_SGB | GB_MODEL_PAL_BIT, - GB_MODEL_SGB_NTSC_NO_SFC = GB_MODEL_SGB | GB_MODEL_NO_SFC_BIT, - GB_MODEL_SGB_NO_SFC = GB_MODEL_SGB_NTSC_NO_SFC, - GB_MODEL_SGB_PAL_NO_SFC = GB_MODEL_SGB | GB_MODEL_NO_SFC_BIT | GB_MODEL_PAL_BIT, - GB_MODEL_MGB = 0x100, - GB_MODEL_SGB2 = 0x101, - GB_MODEL_SGB2_NO_SFC = GB_MODEL_SGB2 | GB_MODEL_NO_SFC_BIT, - GB_MODEL_CGB_0 = 0x200, - GB_MODEL_CGB_A = 0x201, - GB_MODEL_CGB_B = 0x202, - GB_MODEL_CGB_C = 0x203, - GB_MODEL_CGB_D = 0x204, - GB_MODEL_CGB_E = 0x205, - // GB_MODEL_AGB_0 = 0x206, - GB_MODEL_AGB_A = 0x207, - GB_MODEL_AGB = GB_MODEL_AGB_A, - //GB_MODEL_AGB_B = 0x208 -} GB_model_t; - enum { GB_REGISTER_AF, GB_REGISTER_BC, @@ -133,10 +104,21 @@ enum { /* Todo: Actually use these! */ enum { - GB_CARRY_FLAG = 16, - GB_HALF_CARRY_FLAG = 32, - GB_SUBTRACT_FLAG = 64, - GB_ZERO_FLAG = 128, + GB_CARRY_FLAG = 0x10, + GB_HALF_CARRY_FLAG = 0x20, + GB_SUBTRACT_FLAG = 0x40, + GB_ZERO_FLAG = 0x80, +}; + +enum { + GB_LCDC_BG_EN = 1, + GB_LCDC_OBJ_EN = 2, + GB_LCDC_OBJ_SIZE = 4, + GB_LCDC_BG_MAP = 8, + GB_LCDC_TILE_SEL = 0x10, + GB_LCDC_WIN_ENABLE = 0x20, + GB_LCDC_WIN_MAP = 0x40, + GB_LCDC_ENABLE = 0x80, }; typedef enum { @@ -161,7 +143,7 @@ enum { /* Missing */ - GB_IO_IF = 0x0f, // Interrupt Flag (R/W) + GB_IO_IF = 0x0F, // Interrupt Flag (R/W) /* Sound */ GB_IO_NR10 = 0x10, // Channel 1 Sweep register (R/W) @@ -174,11 +156,11 @@ enum { GB_IO_NR22 = 0x17, // Channel 2 Volume Envelope (R/W) GB_IO_NR23 = 0x18, // Channel 2 Frequency lo data (W) GB_IO_NR24 = 0x19, // Channel 2 Frequency hi data (R/W) - GB_IO_NR30 = 0x1a, // Channel 3 Sound on/off (R/W) - GB_IO_NR31 = 0x1b, // Channel 3 Sound Length - GB_IO_NR32 = 0x1c, // Channel 3 Select output level (R/W) - GB_IO_NR33 = 0x1d, // Channel 3 Frequency's lower data (W) - GB_IO_NR34 = 0x1e, // Channel 3 Frequency's higher data (R/W) + GB_IO_NR30 = 0x1A, // Channel 3 Sound on/off (R/W) + GB_IO_NR31 = 0x1B, // Channel 3 Sound Length + GB_IO_NR32 = 0x1C, // Channel 3 Select output level (R/W) + GB_IO_NR33 = 0x1D, // Channel 3 Frequency's lower data (W) + GB_IO_NR34 = 0x1E, // Channel 3 Frequency's higher data (R/W) /* NR40 does not exist */ GB_IO_NR41 = 0x20, // Channel 4 Sound Length (R/W) GB_IO_NR42 = 0x21, // Channel 4 Volume Envelope (R/W) @@ -191,7 +173,7 @@ enum { /* Missing */ GB_IO_WAV_START = 0x30, // Wave pattern start - GB_IO_WAV_END = 0x3f, // Wave pattern end + GB_IO_WAV_END = 0x3F, // Wave pattern end /* Graphics */ GB_IO_LCDC = 0x40, // LCD Control (R/W) @@ -204,18 +186,18 @@ enum { GB_IO_BGP = 0x47, // BG Palette Data (R/W) - Non CGB Mode Only GB_IO_OBP0 = 0x48, // Object Palette 0 Data (R/W) - Non CGB Mode Only GB_IO_OBP1 = 0x49, // Object Palette 1 Data (R/W) - Non CGB Mode Only - GB_IO_WY = 0x4a, // Window Y Position (R/W) - GB_IO_WX = 0x4b, // Window X Position minus 7 (R/W) + GB_IO_WY = 0x4A, // Window Y Position (R/W) + GB_IO_WX = 0x4B, // Window X Position minus 7 (R/W) // Controls DMG mode and PGB mode - GB_IO_KEY0 = 0x4c, + GB_IO_KEY0 = 0x4C, /* General CGB features */ - GB_IO_KEY1 = 0x4d, // CGB Mode Only - Prepare Speed Switch + GB_IO_KEY1 = 0x4D, // CGB Mode Only - Prepare Speed Switch /* Missing */ - GB_IO_VBK = 0x4f, // CGB Mode Only - VRAM Bank - GB_IO_BANK = 0x50, // Write to disable the BIOS mapping + GB_IO_VBK = 0x4F, // CGB Mode Only - VRAM Bank + GB_IO_BANK = 0x50, // Write to disable the boot ROM mapping /* CGB DMA */ GB_IO_HDMA1 = 0x51, // CGB Mode Only - New DMA Source, High @@ -229,16 +211,17 @@ enum { /* Missing */ - /* CGB Paletts */ + /* CGB Palettes */ GB_IO_BGPI = 0x68, // CGB Mode Only - Background Palette Index GB_IO_BGPD = 0x69, // CGB Mode Only - Background Palette Data - GB_IO_OBPI = 0x6a, // CGB Mode Only - Object Palette Index - GB_IO_OBPD = 0x6b, // CGB Mode Only - Object Palette Data - GB_IO_OPRI = 0x6c, // Affects object priority (X based or index based) + GB_IO_OBPI = 0x6A, // CGB Mode Only - Object Palette Index + GB_IO_OBPD = 0x6B, // CGB Mode Only - Object Palette Data + GB_IO_OPRI = 0x6C, // Affects object priority (X based or index based) /* Missing */ GB_IO_SVBK = 0x70, // CGB Mode Only - WRAM Bank + GB_IO_PSM = 0x71, // Palette Selection Mode, controls the PSW and key combo GB_IO_PSWX = 0x72, // X position of the palette switching window GB_IO_PSWY = 0x73, // Y position of the palette switching window GB_IO_PSW = 0x74, // Key combo to trigger the palette switching window @@ -262,6 +245,8 @@ typedef enum { GB_BOOT_ROM_SGB2, GB_BOOT_ROM_CGB_0, GB_BOOT_ROM_CGB, + GB_BOOT_ROM_CGB_E, + GB_BOOT_ROM_AGB_0, GB_BOOT_ROM_AGB, } GB_boot_rom_t; @@ -272,18 +257,18 @@ typedef enum { #define SGB_PAL_FREQUENCY (21281370 / 5) #define DIV_CYCLES (0x100) -#if !defined(MIN) -#define MIN(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) +#ifdef GB_DISABLE_REWIND +#define GB_rewind_reset(...) +#define GB_rewind_push(...) +#define GB_rewind_invalidate_for_backstepping(...) #endif -#if !defined(MAX) -#define MAX(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) -#endif #endif -typedef void (*GB_vblank_callback_t)(GB_gameboy_t *gb); +typedef void (*GB_vblank_callback_t)(GB_gameboy_t *gb, GB_vblank_type_t type); typedef void (*GB_log_callback_t)(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes); typedef char *(*GB_input_callback_t)(GB_gameboy_t *gb); +typedef void (*GB_debugger_reload_callback_t)(GB_gameboy_t *gb); typedef uint32_t (*GB_rgb_encode_callback_t)(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b); typedef void (*GB_infrared_callback_t)(GB_gameboy_t *gb, bool on); typedef void (*GB_rumble_callback_t)(GB_gameboy_t *gb, double rumble_amplitude); @@ -298,6 +283,7 @@ typedef void (*GB_boot_rom_load_callback_t)(GB_gameboy_t *gb, GB_boot_rom_t type typedef void (*GB_execution_callback_t)(GB_gameboy_t *gb, uint16_t address, uint8_t opcode); typedef void (*GB_lcd_line_callback_t)(GB_gameboy_t *gb, uint8_t line); +typedef void (*GB_lcd_status_callback_t)(GB_gameboy_t *gb, bool on); struct GB_breakpoint_s; struct GB_watchpoint_s; @@ -309,11 +295,11 @@ typedef struct { bool bg_priority; // For object FIFO – the BG priority bit. For the BG FIFO – the CGB attributes priority bit } GB_fifo_item_t; -#define GB_FIFO_LENGTH 16 +#define GB_FIFO_LENGTH 8 typedef struct { GB_fifo_item_t fifo[GB_FIFO_LENGTH]; uint8_t read_end; - uint8_t write_end; + uint8_t size; } GB_fifo_t; typedef struct { @@ -355,6 +341,12 @@ typedef union { }; } GB_registers_t; +typedef GB_ENUM(uint8_t, { + GB_ACCESSORY_NONE, + GB_ACCESSORY_PRINTER, + GB_ACCESSORY_WORKBOY, +}) GB_accessory_t; + /* When state saving, each section is dumped independently of other sections. This allows adding data to the end of the section without worrying about future compatibility. Some other changes might be "safe" as well. @@ -409,14 +401,14 @@ struct GB_gameboy_internal_s { /* Misc state */ bool infrared_input; - GB_printer_t printer; - uint8_t extra_oam[0xff00 - 0xfea0]; + uint8_t extra_oam[0xFF00 - 0xFEA0]; uint32_t ram_size; // Different between CGB and DMG - GB_workboy_t workboy; - int32_t ir_sensor; - bool effective_ir_input; - uint16_t address_bus; + int32_t ir_sensor; + bool effective_ir_input; + uint16_t address_bus; + uint8_t data_bus; // cart data bus (MAIN) + uint32_t data_bus_decay_countdown; ) /* DMA and HDMA */ @@ -433,12 +425,14 @@ struct GB_gameboy_internal_s { int8_t dma_cycles_modulo; bool dma_ppu_vram_conflict; uint16_t dma_ppu_vram_conflict_addr; - uint8_t hdma_open_bus; /* Required to emulate HDMA reads from Exxx */ + bool allow_hdma_on_wake; + bool dma_restarting; ) /* MBC */ GB_SECTION(mbc, uint16_t mbc_rom_bank; + uint16_t mbc_rom0_bank; /* For multicart mappings . */ uint8_t mbc_ram_bank; uint32_t mbc_ram_size; bool mbc_ram_enable; @@ -463,28 +457,44 @@ struct GB_gameboy_internal_s { uint8_t rom_bank_low; uint8_t rom_bank_high:1; uint8_t ram_bank:4; - } mbc5; + } mbc5; // Also used for GB_CAMERA - struct { - uint8_t rom_bank; - uint16_t x_latch; - uint16_t y_latch; - bool latch_ready:1; - bool eeprom_do:1; - bool eeprom_di:1; - bool eeprom_clk:1; - bool eeprom_cs:1; - uint16_t eeprom_command:11; - uint16_t read_bits; - uint8_t bits_countdown:5; - bool secondary_ram_enable:1; - bool eeprom_write_enabled:1; - } mbc7; + struct { + uint16_t x_latch; + uint16_t y_latch; + uint8_t rom_bank; + bool latch_ready:1; + bool eeprom_do:1; + bool eeprom_di:1; + bool eeprom_clk:1; + bool eeprom_cs:1; + uint16_t eeprom_command:11; + uint16_t read_bits; + uint8_t argument_bits_left:5; + bool secondary_ram_enable:1; + bool eeprom_write_enabled:1; + } mbc7; + + struct { + uint8_t rom_bank_low:5; + uint8_t rom_bank_mid:2; + bool mbc1_mode:1; + + uint8_t rom_bank_mask:4; + uint8_t rom_bank_high:2; + uint8_t ram_bank_low:2; + + uint8_t ram_bank_high:2; + uint8_t ram_bank_mask:2; + + bool locked:1; + bool mbc1_mode_disable:1; + bool multiplex_mode:1; + } mmm01; struct { uint8_t bank_low:6; uint8_t bank_high:3; - bool mode:1; bool ir_mode:1; } huc1; @@ -492,10 +502,10 @@ struct GB_gameboy_internal_s { uint8_t rom_bank:7; uint8_t padding:1; uint8_t ram_bank:4; - uint8_t mode; - uint8_t access_index; + uint8_t mode:4; uint16_t minutes, days; uint16_t alarm_minutes, alarm_days; + uint8_t access_index; bool alarm_enabled; uint8_t read; uint8_t access_flags; @@ -507,11 +517,13 @@ struct GB_gameboy_internal_s { uint8_t mode; } tpp1; }; - uint16_t mbc_rom0_bank; /* For some MBC1 wirings. */ - bool camera_registers_mapped; - uint8_t camera_registers[0x36]; uint8_t rumble_strength; bool cart_ir; + + bool camera_registers_mapped; + uint8_t camera_registers[0x36]; + uint8_t camera_alignment; + int32_t camera_countdown; ) /* HRAM and HW Registers */ @@ -525,9 +537,13 @@ struct GB_gameboy_internal_s { GB_UNIT(display); GB_UNIT(div); uint16_t div_counter; - uint8_t tima_reload_state; /* After TIMA overflows, it becomes 0 for 4 cycles before actually reloading. */ - uint16_t serial_cycles; - uint16_t serial_length; + GB_ENUM(uint8_t, { + GB_TIMA_RUNNING = 0, + GB_TIMA_RELOADING = 1, + GB_TIMA_RELOADED = 2 + }) tima_reload_state; /* After TIMA overflows, it becomes 0 for 4 cycles before actually reloading. */ + bool serial_master_clock; + uint8_t serial_mask; uint8_t double_speed_alignment; uint8_t serial_count; int32_t speed_switch_halt_countdown; @@ -538,6 +554,9 @@ struct GB_gameboy_internal_s { bool lcd_disabled_outside_of_vblank; int32_t allowed_pending_cycles; uint16_t mode3_batching_length; + uint8_t joyp_switching_delay; + uint8_t joyp_switch_value; + uint16_t key_bounce_timing[GB_KEY_MAX]; ) /* APU */ @@ -562,25 +581,22 @@ struct GB_gameboy_internal_s { uint8_t object_palettes_data[0x40]; uint8_t position_in_line; bool stat_interrupt_line; - uint8_t effective_scx; uint8_t window_y; /* The LCDC will skip the first frame it renders after turning it on. On the CGB, a frame is not skipped if the previous frame was skipped as well. See https://www.reddit.com/r/EmuDev/comments/6exyxu/ */ /* TODO: Drop this and properly emulate the dropped vreset signal*/ - enum { + GB_ENUM(uint8_t, { GB_FRAMESKIP_LCD_TURNED_ON, // On a DMG, the LCD renders a blank screen during this state, // on a CGB, the previous frame is repeated (which might be // blank if the LCD was off for more than a few cycles) - GB_FRAMESKIP_FIRST_FRAME_SKIPPED, // This state is 'skipped' when emulating a DMG - GB_FRAMESKIP_SECOND_FRAME_RENDERED, - } frame_skip_state; + GB_FRAMESKIP_FIRST_FRAME_RENDERED, + }) frame_skip_state; bool oam_read_blocked; bool vram_read_blocked; bool oam_write_blocked; bool vram_write_blocked; - bool fifo_insertion_glitch; uint8_t current_line; uint16_t ly_for_comparison; GB_fifo_t bg_fifo, oam_fifo; @@ -597,11 +613,16 @@ struct GB_gameboy_internal_s { uint8_t objects_x[10]; uint8_t objects_y[10]; uint8_t object_tile_data[2]; - uint8_t object_flags; + uint8_t mode2_y_bus; + // They're the same bus + union { + uint8_t mode2_x_bus; + uint8_t object_flags; + }; uint8_t n_visible_objs; + uint8_t orig_n_visible_objs; uint8_t oam_search_index; uint8_t accessed_oam_row; - uint8_t extra_penalty_for_object_at_0; uint8_t mode_for_interrupt; bool lyc_interrupt_line; bool cgb_palettes_blocked; @@ -619,8 +640,21 @@ struct GB_gameboy_internal_s { bool is_odd_frame; uint16_t last_tile_data_address; uint16_t last_tile_index_address; - bool cgb_repeated_a_frame; uint8_t data_for_sel_glitch; + bool delayed_glitch_hblank_interrupt; + uint32_t frame_repeat_countdown; + bool disable_window_pixel_insertion_glitch; + bool insert_bg_pixel; + uint8_t cpu_vram_bus; + uint32_t frame_parity_ticks; + ) + + GB_SECTION(accessory, + GB_accessory_t accessory; + union { + GB_printer_t printer; + GB_workboy_t workboy; + }; ) /* Unsaved data. This includes all pointers, as well as everything that shouldn't be on a save state */ @@ -660,6 +694,8 @@ struct GB_gameboy_internal_s { bool background_disabled; bool joyp_accessed; bool illegal_inputs_allowed; + bool no_bouncing_emulation; + bool joypad_is_stable; /* Timing */ uint64_t last_sync; @@ -668,6 +704,7 @@ struct GB_gameboy_internal_s { uint32_t rtc_second_length; uint32_t clock_rate; uint32_t unmultiplied_clock_rate; + uint32_t data_bus_decay; /* Audio */ GB_apu_output_t apu_output; @@ -694,14 +731,21 @@ struct GB_gameboy_internal_s { GB_write_memory_callback_t write_memory_callback; GB_boot_rom_load_callback_t boot_rom_load_callback; GB_print_image_callback_t printer_callback; + GB_printer_done_callback_t printer_done_callback; GB_workboy_set_time_callback workboy_set_time_callback; GB_workboy_get_time_callback workboy_get_time_callback; GB_execution_callback_t execution_callback; GB_lcd_line_callback_t lcd_line_callback; - + GB_lcd_status_callback_t lcd_status_callback; + GB_debugger_reload_callback_t debugger_reload_callback; + +#ifndef GB_DISABLE_DEBUGGER /*** Debugger ***/ volatile bool debug_stopped, debug_disable; bool debug_fin_command, debug_next_command; + bool debug_active; // Cached value determining if GB_debugger_run does anything + bool help_shown; + uint32_t backstep_instructions; /* Breakpoints */ uint16_t n_breakpoints; @@ -710,8 +754,6 @@ struct GB_gameboy_internal_s { void *nontrivial_jump_state; bool non_trivial_jump_breakpoint_occured; - /* SLD (Todo: merge with backtrace) */ - bool stack_leak_detection; signed debug_call_depth; uint16_t sp_for_call_depth[0x200]; /* Should be much more than enough */ uint16_t addr_for_call_depth[0x200]; @@ -729,7 +771,8 @@ struct GB_gameboy_internal_s { struct GB_watchpoint_s *watchpoints; /* Symbol tables */ - GB_symbol_map_t *bank_symbols[0x200]; + GB_symbol_map_t **bank_symbols; + size_t n_symbol_maps; GB_reversed_symbol_map_t reversed_symbol_map; /* Ticks command */ @@ -739,15 +782,21 @@ struct GB_gameboy_internal_s { /* Undo */ uint8_t *undo_state; const char *undo_label; +#endif +#ifndef GB_DISABLE_REWIND /* Rewind */ size_t rewind_buffer_length; + size_t rewind_state_size; struct { uint8_t *key_state; uint8_t *compressed_states[GB_REWIND_FRAMES_PER_KEY]; + uint32_t instruction_count[GB_REWIND_FRAMES_PER_KEY + 1]; unsigned pos; } *rewind_sequences; // lasts about 4 seconds size_t rewind_pos; + bool rewind_disable_invalidation; +#endif /* SGB - saved and allocated optionally */ GB_sgb_t *sgb; @@ -756,11 +805,13 @@ struct GB_gameboy_internal_s { double sgb_intro_sweep_phase; double sgb_intro_sweep_previous_sample; - /* Cheats */ +#ifndef GB_DISABLE_CHEATS + /* Cheats */ bool cheat_enabled; size_t cheat_count; GB_cheat_t **cheats; GB_cheat_hash_t *cheat_hash[256]; +#endif /* Misc */ bool turbo; @@ -780,7 +831,12 @@ struct GB_gameboy_internal_s { bool disable_oam_corruption; // For safe memory reads bool in_dma_read; bool hdma_in_progress; + bool returned_open_bus; uint16_t addr_for_hdma_conflict; + bool during_div_write; + + /* Thread safety (debug only) */ + void *running_thread_id; GB_gbs_header_t gbs_header; ) @@ -799,15 +855,39 @@ struct GB_gameboy_s { __attribute__((__format__ (__printf__, fmtarg, firstvararg))) #endif -void GB_init(GB_gameboy_t *gb, GB_model_t model); +/* + There are two instance allocation styles – one where you manage your + own instance allocation, and one where you use provided allocators. + + Managing allocations yourself: + GB_gameboy_t gb; + GB_init(&gb, model); + ... + GB_free(&gb); + + Using the provided allocators: + GB_gameboy_t *gb = GB_init(GB_alloc(), model); + ... + GB_free(gb); // optional + GB_dealloc(gb); + +*/ +GB_gameboy_t *GB_init(GB_gameboy_t *gb, GB_model_t model); +void GB_free(GB_gameboy_t *gb); +GB_gameboy_t *GB_alloc(void); +void GB_dealloc(GB_gameboy_t *gb); + +// For when you want to use your own malloc implementation without having to rely on the header struct +size_t GB_allocation_size(void); + bool GB_is_inited(GB_gameboy_t *gb); bool GB_is_cgb(const GB_gameboy_t *gb); bool GB_is_cgb_in_cgb_mode(GB_gameboy_t *gb); bool GB_is_sgb(GB_gameboy_t *gb); // Returns true if the model is SGB or SGB2 bool GB_is_hle_sgb(GB_gameboy_t *gb); // Returns true if the model is SGB or SGB2 and the SFC/SNES side is HLE'd GB_model_t GB_get_model(GB_gameboy_t *gb); -void GB_free(GB_gameboy_t *gb); void GB_reset(GB_gameboy_t *gb); +void GB_quick_reset(GB_gameboy_t *gb); // Similar to the cart reset line void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model); /* Returns the time passed, in 8MHz ticks. */ @@ -827,6 +907,7 @@ typedef enum { GB_DIRECT_ACCESS_BGP, GB_DIRECT_ACCESS_OBP, GB_DIRECT_ACCESS_IE, + GB_DIRECT_ACCESS_ROM0, // Identical to ROM, but returns the correct rom0 bank in the bank output argument } GB_direct_access_t; /* Returns a mutable pointer to various hardware memories. If that memory is banked, the current bank @@ -860,6 +941,7 @@ void GB_log(GB_gameboy_t *gb, const char *fmt, ...) __printflike(2, 3); void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, ...) __printflike(3, 4); void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output); +uint32_t *GB_get_pixels_output(GB_gameboy_t *gb); void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode); void GB_set_infrared_input(GB_gameboy_t *gb, bool state); @@ -868,6 +950,7 @@ void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback); void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback); void GB_set_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback); void GB_set_async_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback); +void GB_set_debugger_reload_callback(GB_gameboy_t *gb, GB_debugger_reload_callback_t callback); void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback); void GB_set_infrared_callback(GB_gameboy_t *gb, GB_infrared_callback_t callback); void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback); @@ -877,6 +960,7 @@ void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t void GB_set_execution_callback(GB_gameboy_t *gb, GB_execution_callback_t callback); void GB_set_lcd_line_callback(GB_gameboy_t *gb, GB_lcd_line_callback_t callback); +void GB_set_lcd_status_callback(GB_gameboy_t *gb, GB_lcd_status_callback_t callback); void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette); const GB_palette_t *GB_get_palette(GB_gameboy_t *gb); @@ -890,8 +974,10 @@ bool GB_serial_get_data_bit(GB_gameboy_t *gb); void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data); void GB_disconnect_serial(GB_gameboy_t *gb); +GB_accessory_t GB_get_built_in_accessory(GB_gameboy_t *gb); /* For cartridges with an alarm clock */ +bool GB_rom_supports_alarms(GB_gameboy_t *gb); unsigned GB_time_to_alarm(GB_gameboy_t *gb); // 0 if no alarm /* For cartridges motion controls */ @@ -900,6 +986,9 @@ bool GB_has_accelerometer(GB_gameboy_t *gb); // Values within ±4 recommended void GB_set_accelerometer_values(GB_gameboy_t *gb, double x, double y); +// Time it takes for a value in the data bus to decay to FF, in 8MHz units. (0 to never decay, like e.g. an EverDrive) +void GB_set_open_bus_decay_time(GB_gameboy_t *gb, uint32_t decay); + /* For integration with SFC/SNES emulators */ void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback); void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback); @@ -925,4 +1014,29 @@ internal void GB_borrow_sgb_border(GB_gameboy_t *gb); internal void GB_update_clock_rate(GB_gameboy_t *gb); #endif -#endif /* GB_h */ +#ifdef GB_INTERNAL + +#ifndef NDEBUG +#define GB_CONTEXT_SAFETY +#endif + +#ifdef GB_CONTEXT_SAFETY +#include +internal void *GB_get_thread_id(void); +internal void GB_set_running_thread(GB_gameboy_t *gb); +internal void GB_clear_running_thread(GB_gameboy_t *gb); +#define GB_ASSERT_NOT_RUNNING(gb) if (gb->running_thread_id) {GB_log(gb, "Function %s must not be called in a running context.\n", __FUNCTION__); assert(!gb->running_thread_id);} +#define GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) if (gb->running_thread_id && gb->running_thread_id != GB_get_thread_id()) {GB_log(gb, "Function %s must not be called while running in another thread.\n", __FUNCTION__); assert(!gb->running_thread_id || gb->running_thread_id == GB_get_thread_id());} + +#else +#define GB_ASSERT_NOT_RUNNING(gb) +#define GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) +#define GB_set_running_thread(gb) +#define GB_clear_running_thread(gb) +#endif + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/Core/joypad.c b/Core/joypad.c index df3d201..b588dc2 100644 --- a/Core/joypad.c +++ b/Core/joypad.c @@ -1,6 +1,41 @@ #include "gb.h" #include +static inline bool should_bounce(GB_gameboy_t *gb) +{ + // Bouncing is super rare on an AGS, so don't emulate it on GB_MODEL_AGB_B (when addeed) + return !GB_is_sgb(gb) && !gb-> no_bouncing_emulation && !(gb->model & GB_MODEL_GBP_BIT) /*&& gb->model != GB_MODEL_AGB_B*/; +} + +static inline uint16_t bounce_for_key(GB_gameboy_t *gb, GB_key_t key) +{ + if (gb->model > GB_MODEL_CGB_E) { + // AGB are less bouncy + return 0xBFF; + } + if (key == GB_KEY_START || key == GB_KEY_SELECT) { + return 0x1FFF; + } + return 0xFFF; +} + +static inline bool get_input(GB_gameboy_t *gb, uint8_t player, GB_key_t key) +{ + if (player != 0) { + return gb->keys[player][key]; + } + bool ret = gb->keys[player][key]; + + if (likely(gb->key_bounce_timing[key] == 0)) return ret; + if (likely((gb->key_bounce_timing[key] & 0x3FF) > 0x300)) return ret; + uint16_t semi_random = ((((key << 5) + gb->div_counter) * 17) ^ ((gb->apu.apu_cycles + gb->display_cycles) * 13)); + semi_random >>= 3; + if (semi_random < gb->key_bounce_timing[key]) { + ret ^= true; + } + return ret; +} + void GB_update_joyp(GB_gameboy_t *gb) { if (gb->model & GB_MODEL_NO_SFC_BIT) return; @@ -8,7 +43,6 @@ void GB_update_joyp(GB_gameboy_t *gb) uint8_t key_selection = 0; uint8_t previous_state = 0; - /* Todo: add delay to key selection */ previous_state = gb->io_registers[GB_IO_JOYP] & 0xF; key_selection = (gb->io_registers[GB_IO_JOYP] >> 4) & 3; gb->io_registers[GB_IO_JOYP] &= 0xF0; @@ -27,7 +61,7 @@ void GB_update_joyp(GB_gameboy_t *gb) case 2: /* Direction keys */ for (uint8_t i = 0; i < 4; i++) { - gb->io_registers[GB_IO_JOYP] |= (!gb->keys[current_player][i]) << i; + gb->io_registers[GB_IO_JOYP] |= (!get_input(gb, current_player, i)) << i; } /* Forbid pressing two opposing keys, this breaks a lot of games; even if it's somewhat possible. */ if (likely(!gb->illegal_inputs_allowed)) { @@ -43,22 +77,21 @@ void GB_update_joyp(GB_gameboy_t *gb) case 1: /* Other keys */ for (uint8_t i = 0; i < 4; i++) { - gb->io_registers[GB_IO_JOYP] |= (!gb->keys[current_player][i + 4]) << i; + gb->io_registers[GB_IO_JOYP] |= (!get_input(gb, current_player, i + 4)) << i; } break; case 0: for (uint8_t i = 0; i < 4; i++) { - gb->io_registers[GB_IO_JOYP] |= (!(gb->keys[current_player][i] || gb->keys[current_player][i + 4])) << i; + gb->io_registers[GB_IO_JOYP] |= (!(get_input(gb, current_player, i) || get_input(gb, current_player, i + 4))) << i; } break; nodefault; } - /* Todo: This assumes the keys *always* bounce, which is incorrect when emulating an SGB */ - if (previous_state != (gb->io_registers[GB_IO_JOYP] & 0xF)) { - /* The joypad interrupt DOES occur on CGB (Tested on CGB-E), unlike what some documents say. */ + // TODO: Implement the lame anti-debouncing mechanism as seen on the DMG schematics + if (previous_state & ~(gb->io_registers[GB_IO_JOYP] & 0xF)) { if (!(gb->io_registers[GB_IO_IF] & 0x10)) { gb->joyp_accessed = true; gb->io_registers[GB_IO_IF] |= 0x10; @@ -86,6 +119,10 @@ void GB_icd_set_joyp(GB_gameboy_t *gb, uint8_t value) void GB_set_key_state(GB_gameboy_t *gb, GB_key_t index, bool pressed) { assert(index >= 0 && index < GB_KEY_MAX); + if (should_bounce(gb) && pressed != gb->keys[0][index]) { + gb->joypad_is_stable = false; + gb->key_bounce_timing[index] = bounce_for_key(gb, index); + } gb->keys[0][index] = pressed; GB_update_joyp(gb); } @@ -94,20 +131,23 @@ void GB_set_key_state_for_player(GB_gameboy_t *gb, GB_key_t index, unsigned play { assert(index >= 0 && index < GB_KEY_MAX); assert(player < 4); + if (should_bounce(gb) && pressed != gb->keys[player][index]) { + gb->joypad_is_stable = false; + gb->key_bounce_timing[index] = bounce_for_key(gb, index); + } gb->keys[player][index] = pressed; GB_update_joyp(gb); } void GB_set_key_mask(GB_gameboy_t *gb, GB_key_mask_t mask) { - memset(gb->keys, 0, sizeof(gb->keys)); - bool *key = &gb->keys[0][0]; - while (mask) { - if (mask & 1) { - *key = true; + for (unsigned i = 0; i < GB_KEY_MAX; i++) { + bool pressed = mask & (1 << i); + if (should_bounce(gb) && pressed != gb->keys[0][i]) { + gb->joypad_is_stable = false; + gb->key_bounce_timing[i] = bounce_for_key(gb, i); } - mask >>= 1; - key++; + gb->keys[0][i] = pressed; } GB_update_joyp(gb); @@ -115,19 +155,53 @@ void GB_set_key_mask(GB_gameboy_t *gb, GB_key_mask_t mask) void GB_set_key_mask_for_player(GB_gameboy_t *gb, GB_key_mask_t mask, unsigned player) { - memset(gb->keys[player], 0, sizeof(gb->keys[player])); - bool *key = gb->keys[player]; - while (mask) { - if (mask & 1) { - *key = true; + for (unsigned i = 0; i < GB_KEY_MAX; i++) { + bool pressed = mask & (1 << i); + if (should_bounce(gb) && pressed != gb->keys[player][i]) { + gb->joypad_is_stable = false; + gb->key_bounce_timing[i] = bounce_for_key(gb, i); } - mask >>= 1; - key++; + gb->keys[player][i] = pressed; } GB_update_joyp(gb); } +void GB_joypad_run(GB_gameboy_t *gb, unsigned cycles) +{ + if (gb->joypad_is_stable) return; + bool should_update_joyp = false; + gb->joypad_is_stable = true; + if (gb->joyp_switching_delay) { + gb->joypad_is_stable = false; + if (gb->joyp_switching_delay > cycles) { + gb->joyp_switching_delay -= cycles; + } + else { + gb->joyp_switching_delay = 0; + gb->io_registers[GB_IO_JOYP] = (gb->joyp_switch_value & 0xF0) | (gb->io_registers[GB_IO_JOYP] & 0x0F); + should_update_joyp = true; + } + } + + for (unsigned i = 0; i < GB_KEY_MAX; i++) { + if (gb->key_bounce_timing[i]) { + gb->joypad_is_stable = false; + should_update_joyp = true; + if (gb->key_bounce_timing[i] > cycles) { + gb->key_bounce_timing[i] -= cycles; + } + else { + gb->key_bounce_timing[i] = 0; + } + } + } + + if (should_update_joyp) { + GB_update_joyp(gb); + } +} + bool GB_get_joyp_accessed(GB_gameboy_t *gb) { return gb->joyp_accessed; @@ -142,3 +216,8 @@ void GB_set_allow_illegal_inputs(GB_gameboy_t *gb, bool allow) { gb->illegal_inputs_allowed = allow; } + +void GB_set_emulate_joypad_bouncing(GB_gameboy_t *gb, bool emulate) +{ + gb->no_bouncing_emulation = !emulate; +} diff --git a/Core/joypad.h b/Core/joypad.h index 21574e0..3bf258c 100644 --- a/Core/joypad.h +++ b/Core/joypad.h @@ -1,5 +1,4 @@ -#ifndef joypad_h -#define joypad_h +#pragma once #include "defs.h" #include @@ -37,9 +36,9 @@ void GB_icd_set_joyp(GB_gameboy_t *gb, uint8_t value); bool GB_get_joyp_accessed(GB_gameboy_t *gb); void GB_clear_joyp_accessed(GB_gameboy_t *gb); void GB_set_allow_illegal_inputs(GB_gameboy_t *gb, bool allow); - +void GB_set_emulate_joypad_bouncing(GB_gameboy_t *gb, bool emulate); #ifdef GB_INTERNAL internal void GB_update_joyp(GB_gameboy_t *gb); +internal void GB_joypad_run(GB_gameboy_t *gb, unsigned cycles); #endif -#endif /* joypad_h */ diff --git a/Core/mbc.c b/Core/mbc.c index 5ade9aa..03dc8f6 100644 --- a/Core/mbc.c +++ b/Core/mbc.c @@ -5,42 +5,41 @@ const GB_cartridge_t GB_cart_defs[256] = { // From http://gbdev.gg8.se/wiki/articles/The_Cartridge_Header#0147_-_Cartridge_Type - /* MBC SUBTYPE RAM BAT. RTC RUMB. */ - { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 00h ROM ONLY - { GB_MBC1 , GB_STANDARD_MBC, false, false, false, false}, // 01h MBC1 - { GB_MBC1 , GB_STANDARD_MBC, true , false, false, false}, // 02h MBC1+RAM - { GB_MBC1 , GB_STANDARD_MBC, true , true , false, false}, // 03h MBC1+RAM+BATTERY + /* MBC RAM BAT. RTC RUMB. */ + { GB_NO_MBC, false, false, false, false}, // 00h ROM ONLY + { GB_MBC1 , false, false, false, false}, // 01h MBC1 + { GB_MBC1 , true , false, false, false}, // 02h MBC1+RAM + { GB_MBC1 , true , true , false, false}, // 03h MBC1+RAM+BATTERY [5] = - { GB_MBC2 , GB_STANDARD_MBC, true , false, false, false}, // 05h MBC2 - { GB_MBC2 , GB_STANDARD_MBC, true , true , false, false}, // 06h MBC2+BATTERY + { GB_MBC2 , true , false, false, false}, // 05h MBC2 + { GB_MBC2 , true , true , false, false}, // 06h MBC2+BATTERY [8] = - { GB_NO_MBC, GB_STANDARD_MBC, true , false, false, false}, // 08h ROM+RAM - { GB_NO_MBC, GB_STANDARD_MBC, true , true , false, false}, // 09h ROM+RAM+BATTERY + { GB_NO_MBC, true , false, false, false}, // 08h ROM+RAM + { GB_NO_MBC, true , true , false, false}, // 09h ROM+RAM+BATTERY [0xB] = - /* Todo: Not supported yet */ - { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Bh MMM01 - { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Ch MMM01+RAM - { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // 0Dh MMM01+RAM+BATTERY + { GB_MMM01 , false, false, false, false}, // 0Bh MMM01 + { GB_MMM01 , true , false, false, false}, // 0Ch MMM01+RAM + { GB_MMM01 , true , true , false, false}, // 0Dh MMM01+RAM+BATTERY [0xF] = - { GB_MBC3 , GB_STANDARD_MBC, false, true, true , false}, // 0Fh MBC3+TIMER+BATTERY - { GB_MBC3 , GB_STANDARD_MBC, true , true, true , false}, // 10h MBC3+TIMER+RAM+BATTERY - { GB_MBC3 , GB_STANDARD_MBC, false, false, false, false}, // 11h MBC3 - { GB_MBC3 , GB_STANDARD_MBC, true , false, false, false}, // 12h MBC3+RAM - { GB_MBC3 , GB_STANDARD_MBC, true , true , false, false}, // 13h MBC3+RAM+BATTERY + { GB_MBC3 , false, true, true , false}, // 0Fh MBC3+TIMER+BATTERY + { GB_MBC3 , true , true, true , false}, // 10h MBC3+TIMER+RAM+BATTERY + { GB_MBC3 , false, false, false, false}, // 11h MBC3 + { GB_MBC3 , true , false, false, false}, // 12h MBC3+RAM + { GB_MBC3 , true , true , false, false}, // 13h MBC3+RAM+BATTERY [0x19] = - { GB_MBC5 , GB_STANDARD_MBC, false, false, false, false}, // 19h MBC5 - { GB_MBC5 , GB_STANDARD_MBC, true , false, false, false}, // 1Ah MBC5+RAM - { GB_MBC5 , GB_STANDARD_MBC, true , true , false, false}, // 1Bh MBC5+RAM+BATTERY - { GB_MBC5 , GB_STANDARD_MBC, false, false, false, true }, // 1Ch MBC5+RUMBLE - { GB_MBC5 , GB_STANDARD_MBC, true , false, false, true }, // 1Dh MBC5+RUMBLE+RAM - { GB_MBC5 , GB_STANDARD_MBC, true , true , false, true }, // 1Eh MBC5+RUMBLE+RAM+BATTERY + { GB_MBC5 , false, false, false, false}, // 19h MBC5 + { GB_MBC5 , true , false, false, false}, // 1Ah MBC5+RAM + { GB_MBC5 , true , true , false, false}, // 1Bh MBC5+RAM+BATTERY + { GB_MBC5 , false, false, false, true }, // 1Ch MBC5+RUMBLE + { GB_MBC5 , true , false, false, true }, // 1Dh MBC5+RUMBLE+RAM + { GB_MBC5 , true , true , false, true }, // 1Eh MBC5+RUMBLE+RAM+BATTERY [0x22] = - { GB_MBC7 , GB_STANDARD_MBC, true, true, false, false}, // 22h MBC7+ACCEL+EEPROM + { GB_MBC7 , true, true, false, false}, // 22h MBC7+ACCEL+EEPROM [0xFC] = - { GB_MBC5 , GB_CAMERA , true , true , false, false}, // FCh POCKET CAMERA - { GB_NO_MBC, GB_STANDARD_MBC, false, false, false, false}, // FDh BANDAI TAMA5 (Todo: Not supported) - { GB_HUC3 , GB_STANDARD_MBC, true , true , true, false}, // FEh HuC3 - { GB_HUC1 , GB_STANDARD_MBC, true , true , false, false}, // FFh HuC1+RAM+BATTERY + { GB_CAMERA, true , true , false, false}, // FCh POCKET CAMERA + { GB_NO_MBC, false, false, false, false}, // FDh BANDAI TAMA5 (Todo: Not supported) + { GB_HUC3 , true , true , true, false}, // FEh HuC3 + { GB_HUC1 , true , true , false, false}, // FFh HuC1+RAM+BATTERY }; void GB_update_mbc_mappings(GB_gameboy_t *gb) @@ -97,22 +96,51 @@ void GB_update_mbc_mappings(GB_gameboy_t *gb) } break; case GB_MBC5: + case GB_CAMERA: gb->mbc_rom_bank = gb->mbc5.rom_bank_low | (gb->mbc5.rom_bank_high << 8); gb->mbc_ram_bank = gb->mbc5.ram_bank; break; case GB_MBC7: gb->mbc_rom_bank = gb->mbc7.rom_bank; break; - case GB_HUC1: - if (gb->huc1.mode == 0) { - gb->mbc_rom_bank = gb->huc1.bank_low | (gb->mbc1.bank_high << 6); - gb->mbc_ram_bank = 0; + case GB_MMM01: + if (gb->mmm01.locked) { + if (gb->mmm01.multiplex_mode) { + gb->mbc_rom0_bank = (gb->mmm01.rom_bank_low & (gb->mmm01.rom_bank_mask << 1)) | + ((gb->mmm01.mbc1_mode? 0 : gb->mmm01.ram_bank_low) << 5) | + (gb->mmm01.rom_bank_high << 7); + gb->mbc_rom_bank = gb->mmm01.rom_bank_low | + (gb->mmm01.ram_bank_low << 5) | + (gb->mmm01.rom_bank_high << 7); + gb->mbc_ram_bank = gb->mmm01.rom_bank_mid | (gb->mmm01.ram_bank_high << 2); + } + else { + gb->mbc_rom0_bank = (gb->mmm01.rom_bank_low & (gb->mmm01.rom_bank_mask << 1)) | + (gb->mmm01.rom_bank_mid << 5) | + (gb->mmm01.rom_bank_high << 7); + gb->mbc_rom_bank = gb->mmm01.rom_bank_low | + (gb->mmm01.rom_bank_mid << 5) | + (gb->mmm01.rom_bank_high << 7); + if (gb->mmm01.mbc1_mode) { + gb->mbc_ram_bank = gb->mmm01.ram_bank_low | (gb->mmm01.ram_bank_high << 2); + } + else { + gb->mbc_ram_bank = gb->mmm01.ram_bank_low | (gb->mmm01.ram_bank_high << 2); + } + } + if (gb->mbc_rom_bank == gb->mbc_rom0_bank) { + gb->mbc_rom_bank++; + } } else { - gb->mbc_rom_bank = gb->huc1.bank_low; - gb->mbc_ram_bank = gb->huc1.bank_high; + gb->mbc_rom_bank = -1; + gb->mbc_rom0_bank = -2; } break; + case GB_HUC1: + gb->mbc_rom_bank = gb->huc1.bank_low; + gb->mbc_ram_bank = gb->huc1.bank_high; + break; case GB_HUC3: gb->mbc_rom_bank = gb->huc3.rom_bank; gb->mbc_ram_bank = gb->huc3.ram_bank; @@ -128,29 +156,57 @@ void GB_update_mbc_mappings(GB_gameboy_t *gb) void GB_configure_cart(GB_gameboy_t *gb) { + memset(GB_GET_SECTION(gb, mbc), 0, GB_SECTION_SIZE(mbc)); gb->cartridge_type = &GB_cart_defs[gb->rom[0x147]]; - if (gb->rom[0x147] == 0xbc && - gb->rom[0x149] == 0xc1 && - gb->rom[0x14a] == 0x65) { - static const GB_cartridge_t tpp1 = {GB_TPP1, GB_STANDARD_MBC, true, true, true, true}; + if (gb->cartridge_type->mbc_type == GB_MMM01) { + uint8_t *temp = malloc(0x8000); + memcpy(temp, gb->rom, 0x8000); + memmove(gb->rom, gb->rom + 0x8000, gb->rom_size - 0x8000); + memcpy(gb->rom + gb->rom_size - 0x8000, temp, 0x8000); + free(temp); + } + else { + const GB_cartridge_t *maybe_mmm01_type = &GB_cart_defs[gb->rom[gb->rom_size - 0x8000 + 0x147]]; + if (memcmp(gb->rom + 0x104, gb->rom + gb->rom_size - 0x8000 + 0x104, 0x30) == 0) { + if (maybe_mmm01_type->mbc_type == GB_MMM01) { + gb->cartridge_type = maybe_mmm01_type; + } + else if(gb->rom[gb->rom_size - 0x8000 + 0x147] == 0x11) { + GB_log(gb, "ROM header reports MBC3, but it appears to be an MMM01 ROM. Assuming cartridge uses MMM01."); + gb->cartridge_type = &GB_cart_defs[0xB]; + } + } + } + + if (gb->rom[0x147] == 0xBC && + gb->rom[0x149] == 0xC1 && + gb->rom[0x14A] == 0x65) { + static const GB_cartridge_t tpp1 = {GB_TPP1, true, true, true, true}; gb->cartridge_type = &tpp1; gb->tpp1.rom_bank = 1; } - if (gb->rom[0x147] == 0 && gb->rom_size > 0x8000) { - GB_log(gb, "ROM header reports no MBC, but file size is over 32Kb. Assuming cartridge uses MBC3.\n"); - gb->cartridge_type = &GB_cart_defs[0x11]; - } - else if (gb->rom[0x147] != 0 && memcmp(gb->cartridge_type, &GB_cart_defs[0], sizeof(GB_cart_defs[0])) == 0) { - GB_log(gb, "Cartridge type %02x is not yet supported.\n", gb->rom[0x147]); + if (gb->cartridge_type->mbc_type != GB_MMM01) { + if (gb->rom[0x147] == 0 && gb->rom_size > 0x8000) { + GB_log(gb, "ROM header reports no MBC, but file size is over 32Kb. Assuming cartridge uses MBC3.\n"); + gb->cartridge_type = &GB_cart_defs[0x11]; + } + else if (gb->rom[0x147] != 0 && memcmp(gb->cartridge_type, &GB_cart_defs[0], sizeof(GB_cart_defs[0])) == 0) { + GB_log(gb, "Cartridge type %02x is not yet supported.\n", gb->rom[0x147]); + } } - if (gb->mbc_ram) { - free(gb->mbc_ram); - gb->mbc_ram = NULL; - gb->mbc_ram_size = 0; + if (!gb->cartridge_type->has_ram && + gb->cartridge_type->mbc_type != GB_NO_MBC && + gb->cartridge_type->mbc_type != GB_TPP1 && + gb->rom[0x149]) { + GB_log(gb, "ROM header reports no RAM, but also reports a non-zero RAM size. Assuming cartridge has RAM.\n"); + gb->cartridge_type++; } - + + size_t old_mbc_ram_size = gb->mbc_ram_size; + gb->mbc_ram_size = 0; + if (gb->cartridge_type->has_ram) { if (gb->cartridge_type->mbc_type == GB_MBC2) { gb->mbc_ram_size = 0x200; @@ -165,15 +221,24 @@ void GB_configure_cart(GB_gameboy_t *gb) } else { static const unsigned ram_sizes[256] = {0, 0x800, 0x2000, 0x8000, 0x20000, 0x10000}; - gb->mbc_ram_size = ram_sizes[gb->rom[0x149]]; + if (gb->cartridge_type->mbc_type == GB_MMM01) { + gb->mbc_ram_size = ram_sizes[gb->rom[gb->rom_size - 0x8000 + 0x149]]; + } + else { + gb->mbc_ram_size = ram_sizes[gb->rom[0x149]]; + } } - if (gb->mbc_ram_size) { - gb->mbc_ram = malloc(gb->mbc_ram_size); + if (gb->mbc_ram && old_mbc_ram_size != gb->mbc_ram_size) { + free(gb->mbc_ram); + gb->mbc_ram = NULL; + } + + if (gb->mbc_ram_size && !gb->mbc_ram) { + gb->mbc_ram = malloc(gb->mbc_ram_size); + /* Todo: Some games assume unintialized MBC RAM is 0xFF. It this true for all cartridge types? */ + memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size); } - - /* Todo: Some games assume unintialized MBC RAM is 0xFF. It this true for all cartridge types? */ - memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size); } /* MBC1 has at least 3 types of wiring (We currently support two (Standard and 4bit-MBC1M) of these). @@ -193,16 +258,29 @@ void GB_configure_cart(GB_gameboy_t *gb) } } - /* Set MBC5's bank to 1 correctly */ - if (gb->cartridge_type->mbc_type == GB_MBC5) { - gb->mbc5.rom_bank_low = 1; + GB_reset_mbc(gb); +} + +void GB_reset_mbc(GB_gameboy_t *gb) +{ + gb->mbc_rom0_bank = 0; + if (gb->cartridge_type->mbc_type == GB_MMM01) { + gb->mbc_rom_bank = -1; + gb->mbc_rom0_bank = -2; + gb->mmm01.ram_bank_mask = -1; } - - /* Initial MBC7 state */ - if (gb->cartridge_type->mbc_type == GB_MBC7) { + else if (gb->cartridge_type->mbc_type == GB_MBC5 || + gb->cartridge_type->mbc_type == GB_CAMERA) { + gb->mbc5.rom_bank_low = 1; + gb->mbc_rom_bank = 1; + } + else if (gb->cartridge_type->mbc_type == GB_MBC7) { gb->mbc7.x_latch = gb->mbc7.y_latch = 0x8000; gb->mbc7.latch_ready = true; gb->mbc7.read_bits = -1; gb->mbc7.eeprom_do = true; } + else { + gb->mbc_rom_bank = 1; + } } diff --git a/Core/mbc.h b/Core/mbc.h index 7e6cc82..78dd09d 100644 --- a/Core/mbc.h +++ b/Core/mbc.h @@ -1,5 +1,4 @@ -#ifndef MBC_h -#define MBC_h +#pragma once #include "defs.h" #include @@ -11,14 +10,12 @@ typedef struct { GB_MBC3, GB_MBC5, GB_MBC7, + GB_MMM01, GB_HUC1, GB_HUC3, GB_TPP1, - } mbc_type; - enum { - GB_STANDARD_MBC, GB_CAMERA, - } mbc_subtype; + } mbc_type; bool has_ram; bool has_battery; bool has_rtc; @@ -26,9 +23,8 @@ typedef struct { } GB_cartridge_t; #ifdef GB_INTERNAL -extern const GB_cartridge_t GB_cart_defs[256]; +internal extern const GB_cartridge_t GB_cart_defs[256]; internal void GB_update_mbc_mappings(GB_gameboy_t *gb); internal void GB_configure_cart(GB_gameboy_t *gb); +internal void GB_reset_mbc(GB_gameboy_t *gb); #endif - -#endif /* MBC_h */ diff --git a/Core/memory.c b/Core/memory.c index bd50fe2..d9e5a06 100644 --- a/Core/memory.c +++ b/Core/memory.c @@ -1,5 +1,6 @@ #include #include +#include #include "gb.h" typedef uint8_t read_function_t(GB_gameboy_t *gb, uint16_t addr); @@ -95,7 +96,7 @@ void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address) if (address >= 0xFE00 && address < 0xFF00) { GB_display_sync(gb); - if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) { + if (gb->accessed_oam_row != 0xFF && gb->accessed_oam_row >= 8) { uint16_t *base = (uint16_t *)(gb->oam + gb->accessed_oam_row); base[0] = bitwise_glitch(base[0], base[-4], @@ -197,7 +198,7 @@ void GB_trigger_oam_bug_read(GB_gameboy_t *gb, uint16_t address) if (GB_is_cgb(gb)) return; if (address >= 0xFE00 && address < 0xFF00) { - if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) { + if (gb->accessed_oam_row != 0xFF && gb->accessed_oam_row >= 8) { if ((gb->accessed_oam_row & 0x18) == 0x10) { oam_bug_secondary_read_corruption(gb); } @@ -251,16 +252,16 @@ void GB_trigger_oam_bug_read(GB_gameboy_t *gb, uint16_t address) static bool is_addr_in_dma_use(GB_gameboy_t *gb, uint16_t addr) { - if (!GB_is_dma_active(gb) || addr >= 0xfe00) return false; + if (!GB_is_dma_active(gb) || addr >= 0xFE00 || gb->hdma_in_progress) return false; if (gb->dma_current_dest == 0xFF || gb->dma_current_dest == 0x0) return false; // Warm up - if (addr >= 0xfe00) return false; + if (addr >= 0xFE00) return false; if (gb->dma_current_src == addr) return false; // Shortcut for DMA access flow - if (gb->dma_current_src > 0xe000 && (gb->dma_current_src & ~0x2000) == addr) return false; + if (gb->dma_current_src >= 0xE000 && (gb->dma_current_src & ~0x2000) == addr) return false; if (GB_is_cgb(gb)) { - if (addr >= 0xe000) { + if (addr >= 0xC000) { return bus_for_addr(gb, gb->dma_current_src) != GB_BUS_VRAM; } - if (gb->dma_current_src >= 0xe000) { + if (gb->dma_current_src >= 0xE000) { return bus_for_addr(gb, addr) != GB_BUS_VRAM; } } @@ -297,28 +298,42 @@ static uint8_t read_vram(GB_gameboy_t *gb, uint16_t addr) GB_display_sync(gb); } else { - if ((gb->dma_current_dest & 0xE000) == 0x8000) { + if (unlikely((gb->dma_current_dest & 0xE000) == 0x8000)) { // TODO: verify conflict behavior - return gb->vram[(addr & 0x1FFF) + (gb->cgb_vram_bank? 0x2000 : 0)]; + return gb->cpu_vram_bus = gb->vram[(addr & 0x1FFF) + (gb->cgb_vram_bank? 0x2000 : 0)]; } } if (unlikely(gb->vram_read_blocked && !gb->in_dma_read)) { return 0xFF; } - if (unlikely(gb->display_state == 22 && GB_is_cgb(gb) && !gb->cgb_double_speed)) { - if (addr & 0x1000) { - addr = gb->last_tile_index_address; + if (unlikely(gb->display_state == 22)) { + if (!GB_is_cgb(gb)) { + if (addr & 0x1000 && !(gb->last_tile_data_address & 0x1000)) { + addr &= ~0x1000; // TODO: verify + } } - else if (gb->last_tile_data_address & 0x1000) { - /* TODO: This is case is more complicated then the rest and differ between revisions - It's probably affected by how VRAM is layed out, might be easier after a decap is done*/ - } - else { - addr = gb->last_tile_data_address; + else if (!gb->cgb_double_speed) { + if (addr & 0x1000) { + if (gb->model <= GB_MODEL_CGB_C && !(gb->last_tile_data_address & 0x1000)) { + return 0; + } + addr = gb->last_tile_index_address; + } + else if (gb->last_tile_data_address & 0x1000) { + if (gb->model >= GB_MODEL_CGB_E) { + uint8_t ret = gb->cpu_vram_bus; + gb->cpu_vram_bus = gb->vram[(addr & 0x1FFF) + (gb->cgb_vram_bank? 0x2000 : 0)]; + return ret; + } + return gb->cpu_vram_bus; + } + else { + addr = gb->last_tile_data_address; + } } } - return gb->vram[(addr & 0x1FFF) + (gb->cgb_vram_bank? 0x2000 : 0)]; + return gb->cpu_vram_bus = gb->vram[(addr & 0x1FFF) + (gb->cgb_vram_bank? 0x2000 : 0)]; } static uint8_t read_mbc7_ram(GB_gameboy_t *gb, uint16_t addr) @@ -379,18 +394,20 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) case 5: return gb->rtc_latched.data[(addr & 3) ^ 3]; default: - return 0xFF; + gb->returned_open_bus = true; + return gb->data_bus; } } else if ((!gb->mbc_ram_enable) && - gb->cartridge_type->mbc_subtype != GB_CAMERA && + gb->cartridge_type->mbc_type != GB_CAMERA && gb->cartridge_type->mbc_type != GB_HUC1 && gb->cartridge_type->mbc_type != GB_HUC3) { - return 0xFF; + gb->returned_open_bus = true; + return gb->data_bus; } if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) { - return 0xc0 | gb->effective_ir_input; + return 0xC0 | gb->effective_ir_input; } if (gb->cartridge_type->has_rtc && gb->cartridge_type->mbc_type != GB_HUC3 && @@ -403,7 +420,8 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) gb->rtc_latched.high &= 0xC1; return gb->rtc_latched.data[gb->mbc_ram_bank]; } - return 0xFF; + gb->returned_open_bus = true; + return gb->data_bus; } if (gb->camera_registers_mapped) { @@ -411,11 +429,19 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) } if (!gb->mbc_ram || !gb->mbc_ram_size) { - return 0xFF; + gb->returned_open_bus = true; + return gb->data_bus; } - if (gb->cartridge_type->mbc_subtype == GB_CAMERA && gb->mbc_ram_bank == 0 && addr >= 0xa100 && addr < 0xaf00) { - return GB_camera_read_image(gb, addr - 0xa100); + if (gb->cartridge_type->mbc_type == GB_CAMERA) { + /* Forbid reading RAM while the camera is busy. */ + if (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) { + return 0; + } + + if (gb->mbc_ram_bank == 0 && addr >= 0xA100 && addr < 0xAF00) { + return GB_camera_read_image(gb, addr - 0xA100); + } } uint8_t effective_bank = gb->mbc_ram_bank; @@ -473,6 +499,44 @@ static inline void sync_ppu_if_needed(GB_gameboy_t *gb, uint8_t register_accesse } } +internal uint8_t GB_read_oam(GB_gameboy_t *gb, uint8_t addr) +{ + if (addr < 0xA0) { + return gb->oam[addr]; + } + + switch (gb->model) { + case GB_MODEL_CGB_E: + case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: + return (addr & 0xF0) | (addr >> 4); + + case GB_MODEL_CGB_D: + if (addr >= 0xC0) { + addr |= 0xF0; + } + return gb->extra_oam[addr - 0xA0]; + + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_B: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_0: + addr &= ~0x18; + return gb->extra_oam[addr - 0xA0]; + + case GB_MODEL_DMG_B: + case GB_MODEL_MGB: + case GB_MODEL_SGB_NTSC: + case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + return 0; + } + unreachable(); +} + static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) { if (addr < 0xFE00) { @@ -485,12 +549,12 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) if (!gb->disable_oam_corruption) { GB_trigger_oam_bug_read(gb, addr); } - return 0xff; + return 0xFF; } - if (GB_is_dma_active(gb)) { + if (GB_is_dma_active(gb) && (gb->dma_current_dest != 0 || gb->dma_restarting)) { /* Todo: Does reading from OAM during DMA causes the OAM bug? */ - return 0xff; + return 0xFF; } if (gb->oam_read_blocked) { @@ -498,20 +562,20 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) if (addr < 0xFEA0) { uint16_t *oam = (uint16_t *)gb->oam; if (gb->accessed_oam_row == 0) { - oam[(addr & 0xf8) >> 1] = + oam[(addr & 0xF8) >> 1] = oam[0] = bitwise_glitch_read(oam[0], - oam[(addr & 0xf8) >> 1], - oam[(addr & 0xff) >> 1]); + oam[(addr & 0xF8) >> 1], + oam[(addr & 0xFF) >> 1]); for (unsigned i = 2; i < 8; i++) { - gb->oam[i] = gb->oam[(addr & 0xf8) + i]; + gb->oam[i] = gb->oam[(addr & 0xF8) + i]; } } - else if (gb->accessed_oam_row == 0xa0) { + else if (gb->accessed_oam_row == 0xA0) { uint8_t target = (addr & 7) | 0x98; - uint16_t a = oam[0x9c >> 1], + uint16_t a = oam[0x9C >> 1], b = oam[target >> 1], - c = oam[(addr & 0xf8) >> 1]; + c = oam[(addr & 0xF8) >> 1]; switch (addr & 7) { case 0: case 1: @@ -526,7 +590,7 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) case 2: case 3: { /* Probably instance specific */ - c = oam[(addr & 0xfe) >> 1]; + c = oam[(addr & 0xFE) >> 1]; // MGB only: oam[target >> 1] = bitwise_glitch_read(a, b, c); oam[target >> 1] = (a & b) | (a & c) | (b & c); @@ -544,54 +608,15 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) } for (unsigned i = 0; i < 8; i++) { - gb->oam[(addr & 0xf8) + i] = gb->oam[0x98 + i]; + gb->oam[(addr & 0xF8) + i] = gb->oam[0x98 + i]; } } } } - return 0xff; - } - - if (addr < 0xFEA0) { - return gb->oam[addr & 0xFF]; - } - - if (gb->oam_read_blocked) { return 0xFF; } - switch (gb->model) { - case GB_MODEL_CGB_E: - case GB_MODEL_AGB_A: - return (addr & 0xF0) | ((addr >> 4) & 0xF); - - case GB_MODEL_CGB_D: - if (addr > 0xfec0) { - addr |= 0xf0; - } - return gb->extra_oam[addr - 0xfea0]; - - case GB_MODEL_CGB_C: - case GB_MODEL_CGB_B: - case GB_MODEL_CGB_A: - case GB_MODEL_CGB_0: - addr &= ~0x18; - return gb->extra_oam[addr - 0xfea0]; - - case GB_MODEL_DMG_B: - case GB_MODEL_MGB: - case GB_MODEL_SGB_NTSC: - case GB_MODEL_SGB_PAL: - case GB_MODEL_SGB_NTSC_NO_SFC: - case GB_MODEL_SGB_PAL_NO_SFC: - case GB_MODEL_SGB2: - case GB_MODEL_SGB2_NO_SFC: - break; - } - } - - if (addr < 0xFF00) { - return 0; + return GB_read_oam(gb, addr); } if (addr < 0xFF80) { @@ -622,6 +647,10 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) case GB_IO_JOYP: gb->joyp_accessed = true; GB_timing_sync(gb); + if (unlikely(gb->joyp_switching_delay)) { + return (gb->io_registers[addr & 0xFF] & ~0x30) | (gb->joyp_switch_value & 0x30); + } + return gb->io_registers[addr & 0xFF]; case GB_IO_TMA: case GB_IO_LCDC: case GB_IO_SCY: @@ -651,7 +680,8 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) if (!gb->cgb_mode) { return 0xFF; } - return gb->cgb_ram_bank | ~0x7; + + return gb->io_registers[GB_IO_SVBK]; case GB_IO_VBK: if (!GB_is_cgb(gb)) { return 0xFF; @@ -686,7 +716,8 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) return 0xFF; } return (gb->io_registers[GB_IO_KEY1] & 0x7F) | (gb->cgb_double_speed? 0xFE : 0x7E); - + case GB_IO_BANK: + return 0xFE | gb->boot_rom_finished; case GB_IO_RP: { if (!gb->cgb_mode) return 0xFF; /* You will read your own IR LED if it's on. */ @@ -741,20 +772,24 @@ void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t cal uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + +#ifndef GB_DISABLE_DEBUGGER if (unlikely(gb->n_watchpoints)) { GB_debugger_test_read_watchpoint(gb, addr); } +#endif if (unlikely(is_addr_in_dma_use(gb, addr))) { - if (GB_is_cgb(gb) && bus_for_addr(gb, addr) == GB_BUS_MAIN && gb->dma_current_src >= 0xe000) { + if (GB_is_cgb(gb) && bus_for_addr(gb, addr) == GB_BUS_MAIN && gb->dma_current_src >= 0xE000) { /* This is cart specific! Everdrive 7X on a CGB-A or 0 behaves differently. */ return 0xFF; } - if (GB_is_cgb(gb) && bus_for_addr(gb, gb->dma_current_src) != GB_BUS_RAM && addr >= 0xc000) { + if (GB_is_cgb(gb) && bus_for_addr(gb, gb->dma_current_src) != GB_BUS_RAM && addr >= 0xC000) { // TODO: this should probably affect the DMA dest as well addr = ((gb->dma_current_src - 1) & 0x1000) | (addr & 0xFFF) | 0xC000; } - else if (GB_is_cgb(gb) && gb->dma_current_src >= 0xe000 && addr > 0xc000) { + else if (GB_is_cgb(gb) && gb->dma_current_src >= 0xE000 && addr >= 0xC000) { // TODO: this should probably affect the DMA dest as well addr = ((gb->dma_current_src - 1) & 0x1000) | (addr & 0xFFF) | 0xC000; } @@ -767,6 +802,18 @@ uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr) if (unlikely(gb->read_memory_callback)) { data = gb->read_memory_callback(gb, addr, data); } + + /* TODO: this is very naïve due to my lack of a cart that properly handles open-bus scnenarios, + but should be good enough */ + if (bus_for_addr(gb, addr) == GB_BUS_MAIN && addr < 0xFF00) { + if (unlikely(gb->returned_open_bus)) { + gb->returned_open_bus = false; + } + else { + gb->data_bus = data; + gb->data_bus_decay_countdown = gb->data_bus_decay; + } + } return data; } @@ -829,7 +876,16 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) value &= 7; } gb->mbc5.ram_bank = value; - gb->camera_registers_mapped = (value & 0x10) && gb->cartridge_type->mbc_subtype == GB_CAMERA; + break; + } + break; + case GB_CAMERA: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; + case 0x2000: case 0x3000: gb->mbc5.rom_bank_low = value; break; + case 0x4000: case 0x5000: + gb->mbc5.ram_bank = value; + gb->camera_registers_mapped = (value & 0x10); break; } break; @@ -840,18 +896,52 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case 0x4000: case 0x5000: gb->mbc7.secondary_ram_enable = value == 0x40; break; } break; + case GB_MMM01: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: + gb->mbc_ram_enable = (value & 0xF) == 0xA; + if (!gb->mmm01.locked) { + gb->mmm01.ram_bank_mask = value >> 4; + gb->mmm01.locked = value & 0x40; + } + break; + case 0x2000: case 0x3000: + if (!gb->mmm01.locked) { + gb->mmm01.rom_bank_mid = value >> 5; + } + gb->mmm01.rom_bank_low &= (gb->mmm01.rom_bank_mask << 1); + gb->mmm01.rom_bank_low |= ~(gb->mmm01.rom_bank_mask << 1) & value; + break; + case 0x4000: case 0x5000: + gb->mmm01.ram_bank_low = value | ~gb->mmm01.ram_bank_mask; + if (!gb->mmm01.locked) { + gb->mmm01.ram_bank_high = value >> 2; + gb->mmm01.rom_bank_high = value >> 4; + gb->mmm01.mbc1_mode_disable = value & 0x40; + } + break; + case 0x6000: case 0x7000: + if (!gb->mmm01.mbc1_mode_disable) { + gb->mmm01.mbc1_mode = (value & 1); + } + if (!gb->mmm01.locked) { + gb->mmm01.rom_bank_mask = value >> 2; + gb->mmm01.multiplex_mode = value & 0x40; + } + break; + } + break; case GB_HUC1: switch (addr & 0xF000) { case 0x0000: case 0x1000: gb->huc1.ir_mode = (value & 0xF) == 0xE; break; case 0x2000: case 0x3000: gb->huc1.bank_low = value; break; case 0x4000: case 0x5000: gb->huc1.bank_high = value; break; - case 0x6000: case 0x7000: gb->huc1.mode = value; break; } break; case GB_HUC3: switch (addr & 0xF000) { case 0x0000: case 0x1000: - gb->huc3.mode = value & 0xF; + gb->huc3.mode = value; gb->mbc_ram_enable = gb->huc3.mode == 0xA; break; case 0x2000: case 0x3000: gb->huc3.rom_bank = value; break; @@ -947,15 +1037,15 @@ static bool huc3_write(GB_gameboy_t *gb, uint8_t value) gb->huc3.days &= ~(0xF << ((gb->huc3.access_index - 3) * 4)); gb->huc3.days |= ((value & 0xF) << ((gb->huc3.access_index - 3) * 4)); } - else if (gb->huc3.access_index >= 0x58 && gb->huc3.access_index <= 0x5a) { + else if (gb->huc3.access_index >= 0x58 && gb->huc3.access_index <= 0x5A) { gb->huc3.alarm_minutes &= ~(0xF << ((gb->huc3.access_index - 0x58) * 4)); gb->huc3.alarm_minutes |= ((value & 0xF) << ((gb->huc3.access_index - 0x58) * 4)); } - else if (gb->huc3.access_index >= 0x5b && gb->huc3.access_index <= 0x5e) { - gb->huc3.alarm_days &= ~(0xF << ((gb->huc3.access_index - 0x5b) * 4)); - gb->huc3.alarm_days |= ((value & 0xF) << ((gb->huc3.access_index - 0x5b) * 4)); + else if (gb->huc3.access_index >= 0x5B && gb->huc3.access_index <= 0x5E) { + gb->huc3.alarm_days &= ~(0xF << ((gb->huc3.access_index - 0x5B) * 4)); + gb->huc3.alarm_days |= ((value & 0xF) << ((gb->huc3.access_index - 0x5B) * 4)); } - else if (gb->huc3.access_index == 0x5f) { + else if (gb->huc3.access_index == 0x5F) { gb->huc3.alarm_enabled = value & 1; } else { @@ -1030,7 +1120,7 @@ static void write_mbc7_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->mbc7.eeprom_do = gb->mbc7.read_bits >> 15; gb->mbc7.read_bits <<= 1; gb->mbc7.read_bits |= 1; - if (gb->mbc7.bits_countdown == 0) { + if (gb->mbc7.argument_bits_left == 0) { /* Not transferring extra bits for a command*/ gb->mbc7.eeprom_command <<= 1; gb->mbc7.eeprom_command |= gb->mbc7.eeprom_di; @@ -1061,7 +1151,7 @@ static void write_mbc7_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (gb->mbc7.eeprom_write_enabled) { ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] = 0; } - gb->mbc7.bits_countdown = 16; + gb->mbc7.argument_bits_left = 16; // We still need to process this command, don't erase eeprom_command break; case 0xC: @@ -1089,7 +1179,7 @@ static void write_mbc7_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (gb->mbc7.eeprom_write_enabled) { memset(gb->mbc_ram, 0, gb->mbc_ram_size); } - gb->mbc7.bits_countdown = 16; + gb->mbc7.argument_bits_left = 16; // We still need to process this command, don't erase eeprom_command break; } @@ -1097,10 +1187,10 @@ static void write_mbc7_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } else { // We're shifting in extra bits for a WRITE/WRAL command - gb->mbc7.bits_countdown--; + gb->mbc7.argument_bits_left--; gb->mbc7.eeprom_do = true; if (gb->mbc7.eeprom_di) { - uint16_t bit = LE16(1 << gb->mbc7.bits_countdown); + uint16_t bit = LE16(1 << gb->mbc7.argument_bits_left); if (gb->mbc7.eeprom_command & 0x100) { // WRITE ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] |= bit; @@ -1112,7 +1202,7 @@ static void write_mbc7_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } } } - if (gb->mbc7.bits_countdown == 0) { // We're done + if (gb->mbc7.argument_bits_left == 0) { // We're done gb->mbc7.eeprom_command = 0; gb->mbc7.read_bits = (gb->mbc7.eeprom_command & 0x100)? 0xFF : 0x3FFF; // Emulate some time to settle } @@ -1178,7 +1268,12 @@ static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (!gb->mbc_ram || !gb->mbc_ram_size) { return; } - + + if (gb->cartridge_type->mbc_type == GB_CAMERA && (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1)) { + /* Forbid writing to RAM while the camera is busy. */ + return; + } + uint8_t effective_bank = gb->mbc_ram_bank; if (gb->cartridge_type->mbc_type == GB_MBC3 && !gb->is_mbc30) { if (gb->cartridge_type->has_rtc) { @@ -1200,6 +1295,41 @@ static void write_banked_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->ram[(addr & 0x0FFF) + gb->cgb_ram_bank * 0x1000] = value; } +static void write_oam(GB_gameboy_t *gb, uint8_t addr, uint8_t value) +{ + if (addr < 0xA0) { + gb->oam[addr] = value; + return; + } + switch (gb->model) { + case GB_MODEL_CGB_D: + if (addr >= 0xC0) { + addr |= 0xF0; + } + gb->extra_oam[addr - 0xA0] = value; + break; + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_B: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_0: + addr &= ~0x18; + gb->extra_oam[addr - 0xA0] = value; + break; + case GB_MODEL_CGB_E: + case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: + case GB_MODEL_DMG_B: + case GB_MODEL_MGB: + case GB_MODEL_SGB_NTSC: + case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + break; + } +} + static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { if (addr < 0xFE00) { @@ -1221,48 +1351,18 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } if (GB_is_cgb(gb)) { - if (addr < 0xFEA0) { - gb->oam[addr & 0xFF] = value; - return; - } - switch (gb->model) { - case GB_MODEL_CGB_D: - if (addr > 0xfec0) { - addr |= 0xf0; - } - gb->extra_oam[addr - 0xfea0] = value; - break; - case GB_MODEL_CGB_C: - case GB_MODEL_CGB_B: - case GB_MODEL_CGB_A: - case GB_MODEL_CGB_0: - addr &= ~0x18; - gb->extra_oam[addr - 0xfea0] = value; - break; - case GB_MODEL_CGB_E: - case GB_MODEL_AGB_A: - break; - case GB_MODEL_DMG_B: - case GB_MODEL_MGB: - case GB_MODEL_SGB_NTSC: - case GB_MODEL_SGB_PAL: - case GB_MODEL_SGB_NTSC_NO_SFC: - case GB_MODEL_SGB_PAL_NO_SFC: - case GB_MODEL_SGB2: - case GB_MODEL_SGB2_NO_SFC: - unreachable(); - } + write_oam(gb, addr, value); return; } if (addr < 0xFEA0) { - if (gb->accessed_oam_row == 0xa0) { + if (gb->accessed_oam_row == 0xA0) { for (unsigned i = 0; i < 8; i++) { if ((i & 6) != (addr & 6)) { - gb->oam[(addr & 0xf8) + i] = gb->oam[0x98 + i]; + gb->oam[(addr & 0xF8) + i] = gb->oam[0x98 + i]; } else { - gb->oam[(addr & 0xf8) + i] = bitwise_glitch(gb->oam[(addr & 0xf8) + i], gb->oam[0x9c], gb->oam[0x98 + i]); + gb->oam[(addr & 0xF8) + i] = bitwise_glitch(gb->oam[(addr & 0xF8) + i], gb->oam[0x9C], gb->oam[0x98 + i]); } } } @@ -1271,13 +1371,13 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (gb->accessed_oam_row == 0) { gb->oam[0] = bitwise_glitch(gb->oam[0], - gb->oam[(addr & 0xf8)], - gb->oam[(addr & 0xfe)]); + gb->oam[(addr & 0xF8)], + gb->oam[(addr & 0xFE)]); gb->oam[1] = bitwise_glitch(gb->oam[1], - gb->oam[(addr & 0xf8) + 1], - gb->oam[(addr & 0xfe) | 1]); + gb->oam[(addr & 0xF8) + 1], + gb->oam[(addr & 0xFE) | 1]); for (unsigned i = 2; i < 8; i++) { - gb->oam[i] = gb->oam[(addr & 0xf8) + i]; + gb->oam[i] = gb->oam[(addr & 0xF8) + i]; } } } @@ -1375,67 +1475,81 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case GB_IO_LCDC: - if ((value & 0x80) && !(gb->io_registers[GB_IO_LCDC] & 0x80)) { - if (value & 0x80) { - // LCD turned on - if (!gb->lcd_disabled_outside_of_vblank && - (gb->cycles_since_vblank_callback > 10 * 456 || GB_is_sgb(gb))) { - // Trigger a vblank here so we don't exceed LCDC_PERIOD - GB_display_vblank(gb); - } + if ((value & GB_LCDC_ENABLE) && !(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) { + // LCD turned on + if (gb->lcd_status_callback) { + gb->lcd_status_callback(gb, true); } - else { - // LCD turned off - if (gb->current_line < 144) { - // ROM might be repeatedly disabling LCDC outside of vblank, avoid callback spam - gb->lcd_disabled_outside_of_vblank = true; - } + if (!gb->lcd_disabled_outside_of_vblank && + (gb->cycles_since_vblank_callback > 10 * 456 || GB_is_sgb(gb))) { + // Trigger a vblank here so we don't exceed LCDC_PERIOD + GB_display_vblank(gb, GB_VBLANK_TYPE_ARTIFICIAL); } + gb->display_cycles = 0; gb->display_state = 0; gb->double_speed_alignment = 0; + gb->cycles_for_line = 0; if (GB_is_sgb(gb)) { - gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; + gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_RENDERED; } - else if (gb->frame_skip_state == GB_FRAMESKIP_SECOND_FRAME_RENDERED) { + else { gb->frame_skip_state = GB_FRAMESKIP_LCD_TURNED_ON; } GB_timing_sync(gb); } - else if (!(value & 0x80) && (gb->io_registers[GB_IO_LCDC] & 0x80)) { + else if (!(value & GB_LCDC_ENABLE) && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) { /* Sync after turning off LCD */ + if (gb->lcd_status_callback) { + gb->lcd_status_callback(gb, false); + } gb->double_speed_alignment = 0; + if (gb->model <= GB_MODEL_CGB_E) { + /* TODO: Verify this, it's a bit... odd */ + gb->is_odd_frame ^= true; + } GB_timing_sync(gb); GB_lcd_off(gb); } /* Handle disabling objects while already fetching an object */ - if ((gb->io_registers[GB_IO_LCDC] & 2) && !(value & 2)) { + if (!GB_is_cgb(gb) && (gb->io_registers[GB_IO_LCDC] & GB_LCDC_OBJ_EN) && !(value & GB_LCDC_OBJ_EN)) { if (gb->during_object_fetch) { - gb->cycles_for_line += gb->display_cycles; + gb->cycles_for_line += gb->display_cycles / 2; gb->display_cycles = 0; gb->object_fetch_aborted = true; } } gb->io_registers[GB_IO_LCDC] = value; - if (!(value & 0x20)) { + if (!(value & GB_LCDC_WIN_ENABLE)) { gb->wx_triggered = false; gb->wx166_glitch = false; } return; case GB_IO_STAT: - /* Delete previous R/W bits */ gb->io_registers[GB_IO_STAT] &= 7; - /* Set them by value */ gb->io_registers[GB_IO_STAT] |= value & ~7; - /* Set unused bit to 1 */ gb->io_registers[GB_IO_STAT] |= 0x80; - GB_STAT_update(gb); + /* Annoying edge timing case */ + if (gb->cgb_double_speed && + gb->display_state == 8 && + gb->oam_search_index == 0 && + gb->display_cycles == 0 && + (value & 0x20)) { + gb->mode_for_interrupt = 2; + GB_STAT_update(gb); + gb->mode_for_interrupt = -1; + } + else { + GB_STAT_update(gb); + } return; case GB_IO_DIV: + gb->during_div_write = true; GB_set_internal_div_counter(gb, 0); + gb->during_div_write = false; /* Reset the div state machine */ gb->div_state = 0; gb->div_cycles = 0; @@ -1447,6 +1561,31 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) GB_update_joyp(gb); } else if ((gb->io_registers[GB_IO_JOYP] & 0x30) != (value & 0x30)) { + if (!GB_is_cgb(gb) && !GB_is_sgb(gb)) { + if (gb->joyp_switching_delay) { + gb->io_registers[GB_IO_JOYP] = (gb->joyp_switch_value & 0xF0) | (gb->io_registers[GB_IO_JOYP] & 0x0F); + } + gb->joyp_switch_value = value; + uint8_t delay = 0; + switch (((gb->io_registers[GB_IO_JOYP] & 0x30) >> 4) | + ((value & 0x30) >> 2)) { + case 0x4: delay = 48; break; + case 0x6: delay = gb->model == GB_MODEL_MGB? 56 : 48; break; + case 0x8: delay = 24; break; + case 0x9: delay = 24; break; + case 0xC: delay = 48; break; + case 0xD: delay = 24; break; + case 0xE: delay = 48; break; + } + if (delay && gb->model == GB_MODEL_MGB) { + delay -= 16; + } + gb->joyp_switching_delay = MAX(gb->joyp_switching_delay, delay); + if (gb->joyp_switching_delay) { + value &= gb->io_registers[GB_IO_JOYP]; + gb->joypad_is_stable = false; + } + } GB_sgb_write(gb, value); gb->io_registers[GB_IO_JOYP] = (value & 0xF0) | (gb->io_registers[GB_IO_JOYP] & 0x0F); GB_update_joyp(gb); @@ -1454,7 +1593,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) return; case GB_IO_BANK: - gb->boot_rom_finished = true; + gb->boot_rom_finished |= value & 1; return; case GB_IO_KEY0: @@ -1465,11 +1604,13 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) return; case GB_IO_DMA: + gb->dma_restarting = (gb->dma_current_dest != 0xA1 && gb->dma_current_dest != 0xA0); gb->dma_cycles = 0; gb->dma_cycles_modulo = 2; gb->dma_current_dest = 0xFF; gb->dma_current_src = value << 8; gb->io_registers[GB_IO_DMA] = value; + GB_STAT_update(gb); return; case GB_IO_SVBK: if (gb->cgb_mode || (GB_is_cgb(gb) && !gb->boot_rom_finished)) { @@ -1477,6 +1618,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (!gb->cgb_ram_bank) { gb->cgb_ram_bank++; } + gb->io_registers[GB_IO_SVBK] = value | ~0x7; } return; case GB_IO_VBK: @@ -1554,6 +1696,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) return; case GB_IO_HDMA5: if (!gb->cgb_mode) return; + gb->hdma_steps_left = (value & 0x7F) + 1; if ((value & 0x80) == 0 && gb->hdma_on_hblank) { gb->hdma_on_hblank = false; return; @@ -1563,30 +1706,26 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3) == 0 && gb->display_state != 7) { gb->hdma_on = true; } - gb->io_registers[GB_IO_HDMA5] = value; - gb->hdma_steps_left = (gb->io_registers[GB_IO_HDMA5] & 0x7F) + 1; return; - /* Todo: what happens when starting a transfer during a transfer? - What happens when starting a transfer during external clock? - */ + /* TODO: What happens when starting a transfer during external clock? + TODO: When a cable is connected, the clock of the other side affects "zombie" serial clocking */ case GB_IO_SC: + gb->serial_count = 0; if (!gb->cgb_mode) { value |= 2; } + if (gb->serial_master_clock) { + GB_serial_master_edge(gb); + } gb->io_registers[GB_IO_SC] = value | (~0x83); + gb->serial_mask = gb->cgb_mode && (value & 2)? 4 : 0x80; if ((value & 0x80) && (value & 0x1) ) { - gb->serial_length = gb->cgb_mode && (value & 2)? 16 : 512; - gb->serial_count = 0; - /* Todo: This is probably incorrect for CGB's faster clock mode. */ - gb->serial_cycles &= 0xFF; if (gb->serial_transfer_bit_start_callback) { gb->serial_transfer_bit_start_callback(gb, gb->io_registers[GB_IO_SB] & 0x80); } } - else { - gb->serial_length = 0; - } + return; case GB_IO_RP: { @@ -1643,9 +1782,16 @@ void GB_set_write_memory_callback(GB_gameboy_t *gb, GB_write_memory_callback_t c void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) +#ifndef GB_DISABLE_DEBUGGER if (unlikely(gb->n_watchpoints)) { GB_debugger_test_write_watchpoint(gb, addr, value); } +#endif + if (bus_for_addr(gb, addr) == GB_BUS_MAIN && addr < 0xFF00) { + gb->data_bus = value; + gb->data_bus_decay_countdown = gb->data_bus_decay; + } if (unlikely(gb->write_memory_callback)) { if (!gb->write_memory_callback(gb, addr, value)) return; @@ -1653,24 +1799,25 @@ void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (unlikely(is_addr_in_dma_use(gb, addr))) { bool oam_write = addr >= 0xFE00; - if (GB_is_cgb(gb) && bus_for_addr(gb, addr) == GB_BUS_MAIN && gb->dma_current_src >= 0xe000) { + if (GB_is_cgb(gb) && bus_for_addr(gb, addr) == GB_BUS_MAIN && gb->dma_current_src >= 0xE000) { /* This is cart specific! Everdrive 7X on a CGB-A or 0 behaves differently. */ return; } - if (GB_is_cgb(gb) && bus_for_addr(gb, gb->dma_current_src) != GB_BUS_RAM && addr >= 0xc000) { + if (GB_is_cgb(gb) && (gb->dma_current_src < 0xC000 || gb->dma_current_src >= 0xE000) && addr >= 0xC000) { // TODO: this should probably affect the DMA dest as well addr = ((gb->dma_current_src - 1) & 0x1000) | (addr & 0xFFF) | 0xC000; + goto write; } - else if (GB_is_cgb(gb) && gb->dma_current_src >= 0xe000 && addr > 0xc000) { + else if (GB_is_cgb(gb) && gb->dma_current_src >= 0xE000 && addr >= 0xC000) { // TODO: this should probably affect the DMA dest as well addr = ((gb->dma_current_src - 1) & 0x1000) | (addr & 0xFFF) | 0xC000; } else { addr = (gb->dma_current_src - 1); } - if (GB_is_cgb(gb) || addr > 0xc000) { - if (addr < 0xc000) { + if (GB_is_cgb(gb) || addr >= 0xA000) { + if (addr < 0xA000) { gb->oam[gb->dma_current_dest - 1] = 0; } else if ((gb->model < GB_MODEL_CGB_0 || gb->model == GB_MODEL_CGB_B)) { @@ -1679,35 +1826,43 @@ void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) else if ((gb->model < GB_MODEL_CGB_C || gb->model > GB_MODEL_CGB_E) && !oam_write) { gb->oam[gb->dma_current_dest - 1] = value; } - if (gb->model < GB_MODEL_CGB_E || addr >= 0xc000) return; + if (gb->model < GB_MODEL_CGB_E || addr >= 0xA000) return; } } +write: write_map[addr >> 12](gb, addr, value); } bool GB_is_dma_active(GB_gameboy_t *gb) { - return gb->dma_current_dest != 0xa1; + return gb->dma_current_dest != 0xA1; } void GB_dma_run(GB_gameboy_t *gb) { - if (gb->dma_current_dest == 0xa1) return; + if (gb->dma_current_dest == 0xA1) return; if (unlikely(gb->halted || gb->stopped)) return; signed cycles = gb->dma_cycles + gb->dma_cycles_modulo; gb->in_dma_read = true; while (unlikely(cycles >= 4)) { cycles -= 4; - if (gb->dma_current_dest >= 0xa0) { + if (gb->dma_current_dest >= 0xA0) { gb->dma_current_dest++; + if (gb->display_state == 8) { + gb->io_registers[GB_IO_STAT] |= 2; + GB_STAT_update(gb); + } break; } - if (gb->dma_current_src < 0xe000) { + if (unlikely(gb->hdma_in_progress && (gb->hdma_steps_left > 1 || (gb->hdma_current_dest & 0xF) != 0xF))) { + gb->dma_current_dest++; + } + else if (gb->dma_current_src < 0xE000) { gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src); } else { if (GB_is_cgb(gb)) { - gb->oam[gb->dma_current_dest++] = 0; + gb->oam[gb->dma_current_dest++] = 0xFF; } else { gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src & ~0x2000); @@ -1726,27 +1881,38 @@ void GB_dma_run(GB_gameboy_t *gb) void GB_hdma_run(GB_gameboy_t *gb) { unsigned cycles = gb->cgb_double_speed? 4 : 2; + /* TODO: This portion of code is probably inaccurate because it probably depends on my specific GB-Live32 */ + #if 0 /* This is a bit cart, revision and unit specific. TODO: what if PC is in cart RAM? */ if (gb->model < GB_MODEL_CGB_D || gb->pc > 0x8000) { - gb->hdma_open_bus = 0xFF; + gb->data_bus = 0xFF; } + #endif gb->addr_for_hdma_conflict = 0xFFFF; uint16_t vram_base = gb->cgb_vram_bank? 0x2000 : 0; + gb->hdma_in_progress = true; GB_advance_cycles(gb, cycles); while (gb->hdma_on) { - uint8_t byte = gb->hdma_open_bus; + uint8_t byte = gb->data_bus; gb->addr_for_hdma_conflict = 0xFFFF; - gb->hdma_in_progress = true; // TODO: timing? (affects VRAM reads) - GB_advance_cycles(gb, cycles); if (gb->hdma_current_src < 0x8000 || (gb->hdma_current_src & 0xE000) == 0xC000 || (gb->hdma_current_src & 0xE000) == 0xA000) { byte = GB_read_memory(gb, gb->hdma_current_src); } + if (unlikely(GB_is_dma_active(gb)) && (gb->dma_cycles_modulo == 2 || gb->cgb_double_speed)) { + write_oam(gb, gb->hdma_current_src, byte); + } gb->hdma_current_src++; - if (gb->addr_for_hdma_conflict == 0xFFFF /* || (gb->model == GB_MODEL_AGS && gb->cgb_double_speed) */) { - gb->vram[vram_base + (gb->hdma_current_dest++ & 0x1FFF)] = byte; + GB_advance_cycles(gb, cycles); + if (gb->addr_for_hdma_conflict == 0xFFFF /* || ((gb->model & ~GB_MODEL_GBP_BIT) >= GB_MODEL_AGB_B && gb->cgb_double_speed) */) { + uint16_t addr = (gb->hdma_current_dest++ & 0x1FFF); + gb->vram[vram_base + addr] = byte; + // TODO: vram_write_blocked might not be the correct timing + if (gb->vram_write_blocked /* && (gb->model & ~GB_MODEL_GBP_BIT) < GB_MODEL_AGB_B */) { + gb->vram[(vram_base ^ 0x2000) + addr] = byte; + } } else { if (gb->model == GB_MODEL_CGB_E || gb->cgb_double_speed) { @@ -1754,20 +1920,21 @@ void GB_hdma_run(GB_gameboy_t *gb) These corruptions revision (unit?) specific in single speed. They happen only on my CGB-E. */ gb->addr_for_hdma_conflict &= 0x1FFF; - // Can't write to even bitmap bytes in single speed mode - if (gb->cgb_double_speed || gb->addr_for_hdma_conflict >= 0x1900 || (gb->addr_for_hdma_conflict & 1)) { - gb->vram[vram_base + (gb->hdma_current_dest & gb->addr_for_hdma_conflict & 0x1FFF)] = byte; + // TODO: there are *some* scenarions in single speed mode where this write doesn't happen. What's the logic? + uint16_t addr = (gb->hdma_current_dest & gb->addr_for_hdma_conflict & 0x1FFF); + gb->vram[vram_base + addr] = byte; + // TODO: vram_write_blocked might not be the correct timing + if (gb->vram_write_blocked /* && (gb->model & ~GB_MODEL_GBP_BIT) < GB_MODEL_AGB_B */) { + gb->vram[(vram_base ^ 0x2000) + addr] = byte; } } gb->hdma_current_dest++; } - gb->hdma_open_bus = 0xFF; - if ((gb->hdma_current_dest & 0xf) == 0) { + if ((gb->hdma_current_dest & 0xF) == 0) { if (--gb->hdma_steps_left == 0 || gb->hdma_current_dest == 0) { gb->hdma_on = false; gb->hdma_on_hblank = false; - gb->io_registers[GB_IO_HDMA5] &= 0x7F; } else if (gb->hdma_on_hblank) { gb->hdma_on = false; diff --git a/Core/memory.h b/Core/memory.h index 7a78283..0bd8a43 100644 --- a/Core/memory.h +++ b/Core/memory.h @@ -1,5 +1,4 @@ -#ifndef memory_h -#define memory_h +#pragma once #include "defs.h" #include @@ -16,6 +15,5 @@ internal void GB_dma_run(GB_gameboy_t *gb); internal bool GB_is_dma_active(GB_gameboy_t *gb); internal void GB_hdma_run(GB_gameboy_t *gb); internal void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address); +internal uint8_t GB_read_oam(GB_gameboy_t *gb, uint8_t addr); #endif - -#endif /* memory_h */ diff --git a/Core/model.h b/Core/model.h new file mode 100644 index 0000000..fac525c --- /dev/null +++ b/Core/model.h @@ -0,0 +1,39 @@ +#pragma once + +#define GB_MODEL_FAMILY_MASK 0xF00 +#define GB_MODEL_DMG_FAMILY 0x000 +#define GB_MODEL_MGB_FAMILY 0x100 +#define GB_MODEL_CGB_FAMILY 0x200 +#define GB_MODEL_GBP_BIT 0x20 +#define GB_MODEL_PAL_BIT 0x40 +#define GB_MODEL_NO_SFC_BIT 0x80 + +typedef enum { + // GB_MODEL_DMG_0 = 0x000, + // GB_MODEL_DMG_A = 0x001, + GB_MODEL_DMG_B = 0x002, + // GB_MODEL_DMG_C = 0x003, + GB_MODEL_SGB = 0x004, + GB_MODEL_SGB_NTSC = GB_MODEL_SGB, + GB_MODEL_SGB_PAL = GB_MODEL_SGB | GB_MODEL_PAL_BIT, + GB_MODEL_SGB_NTSC_NO_SFC = GB_MODEL_SGB | GB_MODEL_NO_SFC_BIT, + GB_MODEL_SGB_NO_SFC = GB_MODEL_SGB_NTSC_NO_SFC, + GB_MODEL_SGB_PAL_NO_SFC = GB_MODEL_SGB | GB_MODEL_NO_SFC_BIT | GB_MODEL_PAL_BIT, + GB_MODEL_MGB = 0x100, + GB_MODEL_SGB2 = 0x101, + GB_MODEL_SGB2_NO_SFC = GB_MODEL_SGB2 | GB_MODEL_NO_SFC_BIT, + GB_MODEL_CGB_0 = 0x200, + GB_MODEL_CGB_A = 0x201, + GB_MODEL_CGB_B = 0x202, + GB_MODEL_CGB_C = 0x203, + GB_MODEL_CGB_D = 0x204, + GB_MODEL_CGB_E = 0x205, + // GB_MODEL_AGB_0 = 0x206, + GB_MODEL_AGB_A = 0x207, + GB_MODEL_GBP_A = GB_MODEL_AGB_A | GB_MODEL_GBP_BIT, // AGB-A inside a Game Boy Player + GB_MODEL_AGB = GB_MODEL_AGB_A, + GB_MODEL_GBP = GB_MODEL_GBP_A, + //GB_MODEL_AGB_B = 0x208 + //GB_MODEL_AGB_E = 0x209 + //GB_MODEL_GBP_E = GB_MODEL_AGB_E | GB_MODEL_GBP_BIT, // AGB-E inside a Game Boy Player +} GB_model_t; diff --git a/Core/printer.c b/Core/printer.c index c8514b4..005f452 100644 --- a/Core/printer.c +++ b/Core/printer.c @@ -1,11 +1,12 @@ #include "gb.h" +#include /* TODO: Emulation is VERY basic and assumes the ROM correctly uses the printer's interface. Incorrect usage is not correctly emulated, as it's not well documented, nor do I have my own GB Printer to figure it out myself. It also does not currently emulate communication timeout, which means that a bug - might prevent the printer operation until the GameBoy is restarted. + might prevent the printer operation until the Game Boy is restarted. Also, field mask values are assumed. */ @@ -22,14 +23,17 @@ static void handle_command(GB_gameboy_t *gb) gb->printer.status = 6; /* Printing */ uint32_t image[gb->printer.image_offset]; uint8_t palette = gb->printer.command_data[2]; - uint32_t colors[4] = {gb->rgb_encode_callback(gb, 0xff, 0xff, 0xff), - gb->rgb_encode_callback(gb, 0xaa, 0xaa, 0xaa), + uint32_t colors[4] = {gb->rgb_encode_callback(gb, 0xFF, 0xFF, 0xFF), + gb->rgb_encode_callback(gb, 0xAA, 0xAA, 0xAA), gb->rgb_encode_callback(gb, 0x55, 0x55, 0x55), gb->rgb_encode_callback(gb, 0x00, 0x00, 0x00)}; for (unsigned i = 0; i < gb->printer.image_offset; i++) { image[i] = colors[(palette >> (gb->printer.image[i] * 2)) & 3]; } + // One second per 8-pixel row + gb->printer.time_remaining = gb->printer.image_offset / 160 * GB_get_unmultiplied_clock_rate(gb) / 256 / 8; + if (gb->printer_callback) { gb->printer_callback(gb, image, gb->printer.image_offset / 160, gb->printer.command_data[1] >> 4, gb->printer.command_data[1] & 7, @@ -70,7 +74,7 @@ static void handle_command(GB_gameboy_t *gb) } -static void byte_recieve_completed(GB_gameboy_t *gb, uint8_t byte_received) +static void byte_receive_completed(GB_gameboy_t *gb, uint8_t byte_received) { gb->printer.byte_to_send = 0; switch (gb->printer.command_state) { @@ -156,16 +160,13 @@ static void byte_recieve_completed(GB_gameboy_t *gb, uint8_t byte_received) gb->printer.byte_to_send = 0; } else { + if (gb->printer.status == 6 && gb->printer.time_remaining == 0) { + gb->printer.status = 4; /* Done */ + } gb->printer.byte_to_send = gb->printer.status; } break; case GB_PRINTER_COMMAND_STATUS: - - /* Printing is done instantly, but let the game recieve a 6 (Printing) status at least once, for compatibility */ - if (gb->printer.status == 6) { - gb->printer.status = 4; /* Done */ - } - gb->printer.command_state = GB_PRINTER_COMMAND_MAGIC1; handle_command(gb); return; @@ -197,7 +198,7 @@ static void serial_start(GB_gameboy_t *gb, bool bit_received) gb->printer.byte_being_received |= bit_received; gb->printer.bits_received++; if (gb->printer.bits_received == 8) { - byte_recieve_completed(gb, gb->printer.byte_being_received); + byte_receive_completed(gb, gb->printer.byte_being_received); gb->printer.bits_received = 0; gb->printer.byte_being_received = 0; } @@ -211,10 +212,12 @@ static bool serial_end(GB_gameboy_t *gb) return ret; } -void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback) +void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback, GB_printer_done_callback_t done_callback) { memset(&gb->printer, 0, sizeof(gb->printer)); GB_set_serial_transfer_bit_start_callback(gb, serial_start); GB_set_serial_transfer_bit_end_callback(gb, serial_end); gb->printer_callback = callback; + gb->printer_done_callback = done_callback; + gb->accessory = GB_ACCESSORY_PRINTER; } diff --git a/Core/printer.h b/Core/printer.h index f4ccfe4..2a76f30 100644 --- a/Core/printer.h +++ b/Core/printer.h @@ -1,5 +1,5 @@ -#ifndef printer_h -#define printer_h +#pragma once + #include #include #include "defs.h" @@ -13,12 +13,13 @@ typedef void (*GB_print_image_callback_t)(GB_gameboy_t *gb, uint8_t bottom_margin, uint8_t exposure); +typedef void (*GB_printer_done_callback_t)(GB_gameboy_t *gb); typedef struct { /* Communication state machine */ - enum { + GB_ENUM(uint8_t, { GB_PRINTER_COMMAND_MAGIC1, GB_PRINTER_COMMAND_MAGIC2, GB_PRINTER_COMMAND_ID, @@ -30,13 +31,13 @@ typedef struct GB_PRINTER_COMMAND_CHECKSUM_HIGH, GB_PRINTER_COMMAND_ACTIVE, GB_PRINTER_COMMAND_STATUS, - } command_state : 8; - enum { + }) command_state; + GB_ENUM(uint8_t, { GB_PRINTER_INIT_COMMAND = 1, GB_PRINTER_START_COMMAND = 2, GB_PRINTER_DATA_COMMAND = 4, GB_PRINTER_NOP_COMMAND = 0xF, - } command_id : 8; + }) command_id; bool compression; uint16_t length_left; uint8_t command_data[GB_PRINTER_MAX_COMMAND_LENGTH]; @@ -47,8 +48,9 @@ typedef struct uint8_t image[160 * 200]; uint16_t image_offset; - - uint64_t idle_time; + + uint32_t idle_time; + uint32_t time_remaining; uint8_t compression_run_lenth; bool compression_run_is_compressed; @@ -59,5 +61,4 @@ typedef struct } GB_printer_t; -void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback); -#endif +void GB_connect_printer(GB_gameboy_t *gb, GB_print_image_callback_t callback, GB_printer_done_callback_t done_callback); diff --git a/Core/random.h b/Core/random.h index 8ab0e50..6bdacee 100644 --- a/Core/random.h +++ b/Core/random.h @@ -1,5 +1,4 @@ -#ifndef random_h -#define random_h +#pragma once #include #include @@ -8,5 +7,3 @@ uint8_t GB_random(void); uint32_t GB_random32(void); void GB_random_seed(uint64_t seed); void GB_random_set_enabled(bool enable); - -#endif /* random_h */ diff --git a/Core/rewind.c b/Core/rewind.c index d305528..8b18d46 100644 --- a/Core/rewind.c +++ b/Core/rewind.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include static uint8_t *state_compress(const uint8_t *prev, const uint8_t *data, size_t uncompressed_size) { @@ -17,7 +19,7 @@ static uint8_t *state_compress(const uint8_t *prev, const uint8_t *data, size_t while (uncompressed_size) { if (prev_mode) { - if (*data == *prev && COUNTER != 0xffff) { + if (*data == *prev && COUNTER != 0xFFFF) { COUNTER++; data++; prev++; @@ -35,7 +37,7 @@ static uint8_t *state_compress(const uint8_t *prev, const uint8_t *data, size_t } } else { - if (*data != *prev && COUNTER != 0xffff) { + if (*data != *prev && COUNTER != 0xFFFF) { COUNTER++; DATA = *data; data_pos++; @@ -109,6 +111,10 @@ static void state_decompress(const uint8_t *prev, uint8_t *data, uint8_t *dest, void GB_rewind_push(GB_gameboy_t *gb) { const size_t save_size = GB_get_save_state_size_no_bess(gb); + if (gb->rewind_state_size != save_size) { + GB_rewind_reset(gb); + gb->rewind_state_size = save_size; + } if (!gb->rewind_sequences) { if (gb->rewind_buffer_length) { gb->rewind_sequences = malloc(sizeof(*gb->rewind_sequences) * gb->rewind_buffer_length); @@ -132,7 +138,7 @@ void GB_rewind_push(GB_gameboy_t *gb) for (unsigned i = 0; i < GB_REWIND_FRAMES_PER_KEY; i++) { if (gb->rewind_sequences[gb->rewind_pos].compressed_states[i]) { free(gb->rewind_sequences[gb->rewind_pos].compressed_states[i]); - gb->rewind_sequences[gb->rewind_pos].compressed_states[i] = 0; + gb->rewind_sequences[gb->rewind_pos].compressed_states[i] = NULL; } } gb->rewind_sequences[gb->rewind_pos].pos = 0; @@ -140,13 +146,16 @@ void GB_rewind_push(GB_gameboy_t *gb) if (!gb->rewind_sequences[gb->rewind_pos].key_state) { gb->rewind_sequences[gb->rewind_pos].key_state = malloc(save_size); + gb->rewind_sequences[gb->rewind_pos].instruction_count[0] = 0; GB_save_state_to_buffer_no_bess(gb, gb->rewind_sequences[gb->rewind_pos].key_state); } else { uint8_t *save_state = malloc(save_size); + assert(gb->rewind_sequences[gb->rewind_pos].key_state); GB_save_state_to_buffer_no_bess(gb, save_state); gb->rewind_sequences[gb->rewind_pos].compressed_states[gb->rewind_sequences[gb->rewind_pos].pos++] = state_compress(gb->rewind_sequences[gb->rewind_pos].key_state, save_state, save_size); + gb->rewind_sequences[gb->rewind_pos].instruction_count[gb->rewind_sequences[gb->rewind_pos].pos] = 0; free(save_state); } @@ -154,13 +163,17 @@ void GB_rewind_push(GB_gameboy_t *gb) bool GB_rewind_pop(GB_gameboy_t *gb) { + GB_ASSERT_NOT_RUNNING(gb) + if (!gb->rewind_sequences || !gb->rewind_sequences[gb->rewind_pos].key_state) { return false; } const size_t save_size = GB_get_save_state_size_no_bess(gb); if (gb->rewind_sequences[gb->rewind_pos].pos == 0) { + gb->rewind_disable_invalidation = true; GB_load_state_from_buffer(gb, gb->rewind_sequences[gb->rewind_pos].key_state, save_size); + gb->rewind_disable_invalidation = false; free(gb->rewind_sequences[gb->rewind_pos].key_state); gb->rewind_sequences[gb->rewind_pos].key_state = NULL; gb->rewind_pos = gb->rewind_pos == 0? gb->rewind_buffer_length - 1 : gb->rewind_pos - 1; @@ -174,13 +187,17 @@ bool GB_rewind_pop(GB_gameboy_t *gb) save_size); free(gb->rewind_sequences[gb->rewind_pos].compressed_states[gb->rewind_sequences[gb->rewind_pos].pos]); gb->rewind_sequences[gb->rewind_pos].compressed_states[gb->rewind_sequences[gb->rewind_pos].pos] = NULL; + gb->rewind_disable_invalidation = true; GB_load_state_from_buffer(gb, save_state, save_size); + gb->rewind_disable_invalidation = false; free(save_state); return true; } -void GB_rewind_free(GB_gameboy_t *gb) +void GB_rewind_reset(GB_gameboy_t *gb) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + if (!gb->rewind_sequences) return; for (unsigned i = 0; i < gb->rewind_buffer_length; i++) { if (gb->rewind_sequences[i].key_state) { @@ -198,7 +215,7 @@ void GB_rewind_free(GB_gameboy_t *gb) void GB_set_rewind_length(GB_gameboy_t *gb, double seconds) { - GB_rewind_free(gb); + GB_rewind_reset(gb); if (seconds == 0) { gb->rewind_buffer_length = 0; } @@ -206,3 +223,12 @@ void GB_set_rewind_length(GB_gameboy_t *gb, double seconds) gb->rewind_buffer_length = (size_t) ceil(seconds * CPU_FREQUENCY / LCDC_PERIOD / GB_REWIND_FRAMES_PER_KEY); } } + +void GB_rewind_invalidate_for_backstepping(GB_gameboy_t *gb) +{ + if (gb->rewind_disable_invalidation) return;; + if (gb->rewind_sequences && gb->rewind_sequences[gb->rewind_pos].key_state) { + typeof(gb->rewind_sequences[0]) *sequence = &gb->rewind_sequences[gb->rewind_pos]; + sequence->instruction_count[sequence->pos] |= 0x80000000; + } +} diff --git a/Core/rewind.h b/Core/rewind.h index 3cc23ed..c0fb702 100644 --- a/Core/rewind.h +++ b/Core/rewind.h @@ -1,14 +1,14 @@ -#ifndef rewind_h -#define rewind_h +#pragma once +#ifndef GB_DISABLE_REWIND #include #include "defs.h" #ifdef GB_INTERNAL internal void GB_rewind_push(GB_gameboy_t *gb); -internal void GB_rewind_free(GB_gameboy_t *gb); +internal void GB_rewind_invalidate_for_backstepping(GB_gameboy_t *gb); #endif bool GB_rewind_pop(GB_gameboy_t *gb); void GB_set_rewind_length(GB_gameboy_t *gb, double seconds); - +void GB_rewind_reset(GB_gameboy_t *gb); #endif diff --git a/Core/rumble.h b/Core/rumble.h index ca34737..78a3da7 100644 --- a/Core/rumble.h +++ b/Core/rumble.h @@ -1,5 +1,4 @@ -#ifndef rumble_h -#define rumble_h +#pragma once #include "defs.h" @@ -13,5 +12,3 @@ typedef enum { internal void GB_handle_rumble(GB_gameboy_t *gb); #endif void GB_set_rumble_mode(GB_gameboy_t *gb, GB_rumble_mode_t mode); - -#endif /* rumble_h */ diff --git a/Core/save_state.c b/Core/save_state.c index 55f334d..be725a4 100644 --- a/Core/save_state.c +++ b/Core/save_state.c @@ -2,6 +2,8 @@ #include #include #include +#include +#include #ifdef GB_BIG_ENDIAN #define BESS_NAME "SameBoy v" GB_VERSION " (Big Endian)" @@ -17,6 +19,7 @@ _Static_assert((GB_SECTION_OFFSET(timing) & 7) == 0, "Section timing is not alig _Static_assert((GB_SECTION_OFFSET(apu) & 7) == 0, "Section apu is not aligned"); _Static_assert((GB_SECTION_OFFSET(rtc) & 7) == 0, "Section rtc is not aligned"); _Static_assert((GB_SECTION_OFFSET(video) & 7) == 0, "Section video is not aligned"); +_Static_assert((GB_SECTION_OFFSET(accessory) & 7) == 0, "Section accessory is not aligned"); typedef struct __attribute__((packed)) { uint32_t magic; @@ -119,6 +122,28 @@ typedef struct __attribute__((packed)){ GB_huc3_rtc_time_t data; } BESS_HUC3_t; +typedef struct __attribute__((packed)) { + BESS_block_t header; + + // Flags + bool latch_ready:1; + bool eeprom_do:1; + bool eeprom_di:1; + bool eeprom_clk:1; + bool eeprom_cs:1; + bool eeprom_write_enabled:1; + uint8_t padding:2; + + uint8_t argument_bits_left; + + uint16_t eeprom_command; + uint16_t read_bits; + + uint16_t x_latch; + uint16_t y_latch; + +} BESS_MBC7_t; + typedef struct __attribute__((packed)){ BESS_block_t header; uint64_t last_rtc_second; @@ -232,8 +257,14 @@ static size_t bess_size_for_cartridge(const GB_cartridge_t *cart) return sizeof(BESS_block_t) + 3 * sizeof(BESS_MBC_pair_t) + (cart->has_rtc? sizeof(BESS_RTC_t) : 0); case GB_MBC5: return sizeof(BESS_block_t) + 4 * sizeof(BESS_MBC_pair_t); + case GB_CAMERA: + return sizeof(BESS_block_t) + 3 * sizeof(BESS_MBC_pair_t); + case GB_MBC7: + return sizeof(BESS_block_t) + 3 * sizeof(BESS_MBC_pair_t) + sizeof(BESS_MBC7_t); + case GB_MMM01: + return sizeof(BESS_block_t) + 8 * sizeof(BESS_MBC_pair_t); case GB_HUC1: - return sizeof(BESS_block_t) + 4 * sizeof(BESS_MBC_pair_t); + return sizeof(BESS_block_t) + 3 * sizeof(BESS_MBC_pair_t); case GB_HUC3: return sizeof(BESS_block_t) + 3 * sizeof(BESS_MBC_pair_t) + sizeof(BESS_HUC3_t); case GB_TPP1: @@ -252,6 +283,7 @@ size_t GB_get_save_state_size_no_bess(GB_gameboy_t *gb) + GB_SECTION_SIZE(apu ) + sizeof(uint32_t) + GB_SECTION_SIZE(rtc ) + sizeof(uint32_t) + GB_SECTION_SIZE(video ) + sizeof(uint32_t) + + GB_SECTION_SIZE(accessory ) + sizeof(uint32_t) + (GB_is_hle_sgb(gb)? sizeof(*gb->sgb) + sizeof(uint32_t) : 0) + gb->mbc_ram_size + gb->ram_size @@ -268,7 +300,7 @@ size_t GB_get_save_state_size(GB_gameboy_t *gb) + sizeof(BESS_CORE_t) + sizeof(BESS_XOAM_t) + (gb->sgb? sizeof(BESS_SGB_t) : 0) - + bess_size_for_cartridge(gb->cartridge_type) // MBC & RTC/HUC3/TPP1 block + + bess_size_for_cartridge(gb->cartridge_type) // MBC & RTC/HUC3/TPP1/MBC7 block + sizeof(BESS_block_t) // END block + sizeof(BESS_footer_t); } @@ -308,6 +340,10 @@ static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t return false; } + if (gb->accessory != save->accessory) { + memset(GB_GET_SECTION(save, accessory), 0, GB_SECTION_SIZE(accessory)); + } + switch (save->model) { case GB_MODEL_DMG_B: return true; case GB_MODEL_SGB_NTSC: return true; @@ -324,11 +360,13 @@ static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t case GB_MODEL_CGB_D: return true; case GB_MODEL_CGB_E: return true; case GB_MODEL_AGB_A: return true; + case GB_MODEL_GBP_A: return true; } if ((gb->model & GB_MODEL_FAMILY_MASK) == (save->model & GB_MODEL_FAMILY_MASK)) { save->model = gb->model; return true; } + GB_log(gb, "This save state is for an unknown Game Boy model\n"); return false; } @@ -340,10 +378,8 @@ static void sanitize_state(GB_gameboy_t *gb) GB_palette_changed(gb, true, i * 2); } - gb->bg_fifo.read_end &= 0xF; - gb->bg_fifo.write_end &= 0xF; - gb->oam_fifo.read_end &= 0xF; - gb->oam_fifo.write_end &= 0xF; + gb->bg_fifo.read_end &= GB_FIFO_LENGTH - 1; + gb->oam_fifo.read_end &= GB_FIFO_LENGTH - 1; gb->last_tile_index_address &= 0x1FFF; gb->window_tile_x &= 0x1F; @@ -385,6 +421,15 @@ static void sanitize_state(GB_gameboy_t *gb) gb->sgb->current_player &= gb->sgb->player_count - 1; } GB_update_clock_rate(gb); + + if (gb->camera_update_request_callback) { + GB_camera_updated(gb); + } + + if (!gb->apu.apu_cycles_in_2mhz) { + gb->apu.apu_cycles >>= 2; + gb->apu.apu_cycles_in_2mhz = true; + } } static bool dump_section(virtual_file_t *file, const void *src, uint32_t size) @@ -406,7 +451,7 @@ static int save_bess_mbc_block(GB_gameboy_t *gb, virtual_file_t *file) { BESS_block_t mbc_block = {BE32('MBC '), 0}; - BESS_MBC_pair_t pairs[4]; + BESS_MBC_pair_t pairs[8]; switch (gb->cartridge_type->mbc_type) { default: case GB_NO_MBC: return 0; @@ -435,20 +480,41 @@ static int save_bess_mbc_block(GB_gameboy_t *gb, virtual_file_t *file) pairs[3] = (BESS_MBC_pair_t){LE16(0x4000), gb->mbc5.ram_bank}; mbc_block.size = 4 * sizeof(pairs[0]); break; + case GB_CAMERA: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->mbc_ram_enable? 0xA : 0x0}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->mbc5.rom_bank_low}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->mbc5.ram_bank}; + mbc_block.size = 3 * sizeof(pairs[0]); + break; + case GB_MBC7: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->mbc_ram_enable? 0xA : 0x0}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->mbc7.rom_bank}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->mbc7.secondary_ram_enable? 0x40 : 0}; + mbc_block.size = 3 * sizeof(pairs[0]); + break; + case GB_MMM01: + pairs[0] = (BESS_MBC_pair_t){LE16(0x2000), (gb->mmm01.rom_bank_low & (gb->mmm01.rom_bank_mask << 1)) | (gb->mmm01.rom_bank_mid << 5)}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x6000), gb->mmm01.mbc1_mode | (gb->mmm01.rom_bank_mask << 2) | (gb->mmm01.multiplex_mode << 6)}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->mmm01.ram_bank_low | (gb->mmm01.ram_bank_high << 2) | (gb->mmm01.rom_bank_high << 4) | (gb->mmm01.mbc1_mode_disable << 6)}; + pairs[3] = (BESS_MBC_pair_t){LE16(0x0000), (gb->mbc_ram_enable? 0xA : 0x0) | (gb->mmm01.ram_bank_mask << 4) | (gb->mmm01.locked << 6)}; + /* For compatibility with emulators that inaccurately emulate MMM01, and also require two writes per register */ + pairs[4] = (BESS_MBC_pair_t){LE16(0x2000), (gb->mmm01.rom_bank_low & ~(gb->mmm01.rom_bank_mask << 1))}; + pairs[5] = pairs[1]; + pairs[6] = pairs[2]; + pairs[7] = pairs[3]; + mbc_block.size = 8 * sizeof(pairs[0]); + break; case GB_HUC1: pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->huc1.ir_mode? 0xE : 0x0}; pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->huc1.bank_low}; pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->huc1.bank_high}; - pairs[3] = (BESS_MBC_pair_t){LE16(0x6000), gb->huc1.mode}; - mbc_block.size = 4 * sizeof(pairs[0]); - + mbc_block.size = 3 * sizeof(pairs[0]); case GB_HUC3: pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->huc3.mode}; pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->huc3.rom_bank}; pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->huc3.ram_bank}; mbc_block.size = 3 * sizeof(pairs[0]); break; - case GB_TPP1: pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->tpp1.rom_bank}; pairs[1] = (BESS_MBC_pair_t){LE16(0x0001), gb->tpp1.rom_bank >> 8}; @@ -471,6 +537,14 @@ static int save_bess_mbc_block(GB_gameboy_t *gb, virtual_file_t *file) return 0; } +static const uint8_t *get_header_bank(GB_gameboy_t *gb) +{ + if (gb->cartridge_type->mbc_type == GB_MMM01) { + return gb->rom + gb->rom_size - 0x8000; + } + return gb->rom; +} + static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool append_bess) { if (file->write(file, GB_GET_SECTION(gb, header), GB_SECTION_SIZE(header)) != GB_SECTION_SIZE(header)) goto error; @@ -484,7 +558,8 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe if (!DUMP_SECTION(gb, file, rtc )) goto error; uint32_t video_offset = file->tell(file) + 4; if (!DUMP_SECTION(gb, file, video )) goto error; - + if (!DUMP_SECTION(gb, file, accessory )) goto error; + uint32_t sgb_offset = 0; if (GB_is_hle_sgb(gb)) { @@ -540,11 +615,13 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe goto error; } - if (file->write(file, gb->rom + 0x134, 0x10) != 0x10) { + const uint8_t *bank = get_header_bank(gb); + + if (file->write(file, bank + 0x134, 0x10) != 0x10) { goto error; } - if (file->write(file, gb->rom + 0x14e, 2) != 2) { + if (file->write(file, bank + 0x14E, 2) != 2) { goto error; } @@ -576,7 +653,9 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe case GB_MODEL_CGB_C: bess_core.full_model = BE32('CCC '); break; case GB_MODEL_CGB_D: bess_core.full_model = BE32('CCD '); break; case GB_MODEL_CGB_E: bess_core.full_model = BE32('CCE '); break; - case GB_MODEL_AGB_A: bess_core.full_model = BE32('CAA '); break; + case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: + bess_core.full_model = BE32('CAA '); break; } bess_core.pc = LE16(gb->pc); @@ -680,6 +759,30 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe } } + if (gb->cartridge_type ->mbc_type == GB_MBC7) { + BESS_MBC7_t bess_mbc7 = { + .latch_ready = gb->mbc7.latch_ready, + .eeprom_do = gb->mbc7.eeprom_do, + .eeprom_di = gb->mbc7.eeprom_di, + .eeprom_clk = gb->mbc7.eeprom_clk, + .eeprom_cs = gb->mbc7.eeprom_cs, + .eeprom_write_enabled = gb->mbc7.eeprom_write_enabled, + + .argument_bits_left = gb->mbc7.argument_bits_left, + + .eeprom_command = LE16(gb->mbc7.eeprom_command), + .read_bits = LE16(gb->mbc7.read_bits), + + .x_latch = LE16(gb->mbc7.x_latch), + .y_latch = LE16(gb->mbc7.y_latch), + }; + bess_mbc7.header = (BESS_block_t){BE32('MBC7'), LE32(sizeof(bess_mbc7) - sizeof(bess_mbc7.header))}; + + if (file->write(file, &bess_mbc7, sizeof(bess_mbc7)) != sizeof(bess_mbc7)) { + goto error; + } + } + bool needs_sgb_padding = false; if (gb->sgb) { /* BESS SGB */ @@ -738,6 +841,7 @@ error: int GB_save_state(GB_gameboy_t *gb, const char *path) { + GB_ASSERT_NOT_RUNNING(gb) FILE *f = fopen(path, "wb"); if (!f) { GB_log(gb, "Could not open save state: %s.\n", strerror(errno)); @@ -756,6 +860,7 @@ int GB_save_state(GB_gameboy_t *gb, const char *path) void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer) { + GB_ASSERT_NOT_RUNNING(gb) virtual_file_t file = { .write = buffer_write, .seek = buffer_seek, @@ -965,6 +1070,11 @@ static int load_bess_save(GB_gameboy_t *gb, virtual_file_t *file, bool is_samebo // Interrupts GB_write_memory(&save, 0xFF00 + GB_IO_IF, core.io_registers[GB_IO_IF]); + /* Required to be compatible with both SameBoy 0.14.x AND BGB */ + if (GB_is_cgb(&save) && !save.cgb_mode && save.cgb_ram_bank == 7) { + save.cgb_ram_bank = 1; + } + break; case BE32('NAME'): if (LE32(block.size) > sizeof(emulator_name) - 1) { @@ -978,7 +1088,8 @@ static int load_bess_save(GB_gameboy_t *gb, virtual_file_t *file, bool is_samebo BESS_INFO_t bess_info = {0,}; if (LE32(block.size) != sizeof(bess_info) - sizeof(block)) goto parse_error; if (file->read(file, &bess_info.header + 1, LE32(block.size)) != LE32(block.size)) goto error; - if (memcmp(bess_info.title, gb->rom + 0x134, sizeof(bess_info.title))) { + const uint8_t *bank = get_header_bank(gb); + if (memcmp(bess_info.title, bank + 0x134, sizeof(bess_info.title))) { char ascii_title[0x11] = {0,}; for (unsigned i = 0; i < 0x10; i++) { if (bess_info.title[i] < 0x20 || bess_info.title[i] > 0x7E) break; @@ -986,7 +1097,7 @@ static int load_bess_save(GB_gameboy_t *gb, virtual_file_t *file, bool is_samebo } GB_log(gb, "Save state was made on another ROM: '%s'\n", ascii_title); } - else if (memcmp(bess_info.checksum, gb->rom + 0x14E, 2)) { + else if (memcmp(bess_info.checksum, bank + 0x14E, 2)) { GB_log(gb, "Save state was potentially made on another revision of the same ROM.\n"); } break; @@ -999,7 +1110,12 @@ static int load_bess_save(GB_gameboy_t *gb, virtual_file_t *file, bool is_samebo case BE32('MBC '): if (!found_core) goto parse_error; if (LE32(block.size) % 3 != 0) goto parse_error; - if (LE32(block.size) > 0x1000) goto parse_error; + if (LE32(block.size) > 0x1000) goto parse_error; + /* Inject some default writes, as some emulators omit them */ + if (gb->cartridge_type->mbc_type == GB_MMM01) { + GB_write_memory(&save, 0x6000, 0x30); + GB_write_memory(&save, 0x4000, 0x70); + } for (unsigned i = LE32(block.size); i > 0; i -= 3) { BESS_MBC_pair_t pair; file->read(file, &pair, sizeof(pair)); @@ -1058,6 +1174,29 @@ static int load_bess_save(GB_gameboy_t *gb, virtual_file_t *file, bool is_samebo save.rtc_latched.data[i ^ 3] = bess_tpp1.latched_rtc_data[i]; } save.tpp1_mr4 = bess_tpp1.mr4; + break; + case BE32('MBC7'): + if (!found_core) goto parse_error; + BESS_MBC7_t bess_mbc7; + if (LE32(block.size) != sizeof(bess_mbc7) - sizeof(block)) goto parse_error; + if (file->read(file, &bess_mbc7.header + 1, LE32(block.size)) != LE32(block.size)) goto error; + if (gb->cartridge_type->mbc_type != GB_MBC7) break; + + save.mbc7.latch_ready = bess_mbc7.latch_ready; + save.mbc7.eeprom_do = bess_mbc7.eeprom_do; + save.mbc7.eeprom_di = bess_mbc7.eeprom_di; + save.mbc7.eeprom_clk = bess_mbc7.eeprom_clk; + save.mbc7.eeprom_cs = bess_mbc7.eeprom_cs; + save.mbc7.eeprom_write_enabled = bess_mbc7.eeprom_write_enabled; + + save.mbc7.argument_bits_left = bess_mbc7.argument_bits_left; + + save.mbc7.eeprom_command = LE16(bess_mbc7.eeprom_command); + save.mbc7.read_bits = LE16(bess_mbc7.read_bits); + + save.mbc7.x_latch = LE16(bess_mbc7.x_latch); + save.mbc7.y_latch = LE16(bess_mbc7.y_latch); + break; case BE32('SGB '): if (!found_core) goto parse_error; @@ -1182,8 +1321,8 @@ static int load_state_internal(GB_gameboy_t *gb, virtual_file_t *file) if (!READ_SECTION(&save, file, apu )) return errno ?: EIO; if (!READ_SECTION(&save, file, rtc )) return errno ?: EIO; if (!READ_SECTION(&save, file, video )) return errno ?: EIO; -#undef READ_SECTION - + if (!READ_SECTION(&save, file, accessory )) return errno ?: EIO; + bool attempt_bess = false; if (!verify_and_update_state_compatibility(gb, &save, &attempt_bess)) { @@ -1213,17 +1352,20 @@ static int load_state_internal(GB_gameboy_t *gb, virtual_file_t *file) return errno ?: EIO; } - size_t orig_ram_size = gb->ram_size; + uint32_t ram_size = gb->ram_size; + uint32_t mbc_ram_size = gb->mbc_ram_size; memcpy(gb, &save, sizeof(save)); - gb->ram_size = orig_ram_size; - + gb->ram_size = ram_size; + gb->mbc_ram_size = mbc_ram_size; + sanitize_state(gb); - + GB_rewind_invalidate_for_backstepping(gb); return 0; } int GB_load_state(GB_gameboy_t *gb, const char *path) { + GB_ASSERT_NOT_RUNNING(gb) FILE *f = fopen(path, "rb"); if (!f) { GB_log(gb, "Could not open save state: %s.\n", strerror(errno)); @@ -1242,6 +1384,7 @@ int GB_load_state(GB_gameboy_t *gb, const char *path) int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length) { + GB_ASSERT_NOT_RUNNING(gb) virtual_file_t file = { .read = buffer_read, .seek = buffer_seek, @@ -1254,6 +1397,109 @@ int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t le return load_state_internal(gb, &file); } +static int get_state_model_bess(virtual_file_t *file, GB_model_t *model) +{ + file->seek(file, -sizeof(BESS_footer_t), SEEK_END); + BESS_footer_t footer = {0, }; + file->read(file, &footer, sizeof(footer)); + if (footer.magic != BE32('BESS')) { + return -1; + } + + file->seek(file, LE32(footer.start_offset), SEEK_SET); + while (true) { + BESS_block_t block; + if (file->read(file, &block, sizeof(block)) != sizeof(block)) return errno; + switch (block.magic) { + case BE32('CORE'): { + BESS_CORE_t core = {0,}; + if (LE32(block.size) > sizeof(core) - sizeof(block)) { + if (file->read(file, &core.header + 1, sizeof(core) - sizeof(block)) != sizeof(core) - sizeof(block)) return errno; + file->seek(file, LE32(block.size) - (sizeof(core) - sizeof(block)), SEEK_CUR); + } + else { + if (file->read(file, &core.header + 1, LE32(block.size)) != LE32(block.size)) return errno; + } + + switch (core.full_model) { + case BE32('GDB '): *model = GB_MODEL_DMG_B; return 0; + case BE32('GM '): *model = GB_MODEL_MGB; return 0; + case BE32('SN '): *model = GB_MODEL_SGB_NTSC_NO_SFC; return 0; + case BE32('SP '): *model = GB_MODEL_SGB_PAL; return 0; + case BE32('S2 '): *model = GB_MODEL_SGB2; return 0; + case BE32('CC0 '): *model = GB_MODEL_CGB_0; return 0; + case BE32('CCA '): *model = GB_MODEL_CGB_A; return 0; + case BE32('CCB '): *model = GB_MODEL_CGB_B; return 0; + case BE32('CCC '): *model = GB_MODEL_CGB_C; return 0; + case BE32('CCD '): *model = GB_MODEL_CGB_D; return 0; + case BE32('CCE '): *model = GB_MODEL_CGB_E; return 0; + case BE32('CAA '): *model = GB_MODEL_AGB_A; return 0; + } + return -1; + + default: + file->seek(file, LE32(block.size), SEEK_CUR); + break; + } + } + } + return -1; +} + + +static int get_state_model_internal(virtual_file_t *file, GB_model_t *model) +{ + GB_gameboy_t save; + + bool fix_broken_windows_saves = false; + + if (file->read(file, GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header)) != GB_SECTION_SIZE(header)) return errno; + if (save.magic == 0) { + /* Potentially legacy, broken Windows save state*/ + + file->seek(file, 4, SEEK_SET); + if (file->read(file, GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header)) != GB_SECTION_SIZE(header)) return errno; + fix_broken_windows_saves = true; + } + if (save.magic != GB_state_magic()) { + return get_state_model_bess(file, model); + } + if (!READ_SECTION(&save, file, core_state)) return errno ?: EIO; + *model = save.model; + return 0; +} + +int GB_get_state_model(const char *path, GB_model_t *model) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + return errno; + } + virtual_file_t file = { + .read = file_read, + .seek = file_seek, + .tell = file_tell, + .file = f, + }; + int ret = get_state_model_internal(&file, model); + fclose(f); + return ret; +} + +int GB_get_state_model_from_buffer(const uint8_t *buffer, size_t length, GB_model_t *model) +{ + virtual_file_t file = { + .read = buffer_read, + .seek = buffer_seek, + .tell = buffer_tell, + .buffer = (uint8_t *)buffer, + .position = 0, + .size = length, + }; + + return get_state_model_internal(&file, model); +} + bool GB_is_save_state(const char *path) { @@ -1262,7 +1508,7 @@ bool GB_is_save_state(const char *path) if (!f) return false; uint32_t magic = 0; fread(&magic, sizeof(magic), 1, f); - if (magic == state_magic()) { + if (magic == GB_state_magic()) { ret = true; goto exit; } @@ -1270,7 +1516,7 @@ bool GB_is_save_state(const char *path) // Legacy corrupted Windows save state if (magic == 0) { fread(&magic, sizeof(magic), 1, f); - if (magic == state_magic()) { + if (magic == GB_state_magic()) { ret = true; goto exit; } diff --git a/Core/save_state.h b/Core/save_state.h index 75f03d2..ceb89c2 100644 --- a/Core/save_state.h +++ b/Core/save_state.h @@ -1,6 +1,6 @@ +#pragma once + /* Macros to make the GB_gameboy_t struct more future compatible when state saving */ -#ifndef save_state_h -#define save_state_h #include #define GB_PADDING(type, old_usage) type old_usage##__do_not_use @@ -8,16 +8,28 @@ #ifdef __cplusplus /* For bsnes integration. C++ code does not need section information, and throws a fit over certain types such as anonymous enums inside unions */ +#if __clang__ #define GB_SECTION(name, ...) __attribute__ ((aligned (8))) __VA_ARGS__ #else +// GCC's handling of attributes is so awfully bad, that it is alone a good enough reason to never use that compiler +#define GB_SECTION(name, ...) _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wpedantic\"") alignas(8) char _align_##name[0]; __VA_ARGS__ _Pragma("GCC diagnostic pop") +#endif +#else #define GB_SECTION(name, ...) union __attribute__ ((aligned (8))) {uint8_t name##_section_start; struct {__VA_ARGS__};}; uint8_t name##_section_end[0]; +#ifdef GB_INTERNAL #define GB_SECTION_OFFSET(name) (offsetof(GB_gameboy_t, name##_section_start)) #define GB_SECTION_SIZE(name) (offsetof(GB_gameboy_t, name##_section_end) - offsetof(GB_gameboy_t, name##_section_start)) -#define GB_GET_SECTION(gb, name) ((void*)&((gb)->name##_section_start)) +/* This roundabout way to get the section offset is because GCC 9 is a bad compiler and will false-positively complain + about memset buffer overflows otherwise */ +#define GB_GET_SECTION(gb, name) (void *)((uint8_t *)(gb) + GB_SECTION_OFFSET(name)) +#endif #endif -#define GB_aligned_double __attribute__ ((aligned (8))) double - +#if __clang_major__ >= 8 || __GNUC__ >= 13 || defined(__cplusplus) +#define GB_ENUM(type, ...) enum : type __VA_ARGS__ +#else +#define GB_ENUM(type, ...) __typeof__((type)((enum __VA_ARGS__)0)) +#endif /* Public calls related to save states */ int GB_save_state(GB_gameboy_t *gb, const char *path); @@ -28,8 +40,11 @@ void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer); int GB_load_state(GB_gameboy_t *gb, const char *path); int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length); bool GB_is_save_state(const char *path); +int GB_get_state_model(const char *path, GB_model_t *model); +int GB_get_state_model_from_buffer(const uint8_t *buffer, size_t length, GB_model_t *model); + #ifdef GB_INTERNAL -static inline uint32_t state_magic(void) +static inline uint32_t GB_state_magic(void) { if (sizeof(bool) == 1) return 'SAME'; return 'S4ME'; @@ -39,5 +54,3 @@ static inline uint32_t state_magic(void) internal size_t GB_get_save_state_size_no_bess(GB_gameboy_t *gb); internal void GB_save_state_to_buffer_no_bess(GB_gameboy_t *gb, uint8_t *buffer); #endif - -#endif /* save_state_h */ diff --git a/Core/sgb.c b/Core/sgb.c index 7cdd77f..036ef2f 100644 --- a/Core/sgb.c +++ b/Core/sgb.c @@ -2,6 +2,7 @@ #include "random.h" #include #include +#include #ifndef M_PI #define M_PI 3.14159265358979323846 @@ -165,10 +166,10 @@ static void command_ready(GB_gameboy_t *gb) return; } memcpy(&gb->sgb->received_header[index * 14], &gb->sgb->command[2], 14); - if (gb->sgb->command[0] == 0xfb) { + if (gb->sgb->command[0] == 0xFB) { if (gb->sgb->received_header[0x42] != 3 || gb->sgb->received_header[0x47] != 0x33) { gb->sgb->disable_commands = true; - for (unsigned i = 0; i < sizeof(palette_assignments) / sizeof(palette_assignments[0]); i++) { + nounroll for (unsigned i = 0; i < sizeof(palette_assignments) / sizeof(palette_assignments[0]); i++) { if (memcmp(palette_assignments[i].name, &gb->sgb->received_header[0x30], sizeof(palette_assignments[i].name)) == 0) { gb->sgb->effective_palettes[0] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 - 4]); gb->sgb->effective_palettes[1] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 + 1 - 4]); diff --git a/Core/sgb.h b/Core/sgb.h index 3069c36..16f718f 100644 --- a/Core/sgb.h +++ b/Core/sgb.h @@ -1,5 +1,5 @@ -#ifndef sgb_h -#define sgb_h +#pragma once + #include "defs.h" #include #include @@ -67,5 +67,3 @@ internal void GB_sgb_render(GB_gameboy_t *gb); internal void GB_sgb_load_default_data(GB_gameboy_t *gb); #endif - -#endif diff --git a/Core/sm83_cpu.c b/Core/sm83_cpu.c index 14050af..ec63092 100644 --- a/Core/sm83_cpu.c +++ b/Core/sm83_cpu.c @@ -21,23 +21,31 @@ typedef enum { GB_CONFLICT_DMG_LCDC, GB_CONFLICT_SGB_LCDC, GB_CONFLICT_WX, - GB_CONFLICT_CGB_LCDC, - GB_CONFLICT_NR10, + GB_CONFLICT_LCDC_CGB, + GB_CONFLICT_SCX_CGB, + GB_CONFLICT_LCDC_CGB_DOUBLE, + GB_CONFLICT_STAT_CGB_DOUBLE, + GB_CONFLICT_NR10_CGB_DOUBLE, } conflict_t; -/* Todo: How does double speed mode affect these? */ static const conflict_t cgb_conflict_map[0x80] = { - [GB_IO_LCDC] = GB_CONFLICT_CGB_LCDC, + [GB_IO_LCDC] = GB_CONFLICT_LCDC_CGB, [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, [GB_IO_LYC] = GB_CONFLICT_WRITE_CPU, [GB_IO_STAT] = GB_CONFLICT_STAT_CGB, [GB_IO_BGP] = GB_CONFLICT_PALETTE_CGB, [GB_IO_OBP0] = GB_CONFLICT_PALETTE_CGB, [GB_IO_OBP1] = GB_CONFLICT_PALETTE_CGB, - [GB_IO_NR10] = GB_CONFLICT_NR10, - [GB_IO_SCX] = GB_CONFLICT_WRITE_CPU, // TODO: Similar to BGP, there's some time travelling involved + [GB_IO_SCX] = GB_CONFLICT_SCX_CGB, +}; - /* Todo: most values not verified, and probably differ between revisions */ +static const conflict_t cgb_double_conflict_map[0x80] = { + [GB_IO_LCDC] = GB_CONFLICT_LCDC_CGB_DOUBLE, + [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, + [GB_IO_LYC] = GB_CONFLICT_READ_OLD, + [GB_IO_STAT] = GB_CONFLICT_STAT_CGB_DOUBLE, + [GB_IO_NR10] = GB_CONFLICT_NR10_CGB_DOUBLE, + [GB_IO_SCX] = GB_CONFLICT_SCX_CGB, }; /* Todo: verify on an MGB */ @@ -53,7 +61,6 @@ static const conflict_t dmg_conflict_map[0x80] = { [GB_IO_OBP1] = GB_CONFLICT_PALETTE_DMG, [GB_IO_WY] = GB_CONFLICT_READ_OLD, [GB_IO_WX] = GB_CONFLICT_WX, - [GB_IO_NR10] = GB_CONFLICT_NR10, /* Todo: these were not verified at all */ [GB_IO_SCX] = GB_CONFLICT_READ_NEW, @@ -72,7 +79,6 @@ static const conflict_t sgb_conflict_map[0x80] = { [GB_IO_OBP1] = GB_CONFLICT_READ_NEW, [GB_IO_WY] = GB_CONFLICT_READ_OLD, [GB_IO_WX] = GB_CONFLICT_WX, - [GB_IO_NR10] = GB_CONFLICT_NR10, /* Todo: these were not verified at all */ [GB_IO_SCX] = GB_CONFLICT_READ_NEW, @@ -113,7 +119,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if ((addr & 0xFF80) == 0xFF00) { const conflict_t *map = NULL; if (GB_is_cgb(gb)) { - map = cgb_conflict_map; + map = gb->cgb_double_speed? cgb_double_conflict_map : cgb_conflict_map; } else if (GB_is_sgb(gb)) { map = sgb_conflict_map; @@ -145,6 +151,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) /* The DMG STAT-write bug is basically the STAT register being read as FF for a single T-cycle */ case GB_CONFLICT_STAT_DMG: GB_advance_cycles(gb, gb->pending_cycles); + GB_display_sync(gb); /* State 7 is the edge between HBlank and OAM mode, and it behaves a bit weird. The OAM interrupt seems to be blocked by HBlank interrupts in that case, despite the timing not making much sense for that. @@ -163,7 +170,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case GB_CONFLICT_STAT_CGB: { /* Todo: Verify this with SCX adjustments */ /* The LYC bit behaves differently */ - uint8_t old_value = GB_read_memory(gb, addr); + uint8_t old_value = gb->io_registers[GB_IO_STAT]; GB_advance_cycles(gb, gb->pending_cycles); GB_write_memory(gb, addr, (old_value & 0x40) | (value & ~0x40)); GB_advance_cycles(gb, 1); @@ -171,6 +178,16 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->pending_cycles = 3; break; } + + case GB_CONFLICT_STAT_CGB_DOUBLE: { + uint8_t old_value = gb->io_registers[GB_IO_STAT]; + GB_advance_cycles(gb, gb->pending_cycles); + GB_write_memory(gb, addr, (value & ~8) | (old_value & 8)); + GB_advance_cycles(gb, 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 3; + break; + } /* There is some "time travel" going on with these two values, as it appears that there's some off-by-1-T-cycle timing issue in the PPU implementation. @@ -195,25 +212,27 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } case GB_CONFLICT_DMG_LCDC: { - /* Similar to the palette registers, these interact directly with the LCD, so they appear to be affected by it. Both my DMG (B, blob) and Game Boy Light behave this way though. + /* Similar to the palette registers, these interact directly with the LCD, so they appear to be affected by + it. Both my DMG (B, blob) and Game Boy Light behave this way though. - Additionally, LCDC.1 is very nasty because on the it is read both by the FIFO when popping pixels, - and the object-fetching state machine, and both behave differently when it comes to access conflicts. - Hacks ahead. + Additionally, LCDC.1 is very nasty because it is read both by the FIFO when popping pixels, and the + object-fetching state machine, and both behave differently when it comes to access conflicts. Hacks ahead. */ - - uint8_t old_value = GB_read_memory(gb, addr); GB_advance_cycles(gb, gb->pending_cycles - 2); - - if (gb->model != GB_MODEL_MGB && gb->position_in_line == 0 && (old_value & 2) && !(value & 2)) { - old_value &= ~2; + GB_display_sync(gb); + if (gb->model != GB_MODEL_MGB && gb->position_in_line == 0 && (old_value & GB_LCDC_OBJ_EN) && !(value & GB_LCDC_OBJ_EN)) { + old_value &= ~GB_LCDC_OBJ_EN; } - GB_write_memory(gb, addr, old_value | (value & 1)); + GB_write_memory(gb, addr, old_value | (value & GB_LCDC_BG_EN)); GB_advance_cycles(gb, 1); GB_write_memory(gb, addr, value); + + if ((old_value & GB_LCDC_WIN_ENABLE) && !(value & GB_LCDC_WIN_ENABLE) && gb->window_is_being_fetched) { + gb->disable_window_pixel_insertion_glitch = true; + } gb->pending_cycles = 5; break; } @@ -241,12 +260,13 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->pending_cycles = 3; break; - case GB_CONFLICT_CGB_LCDC: - if ((value ^ gb->io_registers[GB_IO_LCDC]) & 0x10) { - // Todo: This is difference is because my timing is off in one of the models + case GB_CONFLICT_LCDC_CGB: { + uint8_t old = gb->io_registers[GB_IO_LCDC]; + if ((~value & old) & GB_LCDC_TILE_SEL) { + // TODO: This is different is because my timing is off in CGB ≤ C if (gb->model > GB_MODEL_CGB_C) { GB_advance_cycles(gb, gb->pending_cycles); - GB_write_memory(gb, addr, value ^ 0x10); // Write with the old TILE_SET first + GB_write_memory(gb, addr, value ^ GB_LCDC_TILE_SEL); // Write with the old TILE_SET first gb->tile_sel_glitch = true; GB_advance_cycles(gb, 1); gb->tile_sel_glitch = false; @@ -255,7 +275,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } else { GB_advance_cycles(gb, gb->pending_cycles - 1); - GB_write_memory(gb, addr, value ^ 0x10); // Write with the old TILE_SET first + GB_write_memory(gb, addr, value ^ GB_LCDC_TILE_SEL); // Write with the old TILE_SET first gb->tile_sel_glitch = true; GB_advance_cycles(gb, 1); gb->tile_sel_glitch = false; @@ -269,26 +289,55 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->pending_cycles = 4; } break; - - case GB_CONFLICT_NR10: - /* Hack: Due to the coupling between DIV and the APU, GB_apu_run only runs at M-cycle - resolutions, but this quirk requires 2MHz even in single speed mode. To work - around this, we specifically just step the calculate countdown if needed. */ - GB_advance_cycles(gb, gb->pending_cycles); - if (gb->model <= GB_MODEL_CGB_C) { - // TODO: Double speed mode? This logic is also a bit weird, it needs more tests - if (gb->apu.square_sweep_calculate_countdown > 3 && gb->apu.enable_zombie_calculate_stepping) { - gb->apu.square_sweep_calculate_countdown -= 2; - } - gb->apu.enable_zombie_calculate_stepping = true; - /* TODO: this causes audio regressions in the Donkey Kong Land series. - The exact behavior of this quirk should be further investigated, as it seems - more complicated than a single FF pseudo-write. */ - // GB_write_memory(gb, addr, 0xFF); + } + case GB_CONFLICT_LCDC_CGB_DOUBLE: { + uint8_t old = gb->io_registers[GB_IO_LCDC]; + // TODO: This is wrong for CGB ≤ C for TILE_SEL, BG_EN and BG_MAP. + // PPU timings for these models appear to be wrong and it'd make more sense to fix those first than hacking + // around them. + + // TODO: This condition is different from single speed mode. Why? What about odd modes? + if ((value ^ old) & GB_LCDC_TILE_SEL) { + GB_advance_cycles(gb, gb->pending_cycles - 2); + GB_write_memory(gb, addr, (value & (GB_LCDC_OBJ_EN | GB_LCDC_BG_EN)) | (old & ~(GB_LCDC_OBJ_EN | GB_LCDC_BG_EN))); + gb->tile_sel_glitch = true; + GB_advance_cycles(gb, 2); + gb->tile_sel_glitch = false; + GB_write_memory(gb, addr, value); + gb->pending_cycles = 4; } + else { + GB_advance_cycles(gb, gb->pending_cycles - 2); + GB_write_memory(gb, addr, (value & (GB_LCDC_OBJ_EN | GB_LCDC_BG_EN)) | (old & ~(GB_LCDC_OBJ_EN | GB_LCDC_BG_EN))); + GB_advance_cycles(gb, 2); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 4; + } + break; + } + + case GB_CONFLICT_SCX_CGB: + if (gb->cgb_double_speed) { + GB_advance_cycles(gb, gb->pending_cycles - 2); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 6; + } + else { + GB_advance_cycles(gb, gb->pending_cycles); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 4; + } + break; + + case GB_CONFLICT_NR10_CGB_DOUBLE: { + GB_advance_cycles(gb, gb->pending_cycles - 1); + gb->apu_output.square_sweep_disable_stepping = gb->model <= GB_MODEL_CGB_C && (value & 7) == 0; + GB_advance_cycles(gb, 1); + gb->apu_output.square_sweep_disable_stepping = false; GB_write_memory(gb, addr, value); gb->pending_cycles = 4; break; + } } gb->address_bus = addr; } @@ -308,16 +357,6 @@ static void cycle_oam_bug(GB_gameboy_t *gb, uint8_t register_id) gb->pending_cycles = 4; } -static void cycle_oam_bug_pc(GB_gameboy_t *gb) -{ - if (gb->pending_cycles) { - GB_advance_cycles(gb, gb->pending_cycles); - } - gb->address_bus = gb->pc; - GB_trigger_oam_bug(gb, gb->pc); /* Todo: test T-cycle timing */ - gb->pending_cycles = 4; -} - static void flush_pending_cycles(GB_gameboy_t *gb) { if (gb->pending_cycles) { @@ -346,6 +385,7 @@ static void enter_stop_mode(GB_gameboy_t *gb) gb->div_cycles = -4; // Emulate the CPU-side DIV-reset signal being held } gb->stopped = true; + gb->allow_hdma_on_wake = (gb->io_registers[GB_IO_STAT] & 3); gb->oam_ppu_blocked = !gb->oam_read_blocked; gb->vram_ppu_blocked = !gb->vram_read_blocked; gb->cgb_palettes_ppu_blocked = !gb->cgb_palettes_blocked; @@ -354,6 +394,9 @@ static void enter_stop_mode(GB_gameboy_t *gb) static void leave_stop_mode(GB_gameboy_t *gb) { gb->stopped = false; + if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3) == 0 && gb->allow_hdma_on_wake) { + gb->hdma_on = true; + } // TODO: verify this gb->dma_cycles = 4; GB_dma_run(gb); @@ -366,6 +409,7 @@ static void leave_stop_mode(GB_gameboy_t *gb) static void stop(GB_gameboy_t *gb, uint8_t opcode) { flush_pending_cycles(gb); + GB_read_memory(gb, gb->pc); // Timing is completely unverified, and only affects STOP triggering the OAM bug if ((gb->io_registers[GB_IO_JOYP] & 0x30) != 0x30) { gb->joyp_accessed = true; } @@ -389,7 +433,7 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode) if (speed_switch) { flush_pending_cycles(gb); - if (gb->io_registers[GB_IO_LCDC] & 0x80 && gb->cgb_double_speed) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE && gb->cgb_double_speed) { GB_log(gb, "ROM triggered a PPU odd mode, which is currently not supported. Reverting to even-mode.\n"); if (gb->double_speed_alignment & 7) { gb->speed_switch_freeze = 2; @@ -398,6 +442,9 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode) if (gb->apu.global_enable && gb->cgb_double_speed) { GB_log(gb, "ROM triggered an APU odd mode, which is currently not tested.\n"); } + if (gb->cartridge_type->mbc_type == GB_CAMERA && (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) && !gb->cgb_double_speed) { + GB_log(gb, "ROM entered double speed mode with a camera cartridge, this could damage a real cartridge's camera.\n"); + } if (gb->cgb_double_speed) { gb->cgb_double_speed = false; @@ -423,6 +470,7 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode) GB_dma_run(gb); gb->halted = true; gb->just_halted = true; + gb->allow_hdma_on_wake = (gb->io_registers[GB_IO_STAT] & 3); } else { gb->speed_switch_halt_countdown = 0; @@ -649,9 +697,9 @@ static void rra(GB_gameboy_t *gb, uint8_t opcode) static void jr_r8(GB_gameboy_t *gb, uint8_t opcode) { - /* Todo: Verify timing */ - gb->pc += (int8_t)cycle_read(gb, gb->pc) + 1; - cycle_no_access(gb); + int8_t offset = (int8_t)cycle_read(gb, gb->pc++); + cycle_oam_bug(gb, GB_REGISTER_PC); + gb->pc += offset; } static bool condition_code(GB_gameboy_t *gb, uint8_t opcode) @@ -676,7 +724,7 @@ static void jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode) int8_t offset = cycle_read(gb, gb->pc++); if (condition_code(gb, opcode)) { gb->pc += offset; - cycle_no_access(gb); + cycle_oam_bug(gb, GB_REGISTER_PC); } } @@ -865,7 +913,7 @@ static void ld_##dhl##_##y(GB_gameboy_t *gb, uint8_t opcode) \ cycle_write(gb, gb->hl, gb->y); \ } -LD_X_Y(b,c) LD_X_Y(b,d) LD_X_Y(b,e) LD_X_Y(b,h) LD_X_Y(b,l) LD_X_DHL(b) LD_X_Y(b,a) + LD_X_Y(b,c) LD_X_Y(b,d) LD_X_Y(b,e) LD_X_Y(b,h) LD_X_Y(b,l) LD_X_DHL(b) LD_X_Y(b,a) LD_X_Y(c,b) LD_X_Y(c,d) LD_X_Y(c,e) LD_X_Y(c,h) LD_X_Y(c,l) LD_X_DHL(c) LD_X_Y(c,a) LD_X_Y(d,b) LD_X_Y(d,c) LD_X_Y(d,e) LD_X_Y(d,h) LD_X_Y(d,l) LD_X_DHL(d) LD_X_Y(d,a) LD_X_Y(e,b) LD_X_Y(e,c) LD_X_Y(e,d) LD_X_Y(e,h) LD_X_Y(e,l) LD_X_DHL(e) LD_X_Y(e,a) @@ -877,9 +925,11 @@ LD_X_Y(a,b) LD_X_Y(a,c) LD_X_Y(a,d) LD_X_Y(a,e) LD_X_Y(a,h) LD_X_Y(a,l) LD_X_DHL // fire the debugger if software breakpoints are enabled static void ld_b_b(GB_gameboy_t *gb, uint8_t opcode) { +#ifndef GB_DISABLE_DEBUGGER if (gb->has_software_breakpoints) { - gb->debug_stopped = true; + GB_debugger_break(gb); } +#endif } static void add_a_r(GB_gameboy_t *gb, uint8_t opcode) @@ -941,7 +991,7 @@ static void sbc_a_r(GB_gameboy_t *gb, uint8_t opcode) value = get_src_value(gb, opcode); a = gb->af >> 8; carry = (gb->af & GB_CARRY_FLAG) != 0; - gb->af = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG; + gb->af = (((uint8_t)(a - value - carry)) << 8) | GB_SUBTRACT_FLAG; if ((uint8_t) (a - value - carry) == 0) { gb->af |= GB_ZERO_FLAG; @@ -1007,9 +1057,9 @@ static void cp_a_r(GB_gameboy_t *gb, uint8_t opcode) static void halt(GB_gameboy_t *gb, uint8_t opcode) { + cycle_read(gb, gb->pc); assert(gb->pending_cycles == 4); gb->pending_cycles = 0; - GB_advance_cycles(gb, 4); /* Despite what some online documentations say, the HALT bug also happens on a CGB, in both CGB and DMG modes. */ if (((gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F) != 0)) { @@ -1024,6 +1074,7 @@ static void halt(GB_gameboy_t *gb, uint8_t opcode) } else { gb->halted = true; + gb->allow_hdma_on_wake = (gb->io_registers[GB_IO_STAT] & 3); } gb->just_halted = true; } @@ -1366,7 +1417,7 @@ static void rlc_r(GB_gameboy_t *gb, uint8_t opcode) if (carry) { gb->af |= GB_CARRY_FLAG; } - if (!(value << 1)) { + if (value == 0) { gb->af |= GB_ZERO_FLAG; } } @@ -1580,7 +1631,7 @@ static opcode_t *opcodes[256] = { }; void GB_cpu_run(GB_gameboy_t *gb) { - if (gb->stopped) { + if (unlikely(gb->stopped)) { GB_timing_sync(gb); GB_advance_cycles(gb, 4); if ((gb->io_registers[GB_IO_JOYP] & 0x30) != 0x30) { @@ -1617,7 +1668,7 @@ void GB_cpu_run(GB_gameboy_t *gb) /* Wake up from HALT mode without calling interrupt code. */ if (gb->halted && !effective_ime && interrupt_queue) { gb->halted = false; - if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3) == 0) { + if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3) == 0 && gb->allow_hdma_on_wake) { gb->hdma_on = true; } gb->dma_cycles = 4; @@ -1626,9 +1677,9 @@ void GB_cpu_run(GB_gameboy_t *gb) } /* Call interrupt */ - else if (effective_ime && interrupt_queue) { + else if (unlikely(effective_ime && interrupt_queue)) { gb->halted = false; - if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3) == 0) { + if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3) == 0 && gb->allow_hdma_on_wake) { gb->hdma_on = true; } // TODO: verify the timing! @@ -1638,7 +1689,7 @@ void GB_cpu_run(GB_gameboy_t *gb) uint16_t call_addr = gb->pc; cycle_read(gb, gb->pc++); - cycle_oam_bug_pc(gb); + cycle_oam_bug(gb, GB_REGISTER_PC); gb->pc--; GB_trigger_oam_bug(gb, gb->sp); /* Todo: test T-cycle timing */ cycle_no_access(gb); @@ -1676,7 +1727,7 @@ void GB_cpu_run(GB_gameboy_t *gb) } /* Run mode */ else if (!gb->halted) { - uint8_t opcode = gb->hdma_open_bus = cycle_read(gb, gb->pc++); + uint8_t opcode = cycle_read(gb, gb->pc++); if (unlikely(gb->hdma_on)) { GB_hdma_run(gb); } diff --git a/Core/sm83_cpu.h b/Core/sm83_cpu.h index 1221fd7..7c188df 100644 --- a/Core/sm83_cpu.h +++ b/Core/sm83_cpu.h @@ -1,11 +1,10 @@ -#ifndef sm83_cpu_h -#define sm83_cpu_h +#pragma once #include "defs.h" #include +#ifndef GB_DISABLE_DEBUGGER void GB_cpu_disassemble(GB_gameboy_t *gb, uint16_t pc, uint16_t count); +#endif #ifdef GB_INTERNAL internal void GB_cpu_run(GB_gameboy_t *gb); #endif - -#endif /* sm83_cpu_h */ diff --git a/Core/sm83_disassembler.c b/Core/sm83_disassembler.c index f85bfc2..0e14a77 100644 --- a/Core/sm83_disassembler.c +++ b/Core/sm83_disassembler.c @@ -519,7 +519,7 @@ static void ld_da8_a(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) { (*pc)++; uint8_t addr = GB_read_memory(gb, (*pc)++); - const char *symbol = GB_debugger_name_for_address(gb, 0xff00 + addr); + const char *symbol = GB_debugger_name_for_address(gb, 0xFF00 + addr); if (symbol) { GB_log(gb, "LDH [%s & $FF], a ; =$%02x\n", symbol, addr); } @@ -532,7 +532,7 @@ static void ld_a_da8(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) { (*pc)++; uint8_t addr = GB_read_memory(gb, (*pc)++); - const char *symbol = GB_debugger_name_for_address(gb, 0xff00 + addr); + const char *symbol = GB_debugger_name_for_address(gb, 0xFF00 + addr); if (symbol) { GB_log(gb, "LDH a, [%s & $FF] ; =$%02x\n", symbol, addr); } @@ -758,7 +758,7 @@ static opcode_t *opcodes[256] = { void GB_cpu_disassemble(GB_gameboy_t *gb, uint16_t pc, uint16_t count) { - const GB_bank_symbol_t *function_symbol = GB_debugger_find_symbol(gb, pc); + const GB_bank_symbol_t *function_symbol = GB_debugger_find_symbol(gb, pc, false); if (function_symbol && pc - function_symbol->addr > 0x1000) { function_symbol = NULL; @@ -771,7 +771,7 @@ void GB_cpu_disassemble(GB_gameboy_t *gb, uint16_t pc, uint16_t count) uint16_t current_function = function_symbol? function_symbol->addr : 0; while (count--) { - function_symbol = GB_debugger_find_symbol(gb, pc); + function_symbol = GB_debugger_find_symbol(gb, pc, false); if (function_symbol && function_symbol->addr == pc) { if (current_function != function_symbol->addr) { GB_log(gb, "\n"); diff --git a/Core/symbol_hash.c b/Core/symbol_hash.c index 66894f1..995ad2d 100644 --- a/Core/symbol_hash.c +++ b/Core/symbol_hash.c @@ -4,7 +4,7 @@ #include #include -static size_t map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr) +static size_t map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr, bool is_local) { if (!map->symbols) { return 0; @@ -13,8 +13,8 @@ static size_t map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr) ssize_t max = map->n_symbols; while (min < max) { size_t pivot = (min + max) / 2; - if (map->symbols[pivot].addr == addr) return pivot; - if (map->symbols[pivot].addr > addr) { + if (map->symbols[pivot].addr == addr && map->symbols[pivot].is_local == is_local) return pivot; + if ((map->symbols[pivot].addr * 2 + !map->symbols[pivot].is_local) > (addr * 2 + !is_local)) { max = pivot; } else { @@ -26,24 +26,29 @@ static size_t map_find_symbol_index(GB_symbol_map_t *map, uint16_t addr) GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name) { - size_t index = map_find_symbol_index(map, addr); + bool is_local = strchr(name, '.'); + size_t index = map_find_symbol_index(map, addr, is_local); map->symbols = realloc(map->symbols, (map->n_symbols + 1) * sizeof(map->symbols[0])); memmove(&map->symbols[index + 1], &map->symbols[index], (map->n_symbols - index) * sizeof(map->symbols[0])); map->symbols[index].addr = addr; map->symbols[index].name = strdup(name); + map->symbols[index].is_local = is_local; map->n_symbols++; return &map->symbols[index]; } -const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr) +const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr, bool prefer_local) { if (!map) return NULL; - size_t index = map_find_symbol_index(map, addr); + size_t index = map_find_symbol_index(map, addr, prefer_local); if (index >= map->n_symbols || map->symbols[index].addr != addr) { index--; } if (index < map->n_symbols) { + while (index && map->symbols[index].addr == map->symbols[index - 1].addr && map->symbols[index].is_local == map->symbols[index - 1].is_local) { + index--; + } return &map->symbols[index]; } return NULL; @@ -74,13 +79,13 @@ static unsigned hash_name(const char *name) unsigned r = 0; while (*name) { r <<= 1; - if (r & 0x400) { - r ^= 0x401; + if (r & 0x2000) { + r ^= 0x2001; } - r += (unsigned char)*(name++); + r ^= (unsigned char)*(name++); } - return r & 0x3FF; + return r; } void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *bank_symbol) diff --git a/Core/symbol_hash.h b/Core/symbol_hash.h index d063312..bdc4a38 100644 --- a/Core/symbol_hash.h +++ b/Core/symbol_hash.h @@ -1,6 +1,6 @@ -#ifndef symbol_hash_h -#define symbol_hash_h +#pragma once +#ifndef GB_DISABLE_DEBUGGER #include #include #include @@ -8,6 +8,7 @@ typedef struct { char *name; uint16_t addr; + bool is_local; } GB_bank_symbol_t; typedef struct GB_symbol_s { @@ -23,16 +24,15 @@ typedef struct { } GB_symbol_map_t; typedef struct { - GB_symbol_t *buckets[0x400]; + GB_symbol_t *buckets[0x2000]; } GB_reversed_symbol_map_t; #ifdef GB_INTERNAL internal void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *symbol); internal const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name); internal GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name); -internal const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr); +internal const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr, bool prefer_local); internal GB_symbol_map_t *GB_map_alloc(void); internal void GB_map_free(GB_symbol_map_t *map); #endif - -#endif /* symbol_hash_h */ +#endif diff --git a/Core/timing.c b/Core/timing.c index b005dd2..3b9ecf7 100644 --- a/Core/timing.c +++ b/Core/timing.c @@ -42,6 +42,9 @@ static void nsleep(uint64_t nanoseconds) bool GB_timing_sync_turbo(GB_gameboy_t *gb) { +#ifndef GB_DISABLE_DEBUGGER + if (unlikely(gb->backstep_instructions)) return false; +#endif if (!gb->turbo_dont_skip) { int64_t nanoseconds = get_nanoseconds(); if (nanoseconds <= gb->last_sync + (1000000000LL * LCDC_PERIOD / GB_get_clock_rate(gb))) { @@ -54,6 +57,9 @@ bool GB_timing_sync_turbo(GB_gameboy_t *gb) void GB_timing_sync(GB_gameboy_t *gb) { +#ifndef GB_DISABLE_DEBUGGER + if (unlikely(gb->backstep_instructions)) return; +#endif /* Prevent syncing if not enough time has passed.*/ if (gb->cycles_since_last_sync < LCDC_PERIOD / 3) return; @@ -95,6 +101,9 @@ bool GB_timing_sync_turbo(GB_gameboy_t *gb) void GB_timing_sync(GB_gameboy_t *gb) { +#ifndef GB_DISABLE_DEBUGGER + if (unlikely(gb->backstep_instructions)) return; +#endif if (gb->cycles_since_last_sync < LCDC_PERIOD / 3) return; gb->cycles_since_last_sync = 0; @@ -116,7 +125,7 @@ static void ir_run(GB_gameboy_t *gb, uint32_t cycles) { /* TODO: the way this thing works makes the CGB IR port behave inaccurately when used together with HUC1/3 IR ports*/ if ((gb->model > GB_MODEL_CGB_E || !gb->cgb_mode) && gb->cartridge_type->mbc_type != GB_HUC1 && gb->cartridge_type->mbc_type != GB_HUC3) return; - bool is_sensing = (gb->io_registers[GB_IO_RP] & 0xc0) == 0xc0 || + bool is_sensing = (gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 || (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) || (gb->cartridge_type->mbc_type == GB_HUC3 && gb->huc3.mode == 0xE); if (is_sensing && (gb->infrared_input || gb->cart_ir || (gb->io_registers[GB_IO_RP] & 1))) { @@ -163,6 +172,56 @@ static void increase_tima(GB_gameboy_t *gb) } } +void GB_serial_master_edge(GB_gameboy_t *gb) +{ + if (gb->printer_callback) { + unsigned ticks = 1 << gb->serial_mask; + if (unlikely((gb->printer.command_state || gb->printer.bits_received))) { + gb->printer.idle_time +=ticks; + } + if (unlikely(gb->printer.time_remaining)) { + if (gb->printer.time_remaining <= ticks) { + gb->printer.time_remaining = 0; + if (gb->printer_done_callback) { + gb->printer_done_callback(gb); + } + } + else { + gb->printer.time_remaining -= ticks; + } + } + } + + gb->serial_master_clock ^= true; + + if (!gb->serial_master_clock && (gb->io_registers[GB_IO_SC] & 0x81) == 0x81) { + gb->serial_count++; + if (gb->serial_count == 8) { + gb->serial_count = 0; + gb->io_registers[GB_IO_SC] &= ~0x80; + gb->io_registers[GB_IO_IF] |= 8; + } + + gb->io_registers[GB_IO_SB] <<= 1; + + if (gb->serial_transfer_bit_end_callback) { + gb->io_registers[GB_IO_SB] |= gb->serial_transfer_bit_end_callback(gb); + } + else { + gb->io_registers[GB_IO_SB] |= 1; + } + + if (gb->serial_count) { + /* Still more bits to send */ + if (gb->serial_transfer_bit_start_callback) { + gb->serial_transfer_bit_start_callback(gb, gb->io_registers[GB_IO_SB] & 0x80); + } + } + + } +} + + void GB_set_internal_div_counter(GB_gameboy_t *gb, uint16_t value) { /* TIMA increases when a specific high-bit becomes a low-bit. */ @@ -171,6 +230,10 @@ void GB_set_internal_div_counter(GB_gameboy_t *gb, uint16_t value) increase_tima(gb); } + if (triggers & gb->serial_mask) { + GB_serial_master_edge(gb); + } + /* TODO: Can switching to double speed mode trigger an event? */ uint16_t apu_bit = gb->cgb_double_speed? 0x2000 : 0x1000; if (triggers & apu_bit) { @@ -189,8 +252,9 @@ static void timers_run(GB_gameboy_t *gb, uint8_t cycles) { if (gb->stopped) { if (GB_is_cgb(gb)) { - gb->apu.apu_cycles += 4 << !gb->cgb_double_speed; + gb->apu.apu_cycles += 1 << !gb->cgb_double_speed; } + gb->apu_output.sample_cycles += (gb->apu_output.sample_rate << !gb->cgb_double_speed) << 1; return; } @@ -203,58 +267,12 @@ static void timers_run(GB_gameboy_t *gb, uint8_t cycles) while (true) { advance_tima_state_machine(gb); GB_set_internal_div_counter(gb, gb->div_counter + 4); - gb->apu.apu_cycles += 4 << !gb->cgb_double_speed; + gb->apu.apu_cycles += 1 << !gb->cgb_double_speed; + gb->apu_output.sample_cycles += (gb->apu_output.sample_rate << !gb->cgb_double_speed) << 1; GB_SLEEP(gb, div, 2, 4); } } -static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) -{ - if (unlikely(gb->printer_callback && (gb->printer.command_state || gb->printer.bits_received))) { - gb->printer.idle_time += cycles; - } - if (likely(gb->serial_length == 0)) { - gb->serial_cycles += cycles; - return; - } - - while (cycles > gb->serial_length) { - advance_serial(gb, gb->serial_length); - cycles -= gb->serial_length; - } - - uint16_t previous_serial_cycles = gb->serial_cycles; - gb->serial_cycles += cycles; - if ((gb->serial_cycles & gb->serial_length) != (previous_serial_cycles & gb->serial_length)) { - gb->serial_count++; - if (gb->serial_count == 8) { - gb->serial_length = 0; - gb->serial_count = 0; - gb->io_registers[GB_IO_SC] &= ~0x80; - gb->io_registers[GB_IO_IF] |= 8; - } - - gb->io_registers[GB_IO_SB] <<= 1; - - if (gb->serial_transfer_bit_end_callback) { - gb->io_registers[GB_IO_SB] |= gb->serial_transfer_bit_end_callback(gb); - } - else { - gb->io_registers[GB_IO_SB] |= 1; - } - - if (gb->serial_length) { - /* Still more bits to send */ - if (gb->serial_transfer_bit_start_callback) { - gb->serial_transfer_bit_start_callback(gb, gb->io_registers[GB_IO_SB] & 0x80); - } - } - - } - return; - -} - void GB_set_rtc_mode(GB_gameboy_t *gb, GB_rtc_mode_t mode) { if (gb->rtc_mode != mode) { @@ -275,6 +293,81 @@ void GB_set_rtc_multiplier(GB_gameboy_t *gb, double multiplier) gb->rtc_second_length = GB_get_unmultiplied_clock_rate(gb) * 2 * multiplier; } +void GB_rtc_set_time(GB_gameboy_t *gb, uint64_t current_time) +{ + if (gb->cartridge_type->mbc_type == GB_HUC3) { + while (gb->last_rtc_second / 60 < current_time / 60) { + gb->last_rtc_second += 60; + gb->huc3.minutes++; + if (gb->huc3.minutes == 60 * 24) { + gb->huc3.days++; + gb->huc3.minutes = 0; + } + } + return; + } + + bool running = false; + if (gb->cartridge_type->mbc_type == GB_TPP1) { + running = gb->tpp1_mr4 & 0x4; + } + else { + running = (gb->rtc_real.high & 0x40) == 0; + } + + if (!running) return; + + while (gb->last_rtc_second + 60 * 60 * 24 < current_time) { + gb->last_rtc_second += 60 * 60 * 24; + if (gb->cartridge_type->mbc_type == GB_TPP1) { + if (++gb->rtc_real.tpp1.weekday == 7) { + gb->rtc_real.tpp1.weekday = 0; + if (++gb->rtc_real.tpp1.weeks == 0) { + gb->tpp1_mr4 |= 8; /* Overflow bit */ + } + } + } + else if (++gb->rtc_real.days == 0) { + if (gb->rtc_real.high & 1) { /* Bit 8 of days*/ + gb->rtc_real.high |= 0x80; /* Overflow bit */ + } + + gb->rtc_real.high ^= 1; + } + } + + while (gb->last_rtc_second < current_time) { + gb->last_rtc_second++; + if (++gb->rtc_real.seconds != 60) continue; + gb->rtc_real.seconds = 0; + + if (++gb->rtc_real.minutes != 60) continue; + gb->rtc_real.minutes = 0; + + if (gb->cartridge_type->mbc_type == GB_TPP1) { + if (++gb->rtc_real.tpp1.hours != 24) continue; + gb->rtc_real.tpp1.hours = 0; + if (++gb->rtc_real.tpp1.weekday != 7) continue; + gb->rtc_real.tpp1.weekday = 0; + if (++gb->rtc_real.tpp1.weeks == 0) { + gb->tpp1_mr4 |= 8; /* Overflow bit */ + } + } + else { + if (++gb->rtc_real.hours != 24) continue; + gb->rtc_real.hours = 0; + + if (++gb->rtc_real.days != 0) continue; + + if (gb->rtc_real.high & 1) { /* Bit 8 of days*/ + gb->rtc_real.high |= 0x80; /* Overflow bit */ + } + + gb->rtc_real.high ^= 1; + } + } +} + static void rtc_run(GB_gameboy_t *gb, uint8_t cycles) { if (likely(gb->cartridge_type->mbc_type != GB_HUC3 && !gb->cartridge_type->has_rtc)) return; @@ -300,75 +393,29 @@ static void rtc_run(GB_gameboy_t *gb, uint8_t cycles) break; } - if (gb->cartridge_type->mbc_type == GB_HUC3) { - while (gb->last_rtc_second / 60 < current_time / 60) { - gb->last_rtc_second += 60; - gb->huc3.minutes++; - if (gb->huc3.minutes == 60 * 24) { - gb->huc3.days++; - gb->huc3.minutes = 0; - } - } - return; - } - bool running = false; - if (gb->cartridge_type->mbc_type == GB_TPP1) { - running = gb->tpp1_mr4 & 0x4; - } - else { - running = (gb->rtc_real.high & 0x40) == 0; - } - - if (running) { /* is timer running? */ - while (gb->last_rtc_second + 60 * 60 * 24 < current_time) { - gb->last_rtc_second += 60 * 60 * 24; - if (gb->cartridge_type->mbc_type == GB_TPP1) { - if (++gb->rtc_real.tpp1.weekday == 7) { - gb->rtc_real.tpp1.weekday = 0; - if (++gb->rtc_real.tpp1.weeks == 0) { - gb->tpp1_mr4 |= 8; /* Overflow bit */ - } - } - } - else if (++gb->rtc_real.days == 0) { - if (gb->rtc_real.high & 1) { /* Bit 8 of days*/ - gb->rtc_real.high |= 0x80; /* Overflow bit */ - } - - gb->rtc_real.high ^= 1; - } - } - - while (gb->last_rtc_second < current_time) { - gb->last_rtc_second++; - if (++gb->rtc_real.seconds != 60) continue; - gb->rtc_real.seconds = 0; - - if (++gb->rtc_real.minutes != 60) continue; - gb->rtc_real.minutes = 0; - - if (gb->cartridge_type->mbc_type == GB_TPP1) { - if (++gb->rtc_real.tpp1.hours != 24) continue; - gb->rtc_real.tpp1.hours = 0; - if (++gb->rtc_real.tpp1.weekday != 7) continue; - gb->rtc_real.tpp1.weekday = 0; - if (++gb->rtc_real.tpp1.weeks == 0) { - gb->tpp1_mr4 |= 8; /* Overflow bit */ - } - } - else { - if (++gb->rtc_real.hours != 24) continue; - gb->rtc_real.hours = 0; - - if (++gb->rtc_real.days != 0) continue; - - if (gb->rtc_real.high & 1) { /* Bit 8 of days*/ - gb->rtc_real.high |= 0x80; /* Overflow bit */ - } - - gb->rtc_real.high ^= 1; - } - } + GB_rtc_set_time(gb, current_time); +} + +static void camera_run(GB_gameboy_t *gb, uint8_t cycles) +{ + /* Do we have a camera? */ + if (likely(gb->cartridge_type->mbc_type != GB_CAMERA)) return; + + /* The camera mapper uses the PHI pin to clock itself */ + + /* PHI does not run in halt nor stop mode */ + if (unlikely(gb->halted || gb->stopped)) return; + + /* Only every other PHI is used (as the camera wants a 512KiHz clock) */ + gb->camera_alignment += cycles; + + /* Is the camera processing an image? */ + if (likely(gb->camera_countdown == 0)) return; + + gb->camera_countdown -= cycles; + if (gb->camera_countdown <= 0) { + gb->camera_countdown = 0; + GB_camera_updated(gb); } } @@ -396,9 +443,7 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) gb->dma_cycles = cycles; timers_run(gb, cycles); - if (unlikely(!gb->stopped)) { - advance_serial(gb, cycles); // TODO: Verify what happens in STOP mode - } + camera_run(gb, cycles); if (unlikely(gb->speed_switch_halt_countdown)) { gb->speed_switch_halt_countdown -= cycles; @@ -407,8 +452,10 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) gb->halted = false; } } - + +#ifndef GB_DISABLE_DEBUGGER gb->debugger_ticks += cycles; +#endif if (gb->speed_switch_freeze) { if (gb->speed_switch_freeze >= cycles) { @@ -423,19 +470,31 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) cycles <<= 1; } +#ifndef GB_DISABLE_DEBUGGER gb->absolute_debugger_ticks += cycles; +#endif // Not affected by speed boost - if (likely(gb->io_registers[GB_IO_LCDC] & 0x80)) { + if (likely(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) { gb->double_speed_alignment += cycles; } - gb->apu_output.sample_cycles += cycles * gb->apu_output.sample_rate; gb->cycles_since_last_sync += cycles; gb->cycles_since_run += cycles; gb->rumble_on_cycles += gb->rumble_strength & 3; gb->rumble_off_cycles += (gb->rumble_strength & 3) ^ 3; - + + if (unlikely(gb->data_bus_decay_countdown)) { + if (gb->data_bus_decay_countdown <= cycles) { + gb->data_bus_decay_countdown = 0; + gb->data_bus = 0xFF; + } + else { + gb->data_bus_decay_countdown -= cycles; + } + } + + GB_joypad_run(gb, cycles); GB_apu_run(gb, false); GB_display_run(gb, cycles, false); if (unlikely(!gb->stopped)) { // TODO: Verify what happens in STOP mode diff --git a/Core/timing.h b/Core/timing.h index ee817d1..b89bf16 100644 --- a/Core/timing.h +++ b/Core/timing.h @@ -1,5 +1,5 @@ -#ifndef timing_h -#define timing_h +#pragma once + #include "defs.h" typedef enum { @@ -19,12 +19,8 @@ internal void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t internal bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */ internal void GB_timing_sync(GB_gameboy_t *gb); internal void GB_set_internal_div_counter(GB_gameboy_t *gb, uint16_t value); -enum { - GB_TIMA_RUNNING = 0, - GB_TIMA_RELOADING = 1, - GB_TIMA_RELOADED = 2 -}; - +internal void GB_serial_master_edge(GB_gameboy_t *gb); +internal void GB_rtc_set_time(GB_gameboy_t *gb, uint64_t time); #define GB_SLEEP(gb, unit, state, cycles) do {\ (gb)->unit##_cycles -= (cycles) * __state_machine_divisor; \ @@ -52,14 +48,12 @@ if ((gb)->unit##_cycles <= 0) {\ return;\ }\ switch ((gb)->unit##_state) -#endif #define GB_BATCHABLE_STATE_MACHINE(gb, unit, cycles, divisor, allow_batching) \ const bool __state_machine_allow_batching = (allow_batching); \ GB_STATE_MACHINE(gb, unit, cycles, divisor) #define GB_STATE(gb, unit, state) case state: goto unit##state +#endif #define GB_UNIT(unit) int32_t unit##_cycles, unit##_state - -#endif /* timing_h */ diff --git a/Core/workboy.c b/Core/workboy.c index 3b10379..c450825 100644 --- a/Core/workboy.c +++ b/Core/workboy.c @@ -1,5 +1,6 @@ #include "gb.h" #include +#include static inline uint8_t int_to_bcd(uint8_t i) { @@ -147,15 +148,18 @@ void GB_connect_workboy(GB_gameboy_t *gb, GB_set_serial_transfer_bit_end_callback(gb, serial_end); gb->workboy_set_time_callback = set_time_callback; gb->workboy_get_time_callback = get_time_callback; + gb->accessory = GB_ACCESSORY_WORKBOY; } bool GB_workboy_is_enabled(GB_gameboy_t *gb) { - return gb->workboy.mode; + return gb->accessory == GB_ACCESSORY_WORKBOY && gb->workboy.mode; } void GB_workboy_set_key(GB_gameboy_t *gb, uint8_t key) { + if (gb->accessory != GB_ACCESSORY_WORKBOY) return; + if (gb->workboy.user_shift_down != gb->workboy.shift_down && (key & (GB_WORKBOY_REQUIRE_SHIFT | GB_WORKBOY_FORBID_SHIFT)) == 0) { if (gb->workboy.user_shift_down) { diff --git a/Core/workboy.h b/Core/workboy.h index c99c272..71bdb44 100644 --- a/Core/workboy.h +++ b/Core/workboy.h @@ -1,5 +1,5 @@ -#ifndef workboy_h -#define workboy_h +#pragma once + #include #include #include @@ -114,5 +114,3 @@ void GB_connect_workboy(GB_gameboy_t *gb, GB_workboy_get_time_callback get_time_callback); bool GB_workboy_is_enabled(GB_gameboy_t *gb); void GB_workboy_set_key(GB_gameboy_t *gb, uint8_t key); - -#endif diff --git a/FreeDesktop/AppIcon/128x128.png b/FreeDesktop/AppIcon/128x128.png index 6303f23..b3259ed 100644 Binary files a/FreeDesktop/AppIcon/128x128.png and b/FreeDesktop/AppIcon/128x128.png differ diff --git a/FreeDesktop/AppIcon/16x16.png b/FreeDesktop/AppIcon/16x16.png index 6c3f81d..335107e 100644 Binary files a/FreeDesktop/AppIcon/16x16.png and b/FreeDesktop/AppIcon/16x16.png differ diff --git a/FreeDesktop/AppIcon/256x256.png b/FreeDesktop/AppIcon/256x256.png index e2a6cee..56ea016 100644 Binary files a/FreeDesktop/AppIcon/256x256.png and b/FreeDesktop/AppIcon/256x256.png differ diff --git a/FreeDesktop/AppIcon/32x32.png b/FreeDesktop/AppIcon/32x32.png index d7f2e4e..ea65e5a 100644 Binary files a/FreeDesktop/AppIcon/32x32.png and b/FreeDesktop/AppIcon/32x32.png differ diff --git a/FreeDesktop/AppIcon/512x512.png b/FreeDesktop/AppIcon/512x512.png index 1608c71..9004db2 100644 Binary files a/FreeDesktop/AppIcon/512x512.png and b/FreeDesktop/AppIcon/512x512.png differ diff --git a/FreeDesktop/AppIcon/64x64.png b/FreeDesktop/AppIcon/64x64.png index 4a54e94..a5675ca 100644 Binary files a/FreeDesktop/AppIcon/64x64.png and b/FreeDesktop/AppIcon/64x64.png differ diff --git a/FreeDesktop/Cartridge/128x128.png b/FreeDesktop/Cartridge/128x128.png index bc14d79..e64a3bb 100644 Binary files a/FreeDesktop/Cartridge/128x128.png and b/FreeDesktop/Cartridge/128x128.png differ diff --git a/FreeDesktop/Cartridge/16x16.png b/FreeDesktop/Cartridge/16x16.png index 3cbd9ae..e378039 100644 Binary files a/FreeDesktop/Cartridge/16x16.png and b/FreeDesktop/Cartridge/16x16.png differ diff --git a/FreeDesktop/Cartridge/256x256.png b/FreeDesktop/Cartridge/256x256.png index 14258ea..b052b31 100644 Binary files a/FreeDesktop/Cartridge/256x256.png and b/FreeDesktop/Cartridge/256x256.png differ diff --git a/FreeDesktop/Cartridge/32x32.png b/FreeDesktop/Cartridge/32x32.png index c8ef62f..270de89 100644 Binary files a/FreeDesktop/Cartridge/32x32.png and b/FreeDesktop/Cartridge/32x32.png differ diff --git a/FreeDesktop/Cartridge/512x512.png b/FreeDesktop/Cartridge/512x512.png index 71314f7..e8257c1 100644 Binary files a/FreeDesktop/Cartridge/512x512.png and b/FreeDesktop/Cartridge/512x512.png differ diff --git a/FreeDesktop/Cartridge/64x64.png b/FreeDesktop/Cartridge/64x64.png index 8835f79..34d006d 100644 Binary files a/FreeDesktop/Cartridge/64x64.png and b/FreeDesktop/Cartridge/64x64.png differ diff --git a/FreeDesktop/ColorCartridge/128x128.png b/FreeDesktop/ColorCartridge/128x128.png index da4757e..978b27b 100644 Binary files a/FreeDesktop/ColorCartridge/128x128.png and b/FreeDesktop/ColorCartridge/128x128.png differ diff --git a/FreeDesktop/ColorCartridge/16x16.png b/FreeDesktop/ColorCartridge/16x16.png index 50e6b2b..899886e 100644 Binary files a/FreeDesktop/ColorCartridge/16x16.png and b/FreeDesktop/ColorCartridge/16x16.png differ diff --git a/FreeDesktop/ColorCartridge/256x256.png b/FreeDesktop/ColorCartridge/256x256.png index 186f5d3..10e6558 100644 Binary files a/FreeDesktop/ColorCartridge/256x256.png and b/FreeDesktop/ColorCartridge/256x256.png differ diff --git a/FreeDesktop/ColorCartridge/32x32.png b/FreeDesktop/ColorCartridge/32x32.png index 47e45b5..83f7f4b 100644 Binary files a/FreeDesktop/ColorCartridge/32x32.png and b/FreeDesktop/ColorCartridge/32x32.png differ diff --git a/FreeDesktop/ColorCartridge/512x512.png b/FreeDesktop/ColorCartridge/512x512.png index 715d68f..417b2e5 100644 Binary files a/FreeDesktop/ColorCartridge/512x512.png and b/FreeDesktop/ColorCartridge/512x512.png differ diff --git a/FreeDesktop/ColorCartridge/64x64.png b/FreeDesktop/ColorCartridge/64x64.png index 403e307..01435ce 100644 Binary files a/FreeDesktop/ColorCartridge/64x64.png and b/FreeDesktop/ColorCartridge/64x64.png differ diff --git a/HexFiend/HFController.m b/HexFiend/HFController.m index 74e033c..9d4b1ae 100644 --- a/HexFiend/HFController.m +++ b/HexFiend/HFController.m @@ -279,8 +279,13 @@ static inline Class preferredByteArrayClass(void) { #if ! NDEBUG HFASSERT(range.location >= 0); HFASSERT(range.length >= 0); - HFASSERT(range.location + range.length <= HFULToFP([self totalLineCount])); #endif + if (range.location + range.length > HFULToFP([self totalLineCount])) { + range.location = [self totalLineCount] - range.length; + if (range.location < 0) { + return; + } + } if (! HFFPRangeEqualsRange(range, displayedLineRange)) { displayedLineRange = range; [self _addPropertyChangeBits:HFControllerDisplayedLineRange]; diff --git a/HexFiend/HFHexTextRepresenter.m b/HexFiend/HFHexTextRepresenter.m index f98382b..4ff9f20 100644 --- a/HexFiend/HFHexTextRepresenter.m +++ b/HexFiend/HFHexTextRepresenter.m @@ -182,7 +182,7 @@ static inline unsigned char hex2char(NSUInteger c) { [[self view] setHidesNullBytes:[[self controller] shouldHideNullBytes]]; } [super controllerDidChange:bits]; - if (bits & (HFControllerContentValue | HFControllerContentLength | HFControllerSelectedRanges)) { + if (bits & (HFControllerSelectedRanges)) { [self _clearOmittedNybble]; } } diff --git a/HexFiend/HFHexTextView.h b/HexFiend/HFHexTextView.h deleted file mode 100644 index 3222b65..0000000 --- a/HexFiend/HFHexTextView.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// HFHexTextView.h -// HexFiend_2 -// -// Copyright 2007 __MyCompanyName__. All rights reserved. -// - -#import - - -@interface HFHexTextView : NSTextView { - -} - -@end diff --git a/HexFiend/HFHexTextView.m b/HexFiend/HFHexTextView.m deleted file mode 100644 index 9e6ae47..0000000 --- a/HexFiend/HFHexTextView.m +++ /dev/null @@ -1,13 +0,0 @@ -// -// HFHexTextView.m -// HexFiend_2 -// -// Copyright 2007 __MyCompanyName__. All rights reserved. -// - -#import "HFHexTextView.h" - - -@implementation HFHexTextView - -@end diff --git a/HexFiend/HFLineCountingView.m b/HexFiend/HFLineCountingView.m index 080599b..d65e369 100644 --- a/HexFiend/HFLineCountingView.m +++ b/HexFiend/HFLineCountingView.m @@ -672,4 +672,10 @@ static inline int common_prefix_length(const char *a, const char *b) { } } +/* Compatibility with Sonoma */ ++ (bool)clipsToBounds +{ + return true; +} + @end diff --git a/HexFiend/HFRepresenterTextView.m b/HexFiend/HFRepresenterTextView.m index 7fcbd0c..463a6fd 100644 --- a/HexFiend/HFRepresenterTextView.m +++ b/HexFiend/HFRepresenterTextView.m @@ -1206,7 +1206,6 @@ static size_t unionAndCleanLists(NSRect *rectList, id *valueList, size_t count) /* Start us off with the horizontal inset and move the baseline down by the ascender so our glyphs just graze the top of our view */ textTransform.tx += [self horizontalContainerInset]; textTransform.ty += [fontObject ascender] - lineHeight * [self verticalOffset]; - NSUInteger lineIndex = 0; const NSUInteger maxGlyphCount = [self maximumGlyphCountForByteCount:bytesPerLine]; NEW_ARRAY(struct HFGlyph_t, glyphs, maxGlyphCount); NEW_ARRAY(CGSize, advances, maxGlyphCount); @@ -1269,7 +1268,6 @@ static size_t unionAndCleanLists(NSRect *rectList, id *valueList, size_t count) else if (NSMinY(lineRectInBoundsSpace) > NSMaxY(clip)) { break; } - lineIndex++; } FREE_ARRAY(glyphs); FREE_ARRAY(advances); @@ -1763,4 +1761,10 @@ static size_t unionAndCleanLists(NSRect *rectList, id *valueList, size_t count) else return YES; } +/* Compatibility with Sonoma */ ++ (bool)clipsToBounds +{ + return true; +} + @end diff --git a/HexFiend/HFStatusBarRepresenter.h b/HexFiend/HFStatusBarRepresenter.h deleted file mode 100644 index e70b893..0000000 --- a/HexFiend/HFStatusBarRepresenter.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// HFStatusBarRepresenter.h -// HexFiend_2 -// -// Copyright 2007 ridiculous_fish. All rights reserved. -// - -#import - -/*! @enum HFStatusBarMode - The HFStatusBarMode enum is used to describe the format of the byte counts displayed by the status bar. -*/ -typedef NS_ENUM(NSUInteger, HFStatusBarMode) { - HFStatusModeDecimal, ///< The status bar should display byte counts in decimal - HFStatusModeHexadecimal, ///< The status bar should display byte counts in hexadecimal - HFStatusModeApproximate, ///< The text should display byte counts approximately (e.g. "56.3 KB") - HFSTATUSMODECOUNT ///< The number of modes, to allow easy cycling -}; - -/*! @class HFStatusBarRepresenter - @brief The HFRepresenter for the status bar. - - HFStatusBarRepresenter is a subclass of HFRepresenter responsible for showing the status bar, which displays information like the total length of the document, or the number of selected bytes. -*/ -@interface HFStatusBarRepresenter : HFRepresenter { - HFStatusBarMode statusMode; -} - -@property (nonatomic) HFStatusBarMode statusMode; - -@end diff --git a/HexFiend/HFStatusBarRepresenter.m b/HexFiend/HFStatusBarRepresenter.m deleted file mode 100644 index 883677f..0000000 --- a/HexFiend/HFStatusBarRepresenter.m +++ /dev/null @@ -1,240 +0,0 @@ -// -// HFStatusBarRepresenter.m -// HexFiend_2 -// -// Copyright 2007 ridiculous_fish. All rights reserved. -// - -#import -#import - -#define kHFStatusBarDefaultModeUserDefaultsKey @"HFStatusBarDefaultMode" - -@interface HFStatusBarView : NSView { - NSCell *cell; - NSSize cellSize; - HFStatusBarRepresenter *representer; - NSDictionary *cellAttributes; - BOOL registeredForAppNotifications; -} - -- (void)setRepresenter:(HFStatusBarRepresenter *)rep; -- (void)setString:(NSString *)string; - -@end - - -@implementation HFStatusBarView - -- (void)_sharedInitStatusBarView { - NSMutableParagraphStyle *style = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease]; - [style setAlignment:NSCenterTextAlignment]; - cellAttributes = [[NSDictionary alloc] initWithObjectsAndKeys:[NSColor windowFrameTextColor], NSForegroundColorAttributeName, [NSFont labelFontOfSize:[NSFont smallSystemFontSize]], NSFontAttributeName, style, NSParagraphStyleAttributeName, nil]; - cell = [[NSCell alloc] initTextCell:@""]; - [cell setAlignment:NSCenterTextAlignment]; - [cell setBackgroundStyle:NSBackgroundStyleRaised]; -} - -- (instancetype)initWithFrame:(NSRect)frame { - self = [super initWithFrame:frame]; - [self _sharedInitStatusBarView]; - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)coder { - HFASSERT([coder allowsKeyedCoding]); - self = [super initWithCoder:coder]; - [self _sharedInitStatusBarView]; - return self; -} - -// nothing to do in encodeWithCoder - -- (BOOL)isFlipped { return YES; } - -- (void)setRepresenter:(HFStatusBarRepresenter *)rep { - representer = rep; -} - -- (void)setString:(NSString *)string { - [cell setAttributedStringValue:[[[NSAttributedString alloc] initWithString:string attributes:cellAttributes] autorelease]]; - cellSize = [cell cellSize]; - [self setNeedsDisplay:YES]; -} - -- (void)drawRect:(NSRect)clip { - USE(clip); - NSRect bounds = [self bounds]; - // [[NSColor colorWithCalibratedWhite:(CGFloat).91 alpha:1] set]; - // NSRectFill(clip); - - - NSRect cellRect = NSMakeRect(NSMinX(bounds), HFCeil(NSMidY(bounds) - cellSize.height / 2), NSWidth(bounds), cellSize.height); - [cell drawWithFrame:cellRect inView:self]; -} - -- (void)setFrame:(NSRect)frame -{ - [super setFrame:frame]; - [self.window setContentBorderThickness:frame.origin.y + frame.size.height forEdge:NSMinYEdge]; -} - - -- (void)mouseDown:(NSEvent *)event { - USE(event); - HFStatusBarMode newMode = ([representer statusMode] + 1) % HFSTATUSMODECOUNT; - [representer setStatusMode:newMode]; - [[NSUserDefaults standardUserDefaults] setInteger:newMode forKey:kHFStatusBarDefaultModeUserDefaultsKey]; -} - -- (void)windowDidChangeKeyStatus:(NSNotification *)note { - USE(note); - [self setNeedsDisplay:YES]; -} - -- (void)viewDidMoveToWindow { - HFRegisterViewForWindowAppearanceChanges(self, @selector(windowDidChangeKeyStatus:), !registeredForAppNotifications); - registeredForAppNotifications = YES; - [self.window setContentBorderThickness:self.frame.origin.y + self.frame.size.height forEdge:NSMinYEdge]; - [super viewDidMoveToWindow]; -} - -- (void)viewWillMoveToWindow:(NSWindow *)newWindow { - HFUnregisterViewForWindowAppearanceChanges(self, NO); - [super viewWillMoveToWindow:newWindow]; -} - -- (void)dealloc { - HFUnregisterViewForWindowAppearanceChanges(self, registeredForAppNotifications); - [cell release]; - [cellAttributes release]; - [super dealloc]; -} - -@end - -@implementation HFStatusBarRepresenter - -- (void)encodeWithCoder:(NSCoder *)coder { - HFASSERT([coder allowsKeyedCoding]); - [super encodeWithCoder:coder]; - [coder encodeInt64:statusMode forKey:@"HFStatusMode"]; -} - -- (instancetype)initWithCoder:(NSCoder *)coder { - HFASSERT([coder allowsKeyedCoding]); - self = [super initWithCoder:coder]; - statusMode = (NSUInteger)[coder decodeInt64ForKey:@"HFStatusMode"]; - return self; -} - -- (instancetype)init { - self = [super init]; - statusMode = [[NSUserDefaults standardUserDefaults] integerForKey:kHFStatusBarDefaultModeUserDefaultsKey]; - return self; -} - -- (NSView *)createView { - HFStatusBarView *view = [[HFStatusBarView alloc] initWithFrame:NSMakeRect(0, 0, 100, 18)]; - [view setRepresenter:self]; - [view setAutoresizingMask:NSViewWidthSizable]; - return view; -} - -- (NSString *)describeLength:(unsigned long long)length { - switch (statusMode) { - case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu byte%s", length, length == 1 ? "" : "s"]; - case HFStatusModeHexadecimal: return [NSString stringWithFormat:@"0x%llX byte%s", length, length == 1 ? "" : "s"]; - case HFStatusModeApproximate: return [NSString stringWithFormat:@"%@", HFDescribeByteCount(length)]; - default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; - } -} - -- (NSString *)describeOffset:(unsigned long long)offset { - switch (statusMode) { - case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu", offset]; - case HFStatusModeHexadecimal: return [NSString stringWithFormat:@"0x%llX", offset]; - case HFStatusModeApproximate: return [NSString stringWithFormat:@"%@", HFDescribeByteCount(offset)]; - default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; - } -} - -/* same as describeOffset, except we treat Approximate like Hexadecimal */ -- (NSString *)describeOffsetExcludingApproximate:(unsigned long long)offset { - switch (statusMode) { - case HFStatusModeDecimal: return [NSString stringWithFormat:@"%llu", offset]; - case HFStatusModeHexadecimal: - case HFStatusModeApproximate: return [NSString stringWithFormat:@"0x%llX", offset]; - default: [NSException raise:NSInternalInconsistencyException format:@"Unknown status mode %lu", (unsigned long)statusMode]; return @""; - } -} - -- (NSString *)stringForEmptySelectionAtOffset:(unsigned long long)offset length:(unsigned long long)length { - return [NSString stringWithFormat:@"%@ out of %@", [self describeOffset:offset], [self describeLength:length]]; -} - -- (NSString *)stringForSingleByteSelectionAtOffset:(unsigned long long)offset length:(unsigned long long)length { - return [NSString stringWithFormat:@"Byte %@ selected out of %@", [self describeOffset:offset], [self describeLength:length]]; -} - -- (NSString *)stringForSingleRangeSelection:(HFRange)range length:(unsigned long long)length { - return [NSString stringWithFormat:@"%@ selected at offset %@ out of %@", [self describeLength:range.length], [self describeOffsetExcludingApproximate:range.location], [self describeLength:length]]; -} - -- (NSString *)stringForMultipleSelectionsWithLength:(unsigned long long)multipleSelectionLength length:(unsigned long long)length { - return [NSString stringWithFormat:@"%@ selected at multiple offsets out of %@", [self describeLength:multipleSelectionLength], [self describeLength:length]]; -} - - -- (void)updateString { - NSString *string = nil; - HFController *controller = [self controller]; - if (controller) { - unsigned long long length = [controller contentsLength]; - NSArray *ranges = [controller selectedContentsRanges]; - NSUInteger rangeCount = [ranges count]; - if (rangeCount == 1) { - HFRange range = [ranges[0] HFRange]; - if (range.length == 0) { - string = [self stringForEmptySelectionAtOffset:range.location length:length]; - } - else if (range.length == 1) { - string = [self stringForSingleByteSelectionAtOffset:range.location length:length]; - } - else { - string = [self stringForSingleRangeSelection:range length:length]; - } - } - else { - unsigned long long totalSelectionLength = 0; - FOREACH(HFRangeWrapper *, wrapper, ranges) { - HFRange range = [wrapper HFRange]; - totalSelectionLength = HFSum(totalSelectionLength, range.length); - } - string = [self stringForMultipleSelectionsWithLength:totalSelectionLength length:length]; - } - } - if (! string) string = @""; - [[self view] setString:string]; -} - -- (HFStatusBarMode)statusMode { - return statusMode; -} - -- (void)setStatusMode:(HFStatusBarMode)mode { - statusMode = mode; - [self updateString]; -} - -- (void)controllerDidChange:(HFControllerPropertyBits)bits { - if (bits & (HFControllerContentLength | HFControllerSelectedRanges)) { - [self updateString]; - } -} - -+ (NSPoint)defaultLayoutPosition { - return NSMakePoint(0, -1); -} - -@end diff --git a/HexFiend/HexFiend.h b/HexFiend/HexFiend.h index 60d69a7..0253cb8 100644 --- a/HexFiend/HexFiend.h +++ b/HexFiend/HexFiend.h @@ -28,7 +28,6 @@ #import #import #import -#import #import #import #import diff --git a/JoyKit/ControllerConfiguration.inc b/JoyKit/ControllerConfiguration.inc index 86988c7..9b97a20 100644 --- a/JoyKit/ControllerConfiguration.inc +++ b/JoyKit/ControllerConfiguration.inc @@ -101,12 +101,12 @@ hacksByManufacturer = @{ @{@"reportID": @(0x31), @"size":@1, @"offset":@0x47, @"usagePage":@(kHIDPage_Button), @"usage":@4}, @{@"reportID": @(0x31), @"size":@1, @"offset":@0x48, @"usagePage":@(kHIDPage_Button), @"usage":@5}, @{@"reportID": @(0x31), @"size":@1, @"offset":@0x49, @"usagePage":@(kHIDPage_Button), @"usage":@6}, - @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4a, @"usagePage":@(kHIDPage_Button), @"usage":@7}, - @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4b, @"usagePage":@(kHIDPage_Button), @"usage":@8}, - @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4c, @"usagePage":@(kHIDPage_Button), @"usage":@9}, - @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4d, @"usagePage":@(kHIDPage_Button), @"usage":@10}, - @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4e, @"usagePage":@(kHIDPage_Button), @"usage":@11}, - @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4f, @"usagePage":@(kHIDPage_Button), @"usage":@12}, + @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4A, @"usagePage":@(kHIDPage_Button), @"usage":@7}, + @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4B, @"usagePage":@(kHIDPage_Button), @"usage":@8}, + @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4C, @"usagePage":@(kHIDPage_Button), @"usage":@9}, + @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4D, @"usagePage":@(kHIDPage_Button), @"usage":@10}, + @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4E, @"usagePage":@(kHIDPage_Button), @"usage":@11}, + @{@"reportID": @(0x31), @"size":@1, @"offset":@0x4F, @"usagePage":@(kHIDPage_Button), @"usage":@12}, @{@"reportID": @(0x31), @"size":@1, @"offset":@0x50, @"usagePage":@(kHIDPage_Button), @"usage":@13}, @{@"reportID": @(0x31), @"size":@1, @"offset":@0x51, @"usagePage":@(kHIDPage_Button), @"usage":@14}, @{@"reportID": @(0x31), @"size":@1, @"offset":@0x52, @"usagePage":@(kHIDPage_Button), @"usage":@15}, @@ -172,7 +172,7 @@ hacksByName = @{ JOYConnectedUsage: @2, JOYConnectedUsagePage: @0xFF00, - JOYActivationReport: [NSData dataWithBytes:(uint8_t[]){0x13} length:1], + JOYActivationReport: [NSData dataWithBytes:&inline_const(uint8_t, 0x13) length:1], JOYCustomReports: @{ @@ -466,6 +466,143 @@ hacksByName = @{ JOYIgnoredReports: @[@(0x30)], // Ignore the real 0x30 report as it's broken }, + + @"Joy-Con (L)": @{ // Switch Pro Controller + JOYIsSwitch: @YES, + JOYJoyCon: @(JOYJoyConTypeLeft), + JOYAxisGroups: @{ + @(kHIDUsage_GD_X): @(0), + @(kHIDUsage_GD_Y): @(0), + @(kHIDUsage_GD_Z): @(0), + }, + + JOYButtonUsageMapping: @{ + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(7): @(JOYButtonUsageL2), + BUTTON(9): @(JOYButtonUsageSelect), + BUTTON(11): @(JOYButtonUsageLStick), + BUTTON(14): @(JOYButtonUsageMisc), + BUTTON(15): @(JOYButtonUsageL3), + BUTTON(16): @(JOYButtonUsageR3), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(1): @(JOYAxes2DUsageLeftStick), + }, + + JOYCustomReports: @{ + @(0x30): @[ + @{@"reportID": @(1), @"size":@1, @"offset":@24, @"usagePage":@(kHIDPage_Button), @"usage":@9}, + @{@"reportID": @(1), @"size":@1, @"offset":@27, @"usagePage":@(kHIDPage_Button), @"usage":@11}, + + @{@"reportID": @(1), @"size":@1, @"offset":@29, @"usagePage":@(kHIDPage_Button), @"usage":@14}, + + @{@"reportID": @(1), @"size":@1, @"offset":@32, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)}, + @{@"reportID": @(1), @"size":@1, @"offset":@33, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)}, + @{@"reportID": @(1), @"size":@1, @"offset":@34, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)}, + @{@"reportID": @(1), @"size":@1, @"offset":@35, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)}, + + @{@"reportID": @(1), @"size":@1, @"offset":@36, @"usagePage":@(kHIDPage_Button), @"usage":@16}, + @{@"reportID": @(1), @"size":@1, @"offset":@37, @"usagePage":@(kHIDPage_Button), @"usage":@15}, + @{@"reportID": @(1), @"size":@1, @"offset":@38, @"usagePage":@(kHIDPage_Button), @"usage":@5}, + @{@"reportID": @(1), @"size":@1, @"offset":@39, @"usagePage":@(kHIDPage_Button), @"usage":@7}, + + /* Sticks */ + @{@"reportID": @(1), @"size":@12, @"offset":@40, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @0xFFF}, + @{@"reportID": @(1), @"size":@12, @"offset":@52, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @0xFFF, @"max": @0}, + + @{@"reportID": @(1), @"size":@16, @"offset":@96, @"usagePage":@(kHIDPage_Sensor), @"min": @0x7FFF, @"max": @-0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ)}, + @{@"reportID": @(1), @"size":@16, @"offset":@112, @"usagePage":@(kHIDPage_Sensor), @"min": @0x7FFF, @"max": @-0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisX)}, + @{@"reportID": @(1), @"size":@16, @"offset":@128, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisY)}, + @{@"reportID": @(1), @"size":@16, @"offset":@144, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@160, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@176, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis)}, + ], + }, + + JOYIgnoredReports: @[@(0x30)], // Ignore the real 0x30 report as it's broken + }, + + @"Joy-Con (R)": @{ // Switch Pro Controller + JOYIsSwitch: @YES, + JOYJoyCon: @(JOYJoyConTypeRight), + JOYAxisGroups: @{ + @(kHIDUsage_GD_Rx): @(1), + @(kHIDUsage_GD_Ry): @(1), + @(kHIDUsage_GD_Rz): @(1), + }, + + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageB), + BUTTON(2): @(JOYButtonUsageA), + BUTTON(3): @(JOYButtonUsageY), + BUTTON(4): @(JOYButtonUsageX), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(8): @(JOYButtonUsageR2), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(12): @(JOYButtonUsageRStick), + BUTTON(13): @(JOYButtonUsageHome), + BUTTON(15): @(JOYButtonUsageL3), + BUTTON(16): @(JOYButtonUsageR3), + }, + + JOYAxes2DUsageMapping: @{ + AXES2D(4): @(JOYAxes2DUsageRightStick), + }, + + JOYCustomReports: @{ + @(0x30): @[ + + @{@"reportID": @(1), @"size":@1, @"offset":@16, @"usagePage":@(kHIDPage_Button), @"usage":@3}, + @{@"reportID": @(1), @"size":@1, @"offset":@17, @"usagePage":@(kHIDPage_Button), @"usage":@4}, + @{@"reportID": @(1), @"size":@1, @"offset":@18, @"usagePage":@(kHIDPage_Button), @"usage":@1}, + @{@"reportID": @(1), @"size":@1, @"offset":@19, @"usagePage":@(kHIDPage_Button), @"usage":@2}, + @{@"reportID": @(1), @"size":@1, @"offset":@20, @"usagePage":@(kHIDPage_Button), @"usage":@16}, + @{@"reportID": @(1), @"size":@1, @"offset":@21, @"usagePage":@(kHIDPage_Button), @"usage":@15}, + @{@"reportID": @(1), @"size":@1, @"offset":@22, @"usagePage":@(kHIDPage_Button), @"usage":@6}, + @{@"reportID": @(1), @"size":@1, @"offset":@23, @"usagePage":@(kHIDPage_Button), @"usage":@8}, + + @{@"reportID": @(1), @"size":@1, @"offset":@25, @"usagePage":@(kHIDPage_Button), @"usage":@10}, + @{@"reportID": @(1), @"size":@1, @"offset":@26, @"usagePage":@(kHIDPage_Button), @"usage":@12}, + @{@"reportID": @(1), @"size":@1, @"offset":@28, @"usagePage":@(kHIDPage_Button), @"usage":@13}, + + /* Sticks */ + @{@"reportID": @(1), @"size":@12, @"offset":@64, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @0xFFF}, + @{@"reportID": @(1), @"size":@12, @"offset":@76, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @0xFFF, @"max": @0}, + + // The X axis is inverted on the right Joy-Con + @{@"reportID": @(1), @"size":@16, @"offset":@96, @"usagePage":@(kHIDPage_Sensor), @"min": @0x7FFF, @"max": @-0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ)}, + @{@"reportID": @(1), @"size":@16, @"offset":@112, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisX)}, + @{@"reportID": @(1), @"size":@16, @"offset":@128, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisY)}, + @{@"reportID": @(1), @"size":@16, @"offset":@144, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@160, @"usagePage":@(kHIDPage_Sensor), @"min": @0x7FFF, @"max": @-0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@176, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis)}, + ], + }, + + JOYIgnoredReports: @[@(0x30)], // Ignore the real 0x30 report as it's broken + }, + + + @"NSW wired controller": @{ // Wired Switch controllers + JOYButtonUsageMapping: @{ + BUTTON(1): @(JOYButtonUsageY), + BUTTON(2): @(JOYButtonUsageB), + BUTTON(3): @(JOYButtonUsageA), + BUTTON(4): @(JOYButtonUsageX), + BUTTON(5): @(JOYButtonUsageL1), + BUTTON(6): @(JOYButtonUsageR1), + BUTTON(7): @(JOYButtonUsageL2), + BUTTON(8): @(JOYButtonUsageR2), + BUTTON(9): @(JOYButtonUsageSelect), + BUTTON(10): @(JOYButtonUsageStart), + BUTTON(11): @(JOYButtonUsageLStick), + BUTTON(12): @(JOYButtonUsageRStick), + BUTTON(13): @(JOYButtonUsageHome), + BUTTON(14): @(JOYButtonUsageMisc), + }, + }, + @"PLAYSTATION(R)3 Controller": @{ // DualShock 3 JOYAxisGroups: @{ @(kHIDUsage_GD_X): @(0), diff --git a/JoyKit/JOYAxes2D.h b/JoyKit/JOYAxes2D.h index b6f6d15..71fdb42 100644 --- a/JoyKit/JOYAxes2D.h +++ b/JoyKit/JOYAxes2D.h @@ -1,4 +1,5 @@ #import +#import "JOYInput.h" typedef enum { JOYAxes2DUsageNone, @@ -11,10 +12,8 @@ typedef enum { JOYAxes2DUsageGeneric0 = 0x10000, } JOYAxes2DUsage; -@interface JOYAxes2D : NSObject -- (NSString *)usageString; +@interface JOYAxes2D : JOYInput + (NSString *)usageToString: (JOYAxes2DUsage) usage; -- (uint64_t)uniqueID; - (double)distance; - (double)angle; - (NSPoint)value; diff --git a/JoyKit/JOYAxes2D.m b/JoyKit/JOYAxes2D.m index 272d34f..dad59bb 100644 --- a/JoyKit/JOYAxes2D.m +++ b/JoyKit/JOYAxes2D.m @@ -1,26 +1,29 @@ #import "JOYAxes2D.h" #import "JOYElement.h" +@interface JOYAxes2D() +@property unsigned rotation; // in 90 degrees units, clockwise +@end + @implementation JOYAxes2D { JOYElement *_element1, *_element2; double _state1, _state2; - int32_t initialX, initialY; - int32_t minX, minY; - int32_t maxX, maxY; - + int32_t _initialX, _initialY; + int32_t _minX, _minY; + int32_t _maxX, _maxY; } + (NSString *)usageToString: (JOYAxes2DUsage) usage { if (usage < JOYAxes2DUsageNonGenericMax) { - return (NSString *[]) { + return inline_const(NSString *[], { @"None", @"Left Stick", @"Right Stick", @"Middle Stick", @"Pointer", - }[usage]; + })[usage]; } if (usage >= JOYAxes2DUsageGeneric0) { return [NSString stringWithFormat:@"Generic 2D Analog Control %d", usage - JOYAxes2DUsageGeneric0]; @@ -36,12 +39,12 @@ - (uint64_t)uniqueID { - return _element1.uniqueID; + return _element1.uniqueID | (uint64_t)self.combinedIndex << 32; } - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %.2f%%, %.2f degrees>", self.className, self, self.usageString, self.uniqueID, self.distance * 100, self.angle]; + return [NSString stringWithFormat:@"<%@: %p, %@ (%llx); State: %.2f%%, %.2f degrees>", self.className, self, self.usageString, self.uniqueID, self.distance * 100, self.angle]; } - (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2 @@ -57,12 +60,12 @@ uint16_t usage = element1.usage; _usage = JOYAxes2DUsageGeneric0 + usage - kHIDUsage_GD_X + 1; } - initialX = 0; - initialY = 0; - minX = element1.max; - minY = element2.max; - maxX = element1.min; - maxY = element2.min; + _initialX = 0; + _initialY = 0; + _minX = element1.max; + _minY = element2.max; + _maxX = element1.min; + _maxY = element2.min; return self; } @@ -72,44 +75,44 @@ return NSMakePoint(_state1, _state2); } --(int32_t) effectiveMinX +- (int32_t)effectiveMinX { int32_t rawMin = _element1.min; int32_t rawMax = _element1.max; - if (initialX == 0) return rawMin; - if (minX <= (rawMin * 2 + initialX) / 3 && maxX >= (rawMax * 2 + initialX) / 3 ) return minX; - if ((initialX - rawMin) < (rawMax - initialX)) return rawMin; - return initialX - (rawMax - initialX); + if (_initialX == 0) return rawMin; + if (_minX <= (rawMin * 2 + _initialX) / 3 && _maxX >= (rawMax * 2 + _initialX) / 3 ) return _minX; + if ((_initialX - rawMin) < (rawMax - _initialX)) return rawMin; + return _initialX - (rawMax - _initialX); } --(int32_t) effectiveMinY +- (int32_t)effectiveMinY { int32_t rawMin = _element2.min; int32_t rawMax = _element2.max; - if (initialY == 0) return rawMin; - if (minX <= (rawMin * 2 + initialY) / 3 && maxY >= (rawMax * 2 + initialY) / 3 ) return minY; - if ((initialY - rawMin) < (rawMax - initialY)) return rawMin; - return initialY - (rawMax - initialY); + if (_initialY == 0) return rawMin; + if (_minX <= (rawMin * 2 + _initialY) / 3 && _maxY >= (rawMax * 2 + _initialY) / 3 ) return _minY; + if ((_initialY - rawMin) < (rawMax - _initialY)) return rawMin; + return _initialY - (rawMax - _initialY); } --(int32_t) effectiveMaxX +- (int32_t)effectiveMaxX { int32_t rawMin = _element1.min; int32_t rawMax = _element1.max; - if (initialX == 0) return rawMax; - if (minX <= (rawMin * 2 + initialX) / 3 && maxX >= (rawMax * 2 + initialX) / 3 ) return maxX; - if ((initialX - rawMin) > (rawMax - initialX)) return rawMax; - return initialX + (initialX - rawMin); + if (_initialX == 0) return rawMax; + if (_minX <= (rawMin * 2 + _initialX) / 3 && _maxX >= (rawMax * 2 + _initialX) / 3 ) return _maxX; + if ((_initialX - rawMin) > (rawMax - _initialX)) return rawMax; + return _initialX + (_initialX - rawMin); } --(int32_t) effectiveMaxY +- (int32_t)effectiveMaxY { int32_t rawMin = _element2.min; int32_t rawMax = _element2.max; - if (initialY == 0) return rawMax; - if (minX <= (rawMin * 2 + initialY) / 3 && maxY >= (rawMax * 2 + initialY) / 3 ) return maxY; - if ((initialY - rawMin) > (rawMax - initialY)) return rawMax; - return initialY + (initialY - rawMin); + if (_initialY == 0) return rawMax; + if (_minX <= (rawMin * 2 + _initialY) / 3 && _maxY >= (rawMax * 2 + _initialY) / 3 ) return _maxY; + if ((_initialY - rawMin) > (rawMax - _initialY)) return rawMax; + return _initialY + (_initialY - rawMin); } - (bool)updateState @@ -118,18 +121,18 @@ int32_t y = [_element2 value]; if (x == 0 && y == 0) return false; - if (initialX == 0 && initialY == 0) { - initialX = x; - initialY = y; + if (_initialX == 0 && _initialY == 0) { + _initialX = x; + _initialY = y; } double old1 = _state1, old2 = _state2; { int32_t value = x; - if (initialX != 0) { - minX = MIN(value, minX); - maxX = MAX(value, maxX); + if (_initialX != 0) { + _minX = MIN(value, _minX); + _maxX = MAX(value, _maxX); } double min = [self effectiveMinX]; @@ -142,9 +145,9 @@ { int32_t value = y; - if (initialY != 0) { - minY = MIN(value, minY); - maxY = MAX(value, maxY); + if (_initialY != 0) { + _minY = MIN(value, _minY); + _maxY = MAX(value, _maxY); } double min = [self effectiveMinY]; @@ -158,11 +161,29 @@ _state2 < -1 || _state2 > 1) { // Makes no sense, recalibrate _state1 = _state2 = 0; - initialX = initialY = 0; - minX = _element1.max; - minY = _element2.max; - maxX = _element1.min; - maxY = _element2.min; + _initialX = _initialY = 0; + _minX = _element1.max; + _minY = _element2.max; + _maxX = _element1.min; + _maxY = _element2.min; + } + + + double temp = _state1; + switch (_rotation & 3) { + case 0: break; + case 1: + _state1 = -_state2; + _state2 = temp; + break; + case 2: + _state1 = -_state1; + _state2 = -_state2; + break; + case 3: + _state1 = _state2; + _state2 = -temp; + break; } return old1 != _state1 || old2 != _state2; @@ -173,9 +194,11 @@ return MIN(sqrt(_state1 * _state1 + _state2 * _state2), 1.0); } -- (double)angle { +- (double)angle +{ double temp = atan2(_state2, _state1) * 180 / M_PI; if (temp >= 0) return temp; return temp + 360; } + @end diff --git a/JoyKit/JOYAxes3D.h b/JoyKit/JOYAxes3D.h index 5c83807..e30d465 100644 --- a/JoyKit/JOYAxes3D.h +++ b/JoyKit/JOYAxes3D.h @@ -1,4 +1,5 @@ #import +#import "JOYInput.h" typedef enum { JOYAxes3DUsageNone, @@ -14,10 +15,8 @@ typedef struct { double x, y, z; } JOYPoint3D; -@interface JOYAxes3D : NSObject -- (NSString *)usageString; +@interface JOYAxes3D : JOYInput + (NSString *)usageToString: (JOYAxes3DUsage) usage; -- (uint64_t)uniqueID; - (JOYPoint3D)rawValue; - (JOYPoint3D)normalizedValue; // For orientation - (JOYPoint3D)gUnitsValue; // For acceleration diff --git a/JoyKit/JOYAxes3D.m b/JoyKit/JOYAxes3D.m index 6ec146a..9c7b86b 100644 --- a/JoyKit/JOYAxes3D.m +++ b/JoyKit/JOYAxes3D.m @@ -1,6 +1,10 @@ #import "JOYAxes3D.h" #import "JOYElement.h" +@interface JOYAxes3D() +@property unsigned rotation; // in 90 degrees units, clockwise +@end + @implementation JOYAxes3D { JOYElement *_element1, *_element2, *_element3; @@ -13,12 +17,12 @@ + (NSString *)usageToString: (JOYAxes3DUsage) usage { if (usage < JOYAxes3DUsageNonGenericMax) { - return (NSString *[]) { + return inline_const(NSString *[], { @"None", @"Acceleretion", @"Orientation", @"Gyroscope", - }[usage]; + })[usage]; } if (usage >= JOYAxes3DUsageGeneric0) { return [NSString stringWithFormat:@"Generic 3D Analog Control %d", usage - JOYAxes3DUsageGeneric0]; @@ -34,12 +38,12 @@ - (uint64_t)uniqueID { - return _element1.uniqueID; + return _element1.uniqueID | (uint64_t)self.combinedIndex << 32; } - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: (%.2f, %.2f, %.2f)>", self.className, self, self.usageString, self.uniqueID, _state1, _state2, _state3]; + return [NSString stringWithFormat:@"<%@: %p, %@ (%llx); State: (%.2f, %.2f, %.2f)>", self.className, self, self.usageString, self.uniqueID, _state1, _state2, _state3]; } - (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2 thirdElement:(JOYElement *)element3 @@ -101,6 +105,23 @@ else { _gApproximation = _gApproximation * 0.9999 + distance * 0.0001; } + + double temp = _state1; + switch (_rotation & 3) { + case 0: break; + case 1: + _state1 = -_state3; + _state3 = temp; + break; + case 2: + _state1 = -_state1; + _state3 = -_state3; + break; + case 3: + _state1 = _state3; + _state3 = -temp; + break; + } return old1 != _state1 || old2 != _state2 || old3 != _state3; } diff --git a/JoyKit/JOYAxis.h b/JoyKit/JOYAxis.h index 8d4b7ab..06c0917 100644 --- a/JoyKit/JOYAxis.h +++ b/JoyKit/JOYAxis.h @@ -1,5 +1,6 @@ #import #import "JOYButton.h" +#import "JOYInput.h" typedef enum { JOYAxisUsageNone, @@ -24,10 +25,8 @@ typedef enum { JOYAxisUsageGeneric0 = 0x10000, } JOYAxisUsage; -@interface JOYAxis : NSObject -- (NSString *)usageString; +@interface JOYAxis : JOYInput + (NSString *)usageToString: (JOYAxisUsage) usage; -- (uint64_t)uniqueID; - (double)value; - (JOYButtonUsage)equivalentButtonUsage; @property JOYAxisUsage usage; diff --git a/JoyKit/JOYAxis.m b/JoyKit/JOYAxis.m index afe90d2..9e15295 100644 --- a/JoyKit/JOYAxis.m +++ b/JoyKit/JOYAxis.m @@ -11,7 +11,7 @@ + (NSString *)usageToString: (JOYAxisUsage) usage { if (usage < JOYAxisUsageNonGenericMax) { - return (NSString *[]) { + return inline_const(NSString *[], { @"None", @"Analog L1", @"Analog L2", @@ -26,7 +26,7 @@ @"Throttle", @"Accelerator", @"Brake", - }[usage]; + })[usage]; } if (usage >= JOYAxisUsageGeneric0) { return [NSString stringWithFormat:@"Generic Analog Control %d", usage - JOYAxisUsageGeneric0]; @@ -42,12 +42,12 @@ - (uint64_t)uniqueID { - return _element.uniqueID; + return _element.uniqueID | (uint64_t)self.combinedIndex << 32; } - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %f%%>", self.className, self, self.usageString, self.uniqueID, _state * 100]; + return [NSString stringWithFormat:@"<%@: %p, %@ (%llx); State: %f%%>", self.className, self, self.usageString, self.uniqueID, _state * 100]; } - (instancetype)initWithElement:(JOYElement *)element diff --git a/JoyKit/JOYButton.h b/JoyKit/JOYButton.h index 08c3ace..c3f13de 100644 --- a/JoyKit/JOYButton.h +++ b/JoyKit/JOYButton.h @@ -1,4 +1,5 @@ #import +#import "JOYInput.h" typedef enum { JOYButtonUsageNone, @@ -46,10 +47,8 @@ typedef enum { JOYButtonTypeHatEmulated, } JOYButtonType; -@interface JOYButton : NSObject -- (NSString *)usageString; +@interface JOYButton : JOYInput + (NSString *)usageToString: (JOYButtonUsage) usage; -- (uint64_t)uniqueID; - (bool) isPressed; @property JOYButtonUsage usage; @property (readonly) JOYButtonType type; diff --git a/JoyKit/JOYButton.m b/JoyKit/JOYButton.m index 568d383..ff26a73 100644 --- a/JoyKit/JOYButton.m +++ b/JoyKit/JOYButton.m @@ -2,6 +2,10 @@ #import "JOYElement.h" #import +@interface JOYButton () +@property JOYButtonUsage originalUsage; +@end + @implementation JOYButton { JOYElement *_element; @@ -11,7 +15,7 @@ + (NSString *)usageToString: (JOYButtonUsage) usage { if (usage < JOYButtonUsageNonGenericMax) { - return (NSString *[]) { + return inline_const(NSString *[], { @"None", @"A", @"B", @@ -35,7 +39,7 @@ @"D-Pad Right", @"D-Pad Up", @"D-Pad Down", - }[usage]; + })[usage]; } if (usage >= JOYButtonUsageGeneric0) { return [NSString stringWithFormat:@"Generic Button %d", usage - JOYButtonUsageGeneric0]; @@ -51,12 +55,12 @@ - (uint64_t)uniqueID { - return _element.uniqueID; + return _element.uniqueID | (uint64_t)self.combinedIndex << 32; } - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %s>", self.className, self, self.usageString, self.uniqueID, _state? "Presssed" : "Released"]; + return [NSString stringWithFormat:@"<%@: %p, %@ (%llx); State: %s>", self.className, self, self.usageString, self.uniqueID, _state? "Presssed" : "Released"]; } - (instancetype)initWithElement:(JOYElement *)element @@ -88,6 +92,8 @@ } } + _originalUsage = _usage; + return self; } diff --git a/JoyKit/JOYController.h b/JoyKit/JOYController.h index a21175c..5bd6338 100644 --- a/JoyKit/JOYController.h +++ b/JoyKit/JOYController.h @@ -23,22 +23,46 @@ static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons"; @end +typedef enum { + JOYControllerCombinedTypeSingle, + JOYControllerCombinedTypeComponent, + JOYControllerCombinedTypeCombined, +} JOYControllerCombinedType; + +typedef enum { + JOYJoyConTypeNone, + JOYJoyConTypeLeft, + JOYJoyConTypeRight, + JOYJoyConTypeDual, +} JOYJoyConType; + @interface JOYController : NSObject + (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options; + (NSArray *) allControllers; + (void) registerListener:(id)listener; + (void) unregisterListener:(id)listener; -- (NSString *)deviceName; -- (NSString *)uniqueID; +- (JOYControllerCombinedType)combinedControllerType; - (NSArray *) buttons; - (NSArray *) axes; - (NSArray *) axes2D; - (NSArray *) axes3D; - (NSArray *) hats; +- (NSArray *) allInputs; - (void)setRumbleAmplitude:(double)amp; - (void)setPlayerLEDs:(uint8_t)mask; - (uint8_t)LEDMaskForPlayer:(unsigned)player; @property (readonly, getter=isConnected) bool connected; +@property (readonly) JOYJoyConType joyconType; +@property (readonly) NSString *deviceName; +@property (readonly) NSString *uniqueID; +@property (nonatomic) bool usesHorizontalJoyConGrip; +@end + +@interface JOYCombinedController : JOYController +- (instancetype)initWithChildren:(NSArray *)children; +- (void)breakApart; +@property (readonly) NSArray *children; @end + diff --git a/JoyKit/JOYController.m b/JoyKit/JOYController.m index caae2cc..21d7565 100644 --- a/JoyKit/JOYController.m +++ b/JoyKit/JOYController.m @@ -5,9 +5,9 @@ #import "JOYFullReportElement.h" #import "JOYButton.h" #import "JOYEmulatedButton.h" -#include +#import -#include +#import extern NSTextField *globalDebugField; #define PWM_RESOLUTION 16 @@ -19,6 +19,7 @@ static NSString const *JOYAxisUsageMapping = @"JOYAxisUsageMapping"; static NSString const *JOYAxes2DUsageMapping = @"JOYAxes2DUsageMapping"; static NSString const *JOYCustomReports = @"JOYCustomReports"; static NSString const *JOYIsSwitch = @"JOYIsSwitch"; +static NSString const *JOYJoyCon = @"JOYJoyCon"; static NSString const *JOYRumbleUsage = @"JOYRumbleUsage"; static NSString const *JOYRumbleUsagePage = @"JOYRumbleUsagePage"; static NSString const *JOYConnectedUsage = @"JOYConnectedUsage"; @@ -54,6 +55,7 @@ static bool hatsEmulateButtons = false; @interface JOYButton () - (instancetype)initWithElement:(JOYElement *)element; - (bool)updateState; +@property JOYButtonUsage originalUsage; @end @interface JOYAxis () @@ -69,6 +71,7 @@ static bool hatsEmulateButtons = false; @interface JOYAxes2D () - (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2; - (bool)updateState; +@property unsigned rotation; // in 90 degrees units, clockwise @end @interface JOYAxes3D () @@ -77,6 +80,11 @@ static bool hatsEmulateButtons = false; } - (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2 thirdElement:(JOYElement *)element2; - (bool)updateState; +@property unsigned rotation; // in 90 degrees units, clockwise +@end + +@interface JOYInput () +@property unsigned combinedIndex; @end static NSDictionary *CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage) @@ -172,6 +180,7 @@ typedef union { @implementation JOYController { + @public // Let JOYCombinedController access everything IOHIDDeviceRef _device; NSMutableDictionary *_buttons; NSMutableDictionary *_axes; @@ -213,6 +222,7 @@ typedef union { unsigned _rumbleCounter; bool _deviceCantSendReports; dispatch_queue_t _rumbleQueue; + JOYCombinedController *_parent; } - (instancetype)initWithDevice:(IOHIDDeviceRef) device hacks:(NSDictionary *)hacks @@ -329,7 +339,7 @@ typedef union { [_buttons setObject:button forKey:element]; NSNumber *replacementUsage = element.usagePage == kHIDPage_Button? _hacks[JOYButtonUsageMapping][@(button.usage)] : nil; if (replacementUsage) { - button.usage = [replacementUsage unsignedIntValue]; + button.originalUsage = button.usage = [replacementUsage unsignedIntValue]; } return; } @@ -357,7 +367,7 @@ typedef union { if (!other) goto single; if (other.usage >= element.usage) goto single; if (other.reportID != element.reportID) goto single; - if (![axisGroups[@(other.usage)] isEqualTo: axisGroups[@(element.usage)]]) goto single; + if (![axisGroups[@(other.usage)] isEqual: axisGroups[@(element.usage)]]) goto single; if (other.parentID != element.parentID) goto single; JOYAxes2D *axes = nil; @@ -444,6 +454,7 @@ typedef union { _device = (IOHIDDeviceRef)CFRetain(device); _serialSuffix = suffix; _playerLEDs = -1; + [self obtainInfo]; IOHIDDeviceRegisterInputValueCallback(device, HIDInput, (void *)self); IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); @@ -464,6 +475,7 @@ typedef union { _isSwitch = [_hacks[JOYIsSwitch] boolValue]; _isDualShock3 = [_hacks[JOYIsDualShock3] boolValue]; _isSony = [_hacks[JOYIsSony] boolValue]; + _joyconType = [_hacks[JOYJoyCon] unsignedIntValue]; NSDictionary *customReports = hacks[JOYCustomReports]; _lastReport = [NSMutableData dataWithLength:MAX( @@ -575,8 +587,8 @@ typedef union { } if (_isSwitch) { - [self sendReport:[NSData dataWithBytes:(uint8_t[]){0x80, 0x04} length:2]]; - [self sendReport:[NSData dataWithBytes:(uint8_t[]){0x80, 0x02} length:2]]; + [self sendReport:[NSData dataWithBytes:inline_const(uint8_t[], {0x80, 0x04}) length:2]]; + [self sendReport:[NSData dataWithBytes:inline_const(uint8_t[], {0x80, 0x02}) length:2]]; _lastVendorSpecificOutput.switchPacket.reportID = 0x1; // Rumble and LEDs _lastVendorSpecificOutput.switchPacket.sequence++; @@ -587,25 +599,34 @@ typedef union { _lastVendorSpecificOutput.switchPacket.sequence++; _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; - _lastVendorSpecificOutput.switchPacket.command = 0x40; // Enable/disableIMU - _lastVendorSpecificOutput.switchPacket.commandData[0] = 1; // Enabled + _lastVendorSpecificOutput.switchPacket.command = 0x48; // Set vibration enabled + _lastVendorSpecificOutput.switchPacket.commandData[0] = 1; // enabled [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; + + // The Joy-Cons don't like having their IMU enabled too quickly + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + _lastVendorSpecificOutput.switchPacket.sequence++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _lastVendorSpecificOutput.switchPacket.command = 0x40; // Enable/disableIMU + _lastVendorSpecificOutput.switchPacket.commandData[0] = 1; // Enabled + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; + }); } if (_isDualShock3) { _lastVendorSpecificOutput.ds3Output = (JOYDualShock3Output){ .reportID = 1, .led = { - {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, - {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, - {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, - {.timeEnabled = 0xff, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xFF, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xFF, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xFF, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, + {.timeEnabled = 0xFF, .dutyLength = 0x27, .enabled = 0x10, .dutyOff = 0, .dutyOn = 0x32}, {.timeEnabled = 0, .dutyLength = 0, .enabled = 0, .dutyOff = 0, .dutyOn = 0}, } }; } if (_isSony) { - _isDualSense = [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)) unsignedIntValue] == 0xce6; + _isDualSense = [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)) unsignedIntValue] == 0xCE6; } if (_isDualSense) { @@ -637,15 +658,10 @@ typedef union { return self; } -- (NSString *)deviceName -{ - if (!_device) return nil; - return IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey)); -} -- (NSString *)uniqueID +- (void)obtainInfo { - if (!_device) return nil; + _deviceName = IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey)); NSString *serial = (__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDSerialNumberKey)); if (!serial || [(__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDTransportKey)) isEqualToString:@"USB"]) { serial = [NSString stringWithFormat:@"%04x%04x%08x", @@ -654,9 +670,15 @@ typedef union { [(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDLocationIDKey)) unsignedIntValue]]; } if (_serialSuffix) { - return [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix]; + _uniqueID = [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix]; + return; } - return serial; + _uniqueID = serial; +} + +- (JOYControllerCombinedType)combinedControllerType +{ + return _parent? JOYControllerCombinedTypeComponent : JOYControllerCombinedTypeSingle; } - (NSString *)description @@ -739,6 +761,7 @@ typedef union { } } else if (old && !self.connected) { + [_parent breakApart]; for (id listener in listeners) { if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { [listener controllerDisconnected:self]; @@ -754,7 +777,7 @@ typedef union { if ([button updateState]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { - [listener controller:self buttonChangedState:button]; + [listener controller:_parent ?: self buttonChangedState:button]; } } } @@ -769,14 +792,14 @@ typedef union { if ([axis updateState]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:movedAxis:)]) { - [listener controller:self movedAxis:axis]; + [listener controller:_parent ?: self movedAxis:axis]; } } - JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID)]; + JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away if ([button updateStateFromAxis:axis]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { - [listener controller:self buttonChangedState:button]; + [listener controller:_parent ?: self buttonChangedState:button]; } } } @@ -791,15 +814,15 @@ typedef union { if ([axes updateState]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:movedAxes2D:)]) { - [listener controller:self movedAxes2D:axes]; + [listener controller:_parent ?: self movedAxes2D:axes]; } } - NSArray *buttons = _axes2DEmulatedButtons[@(axes.uniqueID)]; + NSArray *buttons = _axes2DEmulatedButtons[@(axes.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away for (JOYEmulatedButton *button in buttons) { if ([button updateStateFromAxes2D:axes]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { - [listener controller:self buttonChangedState:button]; + [listener controller:_parent ?: self buttonChangedState:button]; } } } @@ -815,7 +838,7 @@ typedef union { if ([axes updateState]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:movedAxes3D:)]) { - [listener controller:self movedAxes3D:axes]; + [listener controller:_parent ?: self movedAxes3D:axes]; } } } @@ -829,16 +852,16 @@ typedef union { if ([hat updateState]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:movedHat:)]) { - [listener controller:self movedHat:hat]; + [listener controller:_parent ?: self movedHat:hat]; } } - NSArray *buttons = _hatEmulatedButtons[@(hat.uniqueID)]; + NSArray *buttons = _hatEmulatedButtons[@(hat.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away for (JOYEmulatedButton *button in buttons) { if ([button updateStateFromHat:hat]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { - [listener controller:self buttonChangedState:button]; + [listener controller:_parent ?: self buttonChangedState:button]; } } } @@ -851,6 +874,8 @@ typedef union { - (void)disconnected { + _physicallyConnected = false; + [_parent breakApart]; if (_logicallyConnected && [exposedControllers containsObject:self]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { @@ -858,7 +883,6 @@ typedef union { } } } - _physicallyConnected = false; [exposedControllers removeObject:self]; [self setRumbleAmplitude:0]; dispatch_sync(_rumbleQueue, ^{ @@ -888,55 +912,55 @@ typedef union { } _lastVendorSpecificOutput.dualsenseOutput.sequence += 0x10; static const uint32_t table[] = { - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, - 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, - 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, - 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, - 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, - 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, - 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, - 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, - 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, - 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, - 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, - 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, - 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, - 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, - 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, - 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, - 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, - 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, - 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, - 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, - 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, - 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, - 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, - 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, - 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, - 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, - 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, - 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, - 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, - 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, - 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, - 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, - 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, - 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, - 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, - 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, - 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, - 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, - 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, - 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, - 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, - 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, + 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, + 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, + 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, + 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, + 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, + 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, + 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, + 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, + 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, + 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, + 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, + 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, + 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, + 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D }; const uint8_t *byte = (void *)&_lastVendorSpecificOutput.dualsenseOutput; uint32_t size = sizeof(_lastVendorSpecificOutput.dualsenseOutput) - 4; uint32_t ret = 0xFFFFFFFF; - ret = table[(ret ^ 0xa2) & 0xFF] ^ (ret >> 8); + ret = table[(ret ^ 0xA2) & 0xFF] ^ (ret >> 8); while (size--) { ret = table[(ret ^ *byte++) & 0xFF] ^ (ret >> 8); @@ -1049,13 +1073,13 @@ typedef union { } else if (_isDualShock3) { _lastVendorSpecificOutput.ds3Output.reportID = 1; - _lastVendorSpecificOutput.ds3Output.rumbleLeftDuration = _lastVendorSpecificOutput.ds3Output.rumbleRightDuration = _rumbleAmplitude? 0xff : 0; - _lastVendorSpecificOutput.ds3Output.rumbleLeftStrength = _lastVendorSpecificOutput.ds3Output.rumbleRightStrength = round(_rumbleAmplitude * 0xff); + _lastVendorSpecificOutput.ds3Output.rumbleLeftDuration = _lastVendorSpecificOutput.ds3Output.rumbleRightDuration = _rumbleAmplitude? 0xFF : 0; + _lastVendorSpecificOutput.ds3Output.rumbleLeftStrength = _lastVendorSpecificOutput.ds3Output.rumbleRightStrength = round(_rumbleAmplitude * 0xFF); [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]]; } else if (_isDualSense) { - _lastVendorSpecificOutput.dualsenseOutput.rumbleLeftStrength = round(_rumbleAmplitude * _rumbleAmplitude * 0xff); - _lastVendorSpecificOutput.dualsenseOutput.rumbleRightStrength = _rumbleAmplitude > 0.25 ? round(pow(_rumbleAmplitude - 0.25, 2) * 0xff) : 0; + _lastVendorSpecificOutput.dualsenseOutput.rumbleLeftStrength = round(_rumbleAmplitude * _rumbleAmplitude * 0xFF); + _lastVendorSpecificOutput.dualsenseOutput.rumbleRightStrength = _rumbleAmplitude > 0.25 ? round(pow(_rumbleAmplitude - 0.25, 2) * 0xFF) : 0; [self sendDualSenseOutput]; } else { @@ -1076,6 +1100,71 @@ typedef union { return _logicallyConnected && _physicallyConnected; } +- (NSArray *)allInputs +{ + NSMutableArray *ret = [NSMutableArray array]; + [ret addObjectsFromArray:self.buttons]; + [ret addObjectsFromArray:self.axes]; + [ret addObjectsFromArray:self.axes2D]; + [ret addObjectsFromArray:self.axes3D]; + [ret addObjectsFromArray:self.hats]; + return ret; +} + +- (void)setusesHorizontalJoyConGrip:(bool)usesHorizontalJoyConGrip +{ + if (usesHorizontalJoyConGrip == _usesHorizontalJoyConGrip) return; // Nothing to do + _usesHorizontalJoyConGrip = usesHorizontalJoyConGrip; + switch (self.joyconType) { + case JOYJoyConTypeLeft: + case JOYJoyConTypeRight: { + NSArray *buttons = _buttons.allValues; // not self.buttons to skip emulated buttons + if (!usesHorizontalJoyConGrip) { + for (JOYAxes2D *axes in self.axes2D) { + axes.rotation = 0; + } + for (JOYAxes3D *axes in self.axes3D) { + axes.rotation = 0; + } + for (JOYButton *button in buttons) { + button.usage = button.originalUsage; + } + return; + } + for (JOYAxes2D *axes in self.axes2D) { + axes.rotation = self.joyconType == JOYJoyConTypeLeft? -1 : 1; + } + for (JOYAxes3D *axes in self.axes3D) { + axes.rotation = self.joyconType == JOYJoyConTypeLeft? -1 : 1; + } + if (self.joyconType == JOYJoyConTypeLeft) { + for (JOYButton *button in buttons) { + switch (button.originalUsage) { + case JOYButtonUsageDPadLeft: button.usage = JOYButtonUsageB; break; + case JOYButtonUsageDPadRight: button.usage = JOYButtonUsageX; break; + case JOYButtonUsageDPadUp: button.usage = JOYButtonUsageY; break; + case JOYButtonUsageDPadDown: button.usage = JOYButtonUsageA; break; + default: button.usage = button.originalUsage; break; + } + } + } + else { + for (JOYButton *button in buttons) { + switch (button.originalUsage) { + case JOYButtonUsageY: button.usage = JOYButtonUsageX; break; + case JOYButtonUsageA: button.usage = JOYButtonUsageB; break; + case JOYButtonUsageX: button.usage = JOYButtonUsageA; break; + case JOYButtonUsageB: button.usage = JOYButtonUsageY; break; + default: button.usage = button.originalUsage; break; + } + } + } + } + default: + return; + } +} + + (void)controllerAdded:(IOHIDDeviceRef) device { NSString *name = (__bridge NSString *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey)); @@ -1095,8 +1184,6 @@ typedef union { } [controllers setObject:controller forKey:[NSValue valueWithPointer:device]]; - - } + (void)controllerRemoved:(IOHIDDeviceRef) device @@ -1112,7 +1199,7 @@ typedef union { + (void)load { -#include "ControllerConfiguration.inc" +#import "ControllerConfiguration.inc" } +(void)registerListener:(id)listener @@ -1166,3 +1253,237 @@ typedef union { } } @end + + +@implementation JOYCombinedController +- (instancetype)initWithChildren:(NSArray *)children +{ + self = [super init]; + // Sorting makes the device name and unique id consistent + _children = [children sortedArrayUsingComparator:^NSComparisonResult(JOYController *a, JOYController *b) { + return [a.uniqueID compare:b.uniqueID]; + }]; + + if (_children.count == 0) return nil; + + for (JOYController *child in _children) { + if (child.combinedControllerType != JOYControllerCombinedTypeSingle) { + NSLog(@"Cannot combine non-single controller %@", child); + return nil; + } + if (![exposedControllers containsObject:child]) { + NSLog(@"Cannot combine unexposed controller %@", child); + return nil; + } + } + + unsigned index = 0; + for (JOYController *child in _children) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { + [listener controllerDisconnected:child]; + } + } + child->_parent = self; + for (JOYInput *input in child.allInputs) { + input.combinedIndex = index; + } + index++; + [exposedControllers removeObject:child]; + } + + [exposedControllers addObject:self]; + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerConnected:)]) { + [listener controllerConnected:self]; + } + } + + return self; +} + +- (void)breakApart +{ + if (![exposedControllers containsObject:self]) { + // Already broken apart + return; + } + + [exposedControllers removeObject:self]; + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { + [listener controllerDisconnected:self]; + } + } + + for (JOYController *child in _children) { + child->_parent = nil; + for (JOYInput *input in child.allInputs) { + input.combinedIndex = 0; + } + if (!child.connected) break; + [exposedControllers addObject:child]; + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controllerConnected:)]) { + [listener controllerConnected:child]; + } + } + } +} + +- (NSString *)deviceName +{ + NSString *ret = nil; + for (JOYController *child in _children) { + if (ret) { + ret = [ret stringByAppendingFormat:@" + %@", child.deviceName]; + } + else { + ret = child.deviceName; + } + } + return ret; +} + +- (NSString *)uniqueID +{ + NSString *ret = nil; + for (JOYController *child in _children) { + if (ret) { + ret = [ret stringByAppendingFormat:@"+%@", child.uniqueID]; + } + else { + ret = child.uniqueID; + } + } + return ret; +} + +- (JOYControllerCombinedType)combinedControllerType +{ + return JOYControllerCombinedTypeCombined; +} + +- (NSArray *)buttons +{ + NSArray *ret = nil; + for (JOYController *child in _children) { + if (ret) { + ret = [ret arrayByAddingObjectsFromArray:child.buttons]; + } + else { + ret = child.buttons; + } + } + return ret; +} + +- (NSArray *)axes +{ + NSArray *ret = nil; + for (JOYController *child in _children) { + if (ret) { + ret = [ret arrayByAddingObjectsFromArray:child.axes]; + } + else { + ret = child.axes; + } + } + return ret; +} + +- (NSArray *)axes2D +{ + NSArray *ret = nil; + for (JOYController *child in _children) { + if (ret) { + ret = [ret arrayByAddingObjectsFromArray:child.axes2D]; + } + else { + ret = child.axes2D; + } + } + return ret; +} + +- (NSArray *)axes3D +{ + NSArray *ret = nil; + for (JOYController *child in _children) { + if (ret) { + ret = [ret arrayByAddingObjectsFromArray:child.axes3D]; + } + else { + ret = child.axes3D; + } + } + return ret; +} + +- (NSArray *)hats +{ + NSArray *ret = nil; + for (JOYController *child in _children) { + if (ret) { + ret = [ret arrayByAddingObjectsFromArray:child.hats]; + } + else { + ret = child.hats; + } + } + return ret; +} + +- (void)setRumbleAmplitude:(double)amp +{ + for (JOYController *child in _children) { + [child setRumbleAmplitude:amp]; + } +} + +- (void)setPlayerLEDs:(uint8_t)mask +{ + // Mask is actually just the player ID in a combined controller to + // allow combining controllers with different LED layouts + for (JOYController *child in _children) { + [child setPlayerLEDs:[child LEDMaskForPlayer:mask]]; + } +} + +- (uint8_t)LEDMaskForPlayer:(unsigned int)player +{ + return player; +} + +- (bool)isConnected +{ + if (![exposedControllers containsObject:self]) { + // Controller was broken apart + return false; + } + + for (JOYController *child in _children) { + if (!child.isConnected) { + return false; // Should never happen + } + } + + return true; +} + +- (JOYJoyConType)joyconType +{ + if (_children.count != 2) return JOYJoyConTypeNone; + if (_children[0].joyconType == JOYJoyConTypeLeft && + _children[1].joyconType == JOYJoyConTypeRight) { + return JOYJoyConTypeDual; + } + + if (_children[1].joyconType == JOYJoyConTypeLeft && + _children[0].joyconType == JOYJoyConTypeRight) { + return JOYJoyConTypeDual; + } + return JOYJoyConTypeNone; +} + +@end diff --git a/JoyKit/JOYElement.h b/JoyKit/JOYElement.h index 0e917dd..9f2fe46 100644 --- a/JoyKit/JOYElement.h +++ b/JoyKit/JOYElement.h @@ -1,5 +1,7 @@ #import -#include +#import + +#define inline_const(type, ...) (*({static const typeof(type) _= __VA_ARGS__; &_;})) @interface JOYElement : NSObject - (instancetype)initWithElement:(IOHIDElementRef)element; diff --git a/JoyKit/JOYElement.m b/JoyKit/JOYElement.m index 2432002..175cf16 100644 --- a/JoyKit/JOYElement.m +++ b/JoyKit/JOYElement.m @@ -1,6 +1,6 @@ #import "JOYElement.h" -#include -#include +#import +#import @implementation JOYElement { @@ -126,7 +126,7 @@ return self->_element == object; } -- (id)copyWithZone:(nullable NSZone *)zone; +- (id)copyWithZone:(NSZone *)zone; { return self; } diff --git a/JoyKit/JOYEmulatedButton.m b/JoyKit/JOYEmulatedButton.m index 5e6d1b3..841617e 100644 --- a/JoyKit/JOYEmulatedButton.m +++ b/JoyKit/JOYEmulatedButton.m @@ -25,7 +25,7 @@ - (uint64_t)uniqueID { - return _uniqueID; + return _uniqueID | (uint64_t)self.combinedIndex << 32; } - (bool)updateStateFromAxis:(JOYAxis *)axis diff --git a/JoyKit/JOYFullReportElement.h b/JoyKit/JOYFullReportElement.h index 808644e..75fce1f 100644 --- a/JoyKit/JOYFullReportElement.h +++ b/JoyKit/JOYFullReportElement.h @@ -1,6 +1,6 @@ #import -#include -#include "JOYElement.h" +#import +#import "JOYElement.h" @interface JOYFullReportElement : JOYElement - (instancetype)initWithDevice:(IOHIDDeviceRef) device reportID:(unsigned)reportID; diff --git a/JoyKit/JOYFullReportElement.m b/JoyKit/JOYFullReportElement.m index a19a530..f43b31c 100644 --- a/JoyKit/JOYFullReportElement.m +++ b/JoyKit/JOYFullReportElement.m @@ -1,5 +1,5 @@ #import "JOYFullReportElement.h" -#include +#import @implementation JOYFullReportElement { @@ -67,7 +67,7 @@ return self.uniqueID == object.uniqueID; } -- (id)copyWithZone:(nullable NSZone *)zone; +- (id)copyWithZone:(NSZone *)zone; { return self; } diff --git a/JoyKit/JOYHat.h b/JoyKit/JOYHat.h index 05a5829..f430beb 100644 --- a/JoyKit/JOYHat.h +++ b/JoyKit/JOYHat.h @@ -1,7 +1,7 @@ #import +#import "JOYInput.h" -@interface JOYHat : NSObject -- (uint64_t)uniqueID; +@interface JOYHat : JOYInput - (double)angle; - (unsigned)resolution; @property (readonly, getter=isPressed) bool pressed; diff --git a/JoyKit/JOYHat.m b/JoyKit/JOYHat.m index b5a18f0..9cebe65 100644 --- a/JoyKit/JOYHat.m +++ b/JoyKit/JOYHat.m @@ -10,15 +10,15 @@ - (uint64_t)uniqueID { - return _element.uniqueID; + return _element.uniqueID | (uint64_t)self.combinedIndex << 32; } - (NSString *)description { if (self.isPressed) { - return [NSString stringWithFormat:@"<%@: %p (%llu); State: %f degrees>", self.className, self, self.uniqueID, self.angle]; + return [NSString stringWithFormat:@"<%@: %p (%llx); State: %f degrees>", self.className, self, self.uniqueID, self.angle]; } - return [NSString stringWithFormat:@"<%@: %p (%llu); State: released>", self.className, self, self.uniqueID]; + return [NSString stringWithFormat:@"<%@: %p (%llx); State: released>", self.className, self, self.uniqueID]; } @@ -51,7 +51,7 @@ - (bool)updateState { - unsigned state = ([_element value] - _element.min) * 360.0 / self.resolution; + signed state = ([_element value] - _element.min) * 360.0 / self.resolution; if (_state != state) { _state = state; return true; @@ -59,4 +59,9 @@ return false; } +- (NSString *)usageString +{ + return @"Hat switch"; +} + @end diff --git a/JoyKit/JOYInput.h b/JoyKit/JOYInput.h new file mode 100644 index 0000000..ea45b59 --- /dev/null +++ b/JoyKit/JOYInput.h @@ -0,0 +1,8 @@ +#import + +@interface JOYInput : NSObject +@property (readonly) unsigned combinedIndex; +- (NSString *)usageString; +- (uint64_t)uniqueID; +@end + diff --git a/JoyKit/JOYInput.m b/JoyKit/JOYInput.m new file mode 100644 index 0000000..0de83bf --- /dev/null +++ b/JoyKit/JOYInput.m @@ -0,0 +1,21 @@ +#import "JOYInput.h" + +@interface JOYInput () +@property unsigned combinedIndex; +@end + +@implementation JOYInput + +- (uint64_t)uniqueID +{ + [self doesNotRecognizeSelector:_cmd]; + __builtin_unreachable(); +} + +- (NSString *)usageString +{ + [self doesNotRecognizeSelector:_cmd]; + __builtin_unreachable(); +} + +@end diff --git a/JoyKit/JOYMultiplayerController.h b/JoyKit/JOYMultiplayerController.h index 44d7421..34c4d4c 100644 --- a/JoyKit/JOYMultiplayerController.h +++ b/JoyKit/JOYMultiplayerController.h @@ -1,5 +1,5 @@ #import "JOYController.h" -#include +#import @interface JOYMultiplayerController : JOYController - (instancetype)initWithDevice:(IOHIDDeviceRef) device reportIDFilters:(NSArray *>*) reportIDFilters hacks:hacks; diff --git a/JoyKit/JoyKit.h b/JoyKit/JoyKit.h index d56b505..f96659c 100644 --- a/JoyKit/JoyKit.h +++ b/JoyKit/JoyKit.h @@ -1,6 +1,3 @@ -#ifndef JoyKit_h -#define JoyKit_h +#pragma once -#include "JOYController.h" - -#endif +#import "JOYController.h" diff --git a/LICENSE b/LICENSE index 3303e0d..a487d13 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,9 @@ -MIT License + All files and directories in this repository, except for the iOS and HexFiend + directories, are licensed under the Expat License: + +Expat License -Copyright (c) 2015-2021 Lior Halphon +Copyright (c) 2015-2024 Lior Halphon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +21,11 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. + + The files contained under the iOS directory in this repository are subject to + this addition condition: + + A written permission from Lior Halphon is required to distribute copies or + substantial portions of the Software in a digital marketplace, such as + Apple's App Store. \ No newline at end of file diff --git a/Makefile b/Makefile index 73aea5f..582fb97 100644 --- a/Makefile +++ b/Makefile @@ -16,21 +16,29 @@ endif ifeq ($(PLATFORM),windows32) _ := $(shell chcp 65001) EXESUFFIX:=.exe -NATIVE_CC = clang -IWindows -Wno-deprecated-declarations --target=i386-pc-windows +NATIVE_CC = clang -IWindows -Wno-deprecated-declarations --target=x86_64-pc-windows +SDL_AUDIO_DRIVERS ?= xaudio2 sdl else EXESUFFIX:= NATIVE_CC := cc +SDL_AUDIO_DRIVERS ?= sdl endif PB12_COMPRESS := build/pb12$(EXESUFFIX) ifeq ($(PLATFORM),Darwin) DEFAULT := cocoa +ENABLE_OPENAL ?= 1 else DEFAULT := sdl endif -ifneq ($(shell which xdg-open)$(FREEDESKTOP),) +NULL := /dev/null +ifeq ($(PLATFORM),windows32) +NULL := NUL +endif + +ifneq ($(shell which xdg-open 2> $(NULL))$(FREEDESKTOP),) # Running on an FreeDesktop environment, configure for (optional) installation DESTDIR ?= PREFIX ?= /usr/local @@ -44,13 +52,56 @@ ifeq ($(MAKECMDGOALS),) MAKECMDGOALS := $(DEFAULT) endif +ifneq ($(DISABLE_TIMEKEEPING),) +CFLAGS += -DGB_DISABLE_TIMEKEEPING +CPPP_FLAGS += -DGB_DISABLE_TIMEKEEPING +else +CPPP_FLAGS += -UGB_DISABLE_TIMEKEEPING +endif + +ifneq ($(DISABLE_REWIND),) +CFLAGS += -DGB_DISABLE_REWIND +CPPP_FLAGS += -DGB_DISABLE_REWIND +CORE_FILTER += Core/rewind.c +else +CPPP_FLAGS += -UGB_DISABLE_REWIND +endif + +ifneq ($(DISABLE_DEBUGGER),) +CFLAGS += -DGB_DISABLE_DEBUGGER +CPPP_FLAGS += -DGB_DISABLE_DEBUGGER +CORE_FILTER += Core/debugger.c Core/sm83_disassembler.c Core/symbol_hash.c +else +CPPP_FLAGS += -UGB_DISABLE_DEBUGGER +endif + +ifneq ($(DISABLE_CHEATS),) +CFLAGS += -DGB_DISABLE_CHEATS +CPPP_FLAGS += -DGB_DISABLE_CHEATS +CORE_FILTER += Core/cheats.c +else +CPPP_FLAGS += -UGB_DISABLE_CHEATS +endif + +ifneq ($(CORE_FILTER)$(DISABLE_TIMEKEEPING),) +ifneq ($(MAKECMDGOALS),lib) +$(error SameBoy features can only be disabled when compiling the 'lib' target) +endif +endif + +CPPP_FLAGS += -UGB_INTERNAL + + include version.mk +COPYRIGHT_YEAR := $(shell grep -oE "20[2-9][0-9]" LICENSE) export VERSION CONF ?= debug -SDL_AUDIO_DRIVER ?= sdl BIN := build/bin OBJ := build/obj +INC := build/include/sameboy +LIBDIR := build/lib + BOOTROMS_DIR ?= $(BIN)/BootROMs ifdef DATA_DIR @@ -61,15 +112,18 @@ endif # Use clang if it's available. ifeq ($(origin CC),default) -ifneq (, $(shell which clang)) +ifneq (, $(shell which clang 2> $(NULL))) CC := clang endif endif # Find libraries with pkg-config if available. -ifneq (, $(shell which pkg-config)) +ifneq (, $(shell which pkg-config 2> $(NULL))) +# But not on macOS, it's annoying +ifneq ($(PLATFORM),Darwin) PKG_CONFIG := pkg-config endif +endif ifeq ($(PLATFORM),windows32) # To force use of the Unix version instead of the Windows version @@ -89,16 +143,26 @@ override CONF := release FAT_FLAGS += -arch x86_64 -arch arm64 endif +IOS_MIN := 11.0 + +IOS_PNGS := $(shell ls iOS/*.png) +# Support out-of-PATH RGBDS +RGBASM := $(RGBDS)rgbasm +RGBLINK := $(RGBDS)rgblink +RGBGFX := $(RGBDS)rgbgfx + +# RGBASM 0.7+ deprecate and remove `-h` +RGBASM_FLAGS := $(if $(filter $(shell echo 'println __RGBDS_MAJOR__ || (!__RGBDS_MAJOR__ && __RGBDS_MINOR__ > 6)' | $(RGBASM) -), $$0), -h,) --include $(OBJ)/BootROMs/ --include BootROMs/ +# RGBGFX 0.6+ replace `-h` with `-Z`, and need `-c embedded` +RGBGFX_FLAGS := $(if $(filter $(shell echo 'println __RGBDS_MAJOR__ || (!__RGBDS_MAJOR__ && __RGBDS_MINOR__ > 5)' | $(RGBASM) -), $$0), -h -u, -Z -u -c embedded) # Set compilation and linkage flags based on target, platform and configuration OPEN_DIALOG = OpenDialog/gtk.c -NULL := /dev/null ifeq ($(PLATFORM),windows32) OPEN_DIALOG = OpenDialog/windows.c -NULL := NUL endif ifeq ($(PLATFORM),Darwin) @@ -107,7 +171,7 @@ endif # These must come before the -Wno- flags WARNINGS += -Werror -Wall -Wno-unknown-warning -Wno-unknown-warning-option -Wno-missing-braces -WARNINGS += -Wno-nonnull -Wno-unused-result -Wno-strict-aliasing -Wno-multichar -Wno-int-in-bool-context -Wno-format-truncation +WARNINGS += -Wno-nonnull -Wno-unused-result -Wno-multichar -Wno-int-in-bool-context -Wno-format-truncation # Only add this flag if the compiler supports it ifeq ($(shell $(CC) -x c -c $(NULL) -o $(NULL) -Werror -Wpartial-availability 2> $(NULL); echo $$?),0) @@ -121,7 +185,7 @@ endif CFLAGS += $(WARNINGS) -CFLAGS += -std=gnu11 -D_GNU_SOURCE -DGB_VERSION='"$(VERSION)"' -I. -D_USE_MATH_DEFINES +CFLAGS += -std=gnu11 -D_GNU_SOURCE -DGB_VERSION='"$(VERSION)"' -DGB_COPYRIGHT_YEAR='"$(COPYRIGHT_YEAR)"' -I. -D_USE_MATH_DEFINES ifneq (,$(UPDATE_SUPPORT)) CFLAGS += -DUPDATE_SUPPORT endif @@ -129,37 +193,80 @@ endif ifeq (,$(PKG_CONFIG)) SDL_CFLAGS := $(shell sdl2-config --cflags) SDL_LDFLAGS := $(shell sdl2-config --libs) -lpthread + +# We cannot detect the presence of OpenAL dev headers, +# so we must do this manually +ifeq ($(ENABLE_OPENAL),1) +SDL_CFLAGS += -DENABLE_OPENAL +ifeq ($(PLATFORM),Darwin) +SDL_LDFLAGS += -framework OpenAL +else +SDL_LDFLAGS += -lopenal +endif +SDL_AUDIO_DRIVERS += openal +endif else SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2) SDL_LDFLAGS := $(shell $(PKG_CONFIG) --libs sdl2) -lpthread + +# Allow OpenAL to be disabled even if the development libraries are available +ifneq ($(ENABLE_OPENAL),0) +ifeq ($(shell $(PKG_CONFIG) --exists openal && echo 0),0) +SDL_CFLAGS += $(shell $(PKG_CONFIG) --cflags openal) -DENABLE_OPENAL +SDL_LDFLAGS += $(shell $(PKG_CONFIG) --libs openal) +SDL_AUDIO_DRIVERS += openal endif +endif +endif + ifeq (,$(PKG_CONFIG)) GL_LDFLAGS := -lGL else GL_CFLAGS := $(shell $(PKG_CONFIG) --cflags gl) GL_LDFLAGS := $(shell $(PKG_CONFIG) --libs gl || echo -lGL) endif + ifeq ($(PLATFORM),windows32) -CFLAGS += -IWindows -Drandom=rand --target=i386-pc-windows -LDFLAGS += -lmsvcrt -lcomdlg32 -luser32 -lshell32 -lole32 -lSDL2main -Wl,/MANIFESTFILE:NUL --target=i386-pc-windows +CFLAGS += -IWindows -Drandom=rand --target=x86_64-pc-windows +LDFLAGS += -lmsvcrt -lcomdlg32 -luser32 -lshell32 -lole32 -lSDL2main -Wl,/MANIFESTFILE:NUL --target=x86_64-pc-windows SDL_LDFLAGS := -lSDL2 GL_LDFLAGS := -lopengl32 +ifneq ($(REDIST_XAUDIO),) +CFLAGS += -DREDIST_XAUDIO +LDFLAGS += -lxaudio2_9redist +sdl: $(BIN)/SDL/xaudio2_9redist.dll +endif else LDFLAGS += -lc -lm -ldl endif +ifeq ($(MAKECMDGOALS),_ios) +OBJ := build/obj-ios +SYSROOT := $(shell xcodebuild -sdk iphoneos -version Path 2> $(NULL)) +ifeq ($(SYSROOT),) +$(error Could not find an iOS SDK) +endif +CFLAGS += -arch arm64 -miphoneos-version-min=$(IOS_MIN) -isysroot $(SYSROOT) -IAppleCommon -DGB_DISABLE_DEBUGGER +CORE_FILTER += Core/debugger.c Core/sm83_disassembler.c Core/symbol_hash.c +LDFLAGS += -arch arm64 +OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT) +LDFLAGS += -miphoneos-version-min=$(IOS_MIN) -isysroot $(SYSROOT) +IOS_INSTALLER_LDFLAGS := $(LDFLAGS) -lobjc -framework CoreServices -framework Foundation +LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -framework GameController -weak_framework CoreHaptics -framework MobileCoreServices +CODESIGN := codesign -fs - +else ifeq ($(PLATFORM),Darwin) SYSROOT := $(shell xcodebuild -sdk macosx -version Path 2> $(NULL)) ifeq ($(SYSROOT),) -SYSROOT := /Library/Developer/CommandLineTools/SDKs/$(shell ls /Library/Developer/CommandLineTools/SDKs/ | grep 10 | tail -n 1) +SYSROOT := /Library/Developer/CommandLineTools/SDKs/$(shell ls /Library/Developer/CommandLineTools/SDKs/ | grep "[0-9]\." | tail -n 1) endif ifeq ($(SYSROOT),/Library/Developer/CommandLineTools/SDKs/) $(error Could not find a macOS SDK) endif -CFLAGS += -F/Library/Frameworks -mmacosx-version-min=10.9 -isysroot $(SYSROOT) +CFLAGS += -F/Library/Frameworks -mmacosx-version-min=10.9 -isysroot $(SYSROOT) -IAppleCommon OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT) -LDFLAGS += -framework AppKit -framework PreferencePanes -framework Carbon -framework QuartzCore -weak_framework Metal -weak_framework MetalKit -mmacosx-version-min=10.9 -isysroot $(SYSROOT) +LDFLAGS += -framework AppKit -mmacosx-version-min=10.9 -isysroot $(SYSROOT) GL_LDFLAGS := -framework OpenGL endif CFLAGS += -Wno-deprecated-declarations @@ -167,15 +274,36 @@ ifeq ($(PLATFORM),windows32) CFLAGS += -Wno-deprecated-declarations # Seems like Microsoft deprecated every single LIBC function LDFLAGS += -Wl,/NODEFAULTLIB:libcmt.lib endif +endif + +LIBFLAGS := -nostdlib -Wl,-r +ifneq ($(PLATFORM),Darwin) +LIBFLAGS += -no-pie +endif ifeq ($(CONF),debug) CFLAGS += -g else ifeq ($(CONF), release) -CFLAGS += -O3 -DNDEBUG +CFLAGS += -O3 -ffast-math -DNDEBUG +# The frontend code is not time-critical, prefer reducing the size for less memory use and better cache utilization +ifeq ($(shell $(CC) -x c -c $(NULL) -o $(NULL) -Werror -Oz 2> $(NULL); echo $$?),0) +FRONTEND_CFLAGS += -Oz +else +FRONTEND_CFLAGS += -Os +endif + +# Don't use function outlining. I breaks Obj-C ARC optimizations and Apple never bothered to fix it. It also hardly has any effect on file size. +ifeq ($(shell $(CC) -x c -c $(NULL) -o $(NULL) -Werror -mno-outline 2> $(NULL); echo $$?),0) +FRONTEND_CFLAGS += -mno-outline +LDFLAGS += -mno-outline +endif + STRIP := strip +CODESIGN := true ifeq ($(PLATFORM),Darwin) LDFLAGS += -Wl,-exported_symbols_list,$(NULL) STRIP := strip -x +CODESIGN := codesign -fs - endif ifeq ($(PLATFORM),windows32) LDFLAGS += -fuse-ld=lld @@ -188,6 +316,8 @@ else $(error Invalid value for CONF: $(CONF). Use "debug", "release" or "native_release") endif + + # Define our targets ifeq ($(PLATFORM),windows32) @@ -200,35 +330,49 @@ endif cocoa: $(BIN)/SameBoy.app quicklook: $(BIN)/SameBoy.qlgenerator -sdl: $(SDL_TARGET) $(BIN)/SDL/dmg_boot.bin $(BIN)/SDL/mgb_boot.bin $(BIN)/SDL/cgb0_boot.bin $(BIN)/SDL/cgb_boot.bin $(BIN)/SDL/agb_boot.bin $(BIN)/SDL/sgb_boot.bin $(BIN)/SDL/sgb2_boot.bin $(BIN)/SDL/LICENSE $(BIN)/SDL/registers.sym $(BIN)/SDL/background.bmp $(BIN)/SDL/Shaders -bootroms: $(BIN)/BootROMs/agb_boot.bin $(BIN)/BootROMs/cgb_boot.bin $(BIN)/BootROMs/cgb0_boot.bin $(BIN)/BootROMs/dmg_boot.bin $(BIN)/BootROMs/sgb_boot.bin $(BIN)/BootROMs/sgb2_boot.bin +sdl: $(SDL_TARGET) $(BIN)/SDL/dmg_boot.bin $(BIN)/SDL/mgb_boot.bin $(BIN)/SDL/cgb0_boot.bin $(BIN)/SDL/cgb_boot.bin $(BIN)/SDL/agb_boot.bin $(BIN)/SDL/sgb_boot.bin $(BIN)/SDL/sgb2_boot.bin $(BIN)/SDL/LICENSE $(BIN)/SDL/registers.sym $(BIN)/SDL/background.bmp $(BIN)/SDL/Shaders $(BIN)/SDL/Palettes +bootroms: $(BIN)/BootROMs/agb_boot.bin $(BIN)/BootROMs/cgb_boot.bin $(BIN)/BootROMs/cgb0_boot.bin $(BIN)/BootROMs/dmg_boot.bin $(BIN)/BootROMs/mgb_boot.bin $(BIN)/BootROMs/sgb_boot.bin $(BIN)/BootROMs/sgb2_boot.bin tester: $(TESTER_TARGET) $(BIN)/tester/dmg_boot.bin $(BIN)/tester/cgb_boot.bin $(BIN)/tester/agb_boot.bin $(BIN)/tester/sgb_boot.bin $(BIN)/tester/sgb2_boot.bin -all: cocoa sdl tester libretro +_ios: $(BIN)/SameBoy-iOS.app $(OBJ)/installer +ios-ipa: $(BIN)/SameBoy-iOS.ipa +ios-deb: $(BIN)/SameBoy-iOS.deb +ifeq ($(PLATFORM),windows32) +lib: lib-unsupported +else +lib: $(LIBDIR)/libsameboy.o $(LIBDIR)/libsameboy.a +endif +all: sdl tester libretro lib +ifeq ($(PLATFORM),Darwin) +all: cocoa ios-ipa ios-deb +endif # Get a list of our source files and their respective object file targets -CORE_SOURCES := $(shell ls Core/*.c) -SDL_SOURCES := $(shell ls SDL/*.c) $(OPEN_DIALOG) SDL/audio/$(SDL_AUDIO_DRIVER).c +CORE_SOURCES := $(filter-out $(CORE_FILTER),$(shell ls Core/*.c)) +CORE_HEADERS := $(shell ls Core/*.h) +SDL_SOURCES := $(shell ls SDL/*.c) $(OPEN_DIALOG) $(patsubst %,SDL/audio/%.c,$(SDL_AUDIO_DRIVERS)) TESTER_SOURCES := $(shell ls Tester/*.c) - -ifeq ($(PLATFORM),Darwin) -COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m) $(shell ls JoyKit/*.m) +IOS_SOURCES := $(filter-out iOS/installer.m, $(shell ls iOS/*.m)) $(shell ls AppleCommon/*.m) +COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m) $(shell ls JoyKit/*.m) $(shell ls AppleCommon/*.m) QUICKLOOK_SOURCES := $(shell ls QuickLook/*.m) $(shell ls QuickLook/*.c) -endif ifeq ($(PLATFORM),windows32) CORE_SOURCES += $(shell ls Windows/*.c) endif CORE_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(CORE_SOURCES)) +PUBLIC_HEADERS := $(patsubst Core/%,$(INC)/%,$(CORE_HEADERS)) COCOA_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(COCOA_SOURCES)) +IOS_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(IOS_SOURCES)) QUICKLOOK_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(QUICKLOOK_SOURCES)) SDL_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(SDL_SOURCES)) TESTER_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(TESTER_SOURCES)) +lib: $(PUBLIC_HEADERS) + # Automatic dependency generation -ifneq ($(filter-out clean bootroms libretro %.bin, $(MAKECMDGOALS)),) +ifneq ($(filter-out ios ios-ipa ios-deb clean bootroms libretro %.bin, $(MAKECMDGOALS)),) -include $(CORE_OBJECTS:.o=.dep) ifneq ($(filter $(MAKECMDGOALS),sdl),) -include $(SDL_OBJECTS:.o=.dep) @@ -239,11 +383,18 @@ endif ifneq ($(filter $(MAKECMDGOALS),cocoa),) -include $(COCOA_OBJECTS:.o=.dep) endif +ifneq ($(filter $(MAKECMDGOALS),_ios),) +-include $(IOS_OBJECTS:.o=.dep) +endif endif $(OBJ)/SDL/%.dep: SDL/% -@$(MKDIR) -p $(dir $@) $(CC) $(CFLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -MT $(OBJ)/$^.o -M $^ -c -o $@ + +$(OBJ)/OpenDialog/%.dep: OpenDialog/% + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -MT $(OBJ)/$^.o -M $^ -c -o $@ $(OBJ)/%.dep: % -@$(MKDIR) -p $(dir $@) @@ -257,20 +408,58 @@ $(OBJ)/Core/%.c.o: Core/%.c $(OBJ)/SDL/%.c.o: SDL/%.c -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(FAT_FLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -c $< -o $@ + $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -c $< -o $@ + +$(OBJ)/OpenDialog/%.c.o: OpenDialog/%.c + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -c $< -o $@ + $(OBJ)/%.c.o: %.c -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(FAT_FLAGS) -c $< -o $@ + $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) -c $< -o $@ # HexFiend requires more flags $(OBJ)/HexFiend/%.m.o: HexFiend/%.m -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(FAT_FLAGS) $(OCFLAGS) -c $< -o $@ -fno-objc-arc -include HexFiend/HexFiend_2_Framework_Prefix.pch + $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(OCFLAGS) -c $< -o $@ -fno-objc-arc -include HexFiend/HexFiend_2_Framework_Prefix.pch $(OBJ)/%.m.o: %.m -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) $(FAT_FLAGS) $(OCFLAGS) -c $< -o $@ + $(CC) $(CFLAGS) $(FRONTEND_CFLAGS) $(FAT_FLAGS) $(OCFLAGS) -c $< -o $@ + +# iOS Port + +$(BIN)/SameBoy-iOS.app: $(BIN)/SameBoy-iOS.app/SameBoy \ + $(IOS_PNGS) \ + iOS/License.html \ + iOS/Info.plist \ + $(BIN)/SameBoy-iOS.app/dmg_boot.bin \ + $(BIN)/SameBoy-iOS.app/mgb_boot.bin \ + $(BIN)/SameBoy-iOS.app/cgb0_boot.bin \ + $(BIN)/SameBoy-iOS.app/cgb_boot.bin \ + $(BIN)/SameBoy-iOS.app/agb_boot.bin \ + $(BIN)/SameBoy-iOS.app/sgb_boot.bin \ + $(BIN)/SameBoy-iOS.app/sgb2_boot.bin \ + $(BIN)/SameBoy-iOS.app/LaunchScreen.storyboardc \ + Shaders + $(MKDIR) -p $(BIN)/SameBoy-iOS.app + cp $(IOS_PNGS) $(BIN)/SameBoy-iOS.app + sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < iOS/Info.plist > $(BIN)/SameBoy-iOS.app/Info.plist + sed "s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < iOS/License.html > $(BIN)/SameBoy-iOS.app/License.html + $(MKDIR) -p $(BIN)/SameBoy-iOS.app/Shaders + cp Shaders/*.fsh Shaders/*.metal $(BIN)/SameBoy-iOS.app/Shaders + $(CODESIGN) $@ + +$(BIN)/SameBoy-iOS.app/SameBoy: $(CORE_OBJECTS) $(IOS_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) +ifeq ($(CONF), release) + $(STRIP) $@ +endif + +$(OBJ)/installer: iOS/installer.m + $(CC) $< -o $@ $(IOS_INSTALLER_LDFLAGS) $(CFLAGS) # Cocoa Port @@ -286,28 +475,34 @@ $(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \ $(BIN)/SameBoy.app/Contents/Resources/agb_boot.bin \ $(BIN)/SameBoy.app/Contents/Resources/sgb_boot.bin \ $(BIN)/SameBoy.app/Contents/Resources/sgb2_boot.bin \ - $(patsubst %.xib,%.nib,$(addprefix $(BIN)/SameBoy.app/Contents/Resources/Base.lproj/,$(shell cd Cocoa;ls *.xib))) \ + $(patsubst %.xib,%.nib,$(addprefix $(BIN)/SameBoy.app/Contents/Resources/,$(shell cd Cocoa;ls *.xib))) \ $(BIN)/SameBoy.qlgenerator \ Shaders $(MKDIR) -p $(BIN)/SameBoy.app/Contents/Resources cp Cocoa/*.icns Cocoa/*.png Misc/registers.sym $(BIN)/SameBoy.app/Contents/Resources/ - sed s/@VERSION/$(VERSION)/ < Cocoa/Info.plist > $(BIN)/SameBoy.app/Contents/Info.plist - cp Cocoa/License.html $(BIN)/SameBoy.app/Contents/Resources/Credits.html + sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < Cocoa/Info.plist > $(BIN)/SameBoy.app/Contents/Info.plist + sed "s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < Cocoa/License.html > $(BIN)/SameBoy.app/Contents/Resources/Credits.html $(MKDIR) -p $(BIN)/SameBoy.app/Contents/Resources/Shaders cp Shaders/*.fsh Shaders/*.metal $(BIN)/SameBoy.app/Contents/Resources/Shaders $(MKDIR) -p $(BIN)/SameBoy.app/Contents/Library/QuickLook/ cp -rf $(BIN)/SameBoy.qlgenerator $(BIN)/SameBoy.app/Contents/Library/QuickLook/ +ifeq ($(CONF), release) + $(CODESIGN) $@ +endif $(BIN)/SameBoy.app/Contents/MacOS/SameBoy: $(CORE_OBJECTS) $(COCOA_OBJECTS) -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework IOKit + $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework IOKit -framework PreferencePanes -framework Carbon -framework QuartzCore -framework Security -framework WebKit -weak_framework Metal -weak_framework MetalKit ifeq ($(CONF), release) $(STRIP) $@ endif -$(BIN)/SameBoy.app/Contents/Resources/Base.lproj/%.nib: Cocoa/%.xib - ibtool --compile $@ $^ 2>&1 | cat - +$(BIN)/SameBoy.app/Contents/Resources/%.nib: Cocoa/%.xib + ibtool --target-device mac --minimum-deployment-target 10.9 --compile $@ $^ 2>&1 | cat - +$(BIN)/SameBoy-iOS.app/%.storyboardc: iOS/%.storyboard + ibtool --target-device iphone --target-device ipad --minimum-deployment-target $(IOS_MIN) --compile $@ $^ 2>&1 | cat - + # Quick Look generator $(BIN)/SameBoy.qlgenerator: $(BIN)/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL \ @@ -316,13 +511,19 @@ $(BIN)/SameBoy.qlgenerator: $(BIN)/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL $(BIN)/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin $(MKDIR) -p $(BIN)/SameBoy.qlgenerator/Contents/Resources cp QuickLook/*.png $(BIN)/SameBoy.qlgenerator/Contents/Resources/ - sed s/@VERSION/$(VERSION)/ < QuickLook/Info.plist > $(BIN)/SameBoy.qlgenerator/Contents/Info.plist + sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < QuickLook/Info.plist > $(BIN)/SameBoy.qlgenerator/Contents/Info.plist +ifeq ($(CONF), release) + $(CODESIGN) $@ +endif # Currently, SameBoy.app includes two "copies" of each Core .o file once in the app itself and # once in the QL Generator. It should probably become a dylib instead. $(BIN)/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL: $(CORE_OBJECTS) $(QUICKLOOK_OBJECTS) -@$(MKDIR) -p $(dir $@) $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) -Wl,-exported_symbols_list,QuickLook/exports.sym -bundle -framework Cocoa -framework Quicklook +ifeq ($(CONF), release) + $(STRIP) $@ +endif # cgb_boot_fast.bin is not a standard boot ROM, we don't expect it to exist in the user-provided # boot ROM directory. @@ -338,6 +539,7 @@ $(BIN)/SDL/sameboy: $(CORE_OBJECTS) $(SDL_OBJECTS) $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) ifeq ($(CONF), release) $(STRIP) $@ + $(CODESIGN) $@ endif # Windows version builds two, one with a conole and one without it @@ -352,19 +554,20 @@ $(BIN)/SDL/sameboy_debugger.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(OBJ)/Windows/r ifneq ($(USE_WINDRES),) $(OBJ)/%.o: %.rc -@$(MKDIR) -p $(dir $@) - windres --preprocessor cpp -DVERSION=\"$(VERSION)\" $^ $@ + windres --preprocessor cpp -DVERSION=\"$(VERSION)\" -DCOPYRIGHT_YEAR=\"$(COPYRIGHT_YEAR)\" $^ $@ else $(OBJ)/%.res: %.rc -@$(MKDIR) -p $(dir $@) - rc /fo $@ /dVERSION=\"$(VERSION)\" $^ + rc /fo $@ /dVERSION=\"$(VERSION)\" /dCOPYRIGHT_YEAR=\"$(COPYRIGHT_YEAR)\" $^ %.o: %.res cvtres /OUT:"$@" $^ endif -# We must provide SDL2.dll with the Windows port. -$(BIN)/SDL/SDL2.dll: - @$(eval MATCH := $(shell where $$LIB:SDL2.dll)) +# Copy required DLL files for the Windows port +$(BIN)/SDL/%.dll: + -@$(MKDIR) -p $(dir $@) + @$(eval MATCH := $(shell where $$LIB:$(notdir $@))) cp "$(MATCH)" $@ # Tester @@ -374,6 +577,7 @@ $(BIN)/tester/sameboy_tester: $(CORE_OBJECTS) $(TESTER_OBJECTS) $(CC) $^ -o $@ $(LDFLAGS) ifeq ($(CONF), release) $(STRIP) $@ + $(CODESIGN) $@ endif $(BIN)/tester/sameboy_tester.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) @@ -392,10 +596,14 @@ $(BIN)/SameBoy.app/Contents/Resources/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) cp -f $^ $@ -$(BIN)/SDL/LICENSE: LICENSE +$(BIN)/SameBoy-iOS.app/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) cp -f $^ $@ +$(BIN)/SDL/LICENSE: LICENSE + -@$(MKDIR) -p $(dir $@) + grep -v "^ " $^ > $@ + $(BIN)/SDL/registers.sym: Misc/registers.sym -@$(MKDIR) -p $(dir $@) cp -f $^ $@ @@ -407,34 +615,39 @@ $(BIN)/SDL/background.bmp: SDL/background.bmp $(BIN)/SDL/Shaders: Shaders -@$(MKDIR) -p $@ cp -rf Shaders/*.fsh $@ + +$(BIN)/SDL/Palettes: Misc/Palettes + -@$(MKDIR) -p $@ + cp -rf Misc/Palettes/*.sbp $@ # Boot ROMs $(OBJ)/%.2bpp: %.png -@$(MKDIR) -p $(dir $@) - rgbgfx -h -u -o $@ $< + $(RGBGFX) $(RGBGFX_FLAGS) -o $@ $< $(OBJ)/BootROMs/SameBoyLogo.pb12: $(OBJ)/BootROMs/SameBoyLogo.2bpp $(PB12_COMPRESS) - $(realpath $(PB12_COMPRESS)) < $< > $@ + -@$(MKDIR) -p $(dir $@) + "$(realpath $(PB12_COMPRESS))" < $< > $@ $(PB12_COMPRESS): BootROMs/pb12.c + -@$(MKDIR) -p $(dir $@) $(NATIVE_CC) -std=c99 -Wall -Werror $< -o $@ $(BIN)/BootROMs/cgb0_boot.bin: BootROMs/cgb_boot.asm $(BIN)/BootROMs/agb_boot.bin: BootROMs/cgb_boot.asm $(BIN)/BootROMs/cgb_boot_fast.bin: BootROMs/cgb_boot.asm -$(BIN)/BootROMs/sgb2_boot: BootROMs/sgb_boot.asm +$(BIN)/BootROMs/sgb2_boot.bin: BootROMs/sgb_boot.asm $(BIN)/BootROMs/%.bin: BootROMs/%.asm $(OBJ)/BootROMs/SameBoyLogo.pb12 -@$(MKDIR) -p $(dir $@) - rgbasm -i $(OBJ)/BootROMs/ -i BootROMs/ -o $@.tmp $< - rgblink -o $@.tmp2 $@.tmp - dd if=$@.tmp2 of=$@ count=1 bs=$(if $(findstring dmg,$@)$(findstring sgb,$@),256,2304) 2> $(NULL) - @rm $@.tmp $@.tmp2 + $(RGBASM) $(RGBASM_FLAGS) -o $@.tmp $< + $(RGBLINK) -x -o $@ $@.tmp + @rm $@.tmp # Libretro Core (uses its own build system) libretro: - CFLAGS="$(WARNINGS)" $(MAKE) -C libretro + CFLAGS="$(WARNINGS)" $(MAKE) -C libretro BOOTROMS_DIR=$(abspath $(BOOTROMS_DIR)) BIN=$(abspath $(BIN)) # install for Linux/FreeDesktop/etc. # Does not install mimetype icons because FreeDesktop is cursed abomination with no right to exist. @@ -465,22 +678,80 @@ endif $(DESTDIR)$(PREFIX)/share/icons/hicolor/%/apps/sameboy.png: FreeDesktop/AppIcon/%.png -@$(MKDIR) -p $(dir $@) cp -f $^ $@ - + $(DESTDIR)$(PREFIX)/share/icons/hicolor/%/mimetypes/x-gameboy-rom.png: FreeDesktop/Cartridge/%.png -@$(MKDIR) -p $(dir $@) cp -f $^ $@ - + $(DESTDIR)$(PREFIX)/share/icons/hicolor/%/mimetypes/x-gameboy-color-rom.png: FreeDesktop/ColorCartridge/%.png -@$(MKDIR) -p $(dir $@) cp -f $^ $@ - + $(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml: FreeDesktop/sameboy.xml -@$(MKDIR) -p $(dir $@) cp -f $^ $@ endif +ios: + @$(MAKE) _ios + +$(BIN)/SameBoy-iOS.ipa: ios iOS/sideload.entitlements + $(MKDIR) -p $(OBJ)/Payload + cp -rf $(BIN)/SameBoy-iOS.app $(OBJ)/Payload/SameBoy-iOS.app + codesign -fs - --entitlements iOS/sideload.entitlements $(OBJ)/Payload/SameBoy-iOS.app + (cd $(OBJ) && zip -q $(abspath $@) -r Payload) + rm -rf $(OBJ)/Payload + + +$(BIN)/SameBoy-iOS.deb: $(OBJ)/debian-binary $(OBJ)/control.tar.gz $(OBJ)/data.tar.gz + -@$(MKDIR) -p $(dir $@) + (cd $(OBJ) && ar cr $(abspath $@) $(notdir $^)) + +$(OBJ)/data.tar.gz: ios iOS/jailbreak.entitlements iOS/installer.entitlements + $(MKDIR) -p $(OBJ)/private/var/containers/ + cp -rf $(BIN)/SameBoy-iOS.app $(OBJ)/private/var/containers/SameBoy-iOS.app + cp build/obj-ios/installer $(OBJ)/private/var/containers/SameBoy-iOS.app + codesign -fs - --entitlements iOS/installer.entitlements $(OBJ)/private/var/containers/SameBoy-iOS.app/installer + codesign -fs - --entitlements iOS/jailbreak.entitlements $(OBJ)/private/var/containers/SameBoy-iOS.app + (cd $(OBJ) && tar -czf $(abspath $@) --format ustar --uid 501 --gid 501 --numeric-owner ./private) + rm -rf $(OBJ)/private/ + +$(OBJ)/control.tar.gz: iOS/deb-postinst iOS/deb-prerm iOS/deb-control + -@$(MKDIR) -p $(dir $@) + sed "s/@VERSION/$(VERSION)/" < iOS/deb-control > $(OBJ)/control + ln iOS/deb-postinst $(OBJ)/postinst + ln iOS/deb-prerm $(OBJ)/prerm + (cd $(OBJ) && tar -czf $(abspath $@) --format ustar --uid 501 --gid 501 --numeric-owner ./control ./postinst ./prerm) + rm $(OBJ)/control $(OBJ)/postinst $(OBJ)/prerm + +$(OBJ)/debian-binary: + -@$(MKDIR) -p $(dir $@) + echo 2.0 > $@ + +$(LIBDIR)/libsameboy.o: $(CORE_OBJECTS) + -@$(MKDIR) -p $(dir $@) + @# This is a somewhat simple hack to force Clang and GCC to build a native object file out of one or many LTO objects + echo "static const char __attribute__((used)) x=0;"| $(CC) $(filter-out -flto,$(CFLAGS)) -c -x c - -o $(OBJ)/lto_hack.o + @# And this is a somewhat complicated hack to invoke the correct LTO-enabled LD command in a mostly cross-platform nature + $(CC) $(FAT_FLAGS) $(CFLAGS) $(LIBFLAGS) $^ $(OBJ)/lto_hack.o -o $@ + -@rm $(OBJ)/lto_hack.o + +$(LIBDIR)/libsameboy.a: $(LIBDIR)/libsameboy.o + -@$(MKDIR) -p $(dir $@) + -@rm -f $@ + ar -crs $@ $^ + +$(INC)/%.h: Core/%.h + -@$(MKDIR) -p $(dir $@) + -@# CPPP doesn't like multibyte characters, so we replace the single quote character before processing so it doesn't complain + sed "s/'/@SINGLE_QUOTE@/g" $^ | cppp $(CPPP_FLAGS) | sed "s/@SINGLE_QUOTE@/'/g" > $@ + +lib-unsupported: + @echo Due to limitations of lld-link, compiling SameBoy as a library on Windows is not supported. + @false + # Clean clean: rm -rf build -.PHONY: libretro tester +.PHONY: libretro tester cocoa ios _ios ios-ipa ios-deb liblib-unsupported bootroms diff --git a/Misc/Palettes/Desert.sbp b/Misc/Palettes/Desert.sbp new file mode 100644 index 0000000..28625ad Binary files /dev/null and b/Misc/Palettes/Desert.sbp differ diff --git a/Misc/Palettes/Evening.sbp b/Misc/Palettes/Evening.sbp new file mode 100644 index 0000000..e11998a --- /dev/null +++ b/Misc/Palettes/Evening.sbp @@ -0,0 +1 @@ +LPBS&6UiS䦻}^LH+ \ No newline at end of file diff --git a/Misc/Palettes/Fog.sbp b/Misc/Palettes/Fog.sbp new file mode 100644 index 0000000..a79fe00 Binary files /dev/null and b/Misc/Palettes/Fog.sbp differ diff --git a/Misc/Palettes/Magic Eggplant.sbp b/Misc/Palettes/Magic Eggplant.sbp new file mode 100644 index 0000000..6bd5929 Binary files /dev/null and b/Misc/Palettes/Magic Eggplant.sbp differ diff --git a/Misc/Palettes/Radioactive Pea.sbp b/Misc/Palettes/Radioactive Pea.sbp new file mode 100644 index 0000000..57f9d6a Binary files /dev/null and b/Misc/Palettes/Radioactive Pea.sbp differ diff --git a/Misc/Palettes/Seaweed.sbp b/Misc/Palettes/Seaweed.sbp new file mode 100644 index 0000000..3718efd Binary files /dev/null and b/Misc/Palettes/Seaweed.sbp differ diff --git a/Misc/Palettes/Twilight.sbp b/Misc/Palettes/Twilight.sbp new file mode 100644 index 0000000..a5decc1 Binary files /dev/null and b/Misc/Palettes/Twilight.sbp differ diff --git a/Misc/registers.sym b/Misc/registers.sym index affe663..ef027d0 100644 --- a/Misc/registers.sym +++ b/Misc/registers.sym @@ -1,66 +1,67 @@ -00:FF00 IO_JOYP -00:FF01 IO_SB -00:FF02 IO_SC -00:FF04 IO_DIV -00:FF05 IO_TIMA -00:FF06 IO_TMA -00:FF07 IO_TAC -00:FF0F IO_IF -00:FF10 IO_NR10 -00:FF11 IO_NR11 -00:FF12 IO_NR12 -00:FF13 IO_NR13 -00:FF14 IO_NR14 -00:FF16 IO_NR21 -00:FF17 IO_NR22 -00:FF18 IO_NR23 -00:FF19 IO_NR24 -00:FF1A IO_NR30 -00:FF1B IO_NR31 -00:FF1C IO_NR32 -00:FF1D IO_NR33 -00:FF1E IO_NR34 -00:FF20 IO_NR41 -00:FF21 IO_NR42 -00:FF22 IO_NR43 -00:FF23 IO_NR44 -00:FF24 IO_NR50 -00:FF25 IO_NR51 -00:FF26 IO_NR52 -00:FF30 IO_WAV_START -00:FF3F IO_WAV_END -00:FF40 IO_LCDC -00:FF41 IO_STAT -00:FF42 IO_SCY -00:FF43 IO_SCX -00:FF44 IO_LY -00:FF45 IO_LYC -00:FF46 IO_DMA -00:FF47 IO_BGP -00:FF48 IO_OBP0 -00:FF49 IO_OBP1 -00:FF4A IO_WY -00:FF4B IO_WX -00:FF4C IO_KEY0 -00:FF4D IO_KEY1 -00:FF4F IO_VBK -00:FF50 IO_BANK -00:FF51 IO_HDMA1 -00:FF52 IO_HDMA2 -00:FF53 IO_HDMA3 -00:FF54 IO_HDMA4 -00:FF55 IO_HDMA5 -00:FF56 IO_RP -00:FF68 IO_BGPI -00:FF69 IO_BGPD -00:FF6A IO_OBPI -00:FF6B IO_OBPD -00:FF6C IO_OPRI -00:FF70 IO_SVBK -00:FF72 IO_PSWX -00:FF73 IO_PSWY -00:FF74 IO_PSW -00:FF75 IO_UNKNOWN5 -00:FF76 IO_PCM12 -00:FF77 IO_PCM34 -00:FFFF IO_IE +00:FF00 rJOYP +00:FF01 rSB +00:FF02 rSC +00:FF04 rDIV +00:FF05 rTIMA +00:FF06 rTMA +00:FF07 rTAC +00:FF0F rIF +00:FF10 rNR10 +00:FF11 rNR11 +00:FF12 rNR12 +00:FF13 rNR13 +00:FF14 rNR14 +00:FF16 rNR21 +00:FF17 rNR22 +00:FF18 rNR23 +00:FF19 rNR24 +00:FF1A rNR30 +00:FF1B rNR31 +00:FF1C rNR32 +00:FF1D rNR33 +00:FF1E rNR34 +00:FF20 rNR41 +00:FF21 rNR42 +00:FF22 rNR43 +00:FF23 rNR44 +00:FF24 rNR50 +00:FF25 rNR51 +00:FF26 rNR52 +00:FF30 rWAV +00:FF40 rWAV.end +00:FF40 rLCDC +00:FF41 rSTAT +00:FF42 rSCY +00:FF43 rSCX +00:FF44 rLY +00:FF45 rLYC +00:FF46 rDMA +00:FF47 rBGP +00:FF48 rOBP0 +00:FF49 rOBP1 +00:FF4A rWY +00:FF4B rWX +00:FF4C rKEY0 +00:FF4D rKEY1 +00:FF4F rVBK +00:FF50 rBANK +00:FF51 rHDMA1 +00:FF52 rHDMA2 +00:FF53 rHDMA3 +00:FF54 rHDMA4 +00:FF55 rHDMA5 +00:FF56 rRP +00:FF68 rBGPI +00:FF69 rBGPD +00:FF6A rOBPI +00:FF6B rOBPD +00:FF6C rOPRI +00:FF70 rSVBK +00:FF71 rPSM +00:FF72 rPSWX +00:FF73 rPSWY +00:FF74 rPSW +00:FF75 rUNKNOWN5 +00:FF76 rPCM12 +00:FF77 rPCM34 +00:FFFF rIE diff --git a/OpenDialog/cocoa.m b/OpenDialog/cocoa.m index cfb2553..fd9af3c 100644 --- a/OpenDialog/cocoa.m +++ b/OpenDialog/cocoa.m @@ -11,7 +11,7 @@ char *do_open_rom_dialog(void) NSOpenPanel *dialog = [NSOpenPanel openPanel]; dialog.title = @"Open ROM"; dialog.allowedFileTypes = @[@"gb", @"gbc", @"sgb", @"isx"]; - [dialog runModal]; + if ([dialog runModal] != NSModalResponseOK) return nil; [key makeKeyAndOrderFront:nil]; NSString *ret = [[[dialog URLs] firstObject] path]; dup2(stderr_fd, STDERR_FILENO); @@ -32,7 +32,7 @@ char *do_open_folder_dialog(void) dialog.title = @"Select Boot ROMs Folder"; dialog.canChooseDirectories = true; dialog.canChooseFiles = false; - [dialog runModal]; + if ([dialog runModal] != NSModalResponseOK) return nil; [key makeKeyAndOrderFront:nil]; NSString *ret = [[[dialog URLs] firstObject] path]; dup2(stderr_fd, STDERR_FILENO); @@ -42,3 +42,25 @@ char *do_open_folder_dialog(void) return NULL; } } + +/* The Cocoa variant of this function isn't as fully featured as the GTK and Windows ones, as Mac users would use + the fully featured Cocoa port of SameBoy anyway*/ +char *do_save_recording_dialog(unsigned frequency) +{ + @autoreleasepool { + int stderr_fd = dup(STDERR_FILENO); + close(STDERR_FILENO); + NSWindow *key = [NSApp keyWindow]; + NSSavePanel *dialog = [NSSavePanel savePanel]; + dialog.title = @"Audio recording save location"; + dialog.allowedFileTypes = @[@"aiff", @"aif", @"aifc", @"wav", @"raw", @"pcm"]; + if ([dialog runModal] != NSModalResponseOK) return nil; + [key makeKeyAndOrderFront:nil]; + NSString *ret = [[dialog URL] path]; + dup2(stderr_fd, STDERR_FILENO); + if (ret) { + return strdup(ret.UTF8String); + } + return NULL; + } +} diff --git a/OpenDialog/gtk.c b/OpenDialog/gtk.c index 378dcb4..2b08f1d 100644 --- a/OpenDialog/gtk.c +++ b/OpenDialog/gtk.c @@ -1,4 +1,5 @@ #include "open_dialog.h" +#include #include #include #include @@ -6,6 +7,7 @@ #include #define GTK_FILE_CHOOSER_ACTION_OPEN 0 +#define GTK_FILE_CHOOSER_ACTION_SAVE 1 #define GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER 2 #define GTK_RESPONSE_ACCEPT -3 #define GTK_RESPONSE_CANCEL -6 @@ -28,6 +30,19 @@ void _gtk_file_filter_set_name(void *filter, const char *name); void _gtk_file_chooser_add_filter(void *dialog, void *filter); void _gtk_main_iteration(void); bool _gtk_events_pending(void); +unsigned long _g_signal_connect_data(void *instance, + const char *detailed_signal, + void *c_handler, + void *data, + void *destroy_data, + unsigned connect_flags); +void _gtk_file_chooser_set_current_name(void *dialog, + const char *name); +void *_gtk_file_chooser_get_filter(void *dialog); +const char *_gtk_file_filter_get_name (void *dialog); +#define g_signal_connect(instance, detailed_signal, c_handler, data) \ +g_signal_connect_data((instance), (detailed_signal), (c_handler), (data), NULL, 0) + #define LAZY(symbol) static typeof(_##symbol) *symbol = NULL;\ @@ -35,10 +50,20 @@ if (symbol == NULL) symbol = dlsym(handle, #symbol);\ if (symbol == NULL) goto lazy_error #define TRY_DLOPEN(name) handle = handle? handle : dlopen(name, RTLD_NOW) -void nop(){} +void nop(void){} + +static void wait_mouse_up(void) +{ + while (true) { + if (!(SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(SDL_BUTTON_LEFT))) break; + SDL_Event event; + SDL_PollEvent(&event); + } +} char *do_open_rom_dialog(void) { + wait_mouse_up(); static void *handle = NULL; TRY_DLOPEN("libgtk-3.so"); @@ -87,7 +112,7 @@ char *do_open_rom_dialog(void) gtk_file_filter_set_name(filter, "Game Boy ROMs"); gtk_file_chooser_add_filter(dialog, filter); - int res = gtk_dialog_run (dialog); + int res = gtk_dialog_run(dialog); char *ret = NULL; if (res == GTK_RESPONSE_ACCEPT) { @@ -115,6 +140,7 @@ lazy_error: char *do_open_folder_dialog(void) { + wait_mouse_up(); static void *handle = NULL; TRY_DLOPEN("libgtk-3.so"); @@ -155,7 +181,169 @@ char *do_open_folder_dialog(void) NULL ); - int res = gtk_dialog_run (dialog); + int res = gtk_dialog_run(dialog); + char *ret = NULL; + + if (res == GTK_RESPONSE_ACCEPT) { + char *filename; + filename = gtk_file_chooser_get_filename(dialog); + ret = strdup(filename); + g_free(filename); + } + + while (gtk_events_pending()) { + gtk_main_iteration(); + } + + gtk_widget_destroy(dialog); + + while (gtk_events_pending()) { + gtk_main_iteration(); + } + return ret; + +lazy_error: + fprintf(stderr, "Failed to display GTK dialog\n"); + return NULL; +} + +static void filter_changed(void *dialog, + void *unused, + void *unused2) +{ + static void *handle = NULL; + + TRY_DLOPEN("libgtk-3.so"); + TRY_DLOPEN("libgtk-3.so.0"); + TRY_DLOPEN("libgtk-2.so"); + TRY_DLOPEN("libgtk-2.so.0"); + + if (!handle) { + goto lazy_error; + } + + LAZY(gtk_file_chooser_get_filename); + LAZY(gtk_file_chooser_set_current_name); + LAZY(g_free); + LAZY(gtk_file_chooser_get_filter); + LAZY(gtk_file_filter_get_name); + + char *filename = gtk_file_chooser_get_filename(dialog); + if (!filename) return; + char *temp = filename + strlen(filename); + char *basename = filename; + bool deleted_extension = false; + while (temp != filename) { + temp--; + if (*temp == '.' && !deleted_extension) { + *temp = 0; + deleted_extension = true; + } + else if (*temp == '/') { + basename = temp + 1; + break; + } + } + + char *new_filename = NULL; + + switch (gtk_file_filter_get_name(gtk_file_chooser_get_filter(dialog))[1]) { + case 'p': + default: + asprintf(&new_filename, "%s.aiff", basename); + break; + case 'I': + asprintf(&new_filename, "%s.wav", basename); + break; + case 'a': + asprintf(&new_filename, "%s.raw", basename); + break; + } + + + gtk_file_chooser_set_current_name(dialog, new_filename); + free(new_filename); + g_free(filename); + return; + +lazy_error: + fprintf(stderr, "Failed updating the file extension\n"); +} + + +char *do_save_recording_dialog(unsigned frequency) +{ + wait_mouse_up(); + static void *handle = NULL; + + TRY_DLOPEN("libgtk-3.so"); + TRY_DLOPEN("libgtk-3.so.0"); + TRY_DLOPEN("libgtk-2.so"); + TRY_DLOPEN("libgtk-2.so.0"); + + if (!handle) { + goto lazy_error; + } + + + LAZY(gtk_init_check); + LAZY(gtk_file_chooser_dialog_new); + LAZY(gtk_dialog_run); + LAZY(g_free); + LAZY(gtk_widget_destroy); + LAZY(gtk_file_chooser_get_filename); + LAZY(g_log_set_default_handler); + LAZY(gtk_file_filter_new); + LAZY(gtk_file_filter_add_pattern); + LAZY(gtk_file_filter_set_name); + LAZY(gtk_file_chooser_add_filter); + LAZY(gtk_events_pending); + LAZY(gtk_main_iteration); + LAZY(g_signal_connect_data); + LAZY(gtk_file_chooser_set_current_name); + + /* Shut up GTK */ + g_log_set_default_handler(nop, NULL); + + gtk_init_check(0, 0); + + + void *dialog = gtk_file_chooser_dialog_new("Audio recording save location", + 0, + GTK_FILE_CHOOSER_ACTION_SAVE, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Save", GTK_RESPONSE_ACCEPT, + NULL ); + + + void *filter = gtk_file_filter_new(); + gtk_file_filter_add_pattern(filter, "*.aiff"); + gtk_file_filter_add_pattern(filter, "*.aif"); + gtk_file_filter_add_pattern(filter, "*.aifc"); + gtk_file_filter_set_name(filter, "Apple AIFF"); + gtk_file_chooser_add_filter(dialog, filter); + + filter = gtk_file_filter_new(); + gtk_file_filter_add_pattern(filter, "*.wav"); + gtk_file_filter_set_name(filter, "RIFF WAVE"); + gtk_file_chooser_add_filter(dialog, filter); + + filter = gtk_file_filter_new(); + gtk_file_filter_add_pattern(filter, "*.raw"); + gtk_file_filter_add_pattern(filter, "*.pcm"); + static char raw_name[40]; +#ifdef GB_BIG_ENDIAN + sprintf(raw_name, "Raw PCM (Stereo %dHz, 16-bit BE)", frequency); +#else + sprintf(raw_name, "Raw PCM (Stereo %dHz, 16-bit LE)", frequency); +#endif + gtk_file_filter_set_name(filter, raw_name); + gtk_file_chooser_add_filter(dialog, filter); + + g_signal_connect(dialog, "notify::filter", filter_changed, NULL); + gtk_file_chooser_set_current_name(dialog, "Untitled.aiff"); + + int res = gtk_dialog_run(dialog); char *ret = NULL; if (res == GTK_RESPONSE_ACCEPT) { diff --git a/OpenDialog/open_dialog.h b/OpenDialog/open_dialog.h index 6d7fb5b..b1f4ff7 100644 --- a/OpenDialog/open_dialog.h +++ b/OpenDialog/open_dialog.h @@ -1,6 +1,5 @@ -#ifndef open_rom_h -#define open_rom_h +#pragma once char *do_open_rom_dialog(void); char *do_open_folder_dialog(void); -#endif /* open_rom_h */ +char *do_save_recording_dialog(unsigned frequency); diff --git a/OpenDialog/windows.c b/OpenDialog/windows.c index e711032..7913333 100644 --- a/OpenDialog/windows.c +++ b/OpenDialog/windows.c @@ -1,57 +1,120 @@ +#define COBJMACROS #include #include +#include #include "open_dialog.h" +static char *wc_to_utf8_alloc(const wchar_t *wide) +{ + unsigned int cb = WideCharToMultiByte(CP_UTF8, 0, wide, -1, NULL, 0, NULL, NULL); + if (cb) { + char *buffer = (char*) malloc(cb); + if (buffer) { + WideCharToMultiByte(CP_UTF8, 0, wide, -1, buffer, cb, NULL, NULL); + return buffer; + } + } + return NULL; +} + char *do_open_rom_dialog(void) { OPENFILENAMEW dialog; - static wchar_t filename[MAX_PATH] = {0}; - + wchar_t filename[MAX_PATH]; + + filename[0] = '\0'; memset(&dialog, 0, sizeof(dialog)); dialog.lStructSize = sizeof(dialog); dialog.lpstrFile = filename; - dialog.nMaxFile = sizeof(filename); + dialog.nMaxFile = MAX_PATH; dialog.lpstrFilter = L"Game Boy ROMs\0*.gb;*.gbc;*.sgb;*.isx\0All files\0*.*\0\0"; dialog.nFilterIndex = 1; dialog.lpstrFileTitle = NULL; dialog.nMaxFileTitle = 0; dialog.lpstrInitialDir = NULL; - dialog.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; - - if (GetOpenFileNameW(&dialog) == TRUE) { - char *ret = malloc(MAX_PATH * 4); - WideCharToMultiByte(CP_UTF8, 0, filename, sizeof(filename), ret, MAX_PATH * 4, NULL, NULL); - return ret; + dialog.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; + + if (GetOpenFileNameW(&dialog)) { + return wc_to_utf8_alloc(filename); } - + return NULL; } char *do_open_folder_dialog(void) { - - BROWSEINFOW dialog; - memset(&dialog, 0, sizeof(dialog)); - - dialog.ulFlags = BIF_USENEWUI; - dialog.lpszTitle = L"Select Boot ROMs Folder"; - - OleInitialize(NULL); - - LPITEMIDLIST list = SHBrowseForFolderW(&dialog); - static wchar_t filename[MAX_PATH] = {0}; + HRESULT hr, hrCoInit; + char *ret = NULL; + IFileOpenDialog *dialog = NULL; + IShellItem *result = NULL; + wchar_t *path = NULL; - if (list) { - if (!SHGetPathFromIDListW(list, filename)) { - OleUninitialize(); - return NULL; + hrCoInit = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + hr = CoCreateInstance(&CLSID_FileOpenDialog, NULL, CLSCTX_ALL, &IID_IFileOpenDialog, (LPVOID *)&dialog); + if (FAILED(hr)) goto end; + + hr = IFileOpenDialog_SetOptions(dialog, FOS_NOCHANGEDIR | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_NOREADONLYRETURN); + if (FAILED(hr)) goto end; + + hr = IFileOpenDialog_SetTitle(dialog, L"Select Boot ROMs Folder"); + if (FAILED(hr)) goto end; + + hr = IFileOpenDialog_Show(dialog, NULL); + if (FAILED(hr)) goto end; + + hr = IFileOpenDialog_GetResult(dialog, &result); + if (FAILED(hr)) goto end; + + hr = IShellItem_GetDisplayName(result, SIGDN_FILESYSPATH, &path); + if (FAILED(hr)) goto end; + + ret = wc_to_utf8_alloc(path); + +end: + if (path) CoTaskMemFree((void *)path); + if (result) IShellItem_Release(result); + if (dialog) IFileOpenDialog_Release(dialog); + if (SUCCEEDED(hrCoInit)) CoUninitialize(); + return ret; +} + +char *do_save_recording_dialog(unsigned frequency) +{ + OPENFILENAMEW dialog; + wchar_t filename[MAX_PATH + 5] = L"recording.wav"; + static wchar_t filter[] = L"RIFF WAVE\0*.wav\0Apple AIFF\0*.aiff;*.aif;*.aifc\0Raw PCM (Stereo _______Hz, 16-bit LE)\0*.raw;*.pcm;\0All files\0*.*\0\0"; + + memset(&dialog, 0, sizeof(dialog)); + dialog.lStructSize = sizeof(dialog); + dialog.lpstrFile = filename; + dialog.nMaxFile = MAX_PATH; + dialog.lpstrFilter = filter; + swprintf(filter + sizeof("RIFF WAVE\0*.wav\0Apple AIFF\0*.aiff;*.aif;*.aifc\0Raw PCM (Stereo ") - 1, + sizeof("_______Hz, 16-bit LE)"), + L"%dHz, 16-bit LE) ", + frequency); + + dialog.nFilterIndex = 1; + dialog.lpstrInitialDir = NULL; + dialog.Flags = OFN_PATHMUSTEXIST | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT; + + if (GetSaveFileNameW(&dialog)) { + if (dialog.nFileExtension == 0) { + switch (dialog.nFilterIndex) { + case 1: + wcscat(filename, L".wav"); + break; + case 2: + wcscat(filename, L".aiff"); + break; + case 3: + wcscat(filename, L".raw"); + break; + } } - char *ret = malloc(MAX_PATH * 4); - WideCharToMultiByte(CP_UTF8, 0, filename, sizeof(filename), ret, MAX_PATH * 4, NULL, NULL); - CoTaskMemFree(list); - OleUninitialize(); - return ret; + return wc_to_utf8_alloc(filename); } - OleUninitialize(); + return NULL; } diff --git a/QuickLook/CartridgeTemplate.png b/QuickLook/CartridgeTemplate.png index 5bf1a2f..3d6d600 100644 Binary files a/QuickLook/CartridgeTemplate.png and b/QuickLook/CartridgeTemplate.png differ diff --git a/QuickLook/ColorCartridgeTemplate.png b/QuickLook/ColorCartridgeTemplate.png index 5eac562..356e45d 100644 Binary files a/QuickLook/ColorCartridgeTemplate.png and b/QuickLook/ColorCartridgeTemplate.png differ diff --git a/QuickLook/Info.plist b/QuickLook/Info.plist index 9b369ec..f98dfb2 100644 --- a/QuickLook/Info.plist +++ b/QuickLook/Info.plist @@ -14,6 +14,7 @@ com.github.liji32.sameboy.gb com.github.liji32.sameboy.gbc com.github.liji32.sameboy.isx + public.gbrom @@ -48,7 +49,7 @@ CFPlugInUnloadFunction NSHumanReadableCopyright - Copyright © 2015-2021 Lior Halphon + Copyright © 2015-@COPYRIGHT_YEAR Lior Halphon QLNeedsToBeRunInMainThread QLPreviewHeight diff --git a/QuickLook/UniversalCartridgeTemplate.png b/QuickLook/UniversalCartridgeTemplate.png index 1bf4903..7f251f4 100644 Binary files a/QuickLook/UniversalCartridgeTemplate.png and b/QuickLook/UniversalCartridgeTemplate.png differ diff --git a/QuickLook/generator.m b/QuickLook/generator.m index f2651d2..536f754 100644 --- a/QuickLook/generator.m +++ b/QuickLook/generator.m @@ -10,9 +10,12 @@ static OSStatus render(CGContextRef cgContext, CFURLRef url, bool showBorder) static NSImage *templateColor = nil; static NSBundle *bundle = nil; static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + bundle = [NSBundle bundleWithIdentifier:@"com.github.liji32.sameboy.previewer"]; + }); if (showBorder) { + static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - bundle = [NSBundle bundleWithIdentifier:@"com.github.liji32.sameboy.previewer"]; template = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"CartridgeTemplate" ofType:@"png"]]; templateUniversal = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"UniversalCartridgeTemplate" ofType:@"png"]]; templateColor = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"ColorCartridgeTemplate" ofType:@"png"]]; @@ -31,7 +34,7 @@ static OSStatus render(CGContextRef cgContext, CFURLRef url, bool showBorder) /* Convert the screenshot to a CGImageRef */ CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, bitmap, sizeof(bitmap), NULL); CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; + CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; CGImageRef iref = CGImageCreate(160, diff --git a/QuickLook/get_image_for_rom.c b/QuickLook/get_image_for_rom.c index b9f87ed..6c9ac91 100755 --- a/QuickLook/get_image_for_rom.c +++ b/QuickLook/get_image_for_rom.c @@ -25,7 +25,7 @@ static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes } -static void vblank(GB_gameboy_t *gb) +static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type) { struct local_data *local_data = (struct local_data *)GB_get_user_data(gb); @@ -59,7 +59,7 @@ int get_image_for_rom(const char *filename, const char *boot_path, uint32_t *out GB_set_rgb_encode_callback(&gb, rgb_encode); GB_set_async_input_callback(&gb, async_input_callback); GB_set_log_callback(&gb, log_callback); - GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_EMULATE_HARDWARE); + GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_MODERN_BALANCED); size_t length = strlen(filename); char extension[4] = {0,}; diff --git a/QuickLook/get_image_for_rom.h b/QuickLook/get_image_for_rom.h index 598486a..183bba4 100644 --- a/QuickLook/get_image_for_rom.h +++ b/QuickLook/get_image_for_rom.h @@ -1,10 +1,6 @@ -#ifndef get_image_for_rom_h -#define get_image_for_rom_h +#pragma once + #include typedef bool (*cancel_callback_t)(void*); - int get_image_for_rom(const char *filename, const char *boot_path, uint32_t *output, uint8_t *cgb_flag); - - -#endif diff --git a/README.md b/README.md index d06ef4d..9c5f8da 100644 --- a/README.md +++ b/README.md @@ -34,22 +34,32 @@ Features currently supported only with the Cocoa version: SameBoy passes all of [blargg's test ROMs](http://gbdev.gg8.se/wiki/articles/Test_ROMs#Blargg.27s_tests), all of [mooneye-gb's](https://github.com/Gekkio/mooneye-gb) tests (Some tests require the original boot ROMs), and all of [Wilbert Pol's tests](https://github.com/wilbertpol/mooneye-gb/tree/master/tests/acceptance). SameBoy should work with most games and demos, please [report](https://github.com/LIJI32/SameBoy/issues/new) any broken ROM. The latest results for SameBoy's automatic tester are available [here](https://sameboy.github.io/automation/). ## Contributing -SameBoy is an open-source project licensed under the MIT license, and you're welcome to contribute by creating issues, implementing new features, improving emulation accuracy and fixing existing open issues. You can read the [contribution guidelines](CONTRIBUTING.md) to make sure your contributions are as effective as possible. +SameBoy is an open-source project licensed under the Expat license (with an additional exception for the iOS folder), and you're welcome to contribute by creating issues, implementing new features, improving emulation accuracy and fixing existing open issues. You can read the [contribution guidelines](CONTRIBUTING.md) to make sure your contributions are as effective as possible. ## Compilation SameBoy requires the following tools and libraries to build: * clang (Recommended; required for macOS) or GCC * make - * macOS Cocoa port: macOS SDK and Xcode (For command line tools and ibtool) - * SDL port: libsdl2 + * macOS Cocoa frontend: macOS SDK and Xcode (For command line tools and ibtool) + * SDL frontend: libsdl2 * [rgbds](https://github.com/gbdev/rgbds/releases/), for boot ROM compilation + * [cppp](https://github.com/BR903/cppp), for cleaning up headers when compiling SameBoy as a library On Windows, SameBoy also requires: * Visual Studio (For headers, etc.) * [GnuWin](http://gnuwin32.sourceforge.net/) - * Running vcvars32 before running make. Make sure all required tools and libraries are in %PATH% and %lib%, respectively. (see [Build FAQ](https://github.com/LIJI32/SameBoy/blob/master/build-faq.md) for more details on Windows compilation) + * Running vcvars64 before running make. Make sure all required tools and libraries are in %PATH% and %lib%, respectively. (see [Build FAQ](https://github.com/LIJI32/SameBoy/blob/master/build-faq.md) for more details on Windows compilation) -To compile, simply run `make`. The targets are `cocoa` (Default for macOS), `sdl` (Default for everything else), `libretro`, `bootroms` and `tester`. You may also specify `CONF=debug` (default), `CONF=release`, `CONF=native_release` or `CONF=fat_release` to control optimization, symbols and multi-architectures. `native_release` is faster than `release`, but is optimized to the host's CPU and therefore is not portable. `fat_release` is exclusive to macOS and builds x86-64 and ARM64 fat binaries; this requires using a recent enough `clang` and macOS SDK using `xcode-select`, or setting them explicitly with `CC=` and `SYSROOT=`, respectively. All other configurations will build to your host architecture. You may set `BOOTROMS_DIR=...` to a directory containing precompiled boot ROM files, otherwise the build system will compile and use SameBoy's own boot ROMs. +To compile, simply run `make`. The targets are: + * `cocoa` (Default for macOS) + * `sdl` (Default for everything else) + * `lib` (Creates libsameboy.o and libsameboy.a for statically linking SameBoy, as well as a headers directory with corresponding headers; currently not supported on Windows due to linker limitations) + * `ios` (Plain iOS .app bundle), `ios-ipa` (iOS IPA archive for side-loading), `ios-deb` (iOS deb package for jailbroken devices) + * `libretro` + * `bootroms` + * `tester` + +You may also specify `CONF=debug` (default), `CONF=release`, `CONF=native_release` or `CONF=fat_release` to control optimization, symbols and multi-architectures. `native_release` is faster than `release`, but is optimized to the host's CPU and therefore is not portable. `fat_release` is exclusive to macOS and builds x86-64 and ARM64 fat binaries; this requires using a recent enough `clang` and macOS SDK using `xcode-select`, or setting them explicitly with `CC=` and `SYSROOT=`, respectively. All other configurations will build to your host architecture, except for the iOS targets. You may set `BOOTROMS_DIR=...` to a directory containing precompiled boot ROM files, otherwise the build system will compile and use SameBoy's own boot ROMs. The SDL port will look for resource files with a path relative to executable and inside the directory specified by the `DATA_DIR` variable. If you are packaging SameBoy, you may wish to override this by setting the `DATA_DIR` variable during compilation to the target path of the directory containing all files (apart from the executable, that's not necessary) from the `build/bin/SDL` directory in the source tree. Make sure the variable ends with a `/` character. On FreeDesktop environments, `DATA_DIR` will default to `/usr/local/share/sameboy/`. `PREFIX` and `DESTDIR` follow their standard usage and default to an empty string an `/usr/local`, respectively diff --git a/SDL/audio.c b/SDL/audio.c new file mode 100644 index 0000000..c1f2fc7 --- /dev/null +++ b/SDL/audio.c @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include "audio/audio.h" +#include "configuration.h" + +#define likely(x) GB_likely(x) +#define unlikely(x) GB_unlikely(x) + +static const GB_audio_driver_t *driver = NULL; + +bool GB_audio_init(void) +{ + const GB_audio_driver_t *drivers[] = { +#ifdef _WIN32 + GB_AUDIO_DRIVER_REF(XAudio2), +#endif + GB_AUDIO_DRIVER_REF(SDL), +#ifdef ENABLE_OPENAL + GB_AUDIO_DRIVER_REF(OpenAL), +#endif + }; + + // First try the preferred driver + for (unsigned i = 0; i < sizeof(drivers) / sizeof(drivers[0]); i++) { + driver = drivers[i]; + if (strcmp(driver->name, configuration.audio_driver) != 0) { + continue; + } + if (driver->audio_init()) { + if (driver->audio_deinit) { + atexit(driver->audio_deinit); + } + return true; + } + } + + // Else go by priority + for (unsigned i = 0; i < sizeof(drivers) / sizeof(drivers[0]); i++) { + driver = drivers[i]; + if (driver->audio_init()) { + atexit(driver->audio_deinit); + return true; + } + } + + driver = NULL; + return false; +} + +bool GB_audio_is_playing(void) +{ + if (unlikely(!driver)) return false; + return driver->audio_is_playing(); +} + +void GB_audio_set_paused(bool paused) +{ + if (unlikely(!driver)) return; + return driver->audio_set_paused(paused); +} + +void GB_audio_clear_queue(void) +{ + if (unlikely(!driver)) return; + return driver->audio_clear_queue(); +} + +unsigned GB_audio_get_frequency(void) +{ + if (unlikely(!driver)) return 0; + return driver->audio_get_frequency(); +} + +size_t GB_audio_get_queue_length(void) +{ + if (unlikely(!driver)) return 0; + return driver->audio_get_queue_length(); +} + +void GB_audio_queue_sample(GB_sample_t *sample) +{ + if (unlikely(!driver)) return; + return driver->audio_queue_sample(sample); +} + +const char *GB_audio_driver_name(void) +{ + if (unlikely(!driver)) return "None"; + return driver->name; +} + +const char *GB_audio_driver_name_at_index(unsigned index) +{ + const GB_audio_driver_t *drivers[] = { +#ifdef _WIN32 + GB_AUDIO_DRIVER_REF(XAudio2), +#endif + GB_AUDIO_DRIVER_REF(SDL), +#ifdef ENABLE_OPENAL + GB_AUDIO_DRIVER_REF(OpenAL), +#endif + }; + if (index >= sizeof(drivers) / sizeof(drivers[0])) { + return ""; + } + return drivers[index]->name; +} diff --git a/SDL/audio/audio.h b/SDL/audio/audio.h index acaa011..6d42c12 100644 --- a/SDL/audio/audio.h +++ b/SDL/audio/audio.h @@ -1,5 +1,4 @@ -#ifndef sdl_audio_h -#define sdl_audio_h +#pragma once #include #include @@ -11,6 +10,33 @@ void GB_audio_clear_queue(void); unsigned GB_audio_get_frequency(void); size_t GB_audio_get_queue_length(void); void GB_audio_queue_sample(GB_sample_t *sample); -void GB_audio_init(void); +bool GB_audio_init(void); +void GB_audio_deinit(void); +const char *GB_audio_driver_name(void); +const char *GB_audio_driver_name_at_index(unsigned index); -#endif /* sdl_audio_h */ +typedef struct { + typeof(GB_audio_is_playing) *audio_is_playing; + typeof(GB_audio_set_paused) *audio_set_paused; + typeof(GB_audio_clear_queue) *audio_clear_queue; + typeof(GB_audio_get_frequency) *audio_get_frequency; + typeof(GB_audio_get_queue_length) *audio_get_queue_length; + typeof(GB_audio_queue_sample) *audio_queue_sample; + typeof(GB_audio_init) *audio_init; + typeof(GB_audio_deinit) *audio_deinit; + const char *name; +} GB_audio_driver_t; + +#define GB_AUDIO_DRIVER(_name) const GB_audio_driver_t _name##driver = { \ + .audio_is_playing = _audio_is_playing, \ + .audio_set_paused = _audio_set_paused, \ + .audio_clear_queue = _audio_clear_queue, \ + .audio_get_frequency = _audio_get_frequency, \ + .audio_get_queue_length = _audio_get_queue_length, \ + .audio_queue_sample = _audio_queue_sample, \ + .audio_init = _audio_init, \ + .audio_deinit = _audio_deinit, \ + .name = #_name, \ +} + +#define GB_AUDIO_DRIVER_REF(name) ({extern const GB_audio_driver_t name##driver; &name##driver;}) diff --git a/SDL/audio/openal.c b/SDL/audio/openal.c new file mode 100644 index 0000000..fdcaead --- /dev/null +++ b/SDL/audio/openal.c @@ -0,0 +1,317 @@ +#include "audio.h" +#if defined(__APPLE__) +#include +#include + +#else +#include +#include +#endif +#include +#include +#include + +#define BUFFER_LEN_MS 5 + +static ALCdevice *al_device = NULL; +static ALCcontext *al_context = NULL; +static GB_sample_t *audio_buffer = NULL; +static ALuint al_source = 0; +static ALCint sample_rate = 0; +static unsigned buffer_size = 0; +static unsigned buffer_pos = 0; +static bool is_paused = false; + +#define AL_ERR_STRINGIFY(x) #x +#define AL_ERR_TOSTRING(x) AL_ERR_STRINGIFY(x) +#define AL_ERROR(msg) check_al_error(msg, AL_ERR_TOSTRING(__LINE__)) + +// Check if the previous OpenAL call returned an error. +// If an error occurred a message will be logged to stderr. +static bool check_al_error(const char *user_msg, const char *line) +{ + ALCenum error = alGetError(); + const char *description = ""; + + switch (error) { + case AL_NO_ERROR: + return false; + case AL_INVALID_NAME: + description = "A bad name (ID) was passed to an OpenAL function"; + break; + case AL_INVALID_ENUM: + description = "An invalid enum value was passed to an OpenAL function"; + break; + case AL_INVALID_VALUE: + description = "An invalid value was passed to an OpenAL function"; + break; + case AL_INVALID_OPERATION: + description = "The requested operation is not valid"; + break; + case AL_OUT_OF_MEMORY: + description = "The requested operation resulted in OpenAL running out of memory"; + break; + } + + if (user_msg != NULL) { + fprintf(stderr, "[OpenAL:%s] %s: %s\n", line, user_msg, description); + } + else { + fprintf(stderr, "[OpenAL:%s] %s\n", line, description); + } + + return true; +} + +static void _audio_deinit(void) +{ + // Stop the source (this should mark all queued buffers as processed) + alSourceStop(al_source); + + // Check if there are buffers that can be freed + ALint processed; + alGetSourcei(al_source, AL_BUFFERS_PROCESSED, &processed); + if (!AL_ERROR("Failed to query number of processed buffers")) { + // Try to free the buffers, we do not care about potential errors here + while (processed--) { + ALuint buffer; + alSourceUnqueueBuffers(al_source, 1, &buffer); + alDeleteBuffers(1, &buffer); + } + } + + alDeleteSources(1, &al_source); + if (al_context) { + alcDestroyContext(al_context); + al_context = NULL; + } + + if (al_device) { + alcCloseDevice(al_device); + al_device = NULL; + } + + if (audio_buffer) { + free(audio_buffer); + audio_buffer = NULL; + } +} + +static void free_processed_buffers(void) +{ + ALint processed; + alGetSourcei(al_source, AL_BUFFERS_PROCESSED, &processed); + if (AL_ERROR("Failed to query number of processed buffers")) { + return; + } + + while (processed--) { + ALuint buffer; + + alSourceUnqueueBuffers(al_source, 1, &buffer); + if (AL_ERROR("Failed to unqueue buffer")) { + return; + } + + alDeleteBuffers(1, &buffer); + /* Due to a limitation in Apple's OpenAL implementation, this function + can fail once in a few times. If it does, ignore the warning, and let + this buffer be freed in a later call to free_processed_buffers. */ +#if defined(__APPLE__) + if (alGetError()) return; +#else + if (AL_ERROR("Failed to delete buffer")) { + return; + } +#endif + } +} + +static bool _audio_is_playing(void) +{ + ALenum state; + alGetSourcei(al_source, AL_SOURCE_STATE, &state); + if (AL_ERROR("Failed to query source state")) { + return false; + } + + return state == AL_PLAYING; +} + +static void _audio_set_paused(bool paused) +{ + is_paused = paused; + if (paused) { + alSourcePause(al_source); + } + else { + alSourcePlay(al_source); + } +} + +static void _audio_clear_queue(void) +{ + bool is_playing = _audio_is_playing(); + + // Stopping a source clears its queue + alSourceStop(al_source); + if (AL_ERROR(NULL)) { + return; + } + + free_processed_buffers(); + buffer_pos = 0; + + if (is_playing) { + _audio_set_paused(false); + } +} + +static unsigned _audio_get_frequency(void) +{ + return sample_rate; +} + +static size_t _audio_get_queue_length(void) +{ + // Get the number of all attached buffers + ALint buffers; + alGetSourcei(al_source, AL_BUFFERS_QUEUED, &buffers); + if (AL_ERROR("Failed to query number of queued buffers")) { + buffers = 0; + } + + // Get the number of all processed buffers (ready to be detached) + ALint processed; + alGetSourcei(al_source, AL_BUFFERS_PROCESSED, &processed); + if (AL_ERROR("Failed to query number of processed buffers")) { + processed = 0; + } + + return (buffers - processed) * buffer_size + buffer_pos; +} + +static void _audio_queue_sample(GB_sample_t *sample) +{ + if (is_paused) return; + audio_buffer[buffer_pos++] = *sample; + + if (buffer_pos == buffer_size) { + buffer_pos = 0; + + ALuint al_buffer; + alGenBuffers(1, &al_buffer); + if (AL_ERROR("Failed to create audio buffer")) { + return; + } + + alBufferData(al_buffer, AL_FORMAT_STEREO16, audio_buffer, buffer_size * sizeof(GB_sample_t), sample_rate); + if (AL_ERROR("Failed to buffer data")) { + return; + } + + alSourceQueueBuffers(al_source, 1, &al_buffer); + if (AL_ERROR("Failed to queue buffer")) { + return; + } + + // In case of an audio underrun, the source might + // have finished playing all attached buffers + // which means its status will be "AL_STOPPED". + if (!_audio_is_playing()) { + alSourcePlay(al_source); + } + + free_processed_buffers(); + } +} + +static bool _audio_init(void) +{ + // Open the default device + al_device = alcOpenDevice(NULL); + if (!al_device) { + AL_ERROR("Failed to open device"); + return false; + } + + // Create a new audio context without special attributes + al_context = alcCreateContext(al_device, NULL); + if (al_context == NULL) { + AL_ERROR("Failed to create context"); + _audio_deinit(); + return false; + } + + // Enable our audio context + if (!alcMakeContextCurrent(al_context)) { + AL_ERROR("Failed to set context"); + _audio_deinit(); + return false; + } + + // Query the sample rate of the playback device + alcGetIntegerv(al_device, ALC_FREQUENCY, 1, &sample_rate); + if (AL_ERROR("Failed to query sample rate")) { + _audio_deinit(); + return false; + } + if (sample_rate == 0) { + sample_rate = 48000; + } + + // Allocate our working buffer + buffer_size = (sample_rate * BUFFER_LEN_MS) / 1000; + audio_buffer = malloc(buffer_size * sizeof(GB_sample_t)); + if (audio_buffer == NULL) { + fprintf(stderr, "Failed to allocate audio buffer\n"); + _audio_deinit(); + return false; + } + + // Create our playback source + alGenSources(1, &al_source); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Keep the pitch as is + alSourcef(al_source, AL_PITCH, 1); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Keep the volume as is + alSourcef(al_source, AL_GAIN, 1); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Position our source at the center of the 3D space + alSource3f(al_source, AL_POSITION, 0, 0, 0); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Our source is fixed in space + alSource3f(al_source, AL_VELOCITY, 0, 0, 0); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + // Our source does not loop + alSourcei(al_source, AL_LOOPING, AL_FALSE); + if (AL_ERROR(NULL)) { + _audio_deinit(); + return false; + } + + return true; +} + +GB_AUDIO_DRIVER(OpenAL); diff --git a/SDL/audio/sdl.c b/SDL/audio/sdl.c index 12ee69a..9c0cd98 100644 --- a/SDL/audio/sdl.c +++ b/SDL/audio/sdl.c @@ -29,33 +29,33 @@ static SDL_AudioSpec want_aspec, have_aspec; static unsigned buffer_pos = 0; static GB_sample_t audio_buffer[AUDIO_BUFFER_SIZE]; -bool GB_audio_is_playing(void) +static bool _audio_is_playing(void) { return SDL_GetAudioDeviceStatus(device_id) == SDL_AUDIO_PLAYING; } -void GB_audio_set_paused(bool paused) -{ - GB_audio_clear_queue(); - SDL_PauseAudioDevice(device_id, paused); -} - -void GB_audio_clear_queue(void) +static void _audio_clear_queue(void) { SDL_ClearQueuedAudio(device_id); } -unsigned GB_audio_get_frequency(void) +static void _audio_set_paused(bool paused) +{ + _audio_clear_queue(); + SDL_PauseAudioDevice(device_id, paused); +} + +static unsigned _audio_get_frequency(void) { return have_aspec.freq; } -size_t GB_audio_get_queue_length(void) +static size_t _audio_get_queue_length(void) { - return SDL_GetQueuedAudioSize(device_id); + return SDL_GetQueuedAudioSize(device_id) / sizeof(GB_sample_t); } -void GB_audio_queue_sample(GB_sample_t *sample) +static void _audio_queue_sample(GB_sample_t *sample) { audio_buffer[buffer_pos++] = *sample; @@ -65,8 +65,13 @@ void GB_audio_queue_sample(GB_sample_t *sample) } } -void GB_audio_init(void) +static bool _audio_init(void) { + if (SDL_Init(SDL_INIT_AUDIO) != 0) { + printf("Failed to initialize SDL audio: %s", SDL_GetError()); + return false; + } + /* Configure Audio */ memset(&want_aspec, 0, sizeof(want_aspec)); want_aspec.freq = AUDIO_FREQUENCY; @@ -93,4 +98,14 @@ void GB_audio_init(void) #endif device_id = SDL_OpenAudioDevice(0, 0, &want_aspec, &have_aspec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_SAMPLES_CHANGE); + + return true; } + +static void _audio_deinit(void) +{ + _audio_set_paused(true); + SDL_CloseAudioDevice(device_id); +} + +GB_AUDIO_DRIVER(SDL); diff --git a/SDL/audio/xaudio2.c b/SDL/audio/xaudio2.c new file mode 100644 index 0000000..e7fc4f9 --- /dev/null +++ b/SDL/audio/xaudio2.c @@ -0,0 +1,175 @@ +#define COBJMACROS +#include "audio.h" +#include +#ifdef REDIST_XAUDIO +#include +#else +#include +#endif +#include +#include + +// This is a hack, but Windows itself is a hack so I don't care +#define DEFINE_CLSID(className, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ +DEFINE_GUID(CLSID_##className, 0x##l, 0x##w1, 0x##w2, 0x##b1, 0x##b2, 0x##b3, 0x##b4, 0x##b5, 0x##b6, 0x##b7, 0x##b8) + +#define DEFINE_IID(interfaceName, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ +DEFINE_GUID(IID_##interfaceName, 0x##l, 0x##w1, 0x##w2, 0x##b1, 0x##b2, 0x##b3, 0x##b4, 0x##b5, 0x##b6, 0x##b7, 0x##b8) + +DEFINE_CLSID(MMDeviceEnumerator, bcde0395, e52f, 467c, 8e, 3d, c4, 57, 92, 91, 69, 2e); +DEFINE_IID(IMMDeviceEnumerator, a95664d2, 9614, 4f35, a7, 46, de, 8d, b6, 36, 17, e6); + + +static unsigned audio_frequency = 48000; +static IXAudio2 *xaudio2 = NULL; +static IXAudio2MasteringVoice *master_voice = NULL; +static IXAudio2SourceVoice *source_voice = NULL; +static bool playing = false; +static GB_sample_t sample_pool[0x2000]; +static unsigned pos = 0; + +#define BATCH_SIZE 256 + +static WAVEFORMATEX wave_format = { + .wFormatTag = WAVE_FORMAT_PCM, + .nChannels = 2, + .nBlockAlign = 4, + .wBitsPerSample = 16, + .cbSize = 0 +}; + +static bool _audio_is_playing(void) +{ + return playing; +} + +static void _audio_clear_queue(void) +{ + pos = 0; + IXAudio2SourceVoice_FlushSourceBuffers(source_voice); +} + +static void _audio_set_paused(bool paused) +{ + if (paused) { + playing = false; + IXAudio2SourceVoice_Stop(source_voice, 0, XAUDIO2_COMMIT_NOW); + _audio_clear_queue(); + } + else { + playing = true; + IXAudio2SourceVoice_Start(source_voice, 0, XAUDIO2_COMMIT_NOW); + } + +} + + +#define _DEFINE_PROPERTYKEY(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8, pid) static const PROPERTYKEY name = { { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }, pid } +_DEFINE_PROPERTYKEY(_PKEY_AudioEngine_DeviceFormat, 0xf19f064d, 0x82c, 0x4e27, 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c, 0); + + +static void update_frequency(void) +{ + HRESULT hr; + IMMDevice *device = NULL; + IMMDeviceEnumerator *enumerator = NULL; + IPropertyStore *store = NULL; + PWAVEFORMATEX deviceFormatProperties; + PROPVARIANT prop; + + hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, &IID_IMMDeviceEnumerator, (LPVOID *)&enumerator); + if (FAILED(hr)) return; + + // get default audio endpoint + + hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(enumerator, eRender, eMultimedia, &device); + if (FAILED(hr)) return; + + hr = IMMDevice_OpenPropertyStore(device, STGM_READ, &store); + if (FAILED(hr)) return; + + hr = IPropertyStore_GetValue(store, &_PKEY_AudioEngine_DeviceFormat, &prop); + if (FAILED(hr)) return; + + deviceFormatProperties = (PWAVEFORMATEX)prop.blob.pBlobData; + audio_frequency = deviceFormatProperties->nSamplesPerSec; + if (audio_frequency < 8000 || audio_frequency > 192000) { + // Bogus value, revert to 48KHz + audio_frequency = 48000; + } +} + +static unsigned _audio_get_frequency(void) +{ + return audio_frequency; +} + +static size_t _audio_get_queue_length(void) +{ + static XAUDIO2_VOICE_STATE state; + IXAudio2SourceVoice_GetState(source_voice, &state, XAUDIO2_VOICE_NOSAMPLESPLAYED); + + return state.BuffersQueued * BATCH_SIZE + (pos & (BATCH_SIZE - 1)); +} + +static void _audio_queue_sample(GB_sample_t *sample) +{ + if (!playing) return; + + static XAUDIO2_BUFFER buffer = {.AudioBytes = sizeof(*sample) * BATCH_SIZE, }; + sample_pool[pos] = *sample; + buffer.pAudioData = (void *)&sample_pool[pos & ~(BATCH_SIZE - 1)]; + pos++; + pos &= 0x1fff; + if ((pos & (BATCH_SIZE - 1)) == 0) { + IXAudio2SourceVoice_SubmitSourceBuffer(source_voice, &buffer, NULL); + } +} + +static bool _audio_init(void) +{ + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + if (FAILED(hr)) { + fprintf(stderr, "CoInitializeEx failed: %lx\n", hr); + return false; + } + + hr = XAudio2Create(&xaudio2, 0, XAUDIO2_DEFAULT_PROCESSOR); + if (FAILED(hr)) { + fprintf(stderr, "XAudio2Create failed: %lx\n", hr); + return false; + } + + update_frequency(); + + hr = IXAudio2_CreateMasteringVoice(xaudio2, &master_voice, + 2, // 2 channels + audio_frequency, + 0, // Flags + 0, // Device index + NULL, // Effect chain + AudioCategory_GameMedia // Category + ); + if (FAILED(hr)) { + fprintf(stderr, "CreateMasteringVoice failed: %lx\n", hr); + return false; + } + + wave_format.nSamplesPerSec = audio_frequency; + wave_format.nAvgBytesPerSec = audio_frequency * 4; + hr = IXAudio2_CreateSourceVoice(xaudio2, &source_voice, &wave_format, 0, XAUDIO2_DEFAULT_FREQ_RATIO, NULL, NULL, NULL); + + if (FAILED(hr)) { + fprintf(stderr, "CreateSourceVoice failed: %lx\n", hr); + return false; + } + + return true; +} + +static void _audio_deinit(void) +{ + _audio_set_paused(true); +} + +GB_AUDIO_DRIVER(XAudio2); diff --git a/SDL/background.bmp b/SDL/background.bmp index 0f6192d..d356d24 100644 Binary files a/SDL/background.bmp and b/SDL/background.bmp differ diff --git a/SDL/configuration.c b/SDL/configuration.c new file mode 100644 index 0000000..9847a2d --- /dev/null +++ b/SDL/configuration.c @@ -0,0 +1,54 @@ +#include "configuration.h" + +configuration_t configuration = +{ + .keys = { + SDL_SCANCODE_RIGHT, + SDL_SCANCODE_LEFT, + SDL_SCANCODE_UP, + SDL_SCANCODE_DOWN, + SDL_SCANCODE_X, + SDL_SCANCODE_Z, + SDL_SCANCODE_BACKSPACE, + SDL_SCANCODE_RETURN, + SDL_SCANCODE_SPACE + }, + .keys_2 = { + SDL_SCANCODE_TAB, + SDL_SCANCODE_LSHIFT, + }, + .joypad_configuration = { + 13, + 14, + 11, + 12, + 0, + 1, + 9, + 8, + 10, + 4, + -1, + 5, + // The rest are unmapped by default + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }, + .joypad_axises = { + 0, + 1, + }, + .color_correction_mode = GB_COLOR_CORRECTION_MODERN_BALANCED, + .highpass_mode = GB_HIGHPASS_ACCURATE, + .scaling_mode = GB_SDL_SCALING_INTEGER_FACTOR, + .blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE, + .rewind_length = 60 * 2, + .model = MODEL_CGB, + .volume = 100, + .rumble_mode = GB_RUMBLE_CARTRIDGE_ONLY, + .default_scale = 2, + .color_temperature = 10, + .cgb_revision = GB_MODEL_CGB_E - GB_MODEL_CGB_0, + .dmg_palette = 1, // Replacing the old default (0) as of 0.15.2 + .agb_revision = GB_MODEL_AGB_A, +}; diff --git a/SDL/configuration.h b/SDL/configuration.h new file mode 100644 index 0000000..8a47aeb --- /dev/null +++ b/SDL/configuration.h @@ -0,0 +1,137 @@ +#pragma once + +#include +#include +#include "shader.h" + +enum scaling_mode { + GB_SDL_SCALING_ENTIRE_WINDOW, + GB_SDL_SCALING_KEEP_RATIO, + GB_SDL_SCALING_INTEGER_FACTOR, + GB_SDL_SCALING_MAX, +}; + +typedef enum { + JOYPAD_BUTTON_RIGHT, + JOYPAD_BUTTON_LEFT, + JOYPAD_BUTTON_UP, + JOYPAD_BUTTON_DOWN, + JOYPAD_BUTTON_A, + JOYPAD_BUTTON_B, + JOYPAD_BUTTON_SELECT, + JOYPAD_BUTTON_START, + JOYPAD_BUTTON_MENU, + JOYPAD_BUTTON_TURBO, + JOYPAD_BUTTON_REWIND, + JOYPAD_BUTTON_SLOW_MOTION, + JOYPAD_BUTTON_HOTKEY_1, + JOYPAD_BUTTON_HOTKEY_2, + JOYPAD_BUTTONS_MAX +} joypad_button_t; + +typedef enum { + JOYPAD_AXISES_X, + JOYPAD_AXISES_Y, + JOYPAD_AXISES_MAX +} joypad_axis_t; + +typedef enum { + HOTKEY_NONE, + HOTKEY_PAUSE, + HOTKEY_MUTE, + HOTKEY_RESET, + HOTKEY_QUIT, + HOTKEY_SAVE_STATE_1, + HOTKEY_LOAD_STATE_1, + HOTKEY_SAVE_STATE_2, + HOTKEY_LOAD_STATE_2, + HOTKEY_SAVE_STATE_3, + HOTKEY_LOAD_STATE_3, + HOTKEY_SAVE_STATE_4, + HOTKEY_LOAD_STATE_4, + HOTKEY_SAVE_STATE_5, + HOTKEY_LOAD_STATE_5, + HOTKEY_SAVE_STATE_6, + HOTKEY_LOAD_STATE_6, + HOTKEY_SAVE_STATE_7, + HOTKEY_LOAD_STATE_7, + HOTKEY_SAVE_STATE_8, + HOTKEY_LOAD_STATE_8, + HOTKEY_SAVE_STATE_9, + HOTKEY_LOAD_STATE_9, + HOTKEY_SAVE_STATE_10, + HOTKEY_LOAD_STATE_10, + HOTKEY_MAX = HOTKEY_LOAD_STATE_10, +} hotkey_action_t; + +typedef struct { + SDL_Scancode keys[9]; + GB_color_correction_mode_t color_correction_mode; + enum scaling_mode scaling_mode; + uint8_t blending_mode; + + GB_highpass_mode_t highpass_mode; + + bool _deprecated_div_joystick; + bool _deprecated_flip_joystick_bit_1; + bool _deprecated_swap_joysticks_bits_1_and_2; + + char filter[32]; + enum { + MODEL_DMG, + MODEL_CGB, + MODEL_AGB, + MODEL_SGB, + MODEL_MGB, + MODEL_MAX, + } model; + + /* v0.11 */ + uint32_t rewind_length; + SDL_Scancode keys_2[32]; /* Rewind and underclock, + padding for the future */ + uint8_t joypad_configuration[32]; /* 14 Keys + padding for the future*/; + uint8_t joypad_axises[JOYPAD_AXISES_MAX]; + + /* v0.12 */ + enum { + SGB_NTSC, + SGB_PAL, + SGB_2, + SGB_MAX + } sgb_revision; + + /* v0.13 */ + uint8_t dmg_palette; + GB_border_mode_t border_mode; + uint8_t volume; + GB_rumble_mode_t rumble_mode; + + uint8_t default_scale; + + /* v0.14 */ + unsigned padding; + uint8_t color_temperature; + char bootrom_path[4096]; + uint8_t interference_volume; + GB_rtc_mode_t rtc_mode; + + /* v0.14.4 */ + bool osd; + + struct __attribute__((packed, aligned(4))) { + + /* v0.15 */ + bool allow_mouse_controls; + uint8_t cgb_revision; + /* v0.15.1 */ + char audio_driver[16]; + /* v0.15.2 */ + bool allow_background_controllers; + bool gui_pallete_enabled; // Change the GUI palette only once the user changed the DMG palette + char dmg_palette_name[25]; + hotkey_action_t hotkey_actions[2]; + uint16_t agb_revision; + }; +} configuration_t; + +extern configuration_t configuration; diff --git a/SDL/console.c b/SDL/console.c index 4ba6a35..758e877 100644 --- a/SDL/console.c +++ b/SDL/console.c @@ -796,7 +796,7 @@ mainloop(char *(*completer)(const char *substring, uintptr_t *context)) move_word(true); complete_context = completion_length = 0; break; - case MOD_ALT(0x7f): // ALT+Backspace + case MOD_ALT(0x7F): // ALT+Backspace delete_word(false); complete_context = completion_length = 0; break; diff --git a/SDL/font.c b/SDL/font.c index ea2c590..b94dc2d 100644 --- a/SDL/font.c +++ b/SDL/font.c @@ -1119,6 +1119,58 @@ uint8_t font[] = { _, _, X, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + + /* Copyright symbol*/ + _, X, X, X, X, _, + X, _, _, _, _, X, + X, _, X, X, _, X, + X, _, X, _, _, X, + X, _, X, _, _, X, + X, _, X, X, _, X, + X, _, _, _, _, X, + _, X, X, X, X, _, + + /* Alt symbol */ + + _, _, X, X, _, _, + _, X, _, _, X, _, + _, X, _, _, X, _, + _, X, _, _, X, _, + _, X, X, X, X, _, + _, X, _, _, X, _, + _, X, _, _, X, _, + _, _, _, _, _, _, + + X, _, _, X, X, X, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, _, X, _, + _, _, _, _, _, _, + + /* Checkbox */ + + // Unchecked + _, _, _, _, _, _, + X, X, X, X, X, _, + X, X, X, X, X, _, + X, X, X, X, X, _, + X, X, X, X, X, _, + X, X, X, X, X, _, + X, X, X, X, X, _, + _, _, _, _, _, _, + + // Checked + _, _, _, _, _, _, + X, X, X, X, X, _, + X, X, X, X, _, _, + X, X, X, _, X, _, + _, X, _, X, X, _, + X, _, X, X, X, _, + X, X, X, X, X, _, + _, _, _, _, _, _, }; const uint8_t font_max = sizeof(font) / GLYPH_HEIGHT / GLYPH_WIDTH + ' '; diff --git a/SDL/font.h b/SDL/font.h index f2111c3..aec1f4c 100644 --- a/SDL/font.h +++ b/SDL/font.h @@ -1,5 +1,4 @@ -#ifndef font_h -#define font_h +#pragma once #include extern uint8_t font[]; @@ -7,14 +6,16 @@ extern const uint8_t font_max; #define GLYPH_HEIGHT 8 #define GLYPH_WIDTH 6 #define LEFT_ARROW_STRING "\x86" -#define RIGHT_ARROW_STRING "\x7f" +#define RIGHT_ARROW_STRING "\x7F" #define SELECTION_STRING RIGHT_ARROW_STRING #define CTRL_STRING "\x80\x81\x82" #define SHIFT_STRING "\x83" +#define ALT_STRING "\x91\x92" #define CMD_STRING "\x84\x85" #define ELLIPSIS_STRING "\x87" #define MOJIBAKE_STRING "\x88" #define SLIDER_STRING "\x89\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8F\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8B" #define SELECTED_SLIDER_STRING "\x8C\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8E" -#endif /* font_h */ - +#define COPYRIGHT_STRING "\x90" +#define CHECKBOX_OFF_STRING "\x93" +#define CHECKBOX_ON_STRING "\x94" diff --git a/SDL/gui.c b/SDL/gui.c index d5c899b..ad9efae 100644 --- a/SDL/gui.c +++ b/SDL/gui.c @@ -5,9 +5,11 @@ #include #include #include +#include #include "utils.h" #include "gui.h" #include "font.h" +#include "audio/audio.h" static const SDL_Color gui_palette[4] = {{8, 24, 16,}, {57, 97, 57,}, {132, 165, 99}, {198, 222, 140}}; static uint32_t gui_palette_native[4]; @@ -20,6 +22,10 @@ enum pending_command pending_command; unsigned command_parameter; char *dropped_state_file = NULL; +static char **custom_palettes; +static unsigned n_custom_palettes; + + #ifdef __APPLE__ #define MODIFIER_NAME " " CMD_STRING #else @@ -30,6 +36,8 @@ shader_t shader; static SDL_Rect rect; static unsigned factor; +static SDL_Surface *converted_background = NULL; + void render_texture(void *pixels, void *previous) { if (renderer) { @@ -67,57 +75,7 @@ void render_texture(void *pixels, void *previous) } } -configuration_t configuration = -{ - .keys = { - SDL_SCANCODE_RIGHT, - SDL_SCANCODE_LEFT, - SDL_SCANCODE_UP, - SDL_SCANCODE_DOWN, - SDL_SCANCODE_X, - SDL_SCANCODE_Z, - SDL_SCANCODE_BACKSPACE, - SDL_SCANCODE_RETURN, - SDL_SCANCODE_SPACE - }, - .keys_2 = { - SDL_SCANCODE_TAB, - SDL_SCANCODE_LSHIFT, - }, - .joypad_configuration = { - 13, - 14, - 11, - 12, - 0, - 1, - 9, - 8, - 10, - 4, - -1, - 5, - }, - .joypad_axises = { - 0, - 1, - }, - .color_correction_mode = GB_COLOR_CORRECTION_EMULATE_HARDWARE, - .highpass_mode = GB_HIGHPASS_ACCURATE, - .scaling_mode = GB_SDL_SCALING_INTEGER_FACTOR, - .blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE, - .rewind_length = 60 * 2, - .model = MODEL_CGB, - .volume = 100, - .rumble_mode = GB_RUMBLE_ALL_GAMES, - .default_scale = 2, - .color_temperature = 10, -}; - - static const char *help[] = { -"Drop a ROM to play.\n" -"\n" "Keyboard Shortcuts:\n" " Open Menu: Escape\n" " Open ROM: " MODIFIER_NAME "+O\n" @@ -131,7 +89,16 @@ static const char *help[] = { #else " Mute/Unmute: " MODIFIER_NAME "+M\n" #endif -" Break Debugger: " CTRL_STRING "+C" +" Toggle channel: " ALT_STRING "+(1-4)\n" +" Break Debugger: " CTRL_STRING "+C", +"\n" +"SameBoy\n" +"Version " GB_VERSION "\n\n" +"Copyright " COPYRIGHT_STRING " 2015-" GB_COPYRIGHT_YEAR "\n" +"Lior Halphon\n\n" +"Licensed under the Expat\n" +"License, see LICENSE for\n" +"more details." }; void update_viewport(void) @@ -183,6 +150,17 @@ static void draw_char(uint32_t *buffer, unsigned width, unsigned height, unsigne if (ch < ' ' || ch > font_max) { ch = '?'; } + + // Huge dirty hack + if ((signed char)ch == CHECKBOX_ON_STRING[0] || (signed char)ch == CHECKBOX_OFF_STRING[0]) { + if (color == gui_palette_native[3]) { + color = gui_palette_native[0]; + } + else if (color == gui_palette_native[0]) { + color = gui_palette_native[3]; + ch = CHECKBOX_OFF_STRING[0]; + } + } uint8_t *data = &font[(ch - ' ') * GLYPH_WIDTH * GLYPH_HEIGHT]; @@ -250,26 +228,35 @@ void show_osd_text(const char *text) } -enum decoration { - DECORATION_NONE, - DECORATION_SELECTION, - DECORATION_ARROWS, +enum style { + STYLE_LEFT, + STYLE_INDENT, + STYLE_CENTER, + STYLE_SELECTION, + STYLE_ARROWS, }; -static void draw_text_centered(uint32_t *buffer, unsigned width, unsigned height, unsigned y, const char *string, uint32_t color, uint32_t border, enum decoration decoration) +static void draw_styled_text(uint32_t *buffer, unsigned width, unsigned height, unsigned y, const char *string, uint32_t color, uint32_t border, enum style style) { - unsigned x = width / 2 - (unsigned) strlen(string) * GLYPH_WIDTH / 2; + unsigned x = GLYPH_WIDTH * 2 + (width - 160) / 2; + if (style == STYLE_CENTER || style == STYLE_ARROWS) { + x = width / 2 - (unsigned) strlen(string) * GLYPH_WIDTH / 2; + } + else if (style == STYLE_LEFT) { + x = 6 + (width - 160) / 2; + } + draw_text(buffer, width, height, x, y, string, color, border, false); - switch (decoration) { - case DECORATION_SELECTION: + switch (style) { + case STYLE_SELECTION: draw_text(buffer, width, height, x - GLYPH_WIDTH, y, SELECTION_STRING, color, border, false); break; - case DECORATION_ARROWS: + case STYLE_ARROWS: draw_text(buffer, width, height, x - GLYPH_WIDTH, y, LEFT_ARROW_STRING, color, border, false); draw_text(buffer, width, height, width - x, y, RIGHT_ARROW_STRING, color, border, false); break; - case DECORATION_NONE: + default: break; } } @@ -294,8 +281,15 @@ static enum { SHOWING_HELP, WAITING_FOR_KEY, WAITING_FOR_JBUTTON, + TEXT_INPUT, } gui_state; +static char text_input_title[26]; +static char text_input_title2[26]; +static char text_input[26]; + +static void (*text_input_callback)(char ch) = NULL; + static unsigned joypad_configuration_progress = 0; static uint8_t joypad_axis_temp; @@ -311,13 +305,26 @@ static void item_help(unsigned index) gui_state = SHOWING_HELP; } +static void about(unsigned index) +{ + current_help_page = 1; + gui_state = SHOWING_HELP; +} + static void enter_emulation_menu(unsigned index); static void enter_graphics_menu(unsigned index); -static void enter_controls_menu(unsigned index); +static void enter_keyboard_menu(unsigned index); static void enter_joypad_menu(unsigned index); static void enter_audio_menu(unsigned index); +static void enter_controls_menu(unsigned index); +static void enter_help_menu(unsigned index); +static void enter_options_menu(unsigned index); +static void toggle_audio_recording(unsigned index); extern void set_filename(const char *new_filename, typeof(free) *new_free_function); + +static void nop(unsigned index){} + static void open_rom(unsigned index) { char *filename = do_open_rom_dialog(); @@ -327,6 +334,15 @@ static void open_rom(unsigned index) } } +static void cart_swap(unsigned index) +{ + char *filename = do_open_rom_dialog(); + if (filename) { + set_filename(filename, free); + pending_command = GB_SDL_CART_SWAP_COMMAND; + } +} + static void recalculate_menu_height(void) { menu_height = 24; @@ -344,20 +360,36 @@ static void recalculate_menu_height(void) } } -static const struct menu_item paused_menu[] = { - {"Resume", NULL}, - {"Open ROM", open_rom}, - {"Emulation Options", enter_emulation_menu}, - {"Graphic Options", enter_graphics_menu}, - {"Audio Options", enter_audio_menu}, - {"Keyboard", enter_controls_menu}, - {"Joypad", enter_joypad_menu}, - {"Help", item_help}, - {"Quit SameBoy", item_exit}, - {NULL,} -}; +#if SDL_COMPILEDVERSION < 2014 +int SDL_OpenURL(const char *url) +{ + char *string = NULL; +#ifdef __APPLE__ + asprintf(&string, "open '%s'", url); +#else +#ifdef _WIN32 + asprintf(&string, "explorer '%s'", url); +#else + asprintf(&string, "xdg-open '%s'", url); +#endif +#endif + int ret = system(string); + free(string); + return ret; +} +#endif -static const struct menu_item *const nonpaused_menu = &paused_menu[1]; +static char audio_recording_menu_item[] = "Start Audio Recording"; + +static void sponsor(unsigned index) +{ + SDL_OpenURL("https://github.com/sponsors/LIJI32"); +} + +static void debugger_help(unsigned index) +{ + SDL_OpenURL("https://sameboy.github.io/debugger/"); +} static void return_to_root_menu(unsigned index) { @@ -367,6 +399,538 @@ static void return_to_root_menu(unsigned index) recalculate_menu_height(); } +static const struct menu_item options_menu[] = { + {"Emulation Options", enter_emulation_menu}, + {"Graphic Options", enter_graphics_menu}, + {"Audio Options", enter_audio_menu}, + {"Control Options", enter_controls_menu}, + {"Back", return_to_root_menu}, + {NULL,} +}; + +static void enter_options_menu(unsigned index) +{ + current_menu = options_menu; + current_selection = 0; + scroll = 0; + recalculate_menu_height(); +} + +static const GB_cheat_t *current_cheat = NULL; + +extern struct menu_item modify_cheat_menu[]; + +static void save_cheats(void) +{ + extern char *filename; + size_t path_length = strlen(filename); + char cheat_path[path_length + 5]; + replace_extension(filename, path_length, cheat_path, ".cht"); + GB_save_cheats(&gb, cheat_path); +} + +static void rename_callback(char ch) +{ + if (ch == '\b' && text_input[0]) { + text_input[strlen(text_input) - 1] = 0; + return; + } + if (ch == '\n') { + GB_update_cheat(&gb, + current_cheat, + text_input, + current_cheat->address, + current_cheat->bank, + current_cheat->value, + current_cheat->old_value, + current_cheat->use_old_value, + current_cheat->enabled); + modify_cheat_menu[0].string = current_cheat->description; + gui_state = SHOWING_MENU; + save_cheats(); + SDL_StopTextInput(); + return; + } + if (ch < ' ') return; + size_t len = strlen(text_input); + if (len < 21) { + text_input[len] = ch; + text_input[len + 1] = 0; + } +} + +static void rename_cheat(unsigned index) +{ + strcpy(text_input_title, "Rename Cheat"); + text_input_title2[0] = 0; + memcpy(text_input, current_cheat->description, 24); + text_input[24] = 0; + gui_state = TEXT_INPUT; + text_input_callback = rename_callback; + SDL_StartTextInput(); + GB_update_cheat(&gb, + current_cheat, + current_cheat->description, + current_cheat->address, + current_cheat->bank, + current_cheat->value, + current_cheat->old_value, + current_cheat->use_old_value, + current_cheat->enabled); + save_cheats(); +} + + +static void toggle_cheat(unsigned index) +{ + GB_update_cheat(&gb, + current_cheat, + current_cheat->description, + current_cheat->address, + current_cheat->bank, + current_cheat->value, + current_cheat->old_value, + current_cheat->use_old_value, + !current_cheat->enabled); + save_cheats(); +} + +static const char *active_cheat_checkbox(unsigned index) +{ + return current_cheat->enabled? CHECKBOX_ON_STRING : CHECKBOX_OFF_STRING; +} + +static const char *get_cheat_address(unsigned index) +{ + static char ret[12]; + if (current_cheat->bank == GB_CHEAT_ANY_BANK) { + sprintf(ret, "$%04X", current_cheat->address); + } + else { + sprintf(ret, "$%02X:$%04X", current_cheat->bank, current_cheat->address); + } + + return ret; +} + +static void change_cheat_address_callback(char ch) +{ + if (ch == '\b' && text_input[1]) { + size_t len = strlen(text_input); + text_input[len - 1] = 0; + if (text_input[len - 2] == ':') { + text_input[len - 2] = 0; + } + return; + } + if (ch == '\n') { + uint16_t bank = GB_CHEAT_ANY_BANK; + uint16_t address = 0; + + const char *s = text_input + 1; + while (*s) { + if (*s == ':') { + bank = address; + address = 0; + } + else if (*s >= '0' && *s <= '9'){ + address *= 0x10; + address += *s - '0'; + } + else if (*s >= 'A' && *s <= 'F'){ + address *= 0x10; + address += *s - 'A' + 10; + } + s++; + } + + GB_update_cheat(&gb, + current_cheat, + current_cheat->description, + address, + bank, + current_cheat->value, + current_cheat->old_value, + current_cheat->use_old_value, + current_cheat->enabled); + save_cheats(); + gui_state = SHOWING_MENU; + SDL_StopTextInput(); + return; + } + size_t len = strlen(text_input); + if (len >= 12) return; + if (ch == ':' && (len >= 2) && !strchr(text_input, ':')) { + text_input[len] = ':'; + text_input[len + 1] = '$'; + text_input[len + 2] = 0; + return; + } + ch = toupper(ch); + if (!isxdigit(ch)) return; + + unsigned digit_count = 0; + const char *s = text_input + 1; + while (*s) { + if (*s == ':') { + s += 2; + digit_count = 0; + } + else { + s++; + digit_count++; + } + } + + if (digit_count == 4) return; + + text_input[len] = ch; + text_input[len + 1] = 0; +} + +static void change_cheat_address(unsigned index) +{ + strcpy(text_input_title, "Enter Cheat Address"); + text_input_title2[0] = 0; + strcpy(text_input, get_cheat_address(0)); + gui_state = TEXT_INPUT; + text_input_callback = change_cheat_address_callback; + SDL_StartTextInput(); +} + +static const char *get_cheat_value(unsigned index) +{ + static char ret[4]; + sprintf(ret, "$%02X", current_cheat->value); + + return ret; +} + +static void change_cheat_value_callback(char ch) +{ + if (ch == '\b' && text_input[1]) { + size_t len = strlen(text_input); + text_input[len - 1] = 0; + return; + } + if (ch == '\n') { + uint8_t value = 0; + + const char *s = text_input + 1; + while (*s) { + if (*s >= '0' && *s <= '9'){ + value *= 0x10; + value += *s - '0'; + } + else if (*s >= 'A' && *s <= 'F'){ + value *= 0x10; + value += *s - 'A' + 10; + } + s++; + } + + GB_update_cheat(&gb, + current_cheat, + current_cheat->description, + current_cheat->address, + current_cheat->bank, + value, + current_cheat->old_value, + current_cheat->use_old_value, + current_cheat->enabled); + save_cheats(); + gui_state = SHOWING_MENU; + SDL_StopTextInput(); + return; + } + if (!isxdigit(ch)) return; + ch = toupper(ch); + size_t len = strlen(text_input); + if (len == 3) { + text_input[1] = text_input[2]; + text_input[2] = ch; + return; + } + + text_input[len] = ch; + text_input[len + 1] = 0; +} + +static void change_cheat_value(unsigned index) +{ + strcpy(text_input_title, "Enter Cheat Value"); + text_input_title2[0] = 0; + strcpy(text_input, get_cheat_value(0)); + gui_state = TEXT_INPUT; + text_input_callback = change_cheat_value_callback; + SDL_StartTextInput(); +} + +static const char *get_cheat_old_value(unsigned index) +{ + if (!current_cheat->use_old_value) { + return "Any"; + } + static char ret[4]; + sprintf(ret, "$%02X", current_cheat->old_value); + + return ret; +} + +static void change_cheat_old_value_callback(char ch) +{ + if (ch == '\b' && strcmp(text_input, "Any") != 0) { + size_t len = strlen(text_input); + if (len == 2) { + strcpy(text_input, "Any"); + return; + } + text_input[len - 1] = 0; + return; + } + if (ch == '\n') { + uint8_t value = 0; + + bool use_old_value = strcmp(text_input, "Any") != 0; + if (use_old_value) { + const char *s = text_input + 1; + while (*s) { + if (*s >= '0' && *s <= '9'){ + value *= 0x10; + value += *s - '0'; + } + else if (*s >= 'A' && *s <= 'F'){ + value *= 0x10; + value += *s - 'A' + 10; + } + s++; + } + } + + GB_update_cheat(&gb, + current_cheat, + current_cheat->description, + current_cheat->address, + current_cheat->bank, + current_cheat->value, + value, + use_old_value, + current_cheat->enabled); + save_cheats(); + gui_state = SHOWING_MENU; + SDL_StopTextInput(); + return; + } + if (!isxdigit(ch)) return; + ch = toupper(ch); + if (strcmp(text_input, "Any") == 0) { + strcpy(text_input, "$"); + } + size_t len = strlen(text_input); + if (len == 3) { + text_input[1] = text_input[2]; + text_input[2] = ch; + return; + } + + text_input[len] = ch; + text_input[len + 1] = 0; +} + +static void change_cheat_old_value(unsigned index) +{ + strcpy(text_input_title, "Enter Cheat Old Value"); + text_input_title2[0] = 0; + strcpy(text_input, get_cheat_old_value(0)); + gui_state = TEXT_INPUT; + text_input_callback = change_cheat_old_value_callback; + SDL_StartTextInput(); +} + +static void enter_cheats_menu(unsigned index); + +static void delete_cheat(unsigned index) +{ + GB_remove_cheat(&gb, current_cheat); + save_cheats(); + enter_cheats_menu(0); +} + +struct menu_item modify_cheat_menu[] = { + {"", rename_cheat}, + {"Enable", toggle_cheat, active_cheat_checkbox}, + {"Address:", change_cheat_address, get_cheat_address}, + {"Value:", change_cheat_value, get_cheat_value}, + {"Old Value:", change_cheat_old_value, get_cheat_old_value}, + {"Delete Cheat", delete_cheat}, + {"Back", enter_cheats_menu}, + {NULL,} +}; + +static void toggle_cheats(unsigned index) +{ + GB_set_cheats_enabled(&gb, !GB_cheats_enabled(&gb)); +} + +static void add_cheat(unsigned index) +{ + current_cheat = GB_add_cheat(&gb, "New Cheat", 0, 0, 0, 0, false, true); + modify_cheat_menu[0].string = current_cheat->description; + current_menu = modify_cheat_menu; + current_selection = 0; + scroll = 0; + save_cheats(); +} + +static void import_cheat_callback(char ch) +{ + if (ch == '\b' && text_input[0]) { + size_t len = strlen(text_input); + text_input[len - 1] = 0; + return; + } + if (ch == '\n') { + if (!text_input[0]) { + gui_state = SHOWING_MENU; + SDL_StopTextInput(); + return; + } + + current_cheat = GB_import_cheat(&gb, text_input, "Imported Cheat", true); + if (current_cheat) { + gui_state = SHOWING_MENU; + modify_cheat_menu[0].string = current_cheat->description; + current_menu = modify_cheat_menu; + current_selection = 0; + scroll = 0; + save_cheats(); + SDL_StopTextInput(); + return; + } + + strcpy(text_input_title, "Invalid Code."); + strcpy(text_input_title2, "Press Enter to Cancel"); + text_input[0] = 0; + return; + } + if (ch != '-' && !isxdigit(ch)) return; + ch = toupper(ch); + size_t len = strlen(text_input); + if (len >= 12) { + return; + } + + text_input[len] = ch; + text_input[len + 1] = 0; + if (text_input_title[0] != 'E') { + strcpy(text_input_title, "Enter a GameShark"); + strcpy(text_input_title2, "or GameGenie Code"); + } + +} + + +static void import_cheat(unsigned index) +{ + strcpy(text_input_title, "Enter a GameShark"); + strcpy(text_input_title2, "or GameGenie Code"); + text_input[0] = 0; + gui_state = TEXT_INPUT; + text_input_callback = import_cheat_callback; + save_cheats(); + SDL_StartTextInput(); +} + +static void modify_cheat(unsigned index) +{ + const GB_cheat_t *const *cheats = GB_get_cheats(&gb, NULL); + current_cheat = cheats[index - 3]; + modify_cheat_menu[0].string = current_cheat->description; + current_menu = modify_cheat_menu; + current_selection = 0; + scroll = 0; +} + +static const char *checkbox_for_cheat(unsigned index) +{ + const GB_cheat_t *const *cheats = GB_get_cheats(&gb, NULL); + return cheats[index - 3]->enabled? CHECKBOX_ON_STRING : CHECKBOX_OFF_STRING; +} + +static const char *cheats_global_checkbox(unsigned index) +{ + return GB_cheats_enabled(&gb)? CHECKBOX_ON_STRING : CHECKBOX_OFF_STRING; +} + +static void enter_cheats_menu(unsigned index) +{ + struct menu_item *cheats_menu = NULL; + if (cheats_menu) { + free(cheats_menu); + } + size_t cheat_count; + const GB_cheat_t *const *cheats = GB_get_cheats(&gb, &cheat_count); + cheats_menu = calloc(cheat_count + 5, sizeof(struct menu_item)); + cheats_menu[0] = (struct menu_item){"New Cheat", add_cheat}; + cheats_menu[1] = (struct menu_item){"Import Cheat", import_cheat}; + cheats_menu[2] = (struct menu_item){"Enable Cheats", toggle_cheats, cheats_global_checkbox}; + for (size_t i = 0; i < cheat_count; i++) { + cheats_menu[i + 3] = (struct menu_item){cheats[i]->description, modify_cheat, checkbox_for_cheat}; + } + cheats_menu[cheat_count + 3] = (struct menu_item){"Back", return_to_root_menu}; + current_menu = cheats_menu; + current_selection = 0; + scroll = 0; + recalculate_menu_height(); +} + +static const struct menu_item paused_menu[] = { + {"Resume", NULL}, + {"Open ROM", open_rom}, + {"Hot Swap Cartridge", cart_swap}, + {"Options", enter_options_menu}, + {"Cheats", enter_cheats_menu}, + {audio_recording_menu_item, toggle_audio_recording}, + {"Help & About", enter_help_menu}, + {"Sponsor SameBoy", sponsor}, + {"Quit SameBoy", item_exit}, + {NULL,} +}; + +static struct menu_item nonpaused_menu[sizeof(paused_menu) / sizeof(paused_menu[0]) - 3]; + +static void __attribute__((constructor)) build_nonpaused_menu(void) +{ + const struct menu_item *in = paused_menu; + struct menu_item *out = nonpaused_menu; + while (in->string) { + if (in->handler == NULL || in->handler == cart_swap || in->handler == enter_cheats_menu) { + in++; + continue; + } + *out = *in; + out++; + in++; + } +} + +static const struct menu_item help_menu[] = { + {"Shortcuts", item_help}, + {"Debugger Help", debugger_help}, + {"About SameBoy", about}, + {"Back", return_to_root_menu}, + {NULL,} +}; + +static void enter_help_menu(unsigned index) +{ + current_menu = help_menu; + current_selection = 0; + scroll = 0; + recalculate_menu_height(); +} + + static void cycle_model(unsigned index) { @@ -386,12 +950,48 @@ static void cycle_model_backwards(unsigned index) pending_command = GB_SDL_RESET_COMMAND; } -const char *current_model_string(unsigned index) +static const char *current_model_string(unsigned index) { - return (const char *[]){"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy", "Game Boy Pocket"} + return GB_inline_const(const char *[], {"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy", "Game Boy Pocket"}) [configuration.model]; } +static void cycle_cgb_revision(unsigned index) +{ + + if (configuration.cgb_revision == GB_MODEL_CGB_E - GB_MODEL_CGB_0) { + configuration.cgb_revision = 0; + } + else { + configuration.cgb_revision++; + } + pending_command = GB_SDL_RESET_COMMAND; +} + +static void cycle_cgb_revision_backwards(unsigned index) +{ + if (configuration.cgb_revision == 0) { + configuration.cgb_revision = GB_MODEL_CGB_E - GB_MODEL_CGB_0; + } + else { + configuration.cgb_revision--; + } + pending_command = GB_SDL_RESET_COMMAND; +} + +static const char *current_cgb_revision_string(unsigned index) +{ + return GB_inline_const(const char *[], { + "CPU CGB 0 (Exp.)", + "CPU CGB A (Exp.)", + "CPU CGB B (Exp.)", + "CPU CGB C (Exp.)", + "CPU CGB D", + "CPU CGB E", + }) + [configuration.cgb_revision]; +} + static void cycle_sgb_revision(unsigned index) { @@ -411,10 +1011,10 @@ static void cycle_sgb_revision_backwards(unsigned index) pending_command = GB_SDL_RESET_COMMAND; } -const char *current_sgb_revision_string(unsigned index) +static const char *current_sgb_revision_string(unsigned index) { - return (const char *[]){"Super Game Boy NTSC", "Super Game Boy PAL", "Super Game Boy 2"} - [configuration.sgb_revision]; + return GB_inline_const(const char *[], {"Super Game Boy NTSC", "Super Game Boy PAL", "Super Game Boy 2"}) + [configuration.sgb_revision]; } static const uint32_t rewind_lengths[] = {0, 10, 30, 60, 60 * 2, 60 * 5, 60 * 10}; @@ -453,7 +1053,7 @@ static void cycle_rewind_backwards(unsigned index) GB_set_rewind_length(&gb, configuration.rewind_length); } -const char *current_rewind_string(unsigned index) +static const char *current_rewind_string(unsigned index) { for (unsigned i = 0; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]); i++) { if (configuration.rewind_length == rewind_lengths[i]) { @@ -463,7 +1063,7 @@ const char *current_rewind_string(unsigned index) return "Custom"; } -const char *current_bootrom_string(unsigned index) +static const char *current_bootrom_string(unsigned index) { if (!configuration.bootrom_path[0]) { return "Built-in Boot ROMs"; @@ -508,7 +1108,7 @@ static void toggle_rtc_mode(unsigned index) configuration.rtc_mode = !configuration.rtc_mode; } -const char *current_rtc_mode_string(unsigned index) +static const char *current_rtc_mode_string(unsigned index) { switch (configuration.rtc_mode) { case GB_RTC_MODE_SYNC_TO_HOST: return "Sync to System Clock"; @@ -517,13 +1117,30 @@ const char *current_rtc_mode_string(unsigned index) return ""; } +static void cycle_agb_revision(unsigned index) +{ + + configuration.agb_revision ^= GB_MODEL_GBP_BIT; + pending_command = GB_SDL_RESET_COMMAND; +} + +static const char *current_agb_revision_string(unsigned index) +{ + if (configuration.agb_revision == GB_MODEL_GBP_A) { + return "CPU AGB A (GBP)"; + } + return "CPU AGB A (AGB)"; +} + static const struct menu_item emulation_menu[] = { {"Emulated Model:", cycle_model, current_model_string, cycle_model_backwards}, {"SGB Revision:", cycle_sgb_revision, current_sgb_revision_string, cycle_sgb_revision_backwards}, + {"GBC Revision:", cycle_cgb_revision, current_cgb_revision_string, cycle_cgb_revision_backwards}, + {"GBA Revision:", cycle_agb_revision, current_agb_revision_string, cycle_agb_revision}, {"Boot ROMs Folder:", toggle_bootrom, current_bootrom_string, toggle_bootrom}, {"Rewind Length:", cycle_rewind, current_rewind_string, cycle_rewind_backwards}, {"Real Time Clock:", toggle_rtc_mode, current_rtc_mode_string, toggle_rtc_mode}, - {"Back", return_to_root_menu}, + {"Back", enter_options_menu}, {NULL,} }; @@ -535,21 +1152,21 @@ static void enter_emulation_menu(unsigned index) recalculate_menu_height(); } -const char *current_scaling_mode(unsigned index) +static const char *current_scaling_mode(unsigned index) { - return (const char *[]){"Fill Entire Window", "Retain Aspect Ratio", "Retain Integer Factor"} + return GB_inline_const(const char *[], {"Fill Entire Window", "Retain Aspect Ratio", "Retain Integer Factor"}) [configuration.scaling_mode]; } -const char *current_default_scale(unsigned index) +static const char *current_default_scale(unsigned index) { - return (const char *[]){"1x", "2x", "3x", "4x", "5x", "6x", "7x", "8x"} + return GB_inline_const(const char *[], {"1x", "2x", "3x", "4x", "5x", "6x", "7x", "8x"}) [configuration.default_scale - 1]; } const char *current_color_correction_mode(unsigned index) { - return (const char *[]){"Disabled", "Correct Color Curves", "Emulate Hardware", "Preserve Brightness", "Reduce Contrast", "Harsh Reality"} + return GB_inline_const(const char *[], {"Disabled", "Correct Color Curves", "Modern - Balanced", "Modern - Boost Contrast", "Reduce Contrast", "Harsh Reality", "Modern - Accurate"}) [configuration.color_correction_mode]; } @@ -564,17 +1181,20 @@ const char *current_color_temperature(unsigned index) const char *current_palette(unsigned index) { - return (const char *[]){"Greyscale", "Lime (Game Boy)", "Olive (Pocket)", "Teal (Light)"} + if (configuration.dmg_palette == 4) { + return configuration.dmg_palette_name; + } + return GB_inline_const(const char *[], {"Greyscale", "Lime (Game Boy)", "Olive (Pocket)", "Teal (Light)"}) [configuration.dmg_palette]; } const char *current_border_mode(unsigned index) { - return (const char *[]){"SGB Only", "Never", "Always"} + return GB_inline_const(const char *[], {"SGB Only", "Never", "Always"}) [configuration.border_mode]; } -void cycle_scaling(unsigned index) +static void cycle_scaling(unsigned index) { configuration.scaling_mode++; if (configuration.scaling_mode == GB_SDL_SCALING_MAX) { @@ -584,7 +1204,7 @@ void cycle_scaling(unsigned index) render_texture(NULL, NULL); } -void cycle_scaling_backwards(unsigned index) +static void cycle_scaling_backwards(unsigned index) { if (configuration.scaling_mode == 0) { configuration.scaling_mode = GB_SDL_SCALING_MAX - 1; @@ -596,7 +1216,7 @@ void cycle_scaling_backwards(unsigned index) render_texture(NULL, NULL); } -void cycle_default_scale(unsigned index) +static void cycle_default_scale(unsigned index) { if (configuration.default_scale == GB_SDL_DEFAULT_SCALE_MAX) { configuration.default_scale = 1; @@ -609,7 +1229,7 @@ void cycle_default_scale(unsigned index) update_viewport(); } -void cycle_default_scale_backwards(unsigned index) +static void cycle_default_scale_backwards(unsigned index) { if (configuration.default_scale == 1) { configuration.default_scale = GB_SDL_DEFAULT_SCALE_MAX; @@ -627,6 +1247,12 @@ static void cycle_color_correction(unsigned index) if (configuration.color_correction_mode == GB_COLOR_CORRECTION_LOW_CONTRAST) { configuration.color_correction_mode = GB_COLOR_CORRECTION_DISABLED; } + else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_BALANCED) { + configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_ACCURATE; + } + else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_ACCURATE) { + configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST; + } else { configuration.color_correction_mode++; } @@ -637,6 +1263,12 @@ static void cycle_color_correction_backwards(unsigned index) if (configuration.color_correction_mode == GB_COLOR_CORRECTION_DISABLED) { configuration.color_correction_mode = GB_COLOR_CORRECTION_LOW_CONTRAST; } + else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_ACCURATE) { + configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_BALANCED; + } + else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST) { + configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_ACCURATE; + } else { configuration.color_correction_mode--; } @@ -656,24 +1288,125 @@ static void increase_color_temperature(unsigned index) } } +const GB_palette_t *current_dmg_palette(void) +{ + typedef struct __attribute__ ((packed)) { + uint32_t magic; + uint8_t flags; + struct GB_color_s colors[5]; + int32_t brightness_bias; + uint32_t hue_bias; + uint32_t hue_bias_strength; + } theme_t; + + static theme_t theme; + + if (configuration.dmg_palette == 4) { + char *path = resource_path("Palettes"); + sprintf(path + strlen(path), "/%s.sbp", configuration.dmg_palette_name); + FILE *file = fopen(path, "rb"); + if (!file) return &GB_PALETTE_GREY; + memset(&theme, 0, sizeof(theme)); + fread(&theme, sizeof(theme), 1, file); + fclose(file); +#ifdef GB_BIG_ENDIAN + theme.magic = __builtin_bswap32(theme.magic); +#endif + if (theme.magic != 'SBPL') return &GB_PALETTE_GREY; + return (GB_palette_t *)&theme.colors; + } + + switch (configuration.dmg_palette) { + case 1: return &GB_PALETTE_DMG; + case 2: return &GB_PALETTE_MGB; + case 3: return &GB_PALETTE_GBL; + default: return &GB_PALETTE_GREY; + } +} + +static void update_gui_palette(void) +{ + const GB_palette_t *palette = current_dmg_palette(); + + SDL_Color colors[4]; + for (unsigned i = 4; i--; ) { + gui_palette_native[i] = SDL_MapRGB(pixel_format, palette->colors[i].r, palette->colors[i].g, palette->colors[i].b); + colors[i].r = palette->colors[i].r; + colors[i].g = palette->colors[i].g; + colors[i].b = palette->colors[i].b; + } + + SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp")); + + /* Create a blank background if background.bmp could not be loaded */ + if (!background) { + background = SDL_CreateRGBSurface(0, 160, 144, 8, 0, 0, 0, 0); + } + SDL_SetPaletteColors(background->format->palette, colors, 0, 4); + converted_background = SDL_ConvertSurface(background, pixel_format, 0); + SDL_FreeSurface(background); +} + static void cycle_palette(unsigned index) { if (configuration.dmg_palette == 3) { - configuration.dmg_palette = 0; + if (n_custom_palettes == 0) { + configuration.dmg_palette = 0; + } + else { + configuration.dmg_palette = 4; + strcpy(configuration.dmg_palette_name, custom_palettes[0]); + } + } + else if (configuration.dmg_palette == 4) { + for (unsigned i = 0; i < n_custom_palettes; i++) { + if (strcmp(custom_palettes[i], configuration.dmg_palette_name) == 0) { + if (i == n_custom_palettes - 1) { + configuration.dmg_palette = 0; + } + else { + strcpy(configuration.dmg_palette_name, custom_palettes[i + 1]); + } + break; + } + } } else { configuration.dmg_palette++; } + configuration.gui_pallete_enabled = true; + update_gui_palette(); } static void cycle_palette_backwards(unsigned index) { if (configuration.dmg_palette == 0) { - configuration.dmg_palette = 3; + if (n_custom_palettes == 0) { + configuration.dmg_palette = 3; + } + else { + configuration.dmg_palette = 4; + strcpy(configuration.dmg_palette_name, custom_palettes[n_custom_palettes - 1]); + } + } + else if (configuration.dmg_palette == 4) { + for (unsigned i = 0; i < n_custom_palettes; i++) { + if (strcmp(custom_palettes[i], configuration.dmg_palette_name) == 0) { + if (i == 0) { + configuration.dmg_palette = 3; + } + else { + strcpy(configuration.dmg_palette_name, custom_palettes[i - 1]); + } + break; + } + } } else { configuration.dmg_palette--; } + configuration.gui_pallete_enabled = true; + update_gui_palette(); } static void cycle_border_mode(unsigned index) @@ -805,8 +1538,8 @@ static void cycle_blending_mode_backwards(unsigned index) static const char *blending_mode_string(unsigned index) { if (!uses_gl()) return "Requires OpenGL 3.2+"; - return (const char *[]){"Disabled", "Simple", "Accurate"} - [configuration.blending_mode]; + return GB_inline_const(const char *[], {"Disabled", "Simple", "Accurate"}) + [configuration.blending_mode]; } static void toggle_osd(unsigned index) @@ -830,8 +1563,7 @@ static const struct menu_item graphics_menu[] = { {"Mono Palette:", cycle_palette, current_palette, cycle_palette_backwards}, {"Display Border:", cycle_border_mode, current_border_mode, cycle_border_mode_backwards}, {"On-Screen Display:", toggle_osd, current_osd_mode, toggle_osd}, - - {"Back", return_to_root_menu}, + {"Back", enter_options_menu}, {NULL,} }; @@ -843,13 +1575,13 @@ static void enter_graphics_menu(unsigned index) recalculate_menu_height(); } -const char *highpass_filter_string(unsigned index) +static const char *highpass_filter_string(unsigned index) { - return (const char *[]){"None (Keep DC Offset)", "Accurate", "Preserve Waveform"} + return GB_inline_const(const char *[], {"None (Keep DC Offset)", "Accurate", "Preserve Waveform"}) [configuration.highpass_mode]; } -void cycle_highpass_filter(unsigned index) +static void cycle_highpass_filter(unsigned index) { configuration.highpass_mode++; if (configuration.highpass_mode == GB_HIGHPASS_MAX) { @@ -857,7 +1589,7 @@ void cycle_highpass_filter(unsigned index) } } -void cycle_highpass_filter_backwards(unsigned index) +static void cycle_highpass_filter_backwards(unsigned index) { if (configuration.highpass_mode == 0) { configuration.highpass_mode = GB_HIGHPASS_MAX - 1; @@ -867,14 +1599,14 @@ void cycle_highpass_filter_backwards(unsigned index) } } -const char *volume_string(unsigned index) +static const char *volume_string(unsigned index) { static char ret[5]; sprintf(ret, "%d%%", configuration.volume); return ret; } -void increase_volume(unsigned index) +static void increase_volume(unsigned index) { configuration.volume += 5; if (configuration.volume > 100) { @@ -882,7 +1614,7 @@ void increase_volume(unsigned index) } } -void decrease_volume(unsigned index) +static void decrease_volume(unsigned index) { configuration.volume -= 5; if (configuration.volume > 100) { @@ -890,14 +1622,14 @@ void decrease_volume(unsigned index) } } -const char *interference_volume_string(unsigned index) +static const char *interference_volume_string(unsigned index) { static char ret[5]; sprintf(ret, "%d%%", configuration.interference_volume); return ret; } -void increase_interference_volume(unsigned index) +static void increase_interference_volume(unsigned index) { configuration.interference_volume += 5; if (configuration.interference_volume > 100) { @@ -905,7 +1637,7 @@ void increase_interference_volume(unsigned index) } } -void decrease_interference_volume(unsigned index) +static void decrease_interference_volume(unsigned index) { configuration.interference_volume -= 5; if (configuration.interference_volume > 100) { @@ -913,14 +1645,89 @@ void decrease_interference_volume(unsigned index) } } -static const struct menu_item audio_menu[] = { +static const char *audio_driver_string(unsigned index) +{ + return GB_audio_driver_name(); +} + +static const char *preferred_audio_driver_string(unsigned index) +{ + if (configuration.audio_driver[0] == 0) { + return "Auto"; + } + return configuration.audio_driver; +} + +static void audio_driver_changed(void); + +static void cycle_prefrered_audio_driver(unsigned index) +{ + audio_driver_changed(); + if (configuration.audio_driver[0] == 0) { + strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(0)); + return; + } + unsigned i = 0; + while (true) { + const char *name = GB_audio_driver_name_at_index(i); + if (name[0] == 0) { // Not a supported driver? Switch to auto + configuration.audio_driver[0] = 0; + return; + } + if (strcmp(configuration.audio_driver, name) == 0) { + strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i + 1)); + return; + } + i++; + } +} + +static void cycle_preferred_audio_driver_backwards(unsigned index) +{ + audio_driver_changed(); + if (configuration.audio_driver[0] == 0) { + unsigned i = 0; + while (true) { + const char *name = GB_audio_driver_name_at_index(i); + if (name[0] == 0) { + strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i - 1)); + return; + } + i++; + } + return; + } + unsigned i = 0; + while (true) { + const char *name = GB_audio_driver_name_at_index(i); + if (name[0] == 0) { // Not a supported driver? Switch to auto + configuration.audio_driver[0] = 0; + return; + } + if (strcmp(configuration.audio_driver, name) == 0) { + strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i - 1)); + return; + } + i++; + } +} + +static struct menu_item audio_menu[] = { {"Highpass Filter:", cycle_highpass_filter, highpass_filter_string, cycle_highpass_filter_backwards}, {"Volume:", increase_volume, volume_string, decrease_volume}, {"Interference Volume:", increase_interference_volume, interference_volume_string, decrease_interference_volume}, - {"Back", return_to_root_menu}, + {"Preferred Audio Driver:", cycle_prefrered_audio_driver, preferred_audio_driver_string, cycle_preferred_audio_driver_backwards}, + {"Active Driver:", nop, audio_driver_string}, + {"Back", enter_options_menu}, {NULL,} }; +static void audio_driver_changed(void) +{ + audio_menu[4].value_getter = NULL; + audio_menu[4].string = "Relaunch to apply"; +} + static void enter_audio_menu(unsigned index) { current_menu = audio_menu; @@ -936,7 +1743,7 @@ static void modify_key(unsigned index) static const char *key_name(unsigned index); -static const struct menu_item controls_menu[] = { +static const struct menu_item keyboard_menu[] = { {"Right:", modify_key, key_name,}, {"Left:", modify_key, key_name,}, {"Up:", modify_key, key_name,}, @@ -948,7 +1755,7 @@ static const struct menu_item controls_menu[] = { {"Turbo:", modify_key, key_name,}, {"Rewind:", modify_key, key_name,}, {"Slow-Motion:", modify_key, key_name,}, - {"Back", return_to_root_menu}, + {"Back", enter_controls_menu}, {NULL,} }; @@ -960,20 +1767,20 @@ static const char *key_name(unsigned index) return SDL_GetScancodeName(configuration.keys[index]); } -static void enter_controls_menu(unsigned index) +static void enter_keyboard_menu(unsigned index) { - current_menu = controls_menu; + current_menu = keyboard_menu; current_selection = 0; scroll = 0; recalculate_menu_height(); } static unsigned joypad_index = 0; -static SDL_Joystick *joystick = NULL; static SDL_GameController *controller = NULL; SDL_Haptic *haptic = NULL; +SDL_Joystick *joystick = NULL; -const char *current_joypad_name(unsigned index) +static const char *current_joypad_name(unsigned index) { static char name[23] = {0,}; const char *orig_name = joystick? SDL_JoystickName(joystick) : NULL; @@ -1023,7 +1830,8 @@ static void cycle_joypads(unsigned index) } if (joystick) { haptic = SDL_HapticOpenFromJoystick(joystick); - }} + } +} static void cycle_joypads_backwards(unsigned index) { @@ -1082,17 +1890,84 @@ static void cycle_rumble_mode_backwards(unsigned index) } } -const char *current_rumble_mode(unsigned index) +static const char *current_rumble_mode(unsigned index) { - return (const char *[]){"Disabled", "Rumble Game Paks Only", "All Games"} - [configuration.rumble_mode]; + return GB_inline_const(const char *[], {"Disabled", "Rumble Game Paks Only", "All Games"}) + [configuration.rumble_mode]; +} + +static void toggle_allow_background_controllers(unsigned index) +{ + configuration.allow_background_controllers ^= true; + + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, + configuration.allow_background_controllers? "1" : "0"); +} + +static const char *current_background_control_mode(unsigned index) +{ + return configuration.allow_background_controllers? "Always" : "During Window Focus Only"; +} + +static void cycle_hotkey(unsigned index) +{ + if (configuration.hotkey_actions[index - 2] == HOTKEY_MAX) { + configuration.hotkey_actions[index - 2] = 0; + } + else { + configuration.hotkey_actions[index - 2]++; + } +} + +static void cycle_hotkey_backwards(unsigned index) +{ + if (configuration.hotkey_actions[index - 2] == 0) { + configuration.hotkey_actions[index - 2] = HOTKEY_MAX; + } + else { + configuration.hotkey_actions[index - 2]--; + } +} + +static const char *current_hotkey(unsigned index) +{ + return GB_inline_const(const char *[], { + "None", + "Toggle Pause", + "Toggle Mute", + "Reset", + "Quit SameBoy", + "Save State Slot 1", + "Load State Slot 1", + "Save State Slot 2", + "Load State Slot 2", + "Save State Slot 3", + "Load State Slot 3", + "Save State Slot 4", + "Load State Slot 4", + "Save State Slot 5", + "Load State Slot 5", + "Save State Slot 6", + "Load State Slot 6", + "Save State Slot 7", + "Load State Slot 7", + "Save State Slot 8", + "Load State Slot 8", + "Save State Slot 9", + "Load State Slot 9", + "Save State Slot 10", + "Load State Slot 10", + }) [configuration.hotkey_actions[index - 2]]; } static const struct menu_item joypad_menu[] = { {"Joypad:", cycle_joypads, current_joypad_name, cycle_joypads_backwards}, {"Configure layout", detect_joypad_layout}, + {"Hotkey 1 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards}, + {"Hotkey 2 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards}, {"Rumble Mode:", cycle_rumble_mode, current_rumble_mode, cycle_rumble_mode_backwards}, - {"Back", return_to_root_menu}, + {"Enable Control:", toggle_allow_background_controllers, current_background_control_mode, toggle_allow_background_controllers}, + {"Back", enter_controls_menu}, {NULL,} }; @@ -1151,28 +2026,150 @@ void connect_joypad(void) } } +static void toggle_mouse_control(unsigned index) +{ + configuration.allow_mouse_controls = !configuration.allow_mouse_controls; +} + +static const char *mouse_control_string(unsigned index) +{ + return configuration.allow_mouse_controls? "Allow mouse control" : "Disallow mouse control"; +} + +static const struct menu_item controls_menu[] = { + {"Keyboard Options", enter_keyboard_menu}, + {"Joypad Options", enter_joypad_menu}, + {"Motion-controlled games:", toggle_mouse_control, mouse_control_string, toggle_mouse_control}, + {"Back", enter_options_menu}, + {NULL,} +}; + +static void enter_controls_menu(unsigned index) +{ + current_menu = controls_menu; + current_selection = 0; + scroll = 0; + recalculate_menu_height(); +} + +static void toggle_audio_recording(unsigned index) +{ + if (!GB_is_inited(&gb)) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", "Cannot start audio recording, open a ROM file first.", window); + return; + } + static bool is_recording = false; + if (is_recording) { + is_recording = false; + show_osd_text("Audio recording ended"); + int error = GB_stop_audio_recording(&gb); + if (error) { + char *message = NULL; + asprintf(&message, "Could not finalize recording: %s", strerror(error)); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window); + free(message); + } + static const char item_string[] = "Start Audio Recording"; + memcpy(audio_recording_menu_item, item_string, sizeof(item_string)); + return; + } + char *filename = do_save_recording_dialog(GB_get_sample_rate(&gb)); + + /* Drop events as it SDL seems to catch several in-dialog events */ + SDL_Event event; + while (SDL_PollEvent(&event)); + + if (filename) { + GB_audio_format_t format = GB_AUDIO_FORMAT_RAW; + size_t length = strlen(filename); + if (length >= 5) { + if (strcasecmp(".aiff", filename + length - 5) == 0) { + format = GB_AUDIO_FORMAT_AIFF; + } + else if (strcasecmp(".aifc", filename + length - 5) == 0) { + format = GB_AUDIO_FORMAT_AIFF; + } + else if (length >= 4) { + if (strcasecmp(".aif", filename + length - 4) == 0) { + format = GB_AUDIO_FORMAT_AIFF; + } + else if (strcasecmp(".wav", filename + length - 4) == 0) { + format = GB_AUDIO_FORMAT_WAV; + } + } + } + + int error = GB_start_audio_recording(&gb, filename, format); + free(filename); + if (error) { + char *message = NULL; + asprintf(&message, "Could not finalize recording: %s", strerror(error)); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window); + free(message); + return; + } + + is_recording = true; + static const char item_string[] = "Stop Audio Recording"; + memcpy(audio_recording_menu_item, item_string, sizeof(item_string)); + show_osd_text("Audio recording started"); + } +} + +void convert_mouse_coordinates(signed *x, signed *y) +{ + signed width = GB_get_screen_width(&gb); + signed height = GB_get_screen_height(&gb); + signed x_offset = (width - 160) / 2; + signed y_offset = (height - 144) / 2; + + *x = (signed)(*x - rect.x / factor) * width / (signed)(rect.w / factor) - x_offset; + *y = (signed)(*y - rect.y / factor) * height / (signed)(rect.h / factor) - y_offset; + + if (strcmp("CRT", configuration.filter) == 0) { + *y = *y * 8 / 7; + *y -= 144 / 16; + } +} + +void update_swap_interval(void) +{ + SDL_DisplayMode mode; + SDL_GetCurrentDisplayMode(SDL_GetWindowDisplayIndex(window), &mode); + if (mode.refresh_rate >= 60) { + if (SDL_GL_SetSwapInterval(1)) { + SDL_GL_SetSwapInterval(0); + } + } + else { + SDL_GL_SetSwapInterval(0); + } +} + void run_gui(bool is_running) { SDL_ShowCursor(SDL_ENABLE); connect_joypad(); /* Draw the background screen */ - static SDL_Surface *converted_background = NULL; if (!converted_background) { - SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp")); - - /* Create a blank background if background.bmp could not be loaded */ - if (!background) { - background = SDL_CreateRGBSurface(0, 160, 144, 8, 0, 0, 0, 0); + if (configuration.gui_pallete_enabled) { + update_gui_palette(); } - - SDL_SetPaletteColors(background->format->palette, gui_palette, 0, 4); - converted_background = SDL_ConvertSurface(background, pixel_format, 0); - SDL_LockSurface(converted_background); - SDL_FreeSurface(background); - - for (unsigned i = 4; i--; ) { - gui_palette_native[i] = SDL_MapRGB(pixel_format, gui_palette[i].r, gui_palette[i].g, gui_palette[i].b); + else { + SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp")); + + /* Create a blank background if background.bmp could not be loaded */ + if (!background) { + background = SDL_CreateRGBSurface(0, 160, 144, 8, 0, 0, 0, 0); + } + SDL_SetPaletteColors(background->format->palette, gui_palette, 0, 4); + converted_background = SDL_ConvertSurface(background, pixel_format, 0); + SDL_FreeSurface(background); + + for (unsigned i = 4; i--; ) { + gui_palette_native[i] = SDL_MapRGB(pixel_format, gui_palette[i].r, gui_palette[i].g, gui_palette[i].b); + } } } @@ -1195,13 +2192,46 @@ void run_gui(bool is_running) recalculate_menu_height(); current_selection = 0; scroll = 0; - do { + + bool scrollbar_drag = false; + signed scroll_mouse_start = 0; + signed scroll_start = 0; + while (true) { + SDL_WaitEvent(&event); /* Convert Joypad and mouse events (We only generate down events) */ - if (gui_state != WAITING_FOR_KEY && gui_state != WAITING_FOR_JBUTTON) { + if (gui_state != WAITING_FOR_KEY && gui_state != WAITING_FOR_JBUTTON && gui_state != TEXT_INPUT) { switch (event.type) { + case SDL_KEYDOWN: + if (gui_state == WAITING_FOR_KEY) break; + if (event.key.keysym.mod != 0) break; + switch (event.key.keysym.scancode) { + // Do not remap these keys to prevent deadlocking + case SDL_SCANCODE_ESCAPE: + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_RIGHT: + case SDL_SCANCODE_LEFT: + case SDL_SCANCODE_UP: + case SDL_SCANCODE_DOWN: + break; + + default: + if (event.key.keysym.scancode == configuration.keys[GB_KEY_RIGHT]) event.key.keysym.scancode = SDL_SCANCODE_RIGHT; + else if (event.key.keysym.scancode == configuration.keys[GB_KEY_LEFT]) event.key.keysym.scancode = SDL_SCANCODE_LEFT; + else if (event.key.keysym.scancode == configuration.keys[GB_KEY_UP]) event.key.keysym.scancode = SDL_SCANCODE_UP; + else if (event.key.keysym.scancode == configuration.keys[GB_KEY_DOWN]) event.key.keysym.scancode = SDL_SCANCODE_DOWN; + else if (event.key.keysym.scancode == configuration.keys[GB_KEY_A]) event.key.keysym.scancode = SDL_SCANCODE_RETURN; + else if (event.key.keysym.scancode == configuration.keys[GB_KEY_START]) event.key.keysym.scancode = SDL_SCANCODE_RETURN; + else if (event.key.keysym.scancode == configuration.keys[GB_KEY_B]) event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; + break; + } + break; + case SDL_WINDOWEVENT: should_render = true; break; + case SDL_MOUSEBUTTONUP: + scrollbar_drag = false; + break; case SDL_MOUSEBUTTONDOWN: if (gui_state == SHOWING_HELP) { event.type = SDL_KEYDOWN; @@ -1212,12 +2242,25 @@ void run_gui(bool is_running) event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; } else if (gui_state == SHOWING_MENU) { - signed x = (event.button.x - rect.x / factor) * width / (rect.w / factor) - x_offset; - signed y = (event.button.y - rect.y / factor) * height / (rect.h / factor) - y_offset; - - if (strcmp("CRT", configuration.filter) == 0) { - y = y * 8 / 7; - y -= 144 / 16; + signed x = event.button.x; + signed y = event.button.y; + convert_mouse_coordinates(&x, &y); + if (x >= 160 - 6 && x < 160 && menu_height > 144) { + unsigned scrollbar_offset = (140 - scrollbar_size) * scroll / (menu_height - 144); + if (scrollbar_offset + scrollbar_size > 140) { + scrollbar_offset = 140 - scrollbar_size; + } + + if (y < scrollbar_offset || y > scrollbar_offset + scrollbar_size) { + scroll = (menu_height - 144) * y / 143; + should_render = true; + } + + scrollbar_drag = true; + mouse_scroling = true; + scroll_mouse_start = y; + scroll_start = scroll; + break; } y += scroll; @@ -1261,7 +2304,7 @@ void run_gui(bool is_running) if (button == JOYPAD_BUTTON_A) { event.key.keysym.scancode = SDL_SCANCODE_RETURN; } - else if (button == JOYPAD_BUTTON_MENU) { + else if (button == JOYPAD_BUTTON_MENU || button == JOYPAD_BUTTON_B) { event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; } else if (button == JOYPAD_BUTTON_UP) event.key.keysym.scancode = SDL_SCANCODE_UP; @@ -1326,6 +2369,9 @@ void run_gui(bool is_running) } } switch (event.type) { + case SDL_DISPLAYEVENT: + update_swap_interval(); + break; case SDL_QUIT: { if (!is_running) { exit(0); @@ -1341,6 +2387,13 @@ void run_gui(bool is_running) update_viewport(); render_texture(NULL, NULL); } + if (event.window.type == SDL_WINDOWEVENT_MOVED +#if SDL_COMPILEDVERSION > 2018 + || event.window.type == SDL_WINDOWEVENT_DISPLAY_CHANGED +#endif + ) { + update_swap_interval(); + } break; } case SDL_DROPFILE: { @@ -1360,14 +2413,23 @@ void run_gui(bool is_running) return; } } - case SDL_JOYBUTTONDOWN: - { + case SDL_JOYBUTTONDOWN: { if (gui_state == WAITING_FOR_JBUTTON && joypad_configuration_progress != JOYPAD_BUTTONS_MAX) { should_render = true; configuration.joypad_configuration[joypad_configuration_progress++] = event.jbutton.button; } break; } + case SDL_JOYHATMOTION: { + if (gui_state == WAITING_FOR_JBUTTON && joypad_configuration_progress == JOYPAD_BUTTON_RIGHT) { + should_render = true; + configuration.joypad_configuration[joypad_configuration_progress++] = -1; + configuration.joypad_configuration[joypad_configuration_progress++] = -1; + configuration.joypad_configuration[joypad_configuration_progress++] = -1; + configuration.joypad_configuration[joypad_configuration_progress++] = -1; + } + break; + } case SDL_JOYAXISMOTION: { if (gui_state == WAITING_FOR_JBUTTON && @@ -1409,9 +2471,57 @@ void run_gui(bool is_running) break; } + case SDL_MOUSEMOTION: { + if (scrollbar_drag && scrollbar_size < 140 && scrollbar_size > 0) { + signed x = event.motion.x; + signed y = event.motion.y; + convert_mouse_coordinates(&x, &y); + signed delta = scroll_mouse_start - y; + scroll = scroll_start - delta * (signed)(menu_height - 144) / (signed)(140 - scrollbar_size); + if (scroll < 0) { + scroll = 0; + } + if (scroll >= menu_height - 144) { + scroll = menu_height - 144; + } + should_render = true; + } + break; + } + + + case SDL_TEXTINPUT: + if (gui_state == TEXT_INPUT) { + char *s = event.text.text; + while (*s) { + text_input_callback(*(s++)); + } + should_render = true; + } + break; case SDL_KEYDOWN: - if (gui_state == WAITING_FOR_KEY) { + scrollbar_drag = false; + if (gui_state == TEXT_INPUT) { + if (event.key.keysym.sym == SDLK_v && (event.key.keysym.mod & MODIFIER)) { + char *s = SDL_GetClipboardText(); + while (*s) { + text_input_callback(*(s++)); + } + should_render = true; + } + else if (event.key.keysym.scancode == SDL_SCANCODE_BACKSPACE) { + text_input_callback('\b'); + should_render = true; + } + else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN || + event.key.keysym.scancode == SDL_SCANCODE_RETURN2 || + event.key.keysym.scancode == SDL_SCANCODE_KP_ENTER) { + text_input_callback('\n'); + should_render = true; + } + } + else if (gui_state == WAITING_FOR_KEY) { if (current_selection > 8) { configuration.keys_2[current_selection - 9] = event.key.keysym.scancode; } @@ -1428,6 +2538,7 @@ void run_gui(bool is_running) else { SDL_SetWindowFullscreen(window, 0); } + update_swap_interval(); update_viewport(); } else if (event_hotkey_code(&event) == SDL_SCANCODE_O) { @@ -1457,10 +2568,16 @@ void run_gui(bool is_running) } else if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { if (gui_state == SHOWING_MENU && current_menu != root_menu) { - return_to_root_menu(0); + for (const struct menu_item *item = current_menu; item->string; item++) { + if (strcmp(item->string, "Back") == 0) { + item->handler(0); + break; + } + } should_render = true; } else if (is_running) { + SDL_StopTextInput(); return; } else { @@ -1517,10 +2634,7 @@ void run_gui(bool is_running) } } else if (gui_state == SHOWING_HELP) { - current_help_page++; - if (current_help_page == sizeof(help) / sizeof(help[0])) { - gui_state = SHOWING_MENU; - } + gui_state = SHOWING_MENU; should_render = true; } break; @@ -1529,23 +2643,28 @@ void run_gui(bool is_running) if (should_render) { should_render = false; rerender: + SDL_LockSurface(converted_background); if (width == 160 && height == 144) { memcpy(pixels, converted_background->pixels, sizeof(pixels)); } else { + for (unsigned i = 0; i < width * height; i++) { + pixels[i] = gui_palette_native[0]; + } for (unsigned y = 0; y < 144; y++) { memcpy(pixels + x_offset + width * (y + y_offset), ((uint32_t *)converted_background->pixels) + 160 * y, 160 * 4); } } + SDL_UnlockSurface(converted_background); switch (gui_state) { case SHOWING_DROP_MESSAGE: - draw_text_centered(pixels, width, height, 8 + y_offset, "Press ESC for menu", gui_palette_native[3], gui_palette_native[0], false); - draw_text_centered(pixels, width, height, 116 + y_offset, "Drop a GB or GBC", gui_palette_native[3], gui_palette_native[0], false); - draw_text_centered(pixels, width, height, 128 + y_offset, "file to play", gui_palette_native[3], gui_palette_native[0], false); + draw_styled_text(pixels, width, height, 8 + y_offset, "Press ESC for menu", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); + draw_styled_text(pixels, width, height, 116 + y_offset, "Drop a GB or GBC", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); + draw_styled_text(pixels, width, height, 128 + y_offset, "file to play", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); break; case SHOWING_MENU: - draw_text_centered(pixels, width, height, 8 + y_offset, "SameBoy", gui_palette_native[3], gui_palette_native[0], false); + draw_styled_text(pixels, width, height, 8 + y_offset, "SameBoy", gui_palette_native[3], gui_palette_native[0], STYLE_LEFT); unsigned i = 0, y = 24; for (const struct menu_item *item = current_menu; item->string; item++, i++) { if (i == current_selection && !mouse_scroling) { @@ -1568,19 +2687,26 @@ void run_gui(bool is_running) } if (item->value_getter && !item->backwards_handler) { char line[25]; - snprintf(line, sizeof(line), "%s%*s", item->string, 24 - (unsigned)strlen(item->string), item->value_getter(i)); - draw_text_centered(pixels, width, height, y + y_offset, line, gui_palette_native[3], gui_palette_native[0], - i == current_selection ? DECORATION_SELECTION : DECORATION_NONE); + snprintf(line, sizeof(line), "%s%*s", item->string, 23 - (unsigned)strlen(item->string), item->value_getter(i)); + draw_styled_text(pixels, width, height, y + y_offset, line, gui_palette_native[3], gui_palette_native[0], + i == current_selection ? STYLE_SELECTION : STYLE_INDENT); y += 12; } else { - draw_text_centered(pixels, width, height, y + y_offset, item->string, gui_palette_native[3], gui_palette_native[0], - i == current_selection && !item->value_getter ? DECORATION_SELECTION : DECORATION_NONE); + if (item->value_getter) { + draw_styled_text(pixels, width, height, y + y_offset, item->string, gui_palette_native[3], gui_palette_native[0], + STYLE_LEFT); + + } + else { + draw_styled_text(pixels, width, height, y + y_offset, item->string, gui_palette_native[3], gui_palette_native[0], + i == current_selection ? STYLE_SELECTION : STYLE_INDENT); + } y += 12; if (item->value_getter) { - draw_text_centered(pixels, width, height, y + y_offset - 1, item->value_getter(i), gui_palette_native[3], gui_palette_native[0], - i == current_selection ? DECORATION_ARROWS : DECORATION_NONE); + draw_styled_text(pixels, width, height, y + y_offset - 1, item->value_getter(i), gui_palette_native[3], gui_palette_native[0], + i == current_selection ? STYLE_ARROWS : STYLE_CENTER); y += 12; } } @@ -1603,10 +2729,10 @@ void run_gui(bool is_running) for (unsigned y = 0; y < 140; y++) { uint32_t *pixel = pixels + x_offset + 156 + width * (y + y_offset + 2); if (y >= scrollbar_offset && y < scrollbar_offset + scrollbar_size) { - pixel[0] = pixel[1]= gui_palette_native[2]; + pixel[0] = pixel[1] = gui_palette_native[2]; } else { - pixel[0] = pixel[1]= gui_palette_native[1]; + pixel[0] = pixel[1] = gui_palette_native[1]; } } @@ -1616,15 +2742,14 @@ void run_gui(bool is_running) draw_text(pixels, width, height, 2 + x_offset, 2 + y_offset, help[current_help_page], gui_palette_native[3], gui_palette_native[0], false); break; case WAITING_FOR_KEY: - draw_text_centered(pixels, width, height, 68 + y_offset, "Press a Key", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); + draw_styled_text(pixels, width, height, 68 + y_offset, "Press a Key", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); break; case WAITING_FOR_JBUTTON: - draw_text_centered(pixels, width, height, 68 + y_offset, + draw_styled_text(pixels, width, height, 68 + y_offset, joypad_configuration_progress != JOYPAD_BUTTONS_MAX ? "Press button for" : "Move the Analog Stick", - gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); - draw_text_centered(pixels, width, height, 80 + y_offset, - (const char *[]) - { + gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); + draw_styled_text(pixels, width, height, 80 + y_offset, + GB_inline_const(const char *[], { "Right", "Left", "Up", @@ -1637,10 +2762,17 @@ void run_gui(bool is_running) "Turbo", "Rewind", "Slow-Motion", + "Hotkey 1", + "Hotkey 2", "", - } [joypad_configuration_progress], - gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); - draw_text_centered(pixels, width, height, 104 + y_offset, "Press Enter to skip", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); + }) [joypad_configuration_progress], + gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); + draw_styled_text(pixels, width, height, 104 + y_offset, "Press Enter to skip", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); + break; + case TEXT_INPUT: + draw_styled_text(pixels, width, height, 32 + y_offset, text_input_title, gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); + draw_styled_text(pixels, width, height, 44 + y_offset, text_input_title2, gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); + draw_styled_text(pixels, width, height, 64 + y_offset, text_input, gui_palette_native[3], gui_palette_native[0], STYLE_CENTER); break; } @@ -1650,5 +2782,33 @@ void run_gui(bool is_running) render_texture(pixels, NULL); #endif } - } while (SDL_WaitEvent(&event)); + } +} + +static void __attribute__ ((constructor)) list_custom_palettes(void) +{ + char *path = resource_path("Palettes"); + if (!path) return; + if (strlen(path) > 1024 - 30) { + // path too long to safely concat filenames + return; + } + DIR *dir = opendir(path); + if (!dir) return; + + struct dirent *ent; + + while ((ent = readdir(dir))) { + unsigned length = strlen(ent->d_name); + if (length < 5 || length > 28) { + continue; + } + if (strcmp(ent->d_name + length - 4, ".sbp")) continue; + ent->d_name[length - 4] = 0; + custom_palettes = realloc(custom_palettes, + sizeof(custom_palettes[0]) * (n_custom_palettes + 1)); + custom_palettes[n_custom_palettes++] = strdup(ent->d_name); + } + + closedir(dir); } diff --git a/SDL/gui.h b/SDL/gui.h index 1fe8a54..86577e0 100644 --- a/SDL/gui.h +++ b/SDL/gui.h @@ -1,10 +1,10 @@ -#ifndef gui_h -#define gui_h +#pragma once #include #include #include #include "shader.h" +#include "configuration.h" #define JOYSTICK_HIGH 0x4000 #define JOYSTICK_LOW 0x3800 @@ -21,17 +21,10 @@ extern SDL_Window *window; extern SDL_Renderer *renderer; extern SDL_Texture *texture; extern SDL_PixelFormat *pixel_format; +extern SDL_Joystick *joystick; extern SDL_Haptic *haptic; extern shader_t shader; -enum scaling_mode { - GB_SDL_SCALING_ENTIRE_WINDOW, - GB_SDL_SCALING_KEEP_RATIO, - GB_SDL_SCALING_INTEGER_FACTOR, - GB_SDL_SCALING_MAX, -}; - - enum pending_command { GB_SDL_NO_COMMAND, GB_SDL_SAVE_STATE_COMMAND, @@ -40,6 +33,7 @@ enum pending_command { GB_SDL_NEW_FILE_COMMAND, GB_SDL_QUIT_COMMAND, GB_SDL_LOAD_STATE_FROM_FILE_COMMAND, + GB_SDL_CART_SWAP_COMMAND, }; #define GB_SDL_DEFAULT_SCALE_MAX 8 @@ -48,85 +42,6 @@ extern enum pending_command pending_command; extern unsigned command_parameter; extern char *dropped_state_file; -typedef enum { - JOYPAD_BUTTON_RIGHT, - JOYPAD_BUTTON_LEFT, - JOYPAD_BUTTON_UP, - JOYPAD_BUTTON_DOWN, - JOYPAD_BUTTON_A, - JOYPAD_BUTTON_B, - JOYPAD_BUTTON_SELECT, - JOYPAD_BUTTON_START, - JOYPAD_BUTTON_MENU, - JOYPAD_BUTTON_TURBO, - JOYPAD_BUTTON_REWIND, - JOYPAD_BUTTON_SLOW_MOTION, - JOYPAD_BUTTONS_MAX -} joypad_button_t; - -typedef enum { - JOYPAD_AXISES_X, - JOYPAD_AXISES_Y, - JOYPAD_AXISES_MAX -} joypad_axis_t; - -typedef struct { - SDL_Scancode keys[9]; - GB_color_correction_mode_t color_correction_mode; - enum scaling_mode scaling_mode; - uint8_t blending_mode; - - GB_highpass_mode_t highpass_mode; - - bool _deprecated_div_joystick; - bool _deprecated_flip_joystick_bit_1; - bool _deprecated_swap_joysticks_bits_1_and_2; - - char filter[32]; - enum { - MODEL_DMG, - MODEL_CGB, - MODEL_AGB, - MODEL_SGB, - MODEL_MGB, - MODEL_MAX, - } model; - - /* v0.11 */ - uint32_t rewind_length; - SDL_Scancode keys_2[32]; /* Rewind and underclock, + padding for the future */ - uint8_t joypad_configuration[32]; /* 12 Keys + padding for the future*/; - uint8_t joypad_axises[JOYPAD_AXISES_MAX]; - - /* v0.12 */ - enum { - SGB_NTSC, - SGB_PAL, - SGB_2, - SGB_MAX - } sgb_revision; - - /* v0.13 */ - uint8_t dmg_palette; - GB_border_mode_t border_mode; - uint8_t volume; - GB_rumble_mode_t rumble_mode; - - uint8_t default_scale; - - /* v0.14 */ - unsigned padding; - uint8_t color_temperature; - char bootrom_path[4096]; - uint8_t interference_volume; - GB_rtc_mode_t rtc_mode; - - /* v0.14.4 */ - bool osd; -} configuration_t; - -extern configuration_t configuration; - void update_viewport(void); void run_gui(bool is_running); void render_texture(void *pixels, void *previous); @@ -149,5 +64,6 @@ void show_osd_text(const char *text); extern const char *osd_text; extern unsigned osd_countdown; extern unsigned osd_text_lines; - -#endif +void convert_mouse_coordinates(signed *x, signed *y); +const GB_palette_t *current_dmg_palette(void); +void update_swap_interval(void); diff --git a/SDL/main.c b/SDL/main.c index a3be18f..6a39556 100644 --- a/SDL/main.c +++ b/SDL/main.c @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -13,7 +15,7 @@ #include "console.h" #ifndef _WIN32 -#include +#include #else #include #endif @@ -26,7 +28,7 @@ static uint32_t *active_pixel_buffer = pixel_buffer_1, *previous_pixel_buffer = static bool underclock_down = false, rewind_down = false, do_rewind = false, rewind_paused = false, turbo_down = false; static double clock_mutliplier = 1.0; -static char *filename = NULL; +char *filename = NULL; static typeof(free) *free_function = NULL; static char *battery_save_path_ptr = NULL; static SDL_GLContext gl_context = NULL; @@ -44,6 +46,7 @@ void set_filename(const char *new_filename, typeof(free) *new_free_function) } filename = (char *) new_filename; free_function = new_free_function; + GB_rewind_reset(&gb); } static char *completer(const char *substring, uintptr_t *context) @@ -161,22 +164,7 @@ static const char *end_capturing_logs(bool show_popup, bool should_exit, uint32_ static void update_palette(void) { - switch (configuration.dmg_palette) { - case 1: - GB_set_palette(&gb, &GB_PALETTE_DMG); - break; - - case 2: - GB_set_palette(&gb, &GB_PALETTE_MGB); - break; - - case 3: - GB_set_palette(&gb, &GB_PALETTE_GBL); - break; - - default: - GB_set_palette(&gb, &GB_PALETTE_GREY); - } + GB_set_palette(&gb, current_dmg_palette()); } static void screen_size_changed(void) @@ -220,6 +208,9 @@ static void handle_events(GB_gameboy_t *gb) SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { + case SDL_DISPLAYEVENT: + update_swap_interval(); + break; case SDL_QUIT: pending_command = GB_SDL_QUIT_COMMAND; break; @@ -240,8 +231,43 @@ static void handle_events(GB_gameboy_t *gb) if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { update_viewport(); } + if (event.window.type == SDL_WINDOWEVENT_MOVED +#if SDL_COMPILEDVERSION > 2018 + || event.window.type == SDL_WINDOWEVENT_DISPLAY_CHANGED +#endif + ) { + update_swap_interval(); + } break; } + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: { + if (GB_has_accelerometer(gb) && configuration.allow_mouse_controls) { + GB_set_key_state(gb, GB_KEY_A, event.type == SDL_MOUSEBUTTONDOWN); + } + break; + } + + case SDL_MOUSEMOTION: { + if (GB_has_accelerometer(gb) && configuration.allow_mouse_controls) { + signed x = event.motion.x; + signed y = event.motion.y; + convert_mouse_coordinates(&x, &y); + x = SDL_max(SDL_min(x, 160), 0); + y = SDL_max(SDL_min(y, 144), 0); + GB_set_accelerometer_values(gb, (x - 80) / -80.0, (y - 72) / -72.0); + } + break; + } + + case SDL_JOYDEVICEREMOVED: + if (joystick && event.jdevice.which == SDL_JoystickInstanceID(joystick)) { + SDL_JoystickClose(joystick); + joystick = NULL; + } + case SDL_JOYDEVICEADDED: + connect_joypad(); + break; case SDL_JOYBUTTONUP: case SDL_JOYBUTTONDOWN: { @@ -267,14 +293,50 @@ static void handle_events(GB_gameboy_t *gb) else if (button == JOYPAD_BUTTON_MENU && event.type == SDL_JOYBUTTONDOWN) { open_menu(); } + else if ((button == JOYPAD_BUTTON_HOTKEY_1 || button == JOYPAD_BUTTON_HOTKEY_2) && event.type == SDL_JOYBUTTONDOWN) { + hotkey_action_t action = configuration.hotkey_actions[button - JOYPAD_BUTTON_HOTKEY_1]; + switch (action) { + case HOTKEY_NONE: + break; + case HOTKEY_PAUSE: + paused = !paused; + break; + case HOTKEY_MUTE: + GB_audio_set_paused(GB_audio_is_playing()); + break; + case HOTKEY_RESET: + pending_command = GB_SDL_RESET_COMMAND; + break; + case HOTKEY_QUIT: + pending_command = GB_SDL_QUIT_COMMAND; + break; + default: + command_parameter = (action - HOTKEY_SAVE_STATE_1) / 2 + 1; + pending_command = ((action - HOTKEY_SAVE_STATE_1) % 2)? GB_SDL_LOAD_STATE_COMMAND:GB_SDL_SAVE_STATE_COMMAND; + break; + case HOTKEY_SAVE_STATE_10: + command_parameter = 0; + pending_command = GB_SDL_SAVE_STATE_COMMAND; + break; + case HOTKEY_LOAD_STATE_10: + command_parameter = 0; + pending_command = GB_SDL_LOAD_STATE_COMMAND; + break; + } + } } break; case SDL_JOYAXISMOTION: { static bool axis_active[2] = {false, false}; + static double accel_values[2] = {0, 0}; joypad_axis_t axis = get_joypad_axis(event.jaxis.axis); if (axis == JOYPAD_AXISES_X) { - if (event.jaxis.value > JOYSTICK_HIGH) { + if (GB_has_accelerometer(gb)) { + accel_values[0] = event.jaxis.value / (double)32768.0; + GB_set_accelerometer_values(gb, -accel_values[0], -accel_values[1]); + } + else if (event.jaxis.value > JOYSTICK_HIGH) { axis_active[0] = true; GB_set_key_state(gb, GB_KEY_RIGHT, true); GB_set_key_state(gb, GB_KEY_LEFT, false); @@ -291,7 +353,11 @@ static void handle_events(GB_gameboy_t *gb) } } else if (axis == JOYPAD_AXISES_Y) { - if (event.jaxis.value > JOYSTICK_HIGH) { + if (GB_has_accelerometer(gb)) { + accel_values[1] = event.jaxis.value / (double)32768.0; + GB_set_accelerometer_values(gb, -accel_values[0], -accel_values[1]); + } + else if (event.jaxis.value > JOYSTICK_HIGH) { axis_active[1] = true; GB_set_key_state(gb, GB_KEY_DOWN, true); GB_set_key_state(gb, GB_KEY_UP, false); @@ -373,12 +439,13 @@ static void handle_events(GB_gameboy_t *gb) case SDL_SCANCODE_F: if (event.key.keysym.mod & MODIFIER) { - if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == false) { + if (!(SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP)) { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); } else { SDL_SetWindowFullscreen(window, 0); } + update_swap_interval(); update_viewport(); } break; @@ -396,6 +463,16 @@ static void handle_events(GB_gameboy_t *gb) pending_command = GB_SDL_SAVE_STATE_COMMAND; } } + else if ((event.key.keysym.mod & KMOD_ALT) && event.key.keysym.scancode <= SDL_SCANCODE_4) { + GB_channel_t channel = event.key.keysym.scancode - SDL_SCANCODE_1; + bool state = !GB_is_channel_muted(gb, channel); + + GB_set_channel_muted(gb, channel, state); + + static char message[18]; + sprintf(message, "Channel %d %smuted", channel + 1, state? "" : "un"); + show_osd_text(message); + } } break; } @@ -434,7 +511,7 @@ static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) return SDL_MapRGB(pixel_format, r, g, b); } -static void vblank(GB_gameboy_t *gb) +static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type) { if (underclock_down && clock_mutliplier > 0.5) { clock_mutliplier -= 1.0/16; @@ -464,15 +541,17 @@ static void vblank(GB_gameboy_t *gb) true); osd_countdown--; } - if (configuration.blending_mode) { - render_texture(active_pixel_buffer, previous_pixel_buffer); - uint32_t *temp = active_pixel_buffer; - active_pixel_buffer = previous_pixel_buffer; - previous_pixel_buffer = temp; - GB_set_pixels_output(gb, active_pixel_buffer); - } - else { - render_texture(active_pixel_buffer, NULL); + if (type != GB_VBLANK_TYPE_REPEAT) { + if (configuration.blending_mode) { + render_texture(active_pixel_buffer, previous_pixel_buffer); + uint32_t *temp = active_pixel_buffer; + active_pixel_buffer = previous_pixel_buffer; + previous_pixel_buffer = temp; + GB_set_pixels_output(gb, active_pixel_buffer); + } + else { + render_texture(active_pixel_buffer, NULL); + } } do_rewind = rewind_down; handle_events(gb); @@ -497,6 +576,13 @@ static void debugger_interrupt(int ignore) GB_debugger_break(&gb); } +#ifndef _WIN32 +static void debugger_reset(int ignore) +{ + pending_command = GB_SDL_RESET_COMMAND; +} +#endif + static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) { if (turbo_down) { @@ -510,7 +596,7 @@ static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) } } - if (GB_audio_get_queue_length() / sizeof(*sample) > GB_audio_get_frequency() / 4) { + if (GB_audio_get_queue_length() > GB_audio_get_frequency() / 8) { // Maximum lag of 0.125s return; } @@ -523,6 +609,7 @@ static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) } +static bool doing_hot_swap = false; static bool handle_pending_command(void) { switch (pending_command) { @@ -575,6 +662,8 @@ static bool handle_pending_command(void) case GB_SDL_NO_COMMAND: return false; + case GB_SDL_CART_SWAP_COMMAND: + doing_hot_swap = true; case GB_SDL_RESET_COMMAND: case GB_SDL_NEW_FILE_COMMAND: GB_save_battery(&gb, battery_save_path_ptr); @@ -597,21 +686,72 @@ static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) [GB_BOOT_ROM_SGB2] = "sgb2_boot.bin", [GB_BOOT_ROM_CGB_0] = "cgb0_boot.bin", [GB_BOOT_ROM_CGB] = "cgb_boot.bin", + [GB_BOOT_ROM_CGB_E] = "cgbE_boot.bin", + [GB_BOOT_ROM_AGB_0] = "agb0_boot.bin", [GB_BOOT_ROM_AGB] = "agb_boot.bin", }; bool use_built_in = true; if (configuration.bootrom_path[0]) { - static char path[4096]; + static char path[PATH_MAX + 1]; snprintf(path, sizeof(path), "%s/%s", configuration.bootrom_path, names[type]); use_built_in = GB_load_boot_rom(gb, path); } if (use_built_in) { start_capturing_logs(); - GB_load_boot_rom(gb, resource_path(names[type])); + if (GB_load_boot_rom(gb, resource_path(names[type]))) { + if (type == GB_BOOT_ROM_CGB_E) { + load_boot_rom(gb, GB_BOOT_ROM_CGB); + return; + } + if (type == GB_BOOT_ROM_AGB_0) { + load_boot_rom(gb, GB_BOOT_ROM_AGB); + return; + } + } end_capturing_logs(true, false, SDL_MESSAGEBOX_ERROR, "Error"); } } +static bool is_path_writeable(const char *path) +{ + if (!access(path, W_OK)) return true; + int fd = creat(path, 0644); + if (fd == -1) return false; + close(fd); + unlink(path); + return true; +} + +static void debugger_reload_callback(GB_gameboy_t *gb) +{ + size_t path_length = strlen(filename); + char extension[4] = {0,}; + if (path_length > 4) { + if (filename[path_length - 4] == '.') { + extension[0] = tolower((unsigned char)filename[path_length - 3]); + extension[1] = tolower((unsigned char)filename[path_length - 2]); + extension[2] = tolower((unsigned char)filename[path_length - 1]); + } + } + if (strcmp(extension, "isx") == 0) { + GB_load_isx(gb, filename); + } + else { + GB_load_rom(gb, filename); + } + + GB_load_battery(gb, battery_save_path_ptr); + + GB_debugger_clear_symbols(gb); + GB_debugger_load_symbol_file(gb, resource_path("registers.sym")); + + char symbols_path[path_length + 5]; + replace_extension(filename, path_length, symbols_path, ".sym"); + GB_debugger_load_symbol_file(gb, symbols_path); + + GB_reset(gb); +} + static void run(void) { SDL_ShowCursor(SDL_DISABLE); @@ -621,8 +761,8 @@ restart: model = (GB_model_t []) { [MODEL_DMG] = GB_MODEL_DMG_B, - [MODEL_CGB] = GB_MODEL_CGB_E, - [MODEL_AGB] = GB_MODEL_AGB_A, + [MODEL_CGB] = GB_MODEL_CGB_0 + configuration.cgb_revision, + [MODEL_AGB] = configuration.agb_revision, [MODEL_MGB] = GB_MODEL_MGB, [MODEL_SGB] = (GB_model_t []) { @@ -633,7 +773,12 @@ restart: }[configuration.model]; if (GB_is_inited(&gb)) { - GB_switch_model_and_reset(&gb, model); + if (doing_hot_swap) { + doing_hot_swap = false; + } + else { + GB_switch_model_and_reset(&gb, model); + } } else { GB_init(&gb, model); @@ -664,6 +809,8 @@ restart: GB_set_input_callback(&gb, input_callback); GB_set_async_input_callback(&gb, asyc_input_callback); } + + GB_set_debugger_reload_callback(&gb, debugger_reload_callback); } if (stop_on_start) { stop_on_start = false; @@ -693,6 +840,22 @@ restart: else { GB_load_rom(&gb, filename); } + + /* Configure battery */ + char battery_save_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */ + replace_extension(filename, path_length, battery_save_path, ".sav"); + battery_save_path_ptr = battery_save_path; + GB_load_battery(&gb, battery_save_path); + if (GB_save_battery_size(&gb)) { + if (!is_path_writeable(battery_save_path)) { + GB_log(&gb, "The save path for this ROM is not writeable, progress will not be saved.\n"); + } + } + + char cheat_path[path_length + 5]; + replace_extension(filename, path_length, cheat_path, ".cht"); + GB_load_cheats(&gb, cheat_path); + end_capturing_logs(true, error, SDL_MESSAGEBOX_WARNING, "Warning"); static char start_text[64]; @@ -701,13 +864,6 @@ restart: sprintf(start_text, "SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&gb)); show_osd_text(start_text); - - /* Configure battery */ - char battery_save_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */ - replace_extension(filename, path_length, battery_save_path, ".sav"); - battery_save_path_ptr = battery_save_path; - GB_load_battery(&gb, battery_save_path); - /* Configure symbols */ GB_debugger_load_symbol_file(&gb, resource_path("registers.sym")); @@ -746,7 +902,7 @@ restart: } } -static char prefs_path[1024] = {0, }; +static char prefs_path[PATH_MAX + 1] = {0, }; static void save_configuration(void) { @@ -757,6 +913,11 @@ static void save_configuration(void) } } +static void stop_recording(void) +{ + GB_stop_audio_recording(&gb); +} + static bool get_arg_flag(const char *flag, int *argc, char **argv) { for (unsigned i = 1; i < *argc; i++) { @@ -769,6 +930,19 @@ static bool get_arg_flag(const char *flag, int *argc, char **argv) return false; } +static const char *get_arg_option(const char *option, int *argc, char **argv) +{ + for (unsigned i = 1; i < *argc - 1; i++) { + if (strcmp(argv[i], option) == 0) { + const char *ret = argv[i + 1]; + memmove(argv + i, argv + i + 2, (*argc - i - 2) * sizeof(argv[0])); + (*argc) -= 2; + return ret; + } + } + return NULL; +} + #ifdef __APPLE__ #include static void enable_smooth_scrolling(void) @@ -777,6 +951,87 @@ static void enable_smooth_scrolling(void) } #endif +static void handle_model_option(const char *model_string) +{ + static const struct { + const char *name; + GB_model_t model; + const char *description; + } name_to_model[] = { + {"dmg-b", GB_MODEL_DMG_B, "Game Boy, DMG-CPU B"}, + {"dmg", GB_MODEL_DMG_B, "Alias of dmg-b"}, + {"sgb-ntsc", GB_MODEL_SGB_NTSC, "Super Game Boy (NTSC)"}, + {"sgb-pal", GB_MODEL_SGB_PAL, "Super Game Boy (PAL"}, + {"sgb2", GB_MODEL_SGB2, "Super Game Boy 2"}, + {"sgb", GB_MODEL_SGB, "Alias of sgb-ntsc"}, + {"mgb", GB_MODEL_MGB, "Game Boy Pocket/Light"}, + {"cgb-0", GB_MODEL_CGB_0, "Game Boy Color, CPU CGB 0"}, + {"cgb-a", GB_MODEL_CGB_A, "Game Boy Color, CPU CGB A"}, + {"cgb-b", GB_MODEL_CGB_B, "Game Boy Color, CPU CGB B"}, + {"cgb-c", GB_MODEL_CGB_C, "Game Boy Color, CPU CGB C"}, + {"cgb-d", GB_MODEL_CGB_D, "Game Boy Color, CPU CGB D"}, + {"cgb-e", GB_MODEL_CGB_E, "Game Boy Color, CPU CGB E"}, + {"cgb", GB_MODEL_CGB_E, "Alias of cgb-e"}, + {"agb-a", GB_MODEL_AGB_A, "Game Boy Advance, CPU AGB A"}, + {"agb", GB_MODEL_AGB_A, "Alias of agb-a"}, + {"gbp-a", GB_MODEL_GBP_A, "Game Boy Player, CPU AGB A"}, + {"gbp", GB_MODEL_GBP_A, "Alias of gbp-a"}, + }; + + GB_model_t model = -1; + for (unsigned i = 0; i < sizeof(name_to_model) / sizeof(name_to_model[0]); i++) { + if (strcmp(model_string, name_to_model[i].name) == 00) { + model = name_to_model[i].model; + break; + } + } + if (model == -1) { + fprintf(stderr, "'%s' is not a valid model. Valid options are:\n", model_string); + for (unsigned i = 0; i < sizeof(name_to_model) / sizeof(name_to_model[0]); i++) { + fprintf(stderr, "%s - %s\n", name_to_model[i].name, name_to_model[i].description); + } + exit(1); + } + + switch (model) { + case GB_MODEL_DMG_B: + configuration.model = MODEL_DMG; + break; + case GB_MODEL_SGB_NTSC: + configuration.model = MODEL_SGB; + configuration.sgb_revision = SGB_NTSC; + break; + case GB_MODEL_SGB_PAL: + configuration.model = MODEL_SGB; + configuration.sgb_revision = SGB_PAL; + break; + case GB_MODEL_SGB2: + configuration.model = MODEL_SGB; + configuration.sgb_revision = SGB_2; + break; + case GB_MODEL_MGB: + configuration.model = MODEL_DMG; + break; + case GB_MODEL_CGB_0: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_B: + case GB_MODEL_CGB_C: + case GB_MODEL_CGB_D: + case GB_MODEL_CGB_E: + configuration.model = MODEL_CGB; + configuration.cgb_revision = model - GB_MODEL_CGB_0; + break; + case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: + configuration.model = MODEL_AGB; + configuration.agb_revision = model; + break; + + default: + break; + } +} + int main(int argc, char **argv) { #ifdef _WIN32 @@ -786,13 +1041,15 @@ int main(int argc, char **argv) enable_smooth_scrolling(); #endif + const char *model_string = get_arg_option("--model", &argc, argv); bool fullscreen = get_arg_flag("--fullscreen", &argc, argv) || get_arg_flag("-f", &argc, argv); bool nogl = get_arg_flag("--nogl", &argc, argv); stop_on_start = get_arg_flag("--stop-debugger", &argc, argv) || get_arg_flag("-s", &argc, argv); + if (argc > 2 || (argc == 2 && argv[1][0] == '-')) { fprintf(stderr, "SameBoy v" GB_VERSION "\n"); - fprintf(stderr, "Usage: %s [--fullscreen|-f] [--nogl] [--stop-debugger|-s] [rom]\n", argv[0]); + fprintf(stderr, "Usage: %s [--fullscreen|-f] [--nogl] [--stop-debugger|-s] [--model ] \n", argv[0]); exit(1); } @@ -801,8 +1058,15 @@ int main(int argc, char **argv) } signal(SIGINT, debugger_interrupt); +#ifndef _WIN32 + signal(SIGUSR1, debugger_reset); +#endif + + SDL_Init(SDL_INIT_EVERYTHING & ~SDL_INIT_AUDIO); + // This is, essentially, best-effort. + // This function will not be called if the process is terminated in any way, anyhow. + atexit(SDL_Quit); - SDL_Init(SDL_INIT_EVERYTHING); if ((console_supported = CON_start(completer))) { CON_set_repeat_empty(true); CON_printf("SameBoy v" GB_VERSION "\n"); @@ -824,18 +1088,31 @@ int main(int argc, char **argv) fclose(prefs_file); /* Sanitize for stability */ - configuration.color_correction_mode %= GB_COLOR_CORRECTION_LOW_CONTRAST +1; + configuration.color_correction_mode %= GB_COLOR_CORRECTION_MODERN_ACCURATE + 1; configuration.scaling_mode %= GB_SDL_SCALING_MAX; configuration.default_scale %= GB_SDL_DEFAULT_SCALE_MAX + 1; configuration.blending_mode %= GB_FRAME_BLENDING_MODE_ACCURATE + 1; configuration.highpass_mode %= GB_HIGHPASS_MAX; configuration.model %= MODEL_MAX; configuration.sgb_revision %= SGB_MAX; - configuration.dmg_palette %= 4; + configuration.dmg_palette %= 5; + if (configuration.dmg_palette) { + configuration.gui_pallete_enabled = true; + } configuration.border_mode %= GB_BORDER_ALWAYS + 1; configuration.rumble_mode %= GB_RUMBLE_ALL_GAMES + 1; configuration.color_temperature %= 21; configuration.bootrom_path[sizeof(configuration.bootrom_path) - 1] = 0; + configuration.cgb_revision %= GB_MODEL_CGB_E - GB_MODEL_CGB_0 + 1; + configuration.audio_driver[15] = 0; + configuration.dmg_palette_name[24] = 0; + // Fix broken defaults, should keys 12-31 should be unmapped by default + if (configuration.joypad_configuration[31] == 0) { + memset(configuration.joypad_configuration + 12 , -1, 32 - 12); + } + if ((configuration.agb_revision & ~GB_MODEL_GBP_BIT) != GB_MODEL_AGB_A) { + configuration.agb_revision = GB_MODEL_AGB_A; + } } if (configuration.model >= MODEL_MAX) { @@ -846,11 +1123,19 @@ int main(int argc, char **argv) configuration.default_scale = 2; } + if (model_string) { + handle_model_option(model_string); + } + atexit(save_configuration); + atexit(stop_recording); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, + configuration.allow_background_controllers? "1" : "0"); window = SDL_CreateWindow("SameBoy v" GB_VERSION, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 160 * configuration.default_scale, 144 * configuration.default_scale, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); @@ -870,6 +1155,7 @@ int main(int argc, char **argv) if (gl_context) { glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); + update_swap_interval(); } if (gl_context && major * 0x100 + minor < 0x302) { diff --git a/SDL/opengl_compat.h b/SDL/opengl_compat.h index 4b79b0c..da2e128 100644 --- a/SDL/opengl_compat.h +++ b/SDL/opengl_compat.h @@ -1,5 +1,4 @@ -#ifndef opengl_compat_h -#define opengl_compat_h +#pragma once #define GL_GLEXT_PROTOTYPES #include @@ -41,5 +40,3 @@ if (!GL_COMPAT_NAME(func)) GL_COMPAT_NAME(func) = SDL_GL_GetProcAddress(#func); #define glGetShaderiv GL_COMPAT_WRAPPER(glGetShaderiv) #define glGetShaderInfoLog GL_COMPAT_WRAPPER(glGetShaderInfoLog) #endif - -#endif /* opengl_compat_h */ diff --git a/SDL/shader.h b/SDL/shader.h index 149958d..a31ccd1 100644 --- a/SDL/shader.h +++ b/SDL/shader.h @@ -1,5 +1,5 @@ -#ifndef shader_h -#define shader_h +#pragma once + #include "opengl_compat.h" #include @@ -30,5 +30,3 @@ void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous, unsigned x, unsigned y, unsigned w, unsigned h, GB_frame_blending_mode_t blending_mode); void free_shader(struct shader_s *shader); - -#endif /* shader_h */ diff --git a/SDL/utils.c b/SDL/utils.c index 603e34a..32945fd 100644 --- a/SDL/utils.c +++ b/SDL/utils.c @@ -2,6 +2,7 @@ #include #include #include +#include #include "utils.h" static const char *resource_folder(void) @@ -18,7 +19,7 @@ static const char *resource_folder(void) char *resource_path(const char *filename) { - static char path[1024]; + static char path[PATH_MAX + 1]; snprintf(path, sizeof(path), "%s%s", resource_folder(), filename); #ifdef DATA_DIR diff --git a/SDL/utils.h b/SDL/utils.h index 5c0383d..1599913 100644 --- a/SDL/utils.h +++ b/SDL/utils.h @@ -1,8 +1,6 @@ -#ifndef utils_h -#define utils_h +#pragma once + #include char *resource_path(const char *filename); void replace_extension(const char *src, size_t length, char *dest, const char *ext); - -#endif /* utils_h */ diff --git a/Shaders/AAScale2x.fsh b/Shaders/AAScale2x.fsh index d51a9a6..b1b35ce 100644 --- a/Shaders/AAScale2x.fsh +++ b/Shaders/AAScale2x.fsh @@ -1,21 +1,15 @@ STATIC vec4 scale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) -{ - // o = offset, the width of a pixel - vec2 o = 1.0 / input_resolution; - +{ // texel arrangement // A B C // D E F // G H I // vec4 A = texture(image, position + vec2( -o.x, o.y)); - vec4 B = texture(image, position + vec2( 0, o.y)); - // vec4 C = texture(image, position + vec2( o.x, o.y)); - vec4 D = texture(image, position + vec2( -o.x, 0)); - vec4 E = texture(image, position + vec2( 0, 0)); - vec4 F = texture(image, position + vec2( o.x, 0)); - // vec4 G = texture(image, position + vec2( -o.x, -o.y)); - vec4 H = texture(image, position + vec2( 0, -o.y)); - // vec4 I = texture(image, position + vec2( o.x, -o.y)); + vec4 B = texture_relative(image, position, vec2( 0, 1)); + vec4 D = texture_relative(image, position, vec2( -1, 0)); + vec4 E = texture_relative(image, position, vec2( 0, 0)); + vec4 F = texture_relative(image, position, vec2( 1, 0)); + vec4 H = texture_relative(image, position, vec2( 0, -1)); vec2 p = position * input_resolution; // p = the position within a pixel [0...1] p = fract(p); diff --git a/Shaders/AAScale4x.fsh b/Shaders/AAScale4x.fsh index b59b80e..738a38f 100644 --- a/Shaders/AAScale4x.fsh +++ b/Shaders/AAScale4x.fsh @@ -1,20 +1,15 @@ STATIC vec4 scale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { - // o = offset, the width of a pixel - vec2 o = 1.0 / input_resolution; // texel arrangement // A B C // D E F // G H I - // vec4 A = texture(image, position + vec2( -o.x, o.y)); - vec4 B = texture(image, position + vec2( 0, o.y)); - // vec4 C = texture(image, position + vec2( o.x, o.y)); - vec4 D = texture(image, position + vec2( -o.x, 0)); - vec4 E = texture(image, position + vec2( 0, 0)); - vec4 F = texture(image, position + vec2( o.x, 0)); - // vec4 G = texture(image, position + vec2( -o.x, -o.y)); - vec4 H = texture(image, position + vec2( 0, -o.y)); - // vec4 I = texture(image, position + vec2( o.x, -o.y)); + + vec4 B = texture_relative(image, position, vec2( 0, 1)); + vec4 D = texture_relative(image, position, vec2( -1, 0)); + vec4 E = texture_relative(image, position, vec2( 0, 0)); + vec4 F = texture_relative(image, position, vec2( 1, 0)); + vec4 H = texture_relative(image, position, vec2( 0, -1)); vec2 p = position * input_resolution; // p = the position within a pixel [0...1] p = fract(p); @@ -36,30 +31,24 @@ STATIC vec4 scale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 } } } - -STATIC vec4 aaScale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +STATIC vec4 scale2x_wrapper(sampler2D t, vec2 pos, vec2 offset, vec2 input_resolution, vec2 output_resolution) { - return mix(texture(image, position), scale2x(image, position, input_resolution, output_resolution), 0.5); + vec2 origin = (floor(pos * input_resolution * 2.0)) + vec2(0.5, 0.5); + return scale2x(t, (origin + offset) / input_resolution / 2.0, input_resolution, output_resolution); } STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { - // o = offset, the width of a pixel - vec2 o = 1.0 / (input_resolution * 2.); - // texel arrangement // A B C // D E F // G H I - // vec4 A = aaScale2x(image, position + vec2( -o.x, o.y), input_resolution, output_resolution); - vec4 B = aaScale2x(image, position + vec2( 0, o.y), input_resolution, output_resolution); - // vec4 C = aaScale2x(image, position + vec2( o.x, o.y), input_resolution, output_resolution); - vec4 D = aaScale2x(image, position + vec2( -o.x, 0), input_resolution, output_resolution); - vec4 E = aaScale2x(image, position + vec2( 0, 0), input_resolution, output_resolution); - vec4 F = aaScale2x(image, position + vec2( o.x, 0), input_resolution, output_resolution); - // vec4 G = aaScale2x(image, position + vec2( -o.x, -o.y), input_resolution, output_resolution); - vec4 H = aaScale2x(image, position + vec2( 0, -o.y), input_resolution, output_resolution); - // vec4 I = aaScale2x(image, position + vec2( o.x, -o.y), input_resolution, output_resolution); + vec4 B = scale2x_wrapper(image, position, vec2( 0, 1), input_resolution, output_resolution); + vec4 D = scale2x_wrapper(image, position, vec2( -1, 0), input_resolution, output_resolution); + vec4 E = scale2x_wrapper(image, position, vec2( 0, 0), input_resolution, output_resolution); + vec4 F = scale2x_wrapper(image, position, vec2( 1, 0), input_resolution, output_resolution); + vec4 H = scale2x_wrapper(image, position, vec2( 0, -1), input_resolution, output_resolution); + vec4 R; vec2 p = position * input_resolution * 2.; // p = the position within a pixel [0...1] diff --git a/Shaders/CRT.fsh b/Shaders/CRT.fsh index 4cbab72..154f0a2 100644 --- a/Shaders/CRT.fsh +++ b/Shaders/CRT.fsh @@ -1,53 +1,53 @@ -#define COLOR_LOW 0.7 +#define COLOR_LOW 0.45 #define COLOR_HIGH 1.0 #define VERTICAL_BORDER_DEPTH 0.6 -#define SCANLINE_DEPTH 0.3 +#define SCANLINE_DEPTH 0.55 #define CURVENESS 0.3 STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { /* Curve and pixel ratio */ - float y_curve = cos(position.x - 0.5) * CURVENESS + (1 - CURVENESS); + float y_curve = cos(position.x - 0.5) * CURVENESS + (1.0 - CURVENESS); float y_multiplier = 8.0 / 7.0 / y_curve; position.y *= y_multiplier; - position.y -= (y_multiplier - 1) / 2; + position.y -= (y_multiplier - 1.0) / 2.0; if (position.y < 0.0) return vec4(0,0,0,0); if (position.y > 1.0) return vec4(0,0,0,0); - float x_curve = cos(position.y - 0.5) * CURVENESS + (1 - CURVENESS); - float x_multiplier = 1/x_curve; + float x_curve = cos(position.y - 0.5) * CURVENESS + (1.0 - CURVENESS); + float x_multiplier = 1.0/x_curve; position.x *= x_multiplier; - position.x -= (x_multiplier - 1) / 2; + position.x -= (x_multiplier - 1.0) / 2.0; if (position.x < 0.0) return vec4(0,0,0,0); if (position.x > 1.0) return vec4(0,0,0,0); /* Setting up common vars */ vec2 pos = fract(position * input_resolution); - vec2 sub_pos = fract(position * input_resolution * 6); - - vec4 center = texture(image, position); - vec4 left = texture(image, position - vec2(1.0 / input_resolution.x, 0)); - vec4 right = texture(image, position + vec2(1.0 / input_resolution.x, 0)); + vec2 sub_pos = pos * 6.0; + + vec4 center = texture_relative(image, position, vec2(0, 0)); + vec4 left = texture_relative(image, position, vec2(-1, 0)); + vec4 right = texture_relative(image, position, vec2(1, 0)); /* Vertical blurring */ - if (pos.y < 1.0 / 6.0) { - center = mix(center, texture(image, position + vec2(0, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); - left = mix(left, texture(image, position + vec2(-1.0 / input_resolution.x, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); - right = mix(right, texture(image, position + vec2( 1.0 / input_resolution.x, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); + if (sub_pos.y < 1.0) { + center = mix(center, texture_relative(image, position, vec2( 0, -1)), 0.5 - sub_pos.y / 2.0); + left = mix(left, texture_relative(image, position, vec2(-1, -1)), 0.5 - sub_pos.y / 2.0); + right = mix(right, texture_relative(image, position, vec2( 1, -1)), 0.5 - sub_pos.y / 2.0); } - else if (pos.y > 5.0 / 6.0) { - center = mix(center, texture(image, position + vec2(0, 1.0 / input_resolution.y)), sub_pos.y / 2.0); - left = mix(left, texture(image, position + vec2(-1.0 / input_resolution.x, 1.0 / input_resolution.y)), sub_pos.y / 2.0); - right = mix(right, texture(image, position + vec2( 1.0 / input_resolution.x, 1.0 / input_resolution.y)), sub_pos.y / 2.0); + else if (sub_pos.y > 5.0) { + center = mix(center, texture_relative(image, position, vec2( 0, 1)), (sub_pos.y - 5.0) / 2.0); + left = mix(left, texture_relative(image, position, vec2(-1, 1)), (sub_pos.y - 5.0) / 2.0); + right = mix(right, texture_relative(image, position, vec2( 1, 1)), (sub_pos.y - 5.0) / 2.0); } /* Scanlines */ float scanline_multiplier; if (pos.y < 0.5) { - scanline_multiplier = (pos.y * 2) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + scanline_multiplier = (pos.y * 2.0) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); } else { - scanline_multiplier = ((1 - pos.y) * 2) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + scanline_multiplier = ((1.0 - pos.y) * 2.0) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); } center *= scanline_multiplier; @@ -63,45 +63,45 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou if (pos.y < 1.0 / 3.0) { float gradient_position = pos.y * 3.0; - center *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); - left *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); - right *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + center *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); + left *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); + right *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); } else if (pos.y > 2.0 / 3.0) { - float gradient_position = (1 - pos.y) * 3.0; - center *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); - left *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); - right *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + float gradient_position = (1.0 - pos.y) * 3.0; + center *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); + left *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); + right *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); } /* Blur the edges of the separators of adjacent columns */ - if (pos.x < 1.0 / 6.0 || pos.x > 5.0 / 6.0) { + if (sub_pos.x < 1.0 || sub_pos.x > 5.0) { pos.y += 0.5; pos.y = fract(pos.y); if (pos.y < 1.0 / 3.0) { float gradient_position = pos.y * 3.0; if (pos.x < 0.5) { - gradient_position = 1 - (1 - gradient_position) * (1 - (pos.x) * 6.0); + gradient_position = 1.0 - (1.0 - gradient_position) * (1.0 - (pos.x) * 6.0); } else { - gradient_position = 1 - (1 - gradient_position) * (1 - (1 - pos.x) * 6.0); + gradient_position = 1.0 - (1.0 - gradient_position) * (1.0 - (1.0 - pos.x) * 6.0); } - center *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); - left *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); - right *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + center *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); + left *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); + right *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); } else if (pos.y > 2.0 / 3.0) { - float gradient_position = (1 - pos.y) * 3.0; + float gradient_position = (1.0 - pos.y) * 3.0; if (pos.x < 0.5) { - gradient_position = 1 - (1 - gradient_position) * (1 - (pos.x) * 6.0); + gradient_position = 1.0 - (1.0 - gradient_position) * (1.0 - (pos.x) * 6.0); } else { - gradient_position = 1 - (1 - gradient_position) * (1 - (1 - pos.x) * 6.0); + gradient_position = 1.0 - (1.0 - gradient_position) * (1.0 - (1.0 - pos.x) * 6.0); } - center *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); - left *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); - right *= gradient_position * VERTICAL_BORDER_DEPTH + (1 - VERTICAL_BORDER_DEPTH); + center *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); + left *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); + right *= gradient_position * VERTICAL_BORDER_DEPTH + (1.0 - VERTICAL_BORDER_DEPTH); } } @@ -112,49 +112,49 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou vec4 midright = mix(right, center, 0.5); vec4 ret; - if (pos.x < 1.0 / 6.0) { + if (sub_pos.x < 1.0) { ret = mix(vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_HIGH * left.b, 1), vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_LOW * left.b, 1), sub_pos.x); } - else if (pos.x < 2.0 / 6.0) { + else if (sub_pos.x < 2.0) { ret = mix(vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_LOW * left.b, 1), vec4(COLOR_HIGH * center.r, COLOR_HIGH * center.g, COLOR_LOW * midleft.b, 1), - sub_pos.x); + sub_pos.x - 1.0); } - else if (pos.x < 3.0 / 6.0) { + else if (sub_pos.x < 3.0) { ret = mix(vec4(COLOR_HIGH * center.r , COLOR_HIGH * center.g, COLOR_LOW * midleft.b, 1), vec4(COLOR_LOW * midright.r, COLOR_HIGH * center.g, COLOR_LOW * center.b, 1), - sub_pos.x); + sub_pos.x - 2.0); } - else if (pos.x < 4.0 / 6.0) { + else if (sub_pos.x < 4.0) { ret = mix(vec4(COLOR_LOW * midright.r, COLOR_HIGH * center.g , COLOR_LOW * center.b, 1), vec4(COLOR_LOW * right.r , COLOR_HIGH * center.g, COLOR_HIGH * center.b, 1), - sub_pos.x); + sub_pos.x - 3.0); } - else if (pos.x < 5.0 / 6.0) { + else if (sub_pos.x < 5.0) { ret = mix(vec4(COLOR_LOW * right.r, COLOR_HIGH * center.g , COLOR_HIGH * center.b, 1), vec4(COLOR_LOW * right.r, COLOR_LOW * midright.g, COLOR_HIGH * center.b, 1), - sub_pos.x); + sub_pos.x - 4.0); } else { ret = mix(vec4(COLOR_LOW * right.r, COLOR_LOW * midright.g, COLOR_HIGH * center.b, 1), vec4(COLOR_HIGH * right.r, COLOR_LOW * right.g , COLOR_HIGH * center.b, 1), - sub_pos.x); + sub_pos.x - 5.0); } /* Anti alias the curve */ vec2 pixel_position = position * output_resolution; - if (pixel_position.x < 1) { + if (pixel_position.x < 1.0) { ret *= pixel_position.x; } - else if (pixel_position.x > output_resolution.x - 1) { + else if (pixel_position.x > output_resolution.x - 1.0) { ret *= output_resolution.x - pixel_position.x; } - if (pixel_position.y < 1) { + if (pixel_position.y < 1.0) { ret *= pixel_position.y; } - else if (pixel_position.y > output_resolution.y - 1) { + else if (pixel_position.y > output_resolution.y - 1.0) { ret *= output_resolution.y - pixel_position.y; } diff --git a/Shaders/HQ2x.fsh b/Shaders/HQ2x.fsh index 7ae8063..0baf9e1 100644 --- a/Shaders/HQ2x.fsh +++ b/Shaders/HQ2x.fsh @@ -30,7 +30,7 @@ STATIC vec4 interp_3px(vec4 c1, float w1, vec4 c2, float w2, vec4 c3, float w3) STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { // o = offset, the width of a pixel - vec2 o = 1.0 / input_resolution; + vec2 o = vec2(1, 1); /* We always calculate the top left pixel. If we need a different pixel, we flip the image */ @@ -40,17 +40,15 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou if (p.x > 0.5) o.x = -o.x; if (p.y > 0.5) o.y = -o.y; - - - vec4 w0 = texture(image, position + vec2( -o.x, -o.y)); - vec4 w1 = texture(image, position + vec2( 0, -o.y)); - vec4 w2 = texture(image, position + vec2( o.x, -o.y)); - vec4 w3 = texture(image, position + vec2( -o.x, 0)); - vec4 w4 = texture(image, position + vec2( 0, 0)); - vec4 w5 = texture(image, position + vec2( o.x, 0)); - vec4 w6 = texture(image, position + vec2( -o.x, o.y)); - vec4 w7 = texture(image, position + vec2( 0, o.y)); - vec4 w8 = texture(image, position + vec2( o.x, o.y)); + vec4 w0 = texture_relative(image, position, vec2( -o.x, -o.y)); + vec4 w1 = texture_relative(image, position, vec2( 0, -o.y)); + vec4 w2 = texture_relative(image, position, vec2( o.x, -o.y)); + vec4 w3 = texture_relative(image, position, vec2( -o.x, 0)); + vec4 w4 = texture_relative(image, position, vec2( 0, 0)); + vec4 w5 = texture_relative(image, position, vec2( o.x, 0)); + vec4 w6 = texture_relative(image, position, vec2( -o.x, o.y)); + vec4 w7 = texture_relative(image, position, vec2( 0, o.y)); + vec4 w8 = texture_relative(image, position, vec2( o.x, o.y)); int pattern = 0; if (is_different(w0, w4)) pattern |= 1; @@ -62,52 +60,52 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou if (is_different(w7, w4)) pattern |= 64; if (is_different(w8, w4)) pattern |= 128; - if ((P(0xbf,0x37) || P(0xdb,0x13)) && is_different(w1, w5)) { + if ((P(0xBF,0x37) || P(0xDB,0x13)) && is_different(w1, w5)) { return interp_2px(w4, 3.0, w3, 1.0); } - if ((P(0xdb,0x49) || P(0xef,0x6d)) && is_different(w7, w3)) { + if ((P(0xDB,0x49) || P(0xEF,0x6D)) && is_different(w7, w3)) { return interp_2px(w4, 3.0, w1, 1.0); } - if ((P(0x0b,0x0b) || P(0xfe,0x4a) || P(0xfe,0x1a)) && is_different(w3, w1)) { + if ((P(0x0B,0x0B) || P(0xFE,0x4A) || P(0xFE,0x1A)) && is_different(w3, w1)) { return w4; } - if ((P(0x6f,0x2a) || P(0x5b,0x0a) || P(0xbf,0x3a) || P(0xdf,0x5a) || - P(0x9f,0x8a) || P(0xcf,0x8a) || P(0xef,0x4e) || P(0x3f,0x0e) || - P(0xfb,0x5a) || P(0xbb,0x8a) || P(0x7f,0x5a) || P(0xaf,0x8a) || - P(0xeb,0x8a)) && is_different(w3, w1)) { + if ((P(0x6F,0x2A) || P(0x5B,0x0A) || P(0xBF,0x3A) || P(0xDF,0x5A) || + P(0x9F,0x8A) || P(0xCF,0x8A) || P(0xEF,0x4E) || P(0x3F,0x0E) || + P(0xFB,0x5A) || P(0xBB,0x8A) || P(0x7F,0x5A) || P(0xAF,0x8A) || + P(0xEB,0x8A)) && is_different(w3, w1)) { return interp_2px(w4, 3.0, w0, 1.0); } - if (P(0x0b,0x08)) { + if (P(0x0B,0x08)) { return interp_3px(w4, 2.0, w0, 1.0, w1, 1.0); } - if (P(0x0b,0x02)) { + if (P(0x0B,0x02)) { return interp_3px(w4, 2.0, w0, 1.0, w3, 1.0); } - if (P(0x2f,0x2f)) { + if (P(0x2F,0x2F)) { return interp_3px(w4, 4.0, w3, 1.0, w1, 1.0); } - if (P(0xbf,0x37) || P(0xdb,0x13)) { + if (P(0xBF,0x37) || P(0xDB,0x13)) { return interp_3px(w4, 5.0, w1, 2.0, w3, 1.0); } - if (P(0xdb,0x49) || P(0xef,0x6d)) { + if (P(0xDB,0x49) || P(0xEF,0x6D)) { return interp_3px(w4, 5.0, w3, 2.0, w1, 1.0); } - if (P(0x1b,0x03) || P(0x4f,0x43) || P(0x8b,0x83) || P(0x6b,0x43)) { + if (P(0x1B,0x03) || P(0x4F,0x43) || P(0x8B,0x83) || P(0x6B,0x43)) { return interp_2px(w4, 3.0, w3, 1.0); } - if (P(0x4b,0x09) || P(0x8b,0x89) || P(0x1f,0x19) || P(0x3b,0x19)) { + if (P(0x4B,0x09) || P(0x8B,0x89) || P(0x1F,0x19) || P(0x3B,0x19)) { return interp_2px(w4, 3.0, w1, 1.0); } - if (P(0x7e,0x2a) || P(0xef,0xab) || P(0xbf,0x8f) || P(0x7e,0x0e)) { + if (P(0x7E,0x2A) || P(0xEF,0xAB) || P(0xBF,0x8F) || P(0x7E,0x0E)) { return interp_3px(w4, 2.0, w3, 3.0, w1, 3.0); } - if (P(0xfb,0x6a) || P(0x6f,0x6e) || P(0x3f,0x3e) || P(0xfb,0xfa) || - P(0xdf,0xde) || P(0xdf,0x1e)) { + if (P(0xFB,0x6A) || P(0x6F,0x6E) || P(0x3F,0x3E) || P(0xFB,0xFA) || + P(0xDF,0xDE) || P(0xDF,0x1E)) { return interp_2px(w4, 3.0, w0, 1.0); } - if (P(0x0a,0x00) || P(0x4f,0x4b) || P(0x9f,0x1b) || P(0x2f,0x0b) || - P(0xbe,0x0a) || P(0xee,0x0a) || P(0x7e,0x0a) || P(0xeb,0x4b) || - P(0x3b,0x1b)) { + if (P(0x0A,0x00) || P(0x4F,0x4B) || P(0x9F,0x1B) || P(0x2F,0x0B) || + P(0xBE,0x0A) || P(0xEE,0x0A) || P(0x7E,0x0A) || P(0xEB,0x4B) || + P(0x3B,0x1B)) { return interp_3px(w4, 2.0, w3, 1.0, w1, 1.0); } diff --git a/Shaders/LCD.fsh b/Shaders/LCD.fsh index d20a7c9..c8fde24 100644 --- a/Shaders/LCD.fsh +++ b/Shaders/LCD.fsh @@ -1,31 +1,31 @@ -#define COLOR_LOW 0.8 +#define COLOR_LOW 0.6 #define COLOR_HIGH 1.0 -#define SCANLINE_DEPTH 0.1 +#define SCANLINE_DEPTH 0.2 STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { vec2 pos = fract(position * input_resolution); - vec2 sub_pos = fract(position * input_resolution * 6); + vec2 sub_pos = pos * 6.0; - vec4 center = texture(image, position); - vec4 left = texture(image, position - vec2(1.0 / input_resolution.x, 0)); - vec4 right = texture(image, position + vec2(1.0 / input_resolution.x, 0)); + vec4 center = texture_relative(image, position, vec2(0, 0)); + vec4 left = texture_relative(image, position, vec2(-1, 0)); + vec4 right = texture_relative(image, position, vec2(1, 0)); - if (pos.y < 1.0 / 6.0) { - center = mix(center, texture(image, position + vec2(0, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); - left = mix(left, texture(image, position + vec2(-1.0 / input_resolution.x, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); - right = mix(right, texture(image, position + vec2( 1.0 / input_resolution.x, -1.0 / input_resolution.y)), 0.5 - sub_pos.y / 2.0); - center *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); - left *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); - right *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + if (sub_pos.y < 1.0) { + center = mix(center, texture_relative(image, position, vec2( 0, -1)), 0.5 - sub_pos.y / 2.0); + left = mix(left, texture_relative(image, position, vec2(-1, -1)), 0.5 - sub_pos.y / 2.0); + right = mix(right, texture_relative(image, position, vec2( 1, -1)), 0.5 - sub_pos.y / 2.0); + center *= sub_pos.y * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); + left *= sub_pos.y * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); + right *= sub_pos.y * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); } - else if (pos.y > 5.0 / 6.0) { - center = mix(center, texture(image, position + vec2(0, 1.0 / input_resolution.y)), sub_pos.y / 2.0); - left = mix(left, texture(image, position + vec2(-1.0 / input_resolution.x, 1.0 / input_resolution.y)), sub_pos.y / 2.0); - right = mix(right, texture(image, position + vec2( 1.0 / input_resolution.x, 1.0 / input_resolution.y)), sub_pos.y / 2.0); - center *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); - left *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); - right *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + else if (sub_pos.y > 5.0) { + center = mix(center, texture_relative(image, position, vec2( 0, 1)), (sub_pos.y - 5.0) / 2.0); + left = mix(left, texture_relative(image, position, vec2(-1, 1)), (sub_pos.y - 5.0) / 2.0); + right = mix(right, texture_relative(image, position, vec2( 1, 1)), (sub_pos.y - 5.0) / 2.0); + center *= (6.0 - sub_pos.y) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); + left *= (6.0 - sub_pos.y) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); + right *= (6.0 - sub_pos.y) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); } @@ -33,35 +33,35 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou vec4 midright = mix(right, center, 0.5); vec4 ret; - if (pos.x < 1.0 / 6.0) { + if (sub_pos.x < 1.0) { ret = mix(vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_HIGH * left.b, 1), vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_LOW * left.b, 1), sub_pos.x); } - else if (pos.x < 2.0 / 6.0) { + else if (sub_pos.x < 2.0) { ret = mix(vec4(COLOR_HIGH * center.r, COLOR_LOW * center.g, COLOR_LOW * left.b, 1), vec4(COLOR_HIGH * center.r, COLOR_HIGH * center.g, COLOR_LOW * midleft.b, 1), - sub_pos.x); + sub_pos.x - 1.0); } - else if (pos.x < 3.0 / 6.0) { + else if (sub_pos.x < 3.0) { ret = mix(vec4(COLOR_HIGH * center.r , COLOR_HIGH * center.g, COLOR_LOW * midleft.b, 1), vec4(COLOR_LOW * midright.r, COLOR_HIGH * center.g, COLOR_LOW * center.b, 1), - sub_pos.x); + sub_pos.x - 2.0); } - else if (pos.x < 4.0 / 6.0) { + else if (sub_pos.x < 4.0) { ret = mix(vec4(COLOR_LOW * midright.r, COLOR_HIGH * center.g , COLOR_LOW * center.b, 1), vec4(COLOR_LOW * right.r , COLOR_HIGH * center.g, COLOR_HIGH * center.b, 1), - sub_pos.x); + sub_pos.x - 3.0); } - else if (pos.x < 5.0 / 6.0) { + else if (sub_pos.x < 5.0) { ret = mix(vec4(COLOR_LOW * right.r, COLOR_HIGH * center.g , COLOR_HIGH * center.b, 1), vec4(COLOR_LOW * right.r, COLOR_LOW * midright.g, COLOR_HIGH * center.b, 1), - sub_pos.x); + sub_pos.x - 4.0); } else { ret = mix(vec4(COLOR_LOW * right.r, COLOR_LOW * midright.g, COLOR_HIGH * center.b, 1), vec4(COLOR_HIGH * right.r, COLOR_LOW * right.g , COLOR_HIGH * center.b, 1), - sub_pos.x); + sub_pos.x - 5.0); } return ret; diff --git a/Shaders/MasterShader.fsh b/Shaders/MasterShader.fsh index 3f891d5..220bac7 100644 --- a/Shaders/MasterShader.fsh +++ b/Shaders/MasterShader.fsh @@ -18,13 +18,19 @@ vec4 _texture(sampler2D t, vec2 pos) return pow(texture(t, pos), vec4(GAMMA)); } +vec4 texture_relative(sampler2D t, vec2 pos, vec2 offset) +{ + vec2 input_resolution = textureSize(t, 0); + return _texture(t, (floor(pos * input_resolution) + offset + vec2(0.5, 0.5)) / input_resolution); +} + #define texture _texture #line 1 {filter} -#define BLEND_BIAS (2.0/5.0) +#define BLEND_BIAS (1.0/3.0) #define DISABLED 0 #define SIMPLE 1 diff --git a/Shaders/MasterShader.metal b/Shaders/MasterShader.metal index 2f3113e..aaa84d0 100644 --- a/Shaders/MasterShader.metal +++ b/Shaders/MasterShader.metal @@ -40,10 +40,17 @@ static inline float4 texture(texture2d texture, float2 pos) return pow(float4(texture.sample(texture_sampler, pos)), GAMMA); } +__attribute__((unused)) static inline float4 texture_relative(texture2d t, float2 pos, float2 offset) +{ + float2 input_resolution = float2(t.get_width(), t.get_height());; + float2 origin = (floor(pos * input_resolution)) + float2(0.5, 0.5); + return texture(t, (origin + offset) / input_resolution); +} + #line 1 {filter} -#define BLEND_BIAS (2.0/5.0) +#define BLEND_BIAS (1.0/3.0) enum frame_blending_mode { DISABLED, diff --git a/Shaders/MonoLCD.fsh b/Shaders/MonoLCD.fsh index 009e1db..00b63c2 100644 --- a/Shaders/MonoLCD.fsh +++ b/Shaders/MonoLCD.fsh @@ -16,25 +16,26 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou vec4 r2 = mix(q12, q22, s.x); vec2 pos = fract(position * input_resolution); - vec2 sub_pos = fract(position * input_resolution * 6); + vec2 sub_pos = pos * 6.0; float multiplier = 1.0; - if (pos.y < 1.0 / 6.0) { - multiplier *= sub_pos.y * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + if (sub_pos.y < 1.0) { + multiplier *= sub_pos.y * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); } - else if (pos.y > 5.0 / 6.0) { - multiplier *= (1.0 - sub_pos.y) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + else if (sub_pos.y > 5.0) { + multiplier *= (6.0 - sub_pos.y) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); } - if (pos.x < 1.0 / 6.0) { - multiplier *= sub_pos.x * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + if (sub_pos.x < 1.0) { + multiplier *= sub_pos.x * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); } - else if (pos.x > 5.0 / 6.0) { - multiplier *= (1.0 - sub_pos.x) * SCANLINE_DEPTH + (1 - SCANLINE_DEPTH); + else if (sub_pos.x > 5.0) { + multiplier *= (6.0 - sub_pos.x) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); } vec4 pre_shadow = mix(texture(image, position) * multiplier, mix(r1, r2, s.y), BLOOM); + pre_shadow.a = 1.0; pixel += vec2(-0.6, -0.8); q11 = texture(image, (floor(pixel) + 0.5) / input_resolution); diff --git a/Shaders/OmniScale.fsh b/Shaders/OmniScale.fsh index eab27ae..960d08f 100644 --- a/Shaders/OmniScale.fsh +++ b/Shaders/OmniScale.fsh @@ -27,7 +27,7 @@ STATIC bool is_different(vec4 a, vec4 b) STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { // o = offset, the width of a pixel - vec2 o = 1.0 / input_resolution; + vec2 o = vec2(1, 1); /* We always calculate the top left quarter. If we need a different quarter, we flip our co-ordinates */ @@ -43,15 +43,15 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou p.y = 1.0 - p.y; } - vec4 w0 = texture(image, position + vec2( -o.x, -o.y)); - vec4 w1 = texture(image, position + vec2( 0, -o.y)); - vec4 w2 = texture(image, position + vec2( o.x, -o.y)); - vec4 w3 = texture(image, position + vec2( -o.x, 0)); - vec4 w4 = texture(image, position + vec2( 0, 0)); - vec4 w5 = texture(image, position + vec2( o.x, 0)); - vec4 w6 = texture(image, position + vec2( -o.x, o.y)); - vec4 w7 = texture(image, position + vec2( 0, o.y)); - vec4 w8 = texture(image, position + vec2( o.x, o.y)); + vec4 w0 = texture_relative(image, position, vec2( -o.x, -o.y)); + vec4 w1 = texture_relative(image, position, vec2( 0, -o.y)); + vec4 w2 = texture_relative(image, position, vec2( o.x, -o.y)); + vec4 w3 = texture_relative(image, position, vec2( -o.x, 0)); + vec4 w4 = texture_relative(image, position, vec2( 0, 0)); + vec4 w5 = texture_relative(image, position, vec2( o.x, 0)); + vec4 w6 = texture_relative(image, position, vec2( -o.x, o.y)); + vec4 w7 = texture_relative(image, position, vec2( 0, o.y)); + vec4 w8 = texture_relative(image, position, vec2( o.x, o.y)); int pattern = 0; if (is_different(w0, w4)) pattern |= 1 << 0; @@ -63,31 +63,31 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou if (is_different(w7, w4)) pattern |= 1 << 6; if (is_different(w8, w4)) pattern |= 1 << 7; - if ((P(0xbf,0x37) || P(0xdb,0x13)) && is_different(w1, w5)) { + if ((P(0xBF,0x37) || P(0xDB,0x13)) && is_different(w1, w5)) { return mix(w4, w3, 0.5 - p.x); } - if ((P(0xdb,0x49) || P(0xef,0x6d)) && is_different(w7, w3)) { + if ((P(0xDB,0x49) || P(0xEF,0x6D)) && is_different(w7, w3)) { return mix(w4, w1, 0.5 - p.y); } - if ((P(0x0b,0x0b) || P(0xfe,0x4a) || P(0xfe,0x1a)) && is_different(w3, w1)) { + if ((P(0x0B,0x0B) || P(0xFE,0x4A) || P(0xFE,0x1A)) && is_different(w3, w1)) { return w4; } - if ((P(0x6f,0x2a) || P(0x5b,0x0a) || P(0xbf,0x3a) || P(0xdf,0x5a) || - P(0x9f,0x8a) || P(0xcf,0x8a) || P(0xef,0x4e) || P(0x3f,0x0e) || - P(0xfb,0x5a) || P(0xbb,0x8a) || P(0x7f,0x5a) || P(0xaf,0x8a) || - P(0xeb,0x8a)) && is_different(w3, w1)) { + if ((P(0x6F,0x2A) || P(0x5B,0x0A) || P(0xBF,0x3A) || P(0xDF,0x5A) || + P(0x9F,0x8A) || P(0xCF,0x8A) || P(0xEF,0x4E) || P(0x3F,0x0E) || + P(0xFB,0x5A) || P(0xBB,0x8A) || P(0x7F,0x5A) || P(0xAF,0x8A) || + P(0xEB,0x8A)) && is_different(w3, w1)) { return mix(w4, mix(w4, w0, 0.5 - p.x), 0.5 - p.y); } - if (P(0x0b,0x08)) { + if (P(0x0B,0x08)) { return mix(mix(w0 * 0.375 + w1 * 0.25 + w4 * 0.375, w4 * 0.5 + w1 * 0.5, p.x * 2.0), w4, p.y * 2.0); } - if (P(0x0b,0x02)) { + if (P(0x0B,0x02)) { return mix(mix(w0 * 0.375 + w3 * 0.25 + w4 * 0.375, w4 * 0.5 + w3 * 0.5, p.y * 2.0), w4, p.x * 2.0); } - if (P(0x2f,0x2f)) { + if (P(0x2F,0x2F)) { float dist = length(p - vec2(0.5)); float pixel_size = length(1.0 / (output_resolution / input_resolution)); - if (dist < 0.5 - pixel_size / 2) { + if (dist < 0.5 - pixel_size / 2.0) { return w4; } vec4 r; @@ -98,40 +98,40 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); } - if (dist > 0.5 + pixel_size / 2) { + if (dist > 0.5 + pixel_size / 2.0) { return r; } - return mix(w4, r, (dist - 0.5 + pixel_size / 2) / pixel_size); + return mix(w4, r, (dist - 0.5 + pixel_size / 2.0) / pixel_size); } - if (P(0xbf,0x37) || P(0xdb,0x13)) { + if (P(0xBF,0x37) || P(0xDB,0x13)) { float dist = p.x - 2.0 * p.y; float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); - if (dist > pixel_size / 2) { + if (dist > pixel_size / 2.0) { return w1; } vec4 r = mix(w3, w4, p.x + 0.5); - if (dist < -pixel_size / 2) { + if (dist < -pixel_size / 2.0) { return r; } - return mix(r, w1, (dist + pixel_size / 2) / pixel_size); + return mix(r, w1, (dist + pixel_size / 2.0) / pixel_size); } - if (P(0xdb,0x49) || P(0xef,0x6d)) { + if (P(0xDB,0x49) || P(0xEF,0x6D)) { float dist = p.y - 2.0 * p.x; float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); - if (p.y - 2.0 * p.x > pixel_size / 2) { + if (p.y - 2.0 * p.x > pixel_size / 2.0) { return w3; } vec4 r = mix(w1, w4, p.x + 0.5); - if (dist < -pixel_size / 2) { + if (dist < -pixel_size / 2.0) { return r; } - return mix(r, w3, (dist + pixel_size / 2) / pixel_size); + return mix(r, w3, (dist + pixel_size / 2.0) / pixel_size); } - if (P(0xbf,0x8f) || P(0x7e,0x0e)) { + if (P(0xBF,0x8F) || P(0x7E,0x0E)) { float dist = p.x + 2.0 * p.y; float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); - if (dist > 1.0 + pixel_size / 2) { + if (dist > 1.0 + pixel_size / 2.0) { return w4; } @@ -143,18 +143,18 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); } - if (dist < 1.0 - pixel_size / 2) { + if (dist < 1.0 - pixel_size / 2.0) { return r; } - return mix(r, w4, (dist + pixel_size / 2 - 1.0) / pixel_size); + return mix(r, w4, (dist + pixel_size / 2.0 - 1.0) / pixel_size); } - if (P(0x7e,0x2a) || P(0xef,0xab)) { + if (P(0x7E,0x2A) || P(0xEF,0xAB)) { float dist = p.y + 2.0 * p.x; float pixel_size = length(1.0 / (output_resolution / input_resolution)) * sqrt(5.0); - if (p.y + 2.0 * p.x > 1.0 + pixel_size / 2) { + if (p.y + 2.0 * p.x > 1.0 + pixel_size / 2.0) { return w4; } @@ -167,33 +167,33 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); } - if (dist < 1.0 - pixel_size / 2) { + if (dist < 1.0 - pixel_size / 2.0) { return r; } - return mix(r, w4, (dist + pixel_size / 2 - 1.0) / pixel_size); + return mix(r, w4, (dist + pixel_size / 2.0 - 1.0) / pixel_size); } - if (P(0x1b,0x03) || P(0x4f,0x43) || P(0x8b,0x83) || P(0x6b,0x43)) { + if (P(0x1B,0x03) || P(0x4F,0x43) || P(0x8B,0x83) || P(0x6B,0x43)) { return mix(w4, w3, 0.5 - p.x); } - if (P(0x4b,0x09) || P(0x8b,0x89) || P(0x1f,0x19) || P(0x3b,0x19)) { + if (P(0x4B,0x09) || P(0x8B,0x89) || P(0x1F,0x19) || P(0x3B,0x19)) { return mix(w4, w1, 0.5 - p.y); } - if (P(0xfb,0x6a) || P(0x6f,0x6e) || P(0x3f,0x3e) || P(0xfb,0xfa) || - P(0xdf,0xde) || P(0xdf,0x1e)) { + if (P(0xFB,0x6A) || P(0x6F,0x6E) || P(0x3F,0x3E) || P(0xFB,0xFA) || + P(0xDF,0xDE) || P(0xDF,0x1E)) { return mix(w4, w0, (1.0 - p.x - p.y) / 2.0); } - if (P(0x4f,0x4b) || P(0x9f,0x1b) || P(0x2f,0x0b) || - P(0xbe,0x0a) || P(0xee,0x0a) || P(0x7e,0x0a) || P(0xeb,0x4b) || - P(0x3b,0x1b)) { + if (P(0x4F,0x4B) || P(0x9F,0x1B) || P(0x2F,0x0B) || + P(0xBE,0x0A) || P(0xEE,0x0A) || P(0x7E,0x0A) || P(0xEB,0x4B) || + P(0x3B,0x1B)) { float dist = p.x + p.y; float pixel_size = length(1.0 / (output_resolution / input_resolution)); - if (dist > 0.5 + pixel_size / 2) { + if (dist > 0.5 + pixel_size / 2.0) { return w4; } @@ -205,36 +205,36 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0), w1, p.x * 2.0); } - if (dist < 0.5 - pixel_size / 2) { + if (dist < 0.5 - pixel_size / 2.0) { return r; } - return mix(r, w4, (dist + pixel_size / 2 - 0.5) / pixel_size); + return mix(r, w4, (dist + pixel_size / 2.0 - 0.5) / pixel_size); } - if (P(0x0b,0x01)) { + if (P(0x0B,0x01)) { return mix(mix(w4, w3, 0.5 - p.x), mix(w1, (w1 + w3) / 2.0, 0.5 - p.x), 0.5 - p.y); } - if (P(0x0b,0x00)) { + if (P(0x0B,0x00)) { return mix(mix(w4, w3, 0.5 - p.x), mix(w1, w0, 0.5 - p.x), 0.5 - p.y); } float dist = p.x + p.y; float pixel_size = length(1.0 / (output_resolution / input_resolution)); - if (dist > 0.5 + pixel_size / 2) { + if (dist > 0.5 + pixel_size / 2.0) { return w4; } /* We need more samples to "solve" this diagonal */ - vec4 x0 = texture(image, position + vec2( -o.x * 2.0, -o.y * 2.0)); - vec4 x1 = texture(image, position + vec2( -o.x , -o.y * 2.0)); - vec4 x2 = texture(image, position + vec2( 0.0 , -o.y * 2.0)); - vec4 x3 = texture(image, position + vec2( o.x , -o.y * 2.0)); - vec4 x4 = texture(image, position + vec2( -o.x * 2.0, -o.y )); - vec4 x5 = texture(image, position + vec2( -o.x * 2.0, 0.0 )); - vec4 x6 = texture(image, position + vec2( -o.x * 2.0, o.y )); + vec4 x0 = texture_relative(image, position, vec2( -o.x * 2.0, -o.y * 2.0)); + vec4 x1 = texture_relative(image, position, vec2( -o.x , -o.y * 2.0)); + vec4 x2 = texture_relative(image, position, vec2( 0.0 , -o.y * 2.0)); + vec4 x3 = texture_relative(image, position, vec2( o.x , -o.y * 2.0)); + vec4 x4 = texture_relative(image, position, vec2( -o.x * 2.0, -o.y )); + vec4 x5 = texture_relative(image, position, vec2( -o.x * 2.0, 0.0 )); + vec4 x6 = texture_relative(image, position, vec2( -o.x * 2.0, o.y )); if (is_different(x0, w4)) pattern |= 1 << 8; if (is_different(x1, w4)) pattern |= 1 << 9; @@ -252,10 +252,10 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou if (diagonal_bias <= 0) { vec4 r = mix(w1, w3, p.y - p.x + 0.5); - if (dist < 0.5 - pixel_size / 2) { + if (dist < 0.5 - pixel_size / 2.0) { return r; } - return mix(r, w4, (dist + pixel_size / 2 - 0.5) / pixel_size); + return mix(r, w4, (dist + pixel_size / 2.0 - 0.5) / pixel_size); } return w4; diff --git a/Shaders/Scale2x.fsh b/Shaders/Scale2x.fsh index 17b6edb..44bcfc4 100644 --- a/Shaders/Scale2x.fsh +++ b/Shaders/Scale2x.fsh @@ -2,22 +2,16 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { - // o = offset, the width of a pixel - vec2 o = 1.0 / input_resolution; - // texel arrangement // A B C // D E F // G H I - // vec4 A = texture(image, position + vec2( -o.x, o.y)); - vec4 B = texture(image, position + vec2( 0, o.y)); - // vec4 C = texture(image, position + vec2( o.x, o.y)); - vec4 D = texture(image, position + vec2( -o.x, 0)); - vec4 E = texture(image, position + vec2( 0, 0)); - vec4 F = texture(image, position + vec2( o.x, 0)); - // vec4 G = texture(image, position + vec2( -o.x, -o.y)); - vec4 H = texture(image, position + vec2( 0, -o.y)); - // vec4 I = texture(image, position + vec2( o.x, -o.y)); + + vec4 B = texture_relative(image, position, vec2( 0, 1)); + vec4 D = texture_relative(image, position, vec2( -1, 0)); + vec4 E = texture_relative(image, position, vec2( 0, 0)); + vec4 F = texture_relative(image, position, vec2( 1, 0)); + vec4 H = texture_relative(image, position, vec2( 0, -1)); vec2 p = position * input_resolution; // p = the position within a pixel [0...1] p = fract(p); diff --git a/Shaders/Scale4x.fsh b/Shaders/Scale4x.fsh index da1ff14..f76e0ec 100644 --- a/Shaders/Scale4x.fsh +++ b/Shaders/Scale4x.fsh @@ -1,23 +1,16 @@ STATIC vec4 scale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { - // o = offset, the width of a pixel - vec2 o = 1.0 / input_resolution; // texel arrangement // A B C // D E F // G H I - // vec4 A = texture(image, position + vec2( -o.x, o.y)); - vec4 B = texture(image, position + vec2( 0, o.y)); - // vec4 C = texture(image, position + vec2( o.x, o.y)); - vec4 D = texture(image, position + vec2( -o.x, 0)); - vec4 E = texture(image, position + vec2( 0, 0)); - vec4 F = texture(image, position + vec2( o.x, 0)); - // vec4 G = texture(image, position + vec2( -o.x, -o.y)); - vec4 H = texture(image, position + vec2( 0, -o.y)); - // vec4 I = texture(image, position + vec2( o.x, -o.y)); + vec4 B = texture_relative(image, position, vec2( 0, 1)); + vec4 D = texture_relative(image, position, vec2( -1, 0)); + vec4 E = texture_relative(image, position, vec2( 0, 0)); + vec4 F = texture_relative(image, position, vec2( 1, 0)); + vec4 H = texture_relative(image, position, vec2( 0, -1)); vec2 p = position * input_resolution; // p = the position within a pixel [0...1] - vec4 R; p = fract(p); if (p.x > .5) { if (p.y > .5) { @@ -38,24 +31,24 @@ STATIC vec4 scale2x(sampler2D image, vec2 position, vec2 input_resolution, vec2 } } +STATIC vec4 scale2x_wrapper(sampler2D t, vec2 pos, vec2 offset, vec2 input_resolution, vec2 output_resolution) +{ + vec2 origin = (floor(pos * input_resolution * 2.0)) + vec2(0.5, 0.5); + return scale2x(t, (origin + offset) / input_resolution / 2.0, input_resolution, output_resolution); +} + STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) { - // o = offset, the width of a pixel - vec2 o = 1.0 / (input_resolution * 2.); - // texel arrangement // A B C // D E F // G H I - // vec4 A = scale2x(image, position + vec2( -o.x, o.y), input_resolution, output_resolution); - vec4 B = scale2x(image, position + vec2( 0, o.y), input_resolution, output_resolution); - // vec4 C = scale2x(image, position + vec2( o.x, o.y), input_resolution, output_resolution); - vec4 D = scale2x(image, position + vec2( -o.x, 0), input_resolution, output_resolution); - vec4 E = scale2x(image, position + vec2( 0, 0), input_resolution, output_resolution); - vec4 F = scale2x(image, position + vec2( o.x, 0), input_resolution, output_resolution); - // vec4 G = scale2x(image, position + vec2( -o.x, -o.y), input_resolution, output_resolution); - vec4 H = scale2x(image, position + vec2( 0, -o.y), input_resolution, output_resolution); - // vec4 I = scale2x(image, position + vec2( o.x, -o.y), input_resolution, output_resolution); + vec4 B = scale2x_wrapper(image, position, vec2( 0, 1), input_resolution, output_resolution); + vec4 D = scale2x_wrapper(image, position, vec2( -1, 0), input_resolution, output_resolution); + vec4 E = scale2x_wrapper(image, position, vec2( 0, 0), input_resolution, output_resolution); + vec4 F = scale2x_wrapper(image, position, vec2( 1, 0), input_resolution, output_resolution); + vec4 H = scale2x_wrapper(image, position, vec2( 0, -1), input_resolution, output_resolution); + vec2 p = position * input_resolution * 2.; // p = the position within a pixel [0...1] p = fract(p); diff --git a/Tester/main.c b/Tester/main.c index a3add10..a1c89a5 100755 --- a/Tester/main.c +++ b/Tester/main.c @@ -123,7 +123,7 @@ static void handle_buttons(GB_gameboy_t *gb) } -static void vblank(GB_gameboy_t *gb) +static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type) { /* Detect common crashes and stop the test early */ if (frames < test_length - 1) { @@ -432,6 +432,7 @@ int main(int argc, char **argv) GB_set_async_input_callback(&gb, async_input_callback); GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_EMULATE_HARDWARE); GB_set_rtc_mode(&gb, GB_RTC_MODE_ACCURATE); + GB_set_emulate_joypad_bouncing(&gb, false); // Adds too much noise if (GB_load_rom(&gb, filename)) { perror("Failed to load ROM"); diff --git a/Windows/dirent.c b/Windows/dirent.c new file mode 100755 index 0000000..f5fd8b3 --- /dev/null +++ b/Windows/dirent.c @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include "dirent.h" + +DIR *opendir(const char *filename) +{ + wchar_t w_filename[MAX_PATH + 3] = {0,}; + unsigned length = MultiByteToWideChar(CP_UTF8, 0, filename, -1, w_filename, MAX_PATH); + if (length) { + w_filename[length - 1] = L'/'; + w_filename[length] = L'*'; + w_filename[length + 1] = 0; + } + DIR *ret = malloc(sizeof(*ret)); + ret->handle = FindFirstFileW(w_filename, &ret->entry); + if (ret->handle == INVALID_HANDLE_VALUE) { + free(ret); + return NULL; + } + + return ret; +} + +int closedir(DIR *dir) +{ + if (dir->handle != INVALID_HANDLE_VALUE) { + FindClose(dir->handle); + } + free(dir); + return 0; +} + +struct dirent *readdir(DIR *dir) +{ + if (dir->handle == INVALID_HANDLE_VALUE) { + return NULL; + } + + WideCharToMultiByte(CP_UTF8, 0, dir->entry.cFileName, -1, + dir->out_entry.d_name, sizeof(dir->out_entry.d_name), + NULL, NULL); + + if (!FindNextFileW(dir->handle, &dir->entry)) { + FindClose(dir->handle); + dir->handle = INVALID_HANDLE_VALUE; + } + + return &dir->out_entry; +} diff --git a/Windows/dirent.h b/Windows/dirent.h new file mode 100755 index 0000000..7102995 --- /dev/null +++ b/Windows/dirent.h @@ -0,0 +1,15 @@ +#include + +struct dirent { + char d_name[MAX_PATH + 1]; +}; + +typedef struct { + HANDLE handle; + WIN32_FIND_DATAW entry; + struct dirent out_entry; +} DIR; + +DIR *opendir(const char *filename); +int closedir(DIR *dir); +struct dirent *readdir(DIR *dir); diff --git a/Windows/limits.h b/Windows/limits.h new file mode 100644 index 0000000..1ef24fb --- /dev/null +++ b/Windows/limits.h @@ -0,0 +1,2 @@ +#include_next +#define PATH_MAX 1024 diff --git a/Windows/resources.rc b/Windows/resources.rc index 73c1213..7fd16ed 100644 Binary files a/Windows/resources.rc and b/Windows/resources.rc differ diff --git a/Windows/sameboy.ico b/Windows/sameboy.ico index bd8e372..17f072c 100644 Binary files a/Windows/sameboy.ico and b/Windows/sameboy.ico differ diff --git a/Windows/stdio.h b/Windows/stdio.h index 1e6ec02..b8be393 100755 --- a/Windows/stdio.h +++ b/Windows/stdio.h @@ -1,10 +1,23 @@ #pragma once + +#ifdef noinline +#undef noinline #include_next +#define noinline __attribute__((noinline)) +#else +#include_next +#endif + #include +#include + +#if _WIN64 +#define fseek(...) _fseeki64(__VA_ARGS__) +#endif int access(const char *filename, int mode); -#define R_OK 2 -#define W_OK 4 +#define R_OK 4 +#define W_OK 2 #ifndef __MINGW32__ #ifndef __LIBRETRO__ @@ -20,6 +33,16 @@ static inline int vasprintf(char **str, const char *fmt, va_list args) } return ret; } + +static inline int asprintf(char **strp, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + int r = vasprintf(strp, fmt, args); + va_end(args); + return r; +} + #endif #endif diff --git a/Windows/stdlib.h b/Windows/stdlib.h new file mode 100755 index 0000000..7d35615 --- /dev/null +++ b/Windows/stdlib.h @@ -0,0 +1,3 @@ +#pragma once +#include_next +#define alloca _alloca diff --git a/Windows/string.h b/Windows/string.h index b899ca9..f1cf6b1 100755 --- a/Windows/string.h +++ b/Windows/string.h @@ -1,3 +1,4 @@ #pragma once #include_next -#define strdup _strdup \ No newline at end of file +#define strdup _strdup +#define strcasecmp _stricmp diff --git a/Windows/unistd.h b/Windows/unistd.h index 62e2337..c17587e 100644 --- a/Windows/unistd.h +++ b/Windows/unistd.h @@ -6,3 +6,5 @@ #define read(...) _read(__VA_ARGS__) #define write(...) _write(__VA_ARGS__) #define isatty(...) _isatty(__VA_ARGS__) +#define close(...) _close(__VA_ARGS__) +#define creat(...) _creat(__VA_ARGS__) diff --git a/Windows/utf8_compat.c b/Windows/utf8_compat.c index 0347211..9264e2e 100755 --- a/Windows/utf8_compat.c +++ b/Windows/utf8_compat.c @@ -22,3 +22,10 @@ int access(const char *filename, int mode) return _waccess(w_filename, mode); } +int _creat(const char *filename, int mode) +{ + wchar_t w_filename[MAX_PATH] = {0,}; + MultiByteToWideChar(CP_UTF8, 0, filename, -1, w_filename, sizeof(w_filename) / sizeof(w_filename[0])); + return _wcreat(w_filename, mode & 0700); +} + diff --git a/build-faq.md b/build-faq.md index 0921436..b2b791e 100644 --- a/build-faq.md +++ b/build-faq.md @@ -15,11 +15,11 @@ For the various tools and libraries, follow the below guide to ensure easy, prop ### SDL2 -For [libSDL2](https://libsdl.org/download-2.0.php), download the Visual C++ Development Library pack. Place the extracted files within a known folder for later. Both the `\x86\` and `\include\` paths will be needed. +For [libSDL2](https://libsdl.org/download-2.0.php), download the Visual C++ Development Library pack. Place the extracted files within a known folder for later. Both the `\x64\` and `\include\` paths will be needed. The following examples will be referenced later: -- `C:\SDL2\lib\x86\*` +- `C:\SDL2\lib\x64\*` - `C:\SDL2\include\*` ### rgbds @@ -35,8 +35,8 @@ Ensure that the `gnuwin32\bin\` directory is included in `%PATH%`. Like rgbds ab Within a command prompt in the project directory: ``` -vcvars32 -set lib=%lib%;C:\SDL2\lib\x86 +vcvars64 +set lib=%lib%;C:\SDL2\lib\x64 set include=%include%;C:\SDL2\include make ``` diff --git a/iOS/AppIcon60x60@2x.png b/iOS/AppIcon60x60@2x.png new file mode 100644 index 0000000..29d7afb Binary files /dev/null and b/iOS/AppIcon60x60@2x.png differ diff --git a/iOS/AppIcon60x60@3x.png b/iOS/AppIcon60x60@3x.png new file mode 100644 index 0000000..8df5dab Binary files /dev/null and b/iOS/AppIcon60x60@3x.png differ diff --git a/iOS/AppIcon76x76@2x.png b/iOS/AppIcon76x76@2x.png new file mode 100644 index 0000000..5f3ee20 Binary files /dev/null and b/iOS/AppIcon76x76@2x.png differ diff --git a/iOS/AppIcon83.5x83.5@2x.png b/iOS/AppIcon83.5x83.5@2x.png new file mode 100644 index 0000000..7ac8dc2 Binary files /dev/null and b/iOS/AppIcon83.5x83.5@2x.png differ diff --git a/iOS/CameraRotateTemplate@2x.png b/iOS/CameraRotateTemplate@2x.png new file mode 100644 index 0000000..2862152 Binary files /dev/null and b/iOS/CameraRotateTemplate@2x.png differ diff --git a/iOS/CameraRotateTemplate@3x.png b/iOS/CameraRotateTemplate@3x.png new file mode 100644 index 0000000..ece8ad9 Binary files /dev/null and b/iOS/CameraRotateTemplate@3x.png differ diff --git a/iOS/Cartridge.png b/iOS/Cartridge.png new file mode 100644 index 0000000..8b19250 Binary files /dev/null and b/iOS/Cartridge.png differ diff --git a/iOS/ColorCartridge.png b/iOS/ColorCartridge.png new file mode 100644 index 0000000..a158410 Binary files /dev/null and b/iOS/ColorCartridge.png differ diff --git a/iOS/GBAboutController.h b/iOS/GBAboutController.h new file mode 100644 index 0000000..9757309 --- /dev/null +++ b/iOS/GBAboutController.h @@ -0,0 +1,5 @@ +#import + +@interface GBAboutController : UIViewController + +@end diff --git a/iOS/GBAboutController.m b/iOS/GBAboutController.m new file mode 100644 index 0000000..3941a4e --- /dev/null +++ b/iOS/GBAboutController.m @@ -0,0 +1,245 @@ +#import "GBAboutController.h" + +@implementation GBAboutController +{ + UILabel *_titleLabel; + UILabel *_versionLabel; + UIImageView *_logo; + UITextView *_licenseView; + UILabel *_copyrightLabel; + UIView *_buttonsView; +} + +- (UIImage *)buttonImageNamed:(NSString *)name +{ + if (@available(iOS 13.0, *)) { + return [UIImage systemImageNamed:name withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]; + } + return nil; +} + +- (void)alignButton:(UIButton *)button +{ + if (!button.imageView.image) return; + CGSize imageSize = button.imageView.frame.size; + + button.imageEdgeInsets = UIEdgeInsetsMake(0, (32 - imageSize.width) / 2, 0, 0); + button.titleEdgeInsets = UIEdgeInsetsMake(0, 32 - imageSize.width, 0, 0); +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleProminent]; + self.view = [[UIVisualEffectView alloc] initWithEffect:effect]; + UIView *root = ((UIVisualEffectView *)self.view).contentView; + + _titleLabel = [[UILabel alloc] init]; + _titleLabel.text = @"SameBoy"; + _titleLabel.font = [UIFont systemFontOfSize:34 weight:UIFontWeightHeavy]; + [root addSubview:_titleLabel]; + + _versionLabel = [[UILabel alloc] init]; + _versionLabel.text = @"Version " GB_VERSION; + _versionLabel.font = [UIFont systemFontOfSize:24 weight:UIFontWeightLight]; + [root addSubview:_versionLabel]; + + _logo = [[UIImageView alloc] init]; + _logo.image = [UIImage imageNamed:@"logo"]; + _logo.contentMode = UIViewContentModeCenter; + [root addSubview:_logo]; + + _licenseView = [[UITextView alloc] init]; + NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"License" ofType:@"html"]]; + _licenseView.attributedText = [[NSAttributedString alloc] initWithData:data + options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, + NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)} + documentAttributes:nil + error:nil]; + _licenseView.editable = false; + if (@available(iOS 13.0, *)) { + _licenseView.backgroundColor = [UIColor systemBackgroundColor]; + _licenseView.textColor = [UIColor labelColor]; + } + else { + _licenseView.backgroundColor = [UIColor whiteColor]; + } + _licenseView.hidden = true; + _licenseView.userInteractionEnabled = false; + _licenseView.layer.cornerRadius = 6; + [root addSubview:_licenseView]; + + _buttonsView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; + + UIButton *websiteButton = [[UIButton alloc] initWithFrame:CGRectMake(20, 0, 280, 37)]; + [websiteButton setTitle:@"Website" forState:UIControlStateNormal]; + [websiteButton setImage:[self buttonImageNamed:@"globe"] forState:UIControlStateNormal]; + websiteButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; + [websiteButton setTitleColor:websiteButton.tintColor forState:UIControlStateNormal]; + websiteButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + [websiteButton addTarget:self action:@selector(openWebsite) forControlEvents:UIControlEventTouchUpInside]; + [self alignButton:websiteButton]; + [_buttonsView addSubview:websiteButton]; + + UIButton *sponsorButton = [[UIButton alloc] initWithFrame:CGRectMake(20, 45, 280, 37)]; + [sponsorButton setTitle:@"Sponsor SameBoy" forState:UIControlStateNormal]; + [sponsorButton setImage:[self buttonImageNamed:@"heart"] forState:UIControlStateNormal]; + sponsorButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; + [sponsorButton setTitleColor:sponsorButton.tintColor forState:UIControlStateNormal]; + sponsorButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + [sponsorButton addTarget:self action:@selector(openSponsor) forControlEvents:UIControlEventTouchUpInside]; + [self alignButton:sponsorButton]; + [_buttonsView addSubview:sponsorButton]; + + UIButton *licenseButton = [[UIButton alloc] initWithFrame:CGRectMake(20, 90, 280, 37)]; + [licenseButton setTitle:@"License" forState:UIControlStateNormal]; + [licenseButton setImage:[self buttonImageNamed:@"doc.plaintext"] forState:UIControlStateNormal]; + licenseButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; + licenseButton.titleLabel.textColor = licenseButton.tintColor; + [licenseButton setTitleColor:licenseButton.tintColor forState:UIControlStateNormal]; + licenseButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + [licenseButton addTarget:self action:@selector(showLicense) forControlEvents:UIControlEventTouchUpInside]; + [self alignButton:licenseButton]; + [_buttonsView addSubview:licenseButton]; + + [root addSubview:_buttonsView]; + + _copyrightLabel = [[UILabel alloc] init]; + _copyrightLabel.text = [[NSBundle mainBundle] infoDictionary][@"NSHumanReadableCopyright"]; + _copyrightLabel.textAlignment = NSTextAlignmentCenter; + _copyrightLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + _copyrightLabel.font = [UIFont systemFontOfSize:13]; + [root addSubview:_copyrightLabel]; + + [self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(dismissSelf)]]; +} + +- (void)layoutForVerticalLayout +{ + UIView *root = ((UIVisualEffectView *)self.view).contentView; + CGRect savedFrame = root.frame; + root.frame = CGRectMake(0, 0, 320, 480); + + _titleLabel.frame = CGRectMake(0, 20, 320, 47); + _titleLabel.textAlignment = NSTextAlignmentCenter; + _titleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; + + _versionLabel.frame = CGRectMake(0, 75, 320, 36); + _versionLabel.textAlignment = NSTextAlignmentCenter; + _versionLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; + + _logo.frame = CGRectMake(0, 119, 320, 128); + _logo.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; + + _buttonsView.frame = CGRectMake(0, 255, 320, 176); + _buttonsView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + _licenseView.frame = CGRectMake(20, 255, 280, 176); + _licenseView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + + _copyrightLabel.frame = CGRectMake(0, 450, 320, 21); + _copyrightLabel.textAlignment = NSTextAlignmentCenter; + _copyrightLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + + root.frame = savedFrame; +} + +- (void)layoutForHorizontalLayout +{ + UIView *root = ((UIVisualEffectView *)self.view).contentView; + CGRect savedFrame = root.frame; + root.frame = CGRectMake(0, 0, 568, 320); + + _titleLabel.frame = CGRectMake(20, 20, 260, 47); + _titleLabel.textAlignment = NSTextAlignmentLeft; + _titleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleBottomMargin | + UIViewAutoresizingFlexibleRightMargin; + + _versionLabel.frame = CGRectMake(20, 75, 260, 36); + _versionLabel.textAlignment = NSTextAlignmentLeft; + _versionLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleBottomMargin | + UIViewAutoresizingFlexibleRightMargin; + + _logo.frame = CGRectMake(0, 119, 284, 152); + _logo.autoresizingMask = UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleRightMargin | + UIViewAutoresizingFlexibleHeight; + + _buttonsView.frame = _licenseView.frame = CGRectMake(284, 20, 284, 280); + _buttonsView.autoresizingMask = UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleHeight | + UIViewAutoresizingFlexibleLeftMargin; + _licenseView.autoresizingMask = UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleHeight | + UIViewAutoresizingFlexibleLeftMargin; + + + _copyrightLabel.frame = CGRectMake(20, 288, 260, 21); + _copyrightLabel.textAlignment = NSTextAlignmentLeft; + _copyrightLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleTopMargin | + UIViewAutoresizingFlexibleRightMargin; + + root.frame = savedFrame; + CGRect licenseFrame = _licenseView.frame; + licenseFrame.size.width -= 40; + licenseFrame.origin.x += 20; + _licenseView.frame = licenseFrame; +} + +- (void)openWebsite +{ + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://sameboy.github.io"] options:nil completionHandler:nil]; +} + +- (void)openSponsor +{ + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/sponsors/LIJI32"] options:nil completionHandler:nil]; +} + +- (void)showLicense +{ + _buttonsView.hidden = true; + _licenseView.hidden = false; + _licenseView.userInteractionEnabled = true; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) { + UIEdgeInsets insets = self.view.window.safeAreaInsets; + UIView *view = ((UIVisualEffectView *)self.view).contentView; + CGRect parentFrame = self.view.frame; + view.frame = CGRectMake(insets.left, + 0, + parentFrame.size.width - insets.left - insets.right, + parentFrame.size.height - insets.bottom); + } + if (self.view.frame.size.width > self.view.frame.size.height) { + [self layoutForHorizontalLayout]; + } + else { + [self layoutForVerticalLayout]; + } +} + +- (void)dismissSelf +{ + [self.presentingViewController dismissViewControllerAnimated:true completion:nil]; +} + +- (UIModalPresentationStyle)modalPresentationStyle +{ + return UIModalPresentationFormSheet; +} + +- (void)dismissViewController +{ + [self dismissViewControllerAnimated:true completion:nil]; +} +@end diff --git a/iOS/GBBackgroundView.h b/iOS/GBBackgroundView.h new file mode 100644 index 0000000..9659588 --- /dev/null +++ b/iOS/GBBackgroundView.h @@ -0,0 +1,15 @@ +#import +#import "GBLayout.h" +#import "GBView.h" + +@interface GBBackgroundView : UIImageView +- (instancetype)initWithLayout:(GBLayout *)layout; + +@property (readonly) GBView *gbView; +@property (nonatomic) GBLayout *layout; +@property (nonatomic) bool usesSwipePad; + +- (void)enterPreviewMode:(bool)showLabel; +- (void)reloadThemeImages; +- (void)fadeOverlayOut; +@end diff --git a/iOS/GBBackgroundView.m b/iOS/GBBackgroundView.m new file mode 100644 index 0000000..8878717 --- /dev/null +++ b/iOS/GBBackgroundView.m @@ -0,0 +1,582 @@ +#import "GBBackgroundView.h" +#import "GBViewMetal.h" +#import "GBHapticManager.h" +#import "GBMenuViewController.h" +#import "GBViewController.h" +#import "GBROMManager.h" + +double CGPointSquaredDistance(CGPoint a, CGPoint b) +{ + double deltaX = a.x - b.x; + double deltaY = a.y - b.y; + return deltaX * deltaX + deltaY * deltaY; +} + +double CGPointAngle(CGPoint a, CGPoint b) +{ + double deltaX = a.x - b.x; + double deltaY = a.y - b.y; + return atan2(deltaY, deltaX); +} + +static void positionView(UIImageView *view, CGPoint position) +{ + double center = view.image.size.width / 2 * [UIScreen mainScreen].scale; + view.frame = (CGRect){ + { + round(position.x - center) / [UIScreen mainScreen].scale, + round(position.y - center) / [UIScreen mainScreen].scale + }, + view.image.size + }; +} + +static GB_key_mask_t angleToKeyMask(double angle) +{ + signed quantizedAngle = round(angle / M_PI * 16); + if (quantizedAngle < 0) { + quantizedAngle += 32; + } + switch (quantizedAngle) { + case 32: + case 0: return GB_KEY_RIGHT_MASK; + case 1: return GB_KEY_RIGHT_MASK; + case 2: return GB_KEY_RIGHT_MASK; + case 3: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK; + case 4: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK; + case 5: return GB_KEY_DOWN_MASK; + case 6: return GB_KEY_DOWN_MASK; + case 7: return GB_KEY_DOWN_MASK; + + case 8: return GB_KEY_DOWN_MASK; + case 9: return GB_KEY_DOWN_MASK; + case 10: return GB_KEY_DOWN_MASK; + case 11: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK; + case 12: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK; + case 13: return GB_KEY_LEFT_MASK; + case 14: return GB_KEY_LEFT_MASK; + case 15: return GB_KEY_LEFT_MASK; + + case 16: return GB_KEY_LEFT_MASK; + case 17: return GB_KEY_LEFT_MASK; + case 18: return GB_KEY_LEFT_MASK; + case 19: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK; + case 20: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK; + case 21: return GB_KEY_UP_MASK; + case 22: return GB_KEY_UP_MASK; + case 23: return GB_KEY_UP_MASK; + + case 24: return GB_KEY_UP_MASK; + case 25: return GB_KEY_UP_MASK; + case 26: return GB_KEY_UP_MASK; + case 27: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK; + case 28: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK; + case 29: return GB_KEY_RIGHT_MASK; + case 30: return GB_KEY_RIGHT_MASK; + case 31: return GB_KEY_RIGHT_MASK; + } + + return 0; +} + +@implementation GBBackgroundView +{ + NSMutableSet *_touches; + UITouch *_padTouch; + CGPoint _padSwipeOrigin; + UITouch *_screenTouch; + UITouch *_logoTouch; + CGPoint _screenSwipeOrigin; + bool _screenSwiped; + bool _inDynamicSpeedMode; + bool _previewMode; + + UIImageView *_dpadView; + UIImageView *_dpadShadowView; + UIImageView *_aButtonView; + UIImageView *_bButtonView; + UIImageView *_startButtonView; + UIImageView *_selectButtonView; + UILabel *_screenLabel; + + UIVisualEffectView *_overlayView; + UIView *_overlayViewContents; + NSTimer *_fadeTimer; + + GB_key_mask_t _lastMask; +} + +- (void)reloadThemeImages +{ + _aButtonView.image = [_layout.theme imageNamed:@"buttonA"]; + _bButtonView.image = [_layout.theme imageNamed:@"buttonB"]; + _startButtonView.image = [_layout.theme imageNamed:@"button2"]; + _selectButtonView.image = [_layout.theme imageNamed:@"button2"]; + self.usesSwipePad = self.usesSwipePad; +} + +- (instancetype)initWithLayout:(GBLayout *)layout; +{ + self = [super initWithImage:nil]; + if (!self) return nil; + + _layout = layout; + _touches = [NSMutableSet set]; + + _screenLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _screenLabel.text = @"Tap the Game Boy screen to open the menu and load a ROM from the library."; + _screenLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightMedium]; + _screenLabel.textAlignment = NSTextAlignmentCenter; + _screenLabel.textColor = [UIColor whiteColor]; + _screenLabel.lineBreakMode = NSLineBreakByWordWrapping; + _screenLabel.numberOfLines = 0; + [self addSubview:_screenLabel]; + + _dpadView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"dpad"]]; + _aButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"buttonA"]]; + _bButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"buttonB"]]; + _startButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"button2"]]; + _selectButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"button2"]]; + + _dpadShadowView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpadShadow"]]; + _dpadShadowView.hidden = true; + _gbView = [[GBViewMetal alloc] initWithFrame:CGRectZero]; + + [self addSubview:_dpadView]; + [self addSubview:_aButtonView]; + [self addSubview:_bButtonView]; + [self addSubview:_startButtonView]; + [self addSubview:_selectButtonView]; + [self addSubview:_gbView]; + + [_dpadView addSubview:_dpadShadowView]; + + UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleDark]; + _overlayView = [[UIVisualEffectView alloc] initWithEffect:effect]; + _overlayView.frame = CGRectMake(8, 8, 32, 32); + _overlayView.layer.cornerRadius = 12; + _overlayView.layer.masksToBounds = true; + _overlayView.alpha = 0; + + if (@available(iOS 13.0, *)) { + _overlayViewContents = [[UIImageView alloc] init]; + _overlayViewContents.tintColor = [UIColor whiteColor]; + _overlayViewContents.contentMode = UIViewContentModeCenter; + } + else { + _overlayViewContents = [[UILabel alloc] init]; + ((UILabel *)_overlayViewContents).font = [UIFont systemFontOfSize:UIFont.systemFontSize weight:UIFontWeightMedium]; + ((UILabel *)_overlayViewContents).textColor = [UIColor whiteColor]; + } + _overlayViewContents.frame = CGRectMake(8, 8, 160, 20.5); + [_overlayView.contentView addSubview:_overlayViewContents]; + [_gbView addSubview:_overlayView]; + + return self; +} + +- (GBViewController *)viewController +{ + return (GBViewController *)[UIApplication sharedApplication].delegate; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_previewMode) return; + static const double dpadRadius = 75; + CGPoint dpadLocation = _layout.dpadLocation; + double factor = [UIScreen mainScreen].scale; + dpadLocation.x /= factor; + dpadLocation.y /= factor; + CGRect logoRect = _layout.logoRect; + + logoRect.origin.x /= factor; + logoRect.origin.y /= factor; + logoRect.size.width /= factor; + logoRect.size.height /= factor; + + for (UITouch *touch in touches) { + CGPoint point = [touch locationInView:self]; + if (CGRectContainsPoint(self.gbView.frame, point) && !_screenTouch) { + if (self.viewController.runMode != GBRunModeNormal) { + self.viewController.runMode = GBRunModeNormal; + [self fadeOverlayOut]; + } + else { + _screenTouch = touch; + _screenSwipeOrigin = point; + _screenSwiped = false; + _inDynamicSpeedMode = false; + _overlayView.alpha = 0; + [_fadeTimer invalidate]; + _fadeTimer = nil; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + self.viewController.runMode = GBRunModePaused; + [self displayOverlayWithImage:@"pause" orTitle:@"Paused"]; + } + } + } + else if (CGRectContainsPoint(logoRect, point) && !_logoTouch) { + _logoTouch = touch; + } + else if (!_padTouch) { + if (fabs(point.x - dpadLocation.x) <= dpadRadius && + fabs(point.y - dpadLocation.y) <= dpadRadius) { + _padTouch = touch; + _padSwipeOrigin = point; + } + } + } + [_touches unionSet:touches]; + [self touchesChanged]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_padTouch && [touches containsObject:_padTouch]) { + _padTouch = nil; + } + + if (_screenTouch && [touches containsObject:_screenTouch]) { + _screenTouch = nil; + if (self.viewController.runMode == GBRunModePaused) { + self.viewController.runMode = GBRunModeNormal; + [self fadeOverlayOut]; + } + if (!_screenSwiped) { + self.window.backgroundColor = nil; + [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil]; + } + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) { + if (self.viewController.runMode != GBRunModeNormal) { + self.viewController.runMode = GBRunModeNormal; + [self fadeOverlayOut]; + } + } + } + + if (_logoTouch && [touches containsObject:_logoTouch]) { + + double factor = [UIScreen mainScreen].scale; + CGRect logoRect = _layout.logoRect; + + logoRect.origin.x /= factor; + logoRect.origin.y /= factor; + logoRect.size.width /= factor; + logoRect.size.height /= factor; + + CGPoint point = [_logoTouch locationInView:self]; + if (CGRectContainsPoint(logoRect, point)) { + self.window.backgroundColor = nil; + [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil]; + } + _logoTouch = nil; + } + + [_touches minusSet:touches]; + [self touchesChanged]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [self touchesEnded:touches withEvent:event]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [self touchesChanged]; +} + +- (void)touchesChanged +{ + if (_previewMode) return; + if (!GB_is_inited(_gbView.gb)) return; + GB_key_mask_t mask = 0; + double factor = [UIScreen mainScreen].scale; + double buttonRadiusSquared = 36 * 36 * factor * factor; + double dpadRadius = 75 * factor; + bool dpadHandled = false; + if (_usesSwipePad) { + dpadHandled = true; + if (_padTouch) { + CGPoint point = [_padTouch locationInView:self]; + double squaredDistance = CGPointSquaredDistance(point, _padSwipeOrigin); + if (squaredDistance > 16 * 16) { + double angle = CGPointAngle(point, _padSwipeOrigin); + mask |= angleToKeyMask(angle); + if (squaredDistance > 24 * 24) { + double deltaX = point.x - _padSwipeOrigin.x; + double deltaY = point.y - _padSwipeOrigin.y; + double distance = sqrt(squaredDistance); + _padSwipeOrigin.x = point.x - deltaX / distance * 24; + _padSwipeOrigin.y = point.y - deltaY / distance * 24; + } + } + } + } + for (UITouch *touch in _touches) { + if (_usesSwipePad && touch == _padTouch) continue; + CGPoint point = [touch locationInView:self]; + + if (touch == _screenTouch) { + if (_inDynamicSpeedMode) { + double delta = point.x - _screenSwipeOrigin.x; + if (fabs(delta) < 32) { + self.viewController.runMode = GBRunModePaused; + [self displayOverlayWithImage:@"pause" orTitle:@"Paused"]; + continue; + } + + double speed = fabs(delta / _gbView.frame.size.width * 3); + if (delta > 0) { + if (speed > 1) { + [self displayOverlayWithImage:@"forward" orTitle:@"Fast-forwarding…"]; + } + else { + [self displayOverlayWithImage:@"play" orTitle:@"Forward…"]; + } + GB_set_clock_multiplier(_gbView.gb, speed); + self.viewController.runMode = GBRunModeTurbo; + } + else { + [self displayOverlayWithImage:@"backward" orTitle:@"Rewinding…"]; + GB_set_clock_multiplier(_gbView.gb, speed); + self.viewController.runMode = GBRunModeRewind; + + } + continue; + } + if (_screenSwiped) continue; + if (point.x - _screenSwipeOrigin.x > 32) { + [self turboSwipe]; + } + else if (point.x - _screenSwipeOrigin.x < -32) { + [self rewindSwipe]; + } + else if (point.y - _screenSwipeOrigin.y > 32) { + [self saveSwipe]; + } + else if (point.y - _screenSwipeOrigin.y < -32) { + [self loadSwipe]; + } + continue; + } + + point.x *= factor; + point.y *= factor; + if (!dpadHandled && + (touch == _padTouch || + (fabs(point.x - _layout.dpadLocation.x) <= dpadRadius && + fabs(point.y - _layout.dpadLocation.y) <= dpadRadius) + ) && (fabs(point.x - _layout.dpadLocation.x) >= dpadRadius / 5 || + fabs(point.y - _layout.dpadLocation.y) >= dpadRadius / 5)) { + dpadHandled = true; // Don't handle the dpad twice + double angle = CGPointAngle(point, _layout.dpadLocation); + mask |= angleToKeyMask(angle); + } + else if (CGPointSquaredDistance(point, _layout.aLocation) <= buttonRadiusSquared) { + mask |= GB_KEY_A_MASK; + } + else if (CGPointSquaredDistance(point, _layout.bLocation) <= buttonRadiusSquared) { + mask |= GB_KEY_B_MASK; + } + else if (CGPointSquaredDistance(point, _layout.startLocation) <= buttonRadiusSquared) { + mask |= GB_KEY_START_MASK; + } + else if (CGPointSquaredDistance(point, _layout.selectLocation) <= buttonRadiusSquared) { + mask |= GB_KEY_SELECT_MASK; + } + else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBEnableABCombo"] && + CGPointSquaredDistance(point, _layout.abComboLocation) <= buttonRadiusSquared) { + mask |= GB_KEY_A_MASK | GB_KEY_B_MASK; + } + } + if (mask != _lastMask) { + _aButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_A_MASK)? @"buttonAPressed" : @"buttonA"]; + _bButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_B_MASK)? @"buttonBPressed" : @"buttonB"]; + _startButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_START_MASK) ? @"button2Pressed" : @"button2"]; + _selectButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_SELECT_MASK)? @"button2Pressed" : @"button2"]; + + bool hidden = false; + bool diagonal = false; + double rotation = 0; + switch (mask & (GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK | GB_KEY_LEFT_MASK | GB_KEY_UP_MASK)) { + case GB_KEY_RIGHT_MASK: break; + case GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK: diagonal = true; break; + case GB_KEY_DOWN_MASK: rotation = M_PI_2; break; + case GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK: diagonal = true; rotation = M_PI_2; break; + case GB_KEY_LEFT_MASK: rotation = M_PI; break; + case GB_KEY_LEFT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = M_PI; break; + case GB_KEY_UP_MASK: rotation = -M_PI_2; break; + case GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = -M_PI_2; break; + default: + hidden = true; + } + + _dpadShadowView.hidden = hidden; + if (!hidden) { + if (_usesSwipePad) { + _dpadShadowView.image = [UIImage imageNamed:diagonal? @"swipepadShadowDiagonal" : @"swipepadShadow"]; + + } + else { + _dpadShadowView.image = [UIImage imageNamed:diagonal? @"dpadShadowDiagonal" : @"dpadShadow"]; + } + _dpadShadowView.transform = CGAffineTransformMakeRotation(rotation); + } + + GB_set_key_mask(_gbView.gb, mask); + if ((mask & ~_lastMask) && ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBButtonHaptics"])) { + [[GBHapticManager sharedManager] doTapHaptic]; + } + _lastMask = mask; + } +} + +- (BOOL)isMultipleTouchEnabled +{ + return true; +} + +- (BOOL)isUserInteractionEnabled +{ + return true; +} + +- (void)setLayout:(GBLayout *)layout +{ + _layout = layout; + self.image = layout.background; + + positionView(_dpadView, layout.dpadLocation); + positionView(_aButtonView, layout.aLocation); + positionView(_bButtonView, layout.bLocation); + positionView(_startButtonView, layout.startLocation); + positionView(_selectButtonView, layout.selectLocation); + + CGRect screenFrame = layout.screenRect; + screenFrame.origin.x /= [UIScreen mainScreen].scale; + screenFrame.origin.y /= [UIScreen mainScreen].scale; + screenFrame.size.width /= [UIScreen mainScreen].scale; + screenFrame.size.height /= [UIScreen mainScreen].scale; + + _gbView.frame = screenFrame; + + screenFrame.origin.x += 8; + screenFrame.origin.y += 8; + screenFrame.size.width -= 16; + screenFrame.size.height -= 16; + _screenLabel.frame = screenFrame; +} + +- (void)setUsesSwipePad:(bool)usesSwipePad +{ + _usesSwipePad = usesSwipePad; + _dpadView.image = nil; // Some bug in UIImage seems to trigger without this? + _dpadView.image = [_layout.theme imageNamed:usesSwipePad? @"swipepad" : @"dpad"]; +} + +- (void)displayOverlayWithImage:(NSString *)imageName orTitle:(NSString *)title +{ + if (@available(iOS 13.0, *)) { + ((UIImageView *)_overlayViewContents).image = [UIImage systemImageNamed:imageName + withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightMedium]]; + } + else { + ((UILabel *)_overlayViewContents).text = title; + } + [_overlayViewContents sizeToFit]; + + CGRect frame = _overlayViewContents.frame; + frame.size.width = MAX(frame.size.width, 25); + frame.size.height = MAX(frame.size.height, 22); + _overlayViewContents.frame = frame; + frame.origin = (CGPoint){8, 8}; + frame.size.width += 16; + frame.size.height += 16; + _overlayView.frame = frame; + + _overlayView.alpha = 1.0; +} + +- (void)fadeOverlayOut +{ + [UIView animateWithDuration:1 animations:^{ + _overlayView.alpha = 0; + }]; + [_fadeTimer invalidate]; + _fadeTimer = nil; +} + +- (void)turboSwipe +{ + _screenSwiped = true; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + _inDynamicSpeedMode = true; + } + [self displayOverlayWithImage:@"forward" orTitle:@"Fast-forwarding…"]; + self.viewController.runMode = GBRunModeTurbo; +} + +- (void)rewindSwipe +{ + _screenSwiped = true; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + _inDynamicSpeedMode = true; + } + [self displayOverlayWithImage:@"backward" orTitle:@"Rewinding…"]; + self.viewController.runMode = GBRunModeRewind; +} + +- (NSString *)swipeStateFile +{ + return [[GBROMManager sharedManager] stateFile:1]; +} + +- (void)saveSwipe +{ + _screenSwiped = true; + self.viewController.runMode = GBRunModeNormal; + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) { + [self fadeOverlayOut]; + return; + } + [self displayOverlayWithImage:@"square.and.arrow.down" orTitle:@"Saved state to Slot 1"]; + _fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) { + [self fadeOverlayOut]; + }]; + [self.viewController stop]; + [self.viewController saveStateToFile:self.swipeStateFile]; + [self.viewController start]; +} + +- (void)loadSwipe +{ + _screenSwiped = true; + self.viewController.runMode = GBRunModeNormal; + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) { + [self fadeOverlayOut]; + return; + } + [self displayOverlayWithImage:@"square.and.arrow.up" orTitle:@"Loaded state from Slot 1"]; + _fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) { + [self fadeOverlayOut]; + }]; + [self.viewController stop]; + [self.viewController loadStateFromFile:self.swipeStateFile]; + [self.viewController start]; +} + +- (void)enterPreviewMode:(bool)showLabel +{ + if (showLabel) { + _screenLabel.text = [NSString stringWithFormat:@"Previewing Theme “%@”", _layout.theme.name]; + } + else { + [_screenLabel removeFromSuperview]; + _screenLabel = nil; + } + _previewMode = true; +} + +@end diff --git a/iOS/GBHapticManager.h b/iOS/GBHapticManager.h new file mode 100644 index 0000000..7f2e2d9 --- /dev/null +++ b/iOS/GBHapticManager.h @@ -0,0 +1,9 @@ +#import +#import + +@interface GBHapticManager : NSObject ++ (instancetype)sharedManager; +- (void)doTapHaptic; +- (void)setRumbleStrength:(double)rumble; +@property (weak) GCController *controller; +@end diff --git a/iOS/GBHapticManager.m b/iOS/GBHapticManager.m new file mode 100644 index 0000000..3008351 --- /dev/null +++ b/iOS/GBHapticManager.m @@ -0,0 +1,143 @@ +#import "GBHapticManager.h" +#import "GBHapticManagerLegacy.h" +#import + +@implementation GBHapticManager +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + CHHapticEngine *_engine; + CHHapticEngine *_externalEngine; + id _rumblePlayer; +#pragma clang diagnostic pop + __weak GCController *_controller; + double _rumble; + dispatch_queue_t _queue; +} + ++ (instancetype)sharedManager +{ + static dispatch_once_t onceToken; + static GBHapticManager *manager; + dispatch_once(&onceToken, ^{ + manager = [[self alloc] init]; + }); + return manager; +} + +- (instancetype)init +{ + self = [super init]; + if (!self) return nil; + if (self.class != [GBHapticManager class]) return self; + + if (@available(iOS 13.0, *)) { + _engine = [[CHHapticEngine alloc] initAndReturnError:nil]; + _engine.playsHapticsOnly = true; + _engine.autoShutdownEnabled = true; + } + if (!_engine) return [[GBHapticManagerLegacy alloc] init]; + _queue = dispatch_queue_create("SameBoy Haptic Queue", NULL); + return self; +} + +#pragma clang diagnostic ignored "-Wpartial-availability" + +- (CHHapticEvent *) eventWithType:(CHHapticEventType)type + sharpness:(double)sharpness + intensity:(double)intensity + duration:(NSTimeInterval)duration +{ + return [[CHHapticEvent alloc] initWithEventType:type + parameters:@[[[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticSharpness + value:sharpness], + [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity + value:intensity]] + relativeTime:CHHapticTimeImmediate + duration:duration]; +} + +- (void)doTapHaptic +{ + double intensity = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBHapticsStrength"]; + if (_rumble > intensity) return; + + dispatch_async(_queue, ^{ + CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:@[[self eventWithType:CHHapticEventTypeHapticTransient + sharpness:0.25 + intensity:intensity + duration:1.0]] + parameters:nil + error:nil]; + @try { + id player = [_engine createPlayerWithPattern:pattern error:nil]; + [player startAtTime:0 error:nil]; + } + @catch (NSException *exception) {} + }); +} + +- (void)setRumbleStrength:(double)rumble +{ + if (!_controller) { // Controller disconnected + _externalEngine = nil; + } + if (!_externalEngine && _controller && !_controller.isAttachedToDevice) { + /* We have a controller with no rumble support which is not attached to the device, + don't rumble since the user is holding neither the device nor a haptic-enabled + controller. */ + rumble = 0; + } + if (rumble == 0) { + [_rumblePlayer stopAtTime:0 error:nil]; + _rumblePlayer = nil; + _rumble = 0; + return; + } + + // No change + if (rumble == _rumble) return; + _rumble = rumble; + + dispatch_async(_queue, ^{ + CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:@[[self eventWithType:CHHapticEventTypeHapticContinuous + sharpness:0.75 + intensity:rumble + duration:1.0]] + parameters:nil + error:nil]; + @try { + id newPlayer = [_externalEngine ?: _engine createPlayerWithPattern:pattern error:nil]; + [newPlayer startAtTime:0 error:nil]; + [_rumblePlayer stopAtTime:0 error:nil]; + _rumblePlayer = newPlayer; + } + @catch (NSException *exception) { + if (_externalEngine) { + // Something might have happened with our controller? Delete and try again + _externalEngine = nil; + [self setRumbleStrength: rumble]; + } + } + }); +} + +- (void)setController:(GCController *)controller +{ + if (_controller != controller) { + if (@available(iOS 14.0, *)) { + _externalEngine = [controller.haptics createEngineWithLocality:GCHapticsLocalityDefault]; + _externalEngine.playsHapticsOnly = true; + _externalEngine.autoShutdownEnabled = true; + + } + _controller = controller; + } +} + +- (GCController *)controller +{ + return _controller; +} + +@end diff --git a/iOS/GBHapticManagerLegacy.h b/iOS/GBHapticManagerLegacy.h new file mode 100644 index 0000000..1322d92 --- /dev/null +++ b/iOS/GBHapticManagerLegacy.h @@ -0,0 +1,5 @@ +#import "GBHapticManager.h" + +@interface GBHapticManagerLegacy : GBHapticManager + +@end diff --git a/iOS/GBHapticManagerLegacy.m b/iOS/GBHapticManagerLegacy.m new file mode 100644 index 0000000..43ce160 --- /dev/null +++ b/iOS/GBHapticManagerLegacy.m @@ -0,0 +1,27 @@ +#import "GBHapticManagerLegacy.h" +#import +#import + +@implementation GBHapticManagerLegacy + +- (void)doTapHaptic +{ + [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] impactOccurred]; +} + +- (void)setRumbleStrength:(double)rumble +{ + void AudioServicesStopSystemSound(SystemSoundID inSystemSoundID); + void AudioServicesPlaySystemSoundWithVibration(SystemSoundID inSystemSoundID, id arg, NSDictionary* vibratePattern); + if (rumble) { + AudioServicesPlaySystemSoundWithVibration(kSystemSoundID_Vibrate, nil, @{@"Intensity": @(rumble), + @"OffDuration": @0, + @"OnDuration": @100, + @"VibePattern": @[@YES, @1000], + }); + } + else { + AudioServicesStopSystemSound(kSystemSoundID_Vibrate); + } +} +@end diff --git a/iOS/GBHorizontalLayout.h b/iOS/GBHorizontalLayout.h new file mode 100644 index 0000000..1eb9e3d --- /dev/null +++ b/iOS/GBHorizontalLayout.h @@ -0,0 +1,5 @@ +#import "GBLayout.h" + +@interface GBHorizontalLayout : GBLayout + +@end diff --git a/iOS/GBHorizontalLayout.m b/iOS/GBHorizontalLayout.m new file mode 100644 index 0000000..66f7cb3 --- /dev/null +++ b/iOS/GBHorizontalLayout.m @@ -0,0 +1,130 @@ +#define GBLayoutInternal +#import "GBHorizontalLayout.h" + +@implementation GBHorizontalLayout + +- (instancetype)initWithTheme:(GBTheme *)theme +{ + self = [super initWithTheme:theme]; + if (!self) return nil; + + CGSize resolution = {self.resolution.height - self.cutout, self.resolution.width}; + + CGRect screenRect = {0,}; + screenRect.size.height = self.hasFractionalPixels? (resolution.height - self.homeBar) : floor((resolution.height - self.homeBar) / 144) * 144; + screenRect.size.width = screenRect.size.height / 144 * 160; + + double horizontalMargin, verticalMargin; + while (true) { + horizontalMargin = (resolution.width - screenRect.size.width) / 2; + verticalMargin = (resolution.height - self.homeBar - screenRect.size.height) / 2; + if (horizontalMargin / self.factor < 164) { + if (self.hasFractionalPixels) { + screenRect.size.width = resolution.width - 164 * self.factor * 2; + screenRect.size.height = screenRect.size.width / 160 * 144; + continue; + } + screenRect.size.width -= 160; + screenRect.size.height -= 144; + continue; + } + break; + } + + double screenBorderWidth = screenRect.size.width / 40; + + screenRect.origin.x = (resolution.width - screenRect.size.width) / 2; + bool drawSameBoyLogo = false; + if (verticalMargin * 2 > screenBorderWidth * 7) { + drawSameBoyLogo = true; + screenRect.origin.y = (resolution.height - self.homeBar - screenRect.size.height - screenBorderWidth * 5) / 2; + } + else { + screenRect.origin.y = (resolution.height - self.homeBar - screenRect.size.height) / 2; + } + + self.screenRect = screenRect; + + self.dpadLocation = (CGPoint){ + round((screenRect.origin.x - screenBorderWidth) / 2), + round(resolution.height * 3 / 8) + }; + + double wingWidth = (resolution.width - screenRect.size.width) / 2 - screenBorderWidth * 5; + double buttonRadius = 36 * self.factor; + CGSize buttonsDelta = [self buttonDeltaForMaxHorizontalDistance:wingWidth - buttonRadius * 2]; + CGPoint buttonsCenter = { + resolution.width - self.dpadLocation.x, + self.dpadLocation.y, + }; + + self.aLocation = (CGPoint) { + round(buttonsCenter.x + buttonsDelta.width / 2), + round(buttonsCenter.y - buttonsDelta.height / 2) + }; + + self.bLocation = (CGPoint) { + round(buttonsCenter.x - buttonsDelta.width / 2), + round(buttonsCenter.y + buttonsDelta.height / 2) + }; + + self.selectLocation = (CGPoint){ + self.dpadLocation.x, + MIN(round(resolution.height * 3 / 4), self.dpadLocation.y + 180 * self.factor) + }; + + self.startLocation = (CGPoint){ + buttonsCenter.x, + self.selectLocation.y + }; + + resolution.width += self.cutout * 2; + self.screenRect = (CGRect){{self.screenRect.origin.x + self.cutout, self.screenRect.origin.y}, self.screenRect.size}; + self.dpadLocation = (CGPoint){self.dpadLocation.x + self.cutout, self.dpadLocation.y}; + self.aLocation = (CGPoint){self.aLocation.x + self.cutout, self.aLocation.y}; + self.bLocation = (CGPoint){self.bLocation.x + self.cutout, self.bLocation.y}; + self.startLocation = (CGPoint){self.startLocation.x + self.cutout, self.startLocation.y}; + self.selectLocation = (CGPoint){self.selectLocation.x + self.cutout, self.selectLocation.y}; + self.abComboLocation = (CGPoint){(self.aLocation.x + self.bLocation.x) / 2, + (self.aLocation.y + self.bLocation.y) / 2}; + + + if (theme.renderingPreview) { + UIGraphicsBeginImageContextWithOptions((CGSize){resolution.width / 8, resolution.height / 8}, true, 1); + CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1 / 8.0, 1 / 8.0); + } + else { + UIGraphicsBeginImageContextWithOptions(resolution, true, 1); + } + [self drawBackground]; + [self drawScreenBezels]; + + [self drawThemedLabelsWithBlock:^{ + if (drawSameBoyLogo) { + double bezelBottom = screenRect.origin.y + screenRect.size.height + screenBorderWidth; + double freeSpace = resolution.height - bezelBottom; + [self drawLogoInVerticalRange:(NSRange){bezelBottom + screenBorderWidth * 2, freeSpace - screenBorderWidth * 4}]; + } + + [self drawLabels]; + }]; + + self.background = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return self; +} + +- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation +{ + if (orientation == UIInterfaceOrientationLandscapeLeft) { + return CGRectMake(-(signed)self.cutout / (signed)self.factor, 0, self.background.size.width / self.factor, self.background.size.height / self.factor); + } + return CGRectMake(0, 0, self.background.size.width / self.factor, self.background.size.height / self.factor); +} + +- (CGSize)size +{ + return (CGSize){self.resolution.height, self.resolution.width}; +} + +@end diff --git a/iOS/GBHub.h b/iOS/GBHub.h new file mode 100644 index 0000000..2e1ee57 --- /dev/null +++ b/iOS/GBHub.h @@ -0,0 +1,41 @@ +#import + +typedef enum { + GBHubStatusNotReady, + GBHubStatusInProgress, + GBHubStatusReady, + GBHubStatusError, +} GBHubStatus; + +static inline NSString *GBSearchCanonicalString(NSString *string) +{ + return [[string.lowercaseString stringByApplyingTransform:NSStringTransformStripDiacritics reverse:false] stringByApplyingTransform:NSStringTransformStripCombiningMarks reverse:false]; +} + +@interface GBHubGame : NSObject +@property (readonly) NSString *title; +@property (readonly) NSString *developer; +@property (readonly) NSString *type; +@property (readonly) NSString *license; +@property (readonly) NSDate *publicationDate; +@property (readonly) NSArray *tags; +@property (readonly) NSURL *repository; +@property (readonly) NSURL *website; +@property (readonly) NSArray *screenshots; +@property (readonly) NSURL *file; +@property (readonly) NSString *slug; +@property (readonly) NSString *entryDescription; +@property (readonly) NSString *keywords; +@end + +extern NSString *const GBHubStatusChangedNotificationName; + +@interface GBHub : NSObject ++ (instancetype)sharedHub; +- (void)refresh; +- (unsigned)countForTag:(NSString *)tag; +@property (readonly) GBHubStatus status; +@property (readonly) NSDictionary *allGames; +@property (readonly) NSArray *sortedTags; +@property (readonly) NSArray *showcaseGames; +@end diff --git a/iOS/GBHub.m b/iOS/GBHub.m new file mode 100644 index 0000000..73c60fe --- /dev/null +++ b/iOS/GBHub.m @@ -0,0 +1,344 @@ +#import "GBHub.h" +#pragma clang diagnostic ignored "-Warc-retain-cycles" + +NSString *const GBHubStatusChangedNotificationName = @"GBHubStatusChangedNotification"; + +static NSURL *StringToWebURL(NSString *string) +{ + if (![string isKindOfClass:[NSString class]]) return nil; + NSURL *url = [NSURL URLWithString:string]; + if (![url.scheme isEqual:@"http"] && [url.scheme isEqual:@"https"]) { + return nil; + } + + return url; +} + +@implementation GBHubGame + +- (instancetype)initWithJSON:(NSDictionary *)json +{ + self = [super init]; + + // Skip NSFW titles + if ([json[@"nsfw"] boolValue]) return nil; + + if (json[@"tags"] && ![json[@"tags"] isKindOfClass:[NSArray class]]) return nil; + _tags = [NSMutableArray array]; + + for (__strong NSString *tag in json[@"tags"]) { + if (![tag isKindOfClass:[NSString class]]) { + return nil; + } + if ([tag isEqual:@"hw:gbprinter"]) { + continue; + } + + if ([tag hasPrefix:@"event:"]) { + tag = [tag substringFromIndex:strlen("event:")]; + } + if ([tag hasPrefix:@"gb-showdown-"]) { + tag = [NSString stringWithFormat:@"Game Boy Showdown %@", [tag substringFromIndex:strlen("gb-showdown-")]]; + } + if ([tag hasPrefix:@"gbcompo"]) { + tag = [NSString stringWithFormat:@"GBCompo%@", [[tag substringFromIndex:strlen("gbcompo")].capitalizedString stringByReplacingOccurrencesOfString:@"-" withString:@" "]]; + } + if ([tag isEqual:tag.lowercaseString]) { + tag = [tag stringByReplacingOccurrencesOfString:@"-" withString:@" "].capitalizedString; + } + [(NSMutableArray *)_tags addObject:tag]; + } + + NSMutableSet *licenses = [NSMutableSet set]; + + if (json[@"license"]) { + [licenses addObject:json[@"license"]]; + } + if (json[@"gameLicense"]) { + [licenses addObject:json[@"gameLicense"]]; + } + if (json[@"assetsLicense"]) { + [licenses addObject:json[@"assetsLicense"]]; + } + + + if (licenses.count == 1) { + _license = licenses.anyObject; + if (![_license isKindOfClass:[NSString class]]) { + return nil; + } + if (!_license.length) _license = nil; + } + else if (licenses.count > 1) { + if (json[@"license"]) { + return nil; + } + _license = [NSString stringWithFormat:@"%@ (Assets: %@)", json[@"gameLicense"], json[@"assetsLicense"]]; + } + + if (_license && ![_tags containsObject:@"Open Source"]) { + // License is guaranteed to be Open Source by spec + [(NSMutableArray *)_tags addObject:@"Open Source"]; + } + + _title = json[@"title"]; + if (![_title isKindOfClass:[NSString class]]) { + return nil; + } + + _entryDescription = json[@"description"]; + if (_entryDescription && ![_entryDescription isKindOfClass:[NSString class]]) { + return nil; + } + + _developer = json[@"developer"]; + if (![_developer isKindOfClass:[NSString class]]) { + if ([_developer isKindOfClass:[NSArray class]] && ((NSArray *)_developer).count) { + if ([((NSArray *)_developer)[0] isKindOfClass:[NSString class]]) { + _developer = [(NSArray *)_developer componentsJoinedByString:@", "]; + } + else if ([((NSArray *)_developer)[0] isKindOfClass:[NSDictionary class]]) { + NSMutableArray *developers = [NSMutableArray array]; + for (NSDictionary *developer in (NSArray *)_developer) { + if (![developer isKindOfClass:[NSDictionary class]]) return nil; + NSString *name = developer[@"name"]; + if (!name) return nil; + [developers addObject:name]; + } + _developer = [developers componentsJoinedByString:@", "]; + } + else { + return nil; + } + } + else if ([_developer isKindOfClass:[NSDictionary class]]) { + _developer = ((NSDictionary *)_developer)[@"name"]; + } + else { + return nil; + } + } + + + _slug = json[@"slug"]; + if (![_slug isKindOfClass:[NSString class]]) { + return nil; + } + + _type = json[@"typetag"]; + if (![_type isKindOfClass:[NSString class]]) { + return nil; + } + + NSString *dateString = json[@"date"]; + if ([dateString isKindOfClass:[NSString class]]) { + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd"]; + _publicationDate = [dateFormatter dateFromString:dateString]; + } + + _repository = StringToWebURL(json[@"repository"]); + _website = StringToWebURL(json[@"website"]); + + NSURL *base = [NSURL URLWithString:[NSString stringWithFormat:@"https://hh3.gbdev.io/static/database-gb/entries/%@", _slug]]; + + NSMutableArray *screenshots = [NSMutableArray array]; + for (NSString *screenshot in json[@"screenshots"]) { + [screenshots addObject:[base URLByAppendingPathComponent:screenshot]]; + } + + + _screenshots = screenshots; + + for (NSDictionary *file in json[@"files"]) { + NSString *extension = [file[@"filename"] pathExtension].lowercaseString; + if (![extension isEqual:@"gb"] && ![extension isEqual:@"gbc"]) { + // Not a DMG/CGB game + continue; + } + if ([file[@"default"] boolValue] || !_file) { + _file = [base URLByAppendingPathComponent:file[@"filename"]]; + } + } + + if (!_file) { + return nil; + } + + _keywords = [NSString stringWithFormat:@"%@ %@ %@ %@", + GBSearchCanonicalString(_title), + GBSearchCanonicalString(_developer), + GBSearchCanonicalString(_entryDescription) ?: @"", + GBSearchCanonicalString([_tags componentsJoinedByString:@" "]) + ]; + + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p: %@>", self.class, self, self.title]; +} + +@end + +@implementation GBHub +{ + NSMutableDictionary *_allGames; + NSMutableDictionary *_tags; + NSMutableArray *_showcaseGames; + NSSet *_showcaseExtras; +} + ++ (instancetype)sharedHub +{ + static GBHub *hub = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + hub = [[self alloc] init]; + }); + return hub; +} + +- (void)setStatus:(GBHubStatus)status +{ + if (_status != status) { + _status = status; + if ([NSThread isMainThread]) { + [[NSNotificationCenter defaultCenter] postNotificationName:GBHubStatusChangedNotificationName + object:self]; + } + else { + dispatch_sync(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:GBHubStatusChangedNotificationName + object:self]; + }); + } + } +} + +- (void)handleAPIData:(NSData *)data forBaseURL:(NSString *)base completion:(void(^)(GBHubStatus))completion +{ + @try { + if (!data) { + completion(GBHubStatusError); + return; + } + NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if (!json) { + completion(GBHubStatusError); + return; + } + + if (!json[@"page_current"] || !json[@"page_total"]) { + completion(GBHubStatusError); + return; + } + + for (NSDictionary *entry in json[@"entries"]) { + @try { + @autoreleasepool { + GBHubGame *game = [[GBHubGame alloc] initWithJSON:entry]; + if (game && !_allGames[game.slug]) { + _allGames[game.slug] = game; + bool showcase = [_showcaseExtras containsObject:game.slug]; + if (!showcase) { + for (NSString *tag in game.tags) { + _tags[tag] = @(_tags[tag].unsignedIntValue + 1); + if ([tag containsString:@"Shortlist"]) { + showcase = true; + break; + } + } + } + if (showcase) { + [_showcaseGames addObject:game]; + } + } + } + } + @catch (NSException *exception) { + // Just in case I missed some JSON edge cases, let's not abort the entire response over one bad game + } + } + + if ([json[@"page_current"] unsignedIntValue] == [json[@"page_total"] unsignedIntValue]) { + completion(GBHubStatusReady); + return; + } + + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@&page=%u", + base, + [json[@"page_current"] unsignedIntValue] + 1]] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + [self handleAPIData:data forBaseURL:base completion:completion]; + }] resume]; + } + @catch (NSException *exception) { + self.status = GBHubStatusError; + } +} + +- (void)addGamesForURL:(NSString *)url completion:(void(^)(GBHubStatus))completion +{ + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:url] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + completion(GBHubStatusError); + return; + } + [self handleAPIData:data forBaseURL:url completion:completion]; + }] resume]; +} + +- (unsigned int)countForTag:(NSString *)tag +{ + return _tags[tag].unsignedIntValue; +} + +- (void)refresh +{ + if (_status == GBHubStatusInProgress) { + return; + } + self.status = GBHubStatusInProgress; + _allGames = [NSMutableDictionary dictionary]; + _tags = [NSMutableDictionary dictionary]; + _showcaseGames = [NSMutableArray array]; + + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://sameboy.github.io/ios-showcase"] + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (data) { + _showcaseExtras = [NSSet setWithArray:[[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] componentsSeparatedByString:@"\n"]]; + } + [self addGamesForURL:@"https://hh3.gbdev.io/api/search?tags=Open+Source&results=1000" + completion:^(GBHubStatus ret) { + if (ret != GBHubStatusReady) { + self.status = ret; + return; + } + [self addGamesForURL:@"https://hh3.gbdev.io/api/search?thirdparty=sameboy&results=1000" + completion:^(GBHubStatus ret) { + if (ret == GBHubStatusReady) { + _sortedTags = [_tags.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) { + return [obj1 compare:obj2]; + }]; + } + unsigned day = time(NULL) / 60 / 60 / 24; + if (_showcaseGames.count > 5) { + typeof(_showcaseGames) temp = [NSMutableArray array]; + for (unsigned i = 5; i--;) { + unsigned index = day % _showcaseGames.count; + GBHubGame *game = _showcaseGames[index]; + [_showcaseGames removeObjectAtIndex:index]; + [temp addObject:game]; + } + _showcaseGames = temp; + } + self.status = ret; + }]; + }]; + }] resume]; +} + +@end diff --git a/iOS/GBHubCell.h b/iOS/GBHubCell.h new file mode 100644 index 0000000..cf5f900 --- /dev/null +++ b/iOS/GBHubCell.h @@ -0,0 +1,8 @@ +#import +#import "GBHub.h" + +@interface GBHubCell : UITableViewCell + +@property GBHubGame *game; + +@end diff --git a/iOS/GBHubCell.m b/iOS/GBHubCell.m new file mode 100644 index 0000000..1917d1f --- /dev/null +++ b/iOS/GBHubCell.m @@ -0,0 +1,4 @@ +#import "GBHubCell.h" + +@implementation GBHubCell +@end diff --git a/iOS/GBHubGameViewController.h b/iOS/GBHubGameViewController.h new file mode 100644 index 0000000..89b3f1e --- /dev/null +++ b/iOS/GBHubGameViewController.h @@ -0,0 +1,7 @@ +#import +#import "GBHub.h" + +@interface GBHubGameViewController : UIViewController +- (instancetype)initWithGame:(GBHubGame *)game; +@end + diff --git a/iOS/GBHubGameViewController.m b/iOS/GBHubGameViewController.m new file mode 100644 index 0000000..d417928 --- /dev/null +++ b/iOS/GBHubGameViewController.m @@ -0,0 +1,315 @@ +#import "GBHubGameViewController.h" +#import "GBROMManager.h" +#import "UILabel+TapLocation.h" + +@implementation NSMutableAttributedString (append) + +- (void)appendWithAttributes:(NSDictionary *)attributes format:(NSString *)format, ... +{ + va_list args; + va_start(args, format); + NSString *string = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + [self appendAttributedString:[[NSAttributedString alloc] initWithString:string attributes:attributes]]; +} + +@end + +@implementation GBHubGameViewController +{ + GBHubGame *_game; + UIScrollView *_scrollView; + UIScrollView *_screenshotsScrollView; + UILabel *_titleLabel; + UILabel *_descriptionLabel; +} + +- (instancetype)initWithGame:(GBHubGame *)game +{ + self = [super init]; + _game = game; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + UIColor *labelColor, *linkColor, *secondaryLabelColor; + if (@available(iOS 13.0, *)) { + self.view.backgroundColor = [UIColor systemBackgroundColor]; + labelColor = UIColor.labelColor; + linkColor = UIColor.linkColor; + secondaryLabelColor = UIColor.secondaryLabelColor; + + } + else { + self.view.backgroundColor = [UIColor whiteColor]; + labelColor = UIColor.blackColor; + linkColor = UIColor.blueColor; + secondaryLabelColor = [UIColor colorWithWhite:0.55 alpha:1.0]; + } + _scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; + _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _scrollView.scrollEnabled = true; + _scrollView.pagingEnabled = false; + _scrollView.showsVerticalScrollIndicator = true; + _scrollView.showsHorizontalScrollIndicator = false; + [self.view addSubview:_scrollView]; + + _scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, self.view.bounds.size.height * 2); + + _titleLabel = [[UILabel alloc] initWithFrame:(CGRectMake(0, 8, + self.view.bounds.size.width - 16, + 56))]; + NSMutableParagraphStyle *style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; + style.paragraphSpacing = 4; + + NSMutableAttributedString *titleText = [[NSMutableAttributedString alloc] init]; + [titleText appendWithAttributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:34 weight:UIFontWeightBold], + NSForegroundColorAttributeName: labelColor, + NSParagraphStyleAttributeName: style, + } format:@"%@", _game.title]; + + [titleText appendWithAttributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:20], + NSForegroundColorAttributeName: secondaryLabelColor, + NSParagraphStyleAttributeName: style, + } format:@"\n by %@", _game.developer]; + + _titleLabel.attributedText = titleText; + _titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + _titleLabel.numberOfLines = 0; + [_scrollView addSubview:_titleLabel]; + + + NSMutableAttributedString *text = [[NSMutableAttributedString alloc] init]; + NSDictionary *labelAttributes = @{ + NSFontAttributeName: [UIFont boldSystemFontOfSize:UIFont.labelFontSize], + NSForegroundColorAttributeName: labelColor, + }; + NSDictionary *valueAttributes = @{ + NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], + NSForegroundColorAttributeName: labelColor, + }; + + if (_game.entryDescription) { + [text appendWithAttributes:valueAttributes format:@"%@\n\n", _game.entryDescription]; + } + if (_game.publicationDate) { + [text appendWithAttributes:labelAttributes format:@"Published: "]; + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateStyle = NSDateFormatterMediumStyle; + formatter.timeStyle = NSDateFormatterNoStyle; + formatter.locale = [NSLocale currentLocale]; + [text appendWithAttributes:valueAttributes format:@"%@\n", [formatter stringFromDate:_game.publicationDate]]; + } + if (_game.website) { + [text appendWithAttributes:labelAttributes format:@"Website: "]; + [text appendWithAttributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], + @"GBLinkAttribute": _game.website, + NSForegroundColorAttributeName: linkColor, + } format:@"%@\n", _game.website]; + } + if (_game.repository) { + [text appendWithAttributes:labelAttributes format:@"Repository: "]; + [text appendWithAttributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], + @"GBLinkAttribute": _game.repository, + NSForegroundColorAttributeName: linkColor, + } format:@"%@\n", _game.repository]; + } + if (_game.license) { + [text appendWithAttributes:labelAttributes format:@"License: "]; + [text appendWithAttributes:valueAttributes format:@"%@\n", _game.license]; + } + if (_game.tags.count) { + [text appendWithAttributes:labelAttributes format:@"Categories: "]; + bool first = true; + for (NSString *tag in _game.tags) { + if (!first) { + [text appendWithAttributes:valueAttributes format:@", ", _game.license]; + } + first = false; + [text appendWithAttributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], + @"GBHubTag": tag, + NSForegroundColorAttributeName: linkColor, + } format:@"%@", tag]; + } + [text appendWithAttributes:valueAttributes format:@"\n"]; + } + + _descriptionLabel = [[UILabel alloc] init]; + _descriptionLabel.numberOfLines = 0; + _descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping; + if (@available(iOS 14.0, *)) { + _descriptionLabel.lineBreakStrategy = NSLineBreakStrategyNone; + } + _descriptionLabel.attributedText = text; + [_scrollView addSubview:_descriptionLabel]; + + unsigned screenshotWidth = (unsigned)(MIN(self.view.bounds.size.width, self.view.bounds.size.height) - 16) / 160 * 160; + unsigned screenshotHeight = screenshotWidth / 160 * 144; + if (_game.screenshots.count) { + _screenshotsScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, + self.view.bounds.size.width, + screenshotHeight + 8)]; + _screenshotsScrollView.scrollEnabled = true; + _screenshotsScrollView.pagingEnabled = false; + _screenshotsScrollView.showsVerticalScrollIndicator = false; + _screenshotsScrollView.showsHorizontalScrollIndicator = true; + + unsigned x = 0; + for (NSURL *url in _game.screenshots) { + x += 8; + UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(x, 0, + screenshotWidth, + screenshotHeight)]; + [imageView.layer setMinificationFilter:kCAFilterLinear]; + [imageView.layer setMagnificationFilter:kCAFilterNearest]; + imageView.layer.cornerRadius = 4; + imageView.layer.borderWidth = 1; + imageView.layer.masksToBounds = true; + + if (@available(iOS 13.0, *)) { + imageView.layer.borderColor = [UIColor tertiaryLabelColor].CGColor; + } + else { + imageView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.5].CGColor; + } + + [_screenshotsScrollView addSubview:imageView]; + [[[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + if (location) { + UIImage *image = [UIImage imageWithContentsOfFile:location.path]; + dispatch_async(dispatch_get_main_queue(), ^{ + CGRect frame = imageView.frame; + imageView.image = image; + imageView.frame = frame; + imageView.contentMode = UIViewContentModeScaleAspectFit; + }); + } + }] resume]; + x += screenshotWidth + 8; + } + _screenshotsScrollView.contentSize = CGSizeMake(x, screenshotHeight); + _screenshotsScrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin; + + [_scrollView addSubview:_screenshotsScrollView]; + } + [self viewDidLayoutSubviews]; + _descriptionLabel.userInteractionEnabled = true; + [_descriptionLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(tappedLabel:)]]; + + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Download" + style:UIBarButtonItemStylePlain + target:self + action:@selector(rightButtonPressed)]; + if ([GBROMManager.sharedManager romFileForROM:_game.title]) { + self.navigationItem.rightBarButtonItem.title = @"Open"; + } +} + +- (void)rightButtonPressed +{ + if ([GBROMManager.sharedManager romFileForROM:_game.title]) { + [GBROMManager sharedManager].currentROM = _game.title; + [self.navigationController dismissViewControllerAnimated:true completion:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil]; + } + else { + UIActivityIndicatorViewStyle style = UIActivityIndicatorViewStyleWhite; + if (@available(iOS 13.0, *)) { + style = UIActivityIndicatorViewStyleMedium; + } + UIActivityIndicatorView *view = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:style]; + CGRect frame = view.frame; + frame.size.width += 16; + view.frame = frame; + [view startAnimating]; + self.navigationItem.rightBarButtonItem.customView = view; + [[[NSURLSession sharedSession] downloadTaskWithURL:_game.file completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + if (!location) { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Could not download ROM" + message:@"Could not download this ROM from Homebrew Hub. Please try again later." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; + self.navigationItem.rightBarButtonItem.customView = nil; + }); + return; + } + NSString *newTempName = [[location.path stringByDeletingLastPathComponent] stringByAppendingPathComponent:_game.file.lastPathComponent]; + [[NSFileManager defaultManager] moveItemAtPath:location.path toPath:newTempName error:nil]; + [[GBROMManager sharedManager] importROM:newTempName withName:_game.title keepOriginal:false]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.navigationItem.rightBarButtonItem.title = @"Open"; + self.navigationItem.rightBarButtonItem.customView = nil; + }); + }] resume]; + } +} + +- (void)tappedLabel:(UITapGestureRecognizer *)tap +{ + unsigned characterIndex = [(UILabel *)tap.view characterAtTap:tap]; + + NSURL *url = [((UILabel *)tap.view).attributedText attribute:@"GBLinkAttribute" atIndex:characterIndex effectiveRange:NULL]; + + if (url) { + [[UIApplication sharedApplication] openURL:url options:nil completionHandler:nil]; + return; + } + + NSString *tag = [((UILabel *)tap.view).attributedText attribute:@"GBHubTag" atIndex:characterIndex effectiveRange:NULL]; + + if (tag) { + UINavigationItem *parent = self.navigationController.navigationBar.items[self.navigationController.navigationBar.items.count - 2]; + [self.navigationController popViewControllerAnimated:true]; + if (@available(iOS 13.0, *)) { + parent.searchController.searchBar.searchTextField.text = @""; + parent.searchController.searchBar.searchTextField.tokens = nil; + UISearchToken *token = [UISearchToken tokenWithIcon:nil text:tag]; + token.representedObject = tag; + [parent.searchController.searchBar.searchTextField insertToken:token atIndex:0]; + } + else { + parent.searchController.searchBar.text = tag; + } + parent.searchController.active = true; + return; + } + +} + +- (void)viewDidLayoutSubviews +{ + unsigned y = 12; + CGSize size = [_titleLabel sizeThatFits:(CGSize){_scrollView.bounds.size.width - 32, INFINITY}];; + _titleLabel.frame = (CGRect){{16, y}, {_scrollView.bounds.size.width - 32, size.height}}; + y += size.height + 24; + + if (_screenshotsScrollView) { + _screenshotsScrollView.frame = CGRectMake(0, y, _scrollView.bounds.size.width, _screenshotsScrollView.frame.size.height); + y += _screenshotsScrollView.frame.size.height + 8; + if (_game.screenshots.count == 1) { + CGRect frame = _screenshotsScrollView.frame; + frame.origin.x = (_scrollView.bounds.size.width - _screenshotsScrollView.contentSize.width) / 2; + _screenshotsScrollView.frame = frame; + } + } + + size = [_descriptionLabel sizeThatFits:(CGSize){_scrollView.bounds.size.width - 32, INFINITY}];; + _descriptionLabel.frame = (CGRect){{16, y}, {_scrollView.bounds.size.width - 32, size.height}}; + y += size.height; + + _scrollView.contentSize = CGSizeMake(_scrollView.bounds.size.width, y); +} + +@end diff --git a/iOS/GBHubViewController.h b/iOS/GBHubViewController.h new file mode 100644 index 0000000..dea83f2 --- /dev/null +++ b/iOS/GBHubViewController.h @@ -0,0 +1,6 @@ +#import + +@interface GBHubViewController : UITableViewController + +@end + diff --git a/iOS/GBHubViewController.m b/iOS/GBHubViewController.m new file mode 100644 index 0000000..7cabe24 --- /dev/null +++ b/iOS/GBHubViewController.m @@ -0,0 +1,393 @@ +#import "GBHubViewController.h" +#import "GBHub.h" +#import "GBHubGameViewController.h" +#import "GBHubCell.h" +#import "UILabel+TapLocation.h" + +@interface GBHubViewController() +@end + +@implementation GBHubViewController +{ + UISearchController *_searchController; + NSMutableDictionary *_imageCache; + NSArray *_results; + NSString *_resultsTitle; + bool _showingAllGames; +} + +- (instancetype)init +{ + self = [self initWithStyle:UITableViewStyleGrouped]; + [[NSNotificationCenter defaultCenter] addObserver:self.tableView + selector:@selector(reloadData) + name:GBHubStatusChangedNotificationName + object:nil]; + _imageCache = [NSMutableDictionary dictionary]; + self.tableView.rowHeight = UITableViewAutomaticDimension; + [GBHub.sharedHub refresh]; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.navigationItem.searchController = + _searchController = [[UISearchController alloc] initWithSearchResultsController:nil]; + _searchController.searchResultsUpdater = self; + self.tableView.scrollsToTop = true; + self.navigationItem.hidesSearchBarWhenScrolling = false; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UIColor *labelColor; + UIColor *secondaryLabelColor; + if (@available(iOS 13.0, *)) { + labelColor = UIColor.labelColor; + secondaryLabelColor = UIColor.secondaryLabelColor; + } + else { + labelColor = UIColor.blackColor; + secondaryLabelColor = [UIColor colorWithWhite:0.55 alpha:1.0]; + } + switch (GBHub.sharedHub.status) { + case GBHubStatusNotReady: return nil; + case GBHubStatusInProgress: { + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:0]; + UIActivityIndicatorViewStyle style = UIActivityIndicatorViewStyleWhite; + if (@available(iOS 13.0, *)) { + style = UIActivityIndicatorViewStyleMedium; + } + + UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:style]; + cell.bounds = spinner.bounds; + [cell addSubview:spinner]; + [spinner startAnimating]; + spinner.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + return cell; + } + case GBHubStatusReady: { + if (indexPath.section == 0) { + GBHubCell *cell = [[GBHubCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:0]; + cell.game = _results? _results[indexPath.item] : GBHub.sharedHub.showcaseGames[indexPath.item]; + NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:cell.game.title + attributes:@{ + NSFontAttributeName: [UIFont boldSystemFontOfSize:UIFont.labelFontSize], + NSForegroundColorAttributeName: labelColor + }]; + [text appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" by %@\n", + cell.game.developer] + attributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:UIFont.labelFontSize], + NSForegroundColorAttributeName: labelColor + }]]; + [text appendAttributedString:[[NSAttributedString alloc] initWithString:cell.game.entryDescription ?: [cell.game.tags componentsJoinedByString:@", "] + attributes:@{ + NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize], + NSForegroundColorAttributeName: secondaryLabelColor + }]]; + cell.textLabel.attributedText = text; + cell.textLabel.numberOfLines = 2; + cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail; + + static UIImage *emptyImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions((CGSize){60, 60}, false, tableView.window.screen.scale); + UIBezierPath *mask = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 3, 60, 54) cornerRadius:4]; + [mask addClip]; + [[UIColor whiteColor] set]; + [mask fill]; + if (@available(iOS 13.0, *)) { + [[UIColor tertiaryLabelColor] set]; + } + else { + [[UIColor colorWithWhite:0 alpha:0.5] set]; + } + [mask stroke]; + emptyImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + }); + cell.imageView.image = emptyImage; + return cell; + } + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:0]; + cell.selectionStyle = UITableViewCellSelectionStyleBlue; + NSString *tag = GBHub.sharedHub.sortedTags[indexPath.item]; + cell.textLabel.text = tag; + unsigned count = [GBHub.sharedHub countForTag:tag]; + if (count == 1) { + cell.detailTextLabel.text = @"1 Game"; + } + else { + cell.detailTextLabel.text = [NSString stringWithFormat:@"%u Games", count]; + } + cell.textLabel.numberOfLines = 2; + cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; + return cell; + } + case GBHubStatusError: { + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:0]; + cell.textLabel.text = @"Could not connect to Homebrew Hub"; + return cell; + } + } + return nil; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + switch (GBHub.sharedHub.status) { + case GBHubStatusNotReady: return 0; + case GBHubStatusInProgress: return 1; + case GBHubStatusReady: { + if (_results) return _results.count; + if (section == 0) return GBHub.sharedHub.showcaseGames.count; + return GBHub.sharedHub.sortedTags.count; + } + case GBHubStatusError: return 1; + } + return 0; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + switch (GBHub.sharedHub.status) { + case GBHubStatusNotReady: return 0; + case GBHubStatusInProgress: return 1; + case GBHubStatusReady: return _results? 1 : 2; + case GBHubStatusError: return 1; + } + return 0; +} + +- (NSString *)title +{ + return @"Homebrew Hub"; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section +{ + if (GBHub.sharedHub.status != GBHubStatusReady) return nil; + if (section == 0) return _results? _resultsTitle : @"Homebrew Showcase"; + return @"Categories"; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (GBHub.sharedHub.status == GBHubStatusReady && indexPath.section == 0) { + return 60; + } + return 45; +} + +- (void)tableView:(UITableView *)tableView willDisplayCell:(GBHubCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (![cell isKindOfClass:[GBHubCell class]]) return; + if (!cell.game.screenshots.count) return; + + NSURL *url = cell.game.screenshots[0]; + UIImage *image = _imageCache[url]; + if ([image isKindOfClass:[UIImage class]]) { + cell.imageView.image = image; + return; + } + if (!image) { + _imageCache[url] = (id)[NSNull null]; + [[[NSURLSession sharedSession] downloadTaskWithURL:url + completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + if (!location) return; + dispatch_sync(dispatch_get_main_queue(), ^{ + UIGraphicsBeginImageContextWithOptions((CGSize){60, 60}, false, tableView.window.screen.scale); + UIBezierPath *mask = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 3, 60, 54) cornerRadius:4]; + [mask addClip]; + UIImage *image = [UIImage imageWithContentsOfFile:location.path]; + [image drawInRect:mask.bounds]; + if (@available(iOS 13.0, *)) { + [[UIColor tertiaryLabelColor] set]; + } + else { + [[UIColor colorWithWhite:0 alpha:0.5] set]; + } + [mask stroke]; + _imageCache[url] = cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + }); + }] resume]; + } +} + +- (void)updateSearchResultsForSearchController:(UISearchController *)searchController +{ + static unsigned cookie = 0; + NSArray *keywords = [GBSearchCanonicalString([searchController.searchBar.text + stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]) + componentsSeparatedByString:@" "]; + if (keywords.count == 1 && keywords[0].length == 0) { + keywords = @[]; + } + NSArray *tokens = nil; + if (@available(iOS 13.0, *)) { + tokens = searchController.searchBar.searchTextField.tokens; + } + + if (!searchController.isActive && tokens.count == 0 && !keywords.count) { + cookie++; + _results = nil; + _showingAllGames = false; + [self.tableView reloadData]; + [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndexes:(NSUInteger[]){0,0} length:2] + atScrollPosition:UITableViewScrollPositionTop + animated:false]; + return; + } + if (tokens.count || keywords.count) { + _showingAllGames = false; + cookie++; + unsigned myCookie = cookie; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSMutableArray *results = [NSMutableArray array]; + for (GBHubGame *game in GBHub.sharedHub.allGames.allValues) { + bool matches = true; + if (@available(iOS 13.0, *)) { + for (UISearchToken *token in tokens) { + if (![game.tags containsObject:token.representedObject]) { + matches = false; + break; + } + } + if (!matches) continue; + } + for (NSString *keyword in keywords) { + if (keyword.length == 0) continue; + if (![game.keywords containsString:keyword]) { + matches = false; + break; + } + } + if (matches) { + [results addObject:game]; + } + } + dispatch_async(dispatch_get_main_queue(), ^{ + if (myCookie != cookie) return; + if (tokens.count) { + if (searchController.searchBar.text.length) { + _resultsTitle = [NSString stringWithFormat:@"Showing %@ games matching “%@”", [tokens[0] representedObject], searchController.searchBar.text]; + } + else { + _resultsTitle = [NSString stringWithFormat:@"Showing %@ games", [tokens[0] representedObject]]; + } + } + else { + _resultsTitle = [NSString stringWithFormat:@"Showing results for “%@”", searchController.searchBar.text]; + } + _results = results; + _results = [results sortedArrayUsingComparator:^NSComparisonResult(GBHubGame *obj1, GBHubGame *obj2) { + return [obj1.title.lowercaseString compare:obj2.title.lowercaseString]; + }]; + [self.tableView reloadData]; + if (_results.count) { + [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndexes:(NSUInteger[]){0,0} length:2] + atScrollPosition:UITableViewScrollPositionTop + animated:false]; + } + }); + }); + } + else { + if (_showingAllGames) return; + cookie++; + _showingAllGames = true; + _resultsTitle = @"Showing all games"; + _results = [GBHub.sharedHub.allGames.allValues sortedArrayUsingComparator:^NSComparisonResult(GBHubGame *obj1, GBHubGame *obj2) { + return [obj1.title.lowercaseString compare:obj2.title.lowercaseString]; + }]; + [self.tableView reloadData]; + if (_results.count) { + [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndexes:(NSUInteger[]){0,0} length:2] + atScrollPosition:UITableViewScrollPositionTop + animated:false]; + } + return; + } +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (GBHub.sharedHub.status == GBHubStatusReady) return indexPath; + return nil; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (GBHub.sharedHub.status != GBHubStatusReady) return; + if (indexPath.section == 1) { + NSString *tag = GBHub.sharedHub.sortedTags[indexPath.item]; + if (@available(iOS 13.0, *)) { + UISearchToken *token = [UISearchToken tokenWithIcon:nil + text:tag]; + token.representedObject = tag; + [_searchController.searchBar.searchTextField insertToken:token + atIndex:0]; + } + else { + _searchController.searchBar.text = tag; + } + [_searchController setActive:true]; + return; + } + + GBHubCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + if ([cell isKindOfClass:[GBHubCell class]]) { + GBHubGameViewController *controller = [[GBHubGameViewController alloc] initWithGame:cell.game]; + [self.navigationController pushViewController:controller animated:true]; + } +} + +- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section +{ + UIColor *linkColor; + if (@available(iOS 13.0, *)) { + linkColor = UIColor.linkColor; + + } + else { + linkColor = UIColor.blueColor; + } + + if (section != [self numberOfSectionsInTableView:nil] - 1) return; + UITableViewHeaderFooterView *footer = (UITableViewHeaderFooterView *)view; + NSMutableAttributedString *string = footer.textLabel.attributedText.mutableCopy; + + [string addAttributes:@{ + @"GBLinkAttribute": [NSURL URLWithString:@"https://hh.gbdev.io"], + NSForegroundColorAttributeName: linkColor, + } range:[string.string rangeOfString:@"Homebrew Hub"]]; + + footer.textLabel.attributedText = string; + footer.textLabel.userInteractionEnabled = true; + [footer.textLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(tappedFooterLabel:)]]; +} + +- (void)tappedFooterLabel:(UITapGestureRecognizer *)tap +{ + unsigned characterIndex = [(UILabel *)tap.view characterAtTap:tap]; + + NSURL *url = [((UILabel *)tap.view).attributedText attribute:@"GBLinkAttribute" atIndex:characterIndex effectiveRange:NULL]; + + if (url) { + [[UIApplication sharedApplication] openURL:url options:nil completionHandler:nil]; + } +} + +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section +{ + if (section != [self numberOfSectionsInTableView:tableView] - 1) return nil; + return @"Powered by Homebrew Hub"; +} +@end diff --git a/iOS/GBLayout.h b/iOS/GBLayout.h new file mode 100644 index 0000000..cda5741 --- /dev/null +++ b/iOS/GBLayout.h @@ -0,0 +1,49 @@ +#import +#import "GBTheme.h" + +@interface GBLayout : NSObject +- (instancetype)initWithTheme:(GBTheme *)theme; +@property (readonly) GBTheme *theme; + +@property (readonly) UIImage *background; +@property (readonly) CGRect screenRect; +@property (readonly) CGRect logoRect; +@property (readonly) CGPoint dpadLocation; +@property (readonly) CGPoint aLocation; +@property (readonly) CGPoint bLocation; +@property (readonly) CGPoint abComboLocation; +@property (readonly) CGPoint startLocation; +@property (readonly) CGPoint selectLocation; + +- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation; +@end + +#ifdef GBLayoutInternal + +@interface GBLayout() +@property UIImage *background; +@property CGRect screenRect; +@property CGPoint dpadLocation; +@property CGPoint aLocation; +@property CGPoint bLocation; +@property CGPoint abComboLocation; +@property CGPoint startLocation; +@property CGPoint selectLocation; +@property (readonly) CGSize resolution; // Always vertical +@property (readonly) CGSize size; // Size in pixels, override to make horizontal +@property (readonly) unsigned factor; +@property (readonly) unsigned minY; +@property (readonly) unsigned homeBar; +@property (readonly) unsigned cutout; +@property (readonly) bool hasFractionalPixels; + +- (void)drawBackground; +- (void)drawScreenBezels; +- (void)drawLogoInVerticalRange:(NSRange)range; +- (void)drawLabels; +- (void)drawThemedLabelsWithBlock:(void (^)(void))block; + +- (CGSize)buttonDeltaForMaxHorizontalDistance:(double)distance; +@end + +#endif diff --git a/iOS/GBLayout.m b/iOS/GBLayout.m new file mode 100644 index 0000000..9331c51 --- /dev/null +++ b/iOS/GBLayout.m @@ -0,0 +1,211 @@ +#define GBLayoutInternal +#import "GBLayout.h" + +static double StatusBarHeight(void) +{ + static double ret = 0; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + @autoreleasepool { + UIWindow *window = [[UIWindow alloc] init]; + [window makeKeyAndVisible]; + UIEdgeInsets insets = window.safeAreaInsets; + ret = MAX(MAX(insets.left, insets.right), MAX(insets.top, insets.bottom)) ?: 20; + [window setHidden:true]; + } + }); + return ret; +} + +static bool HasHomeBar(void) +{ + static bool ret = false; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ret = [UIApplication sharedApplication].windows[0].safeAreaInsets.bottom; + }); + return ret; +} + +@implementation GBLayout +{ + bool _isRenderingMask; +} + +- (instancetype)initWithTheme:(GBTheme *)theme +{ + self = [super init]; + if (!self) return nil; + + _theme = theme; + _factor = [UIScreen mainScreen].scale; + _resolution = [UIScreen mainScreen].bounds.size; + _resolution.width *= _factor; + _resolution.height *= _factor; + if (_resolution.width > _resolution.height) { + _resolution = (CGSize){_resolution.height, _resolution.width}; + } + + _minY = StatusBarHeight() * _factor; + _cutout = _minY <= 24 * _factor? 0 : _minY; + + if (HasHomeBar()) { + _homeBar = 21 * _factor; + } + + // The Plus series will scale things lossily anyway, so no need to bother with integer scale things + // This also "catches" zoomed display modes + _hasFractionalPixels = _factor != [UIScreen mainScreen].nativeScale; + return self; +} + +- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation +{ + return CGRectMake(0, 0, self.background.size.width / self.factor, self.background.size.height / self.factor); +} + +- (void)drawBackground +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + CGColorRef top = _theme.backgroundGradientTop.CGColor; + CGColorRef bottom = _theme.backgroundGradientBottom.CGColor; + CGColorRef colors[] = {top, bottom}; + CFArrayRef colorsArray = CFArrayCreate(NULL, (const void **)colors, 2, &kCFTypeArrayCallBacks); + + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorspace, colorsArray, NULL); + CGContextDrawLinearGradient(context, + gradient, + (CGPoint){0, 0}, + (CGPoint){0, self.size.height}, + 0); + + CFRelease(gradient); + CFRelease(colorsArray); + CFRelease(colorspace); +} + +- (void)drawScreenBezels +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + CGColorRef top = _theme.bezelsGradientTop.CGColor; + CGColorRef bottom = _theme.bezelsGradientBottom.CGColor; + CGColorRef colors[] = {top, bottom}; + CFArrayRef colorsArray = CFArrayCreate(NULL, (const void **)colors, 2, &kCFTypeArrayCallBacks); + + double borderWidth = self.screenRect.size.width / 40; + CGRect bezelRect = self.screenRect; + bezelRect.origin.x -= borderWidth; + bezelRect.origin.y -= borderWidth; + bezelRect.size.width += borderWidth * 2; + bezelRect.size.height += borderWidth * 2; + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bezelRect cornerRadius:borderWidth]; + CGContextSaveGState(context); + CGContextSetShadowWithColor(context, (CGSize){0, _factor}, _factor, [UIColor colorWithWhite:1 alpha:0.25].CGColor); + [_theme.backgroundGradientBottom setFill]; + [path fill]; + [path addClip]; + + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorspace, colorsArray, NULL); + CGContextDrawLinearGradient(context, + gradient, + bezelRect.origin, + (CGPoint){bezelRect.origin.x, bezelRect.origin.y + bezelRect.size.height}, + 0); + + CGContextSetShadowWithColor(context, (CGSize){0, _factor}, _factor, [UIColor colorWithWhite:0 alpha:0.25].CGColor); + + path.usesEvenOddFillRule = true; + [path appendPath:[UIBezierPath bezierPathWithRect:(CGRect){{0, 0}, self.size}]]; + [path fill]; + + + CGContextRestoreGState(context); + + CGContextSaveGState(context); + CGContextSetShadowWithColor(context, (CGSize){0, 0}, borderWidth / 4, [UIColor colorWithWhite:0 alpha:0.125].CGColor); + + [[UIColor blackColor] setFill]; + UIRectFill(self.screenRect); + CGContextRestoreGState(context); + + CFRelease(gradient); + CFRelease(colorsArray); + CFRelease(colorspace); +} + +- (void)drawLogoInVerticalRange:(NSRange)range +{ + UIFont *font = [UIFont fontWithName:@"AvenirNext-BoldItalic" size:range.length * 4 / 3]; + + CGRect rect = CGRectMake(0, + range.location - range.length / 3, + self.size.width, range.length * 2); + NSMutableParagraphStyle *style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; + style.alignment = NSTextAlignmentCenter; + [@"SAMEBOY" drawInRect:rect + withAttributes:@{ + NSFontAttributeName: font, + NSForegroundColorAttributeName:_isRenderingMask? [UIColor whiteColor] : _theme.brandColor, + NSParagraphStyleAttributeName: style, + }]; + + _logoRect = (CGRect){ + {(self.size.width - _screenRect.size.width) / 2, rect.origin.y}, + {_screenRect.size.width, rect.size.height} + }; +} + +- (void)drawThemedLabelsWithBlock:(void (^)(void))block +{ + // Start with a normal normal pass + block(); +} + +- (void)drawRotatedLabel:(NSString *)label withFont:(UIFont *)font origin:(CGPoint)origin distance:(double)distance +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSaveGState(context); + CGContextConcatCTM(context, CGAffineTransformMakeTranslation(origin.x, origin.y)); + CGContextConcatCTM(context, CGAffineTransformMakeRotation(-M_PI / 6)); + + NSMutableParagraphStyle *style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; + style.alignment = NSTextAlignmentCenter; + + [label drawInRect:CGRectMake(-256, distance, 512, 256) + withAttributes:@{ + NSFontAttributeName: font, + NSForegroundColorAttributeName:_isRenderingMask? [UIColor whiteColor] : _theme.brandColor, + NSParagraphStyleAttributeName: style, + }]; + CGContextRestoreGState(context); +} + +- (void)drawLabels +{ + + UIFont *labelFont = [UIFont fontWithName:@"AvenirNext-Bold" size:24 * _factor]; + UIFont *smallLabelFont = [UIFont fontWithName:@"AvenirNext-DemiBold" size:20 * _factor]; + + [self drawRotatedLabel:@"A" withFont:labelFont origin:self.aLocation distance:40 * self.factor]; + [self drawRotatedLabel:@"B" withFont:labelFont origin:self.bLocation distance:40 * self.factor]; + [self drawRotatedLabel:@"SELECT" withFont:smallLabelFont origin:self.selectLocation distance:24 * self.factor]; + [self drawRotatedLabel:@"START" withFont:smallLabelFont origin:self.startLocation distance:24 * self.factor]; +} + +- (CGSize)buttonDeltaForMaxHorizontalDistance:(double)maxDistance +{ + CGSize buttonsDelta = {90 * self.factor, 45 * self.factor}; + if (buttonsDelta.width <= maxDistance) { + return buttonsDelta; + } + return (CGSize){maxDistance, floor(sqrt(100 * 100 * self.factor * self.factor - maxDistance * maxDistance))}; +} + +- (CGSize)size +{ + return _resolution; +} +@end diff --git a/iOS/GBLoadROMTableViewController.h b/iOS/GBLoadROMTableViewController.h new file mode 100644 index 0000000..8ac2602 --- /dev/null +++ b/iOS/GBLoadROMTableViewController.h @@ -0,0 +1,5 @@ +#import + +@interface GBLoadROMTableViewController : UITableViewController + +@end diff --git a/iOS/GBLoadROMTableViewController.m b/iOS/GBLoadROMTableViewController.m new file mode 100644 index 0000000..d0a63f0 --- /dev/null +++ b/iOS/GBLoadROMTableViewController.m @@ -0,0 +1,338 @@ +#import "GBLoadROMTableViewController.h" +#import "GBROMManager.h" +#import "GBViewController.h" +#import "GBHubViewController.h" +#import +#import + +@interface GBLoadROMTableViewController() +@end + +@implementation GBLoadROMTableViewController +{ + NSIndexPath *_renamingPath; +} + +- (instancetype)init +{ + self = [super initWithStyle:UITableViewStyleGrouped]; + self.navigationItem.rightBarButtonItem = self.editButtonItem; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deselectRow) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + + return self; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 2; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + if (section == 1) return 3; + return [GBROMManager sharedManager].allROMs.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) { + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + switch (indexPath.item) { + case 0: cell.textLabel.text = @"Import ROM files"; break; + case 1: cell.textLabel.text = @"Browse Homebrew Hub"; break; + case 2: cell.textLabel.text = @"Show Library in Files"; break; + } + return cell; + } + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + NSString *rom = [GBROMManager sharedManager].allROMs[[indexPath indexAtPosition:1]]; + cell.textLabel.text = rom; + cell.accessoryType = [rom isEqualToString:[GBROMManager sharedManager].currentROM]? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; + + NSString *pngPath = [[[GBROMManager sharedManager] autosaveStateFileForROM:rom] stringByAppendingPathExtension:@"png"]; + UIGraphicsBeginImageContextWithOptions((CGSize){60, 60}, false, self.view.window.screen.scale); + UIBezierPath *mask = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 3, 60, 54) cornerRadius:4]; + [mask addClip]; + UIImage *image = [UIImage imageWithContentsOfFile:pngPath]; + [image drawInRect:mask.bounds]; + if (@available(iOS 13.0, *)) { + [[UIColor tertiaryLabelColor] set]; + } + else { + [[UIColor colorWithWhite:0 alpha:0.5] set]; + } + [mask stroke]; + cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) return [super tableView:tableView heightForRowAtIndexPath:indexPath]; + + return 60; +} + +- (NSString *)title +{ + return @"ROM Library"; +} + +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section +{ + if (section == 0) return nil; + + return @"You can also import ROM files by opening them in SameBoy using the Files app or a web browser, or by sending them over with AirDrop."; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) { + switch (indexPath.item) { + case 0: { + UIViewController *parent = self.presentingViewController; + NSString *gbUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"gb", NULL); + NSString *gbcUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"gbc", NULL); + NSString *isxUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"isx", NULL); + + NSMutableSet *extensions = [NSMutableSet set]; + [extensions addObjectsFromArray:(__bridge NSArray *)UTTypeCopyAllTagsWithClass((__bridge CFStringRef)gbUTI, kUTTagClassFilenameExtension)]; + [extensions addObjectsFromArray:(__bridge NSArray *)UTTypeCopyAllTagsWithClass((__bridge CFStringRef)gbcUTI, kUTTagClassFilenameExtension)]; + [extensions addObjectsFromArray:(__bridge NSArray *)UTTypeCopyAllTagsWithClass((__bridge CFStringRef)isxUTI, kUTTagClassFilenameExtension)]; + + if (extensions.count != 3) { + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBShownUTIWarning"]) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"File Association Conflict" + message:@"Due to a limitation in iOS, the file picker will allow you to select files not supported by SameBoy. SameBoy will only import GB, GBC and ISX files.\n\nIf you have a multi-system emulator installed, updating it could fix this problem." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBShownUTIWarning"]; + [self tableView:tableView didSelectRowAtIndexPath:indexPath]; + }]]; + [self presentViewController:alert animated:true completion:nil]; + return; + } + } + + [self.presentingViewController dismissViewControllerAnimated:true completion:^{ + UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"com.github.liji32.sameboy.gb", + @"com.github.liji32.sameboy.gbc", + @"com.github.liji32.sameboy.isx", + gbUTI ?: @"", + gbcUTI ?: @"", + isxUTI ?: @""] + inMode:UIDocumentPickerModeImport]; + picker.allowsMultipleSelection = true; + if (@available(iOS 13.0, *)) { + picker.shouldShowFileExtensions = true; + } + picker.delegate = self; + objc_setAssociatedObject(picker, @selector(delegate), self, OBJC_ASSOCIATION_RETAIN); + [parent presentViewController:picker animated:true completion:nil]; + }]; + return; + } + case 1: { + [self.navigationController pushViewController:[[GBHubViewController alloc] init] + animated:true]; + return; + } + case 2: { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"shareddocuments://%@", NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject]] + options:nil + completionHandler:nil]; + return; + } + } + } + [GBROMManager sharedManager].currentROM = [GBROMManager sharedManager].allROMs[[indexPath indexAtPosition:1]]; + [self.presentingViewController dismissViewControllerAnimated:true completion:^{ + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil]; + }]; +} + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls +{ + NSMutableArray *validURLs = [NSMutableArray array]; + NSMutableArray *skippedBasenames = [NSMutableArray array]; + + for (NSURL *url in urls) { + if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) { + [validURLs addObject:url]; + } + else { + [skippedBasenames addObject:url.lastPathComponent]; + } + } + + if (skippedBasenames.count) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Unsupported Files" + message:[NSString stringWithFormat:@"Could not import the following files because they're not supported:\n%@", + [skippedBasenames componentsJoinedByString:@"\n"]] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + [[NSUserDefaults standardUserDefaults] setBool:false forKey:@"GBShownUTIWarning"]; // Somebody might need a reminder + }]]; + [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:true completion:nil]; + urls = validURLs; + } + + if (urls.count == 1) { + NSURL *url = urls.firstObject; + NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; + if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { + [GBROMManager sharedManager].currentROM = potentialROM; + } + else { + [url startAccessingSecurityScopedResource]; + [GBROMManager sharedManager].currentROM = + [[GBROMManager sharedManager] importROM:url.path + keepOriginal:true]; + [url stopAccessingSecurityScopedResource]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil]; + } + else { + for (NSURL *url in urls) { + NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; + if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { + // That's an already imported ROM + continue; + } + [url startAccessingSecurityScopedResource]; + [[GBROMManager sharedManager] importROM:url.path + keepOriginal:true]; + [url stopAccessingSecurityScopedResource]; + } + [(GBViewController *)[UIApplication sharedApplication].keyWindow.rootViewController openLibrary]; + } +} + +- (UIModalPresentationStyle)modalPresentationStyle +{ + return UIModalPresentationOverFullScreen; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) return; + + if (editingStyle != UITableViewCellEditingStyleDelete) return; + NSString *rom = [GBROMManager sharedManager].allROMs[[indexPath indexAtPosition:1]]; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Delete ROM “%@”?", rom] + message: @"Save data for this ROM will also be deleted." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Delete" + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *action) { + [[GBROMManager sharedManager] deleteROM:rom]; + [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + if ([[GBROMManager sharedManager].currentROM isEqualToString:rom]) { + [GBROMManager sharedManager].currentROM = nil; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil]; + } + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; +} + +- (void)renameRow:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) return; + + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; + UITextField *field = [[UITextField alloc] initWithFrame:cell.textLabel.frame]; + field.font = cell.textLabel.font; + field.text = cell.textLabel.text; + cell.textLabel.text = @""; + [[cell.textLabel superview] addSubview:field]; + [field becomeFirstResponder]; + [field selectAll:nil]; + _renamingPath = indexPath; + [field addTarget:self action:@selector(doneRename:) forControlEvents:UIControlEventEditingDidEnd | UIControlEventEditingDidEndOnExit]; +} + +- (void)doneRename:(UITextField *)sender +{ + if (!_renamingPath) return; + NSString *newName = sender.text; + NSString *oldName = [GBROMManager sharedManager].allROMs[[_renamingPath indexAtPosition:1]]; + _renamingPath = nil; + if ([newName isEqualToString:oldName]) { + [self.tableView reloadData]; + return; + } + if ([newName containsString:@"/"]) { + [self.tableView reloadData]; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"You can't use a name that contains “/”. Please choose another name." + message:nil + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; + return; + } + [[GBROMManager sharedManager] renameROM:oldName toName:newName]; + [self.tableView reloadData]; + _renamingPath = nil; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + return indexPath.section == 0; +} + +// Leave these ROM management to iOS 13.0 and up for now +- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView +contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath + point:(CGPoint)point API_AVAILABLE(ios(13.0)) +{ + if (indexPath.section == 1) return nil; + + return [UIContextMenuConfiguration configurationWithIdentifier:nil + previewProvider:nil + actionProvider:^UIMenu *(NSArray *suggestedActions) { + return [UIMenu menuWithTitle:nil children:@[ + [UIAction actionWithTitle:@"Rename" + image:[UIImage systemImageNamed:@"pencil"] + identifier:nil + handler:^(__kindof UIAction *action) { + [self renameRow:indexPath]; + }], + [UIAction actionWithTitle:@"Duplicate" + image:[UIImage systemImageNamed:@"plus.square.on.square"] + identifier:nil + handler:^(__kindof UIAction *action) { + [[GBROMManager sharedManager] duplicateROM:[GBROMManager sharedManager].allROMs[[indexPath indexAtPosition:1]]]; + [self.tableView reloadData]; + }], + ]]; + }]; +} + +- (void)deselectRow +{ + if (self.tableView.indexPathForSelectedRow) { + [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:true]; + } +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self.tableView reloadData]; +} + +@end diff --git a/iOS/GBMenuButton.h b/iOS/GBMenuButton.h new file mode 100644 index 0000000..846f050 --- /dev/null +++ b/iOS/GBMenuButton.h @@ -0,0 +1,5 @@ +#import + +@interface GBMenuButton : UIButton + +@end diff --git a/iOS/GBMenuButton.m b/iOS/GBMenuButton.m new file mode 100644 index 0000000..6c4fb52 --- /dev/null +++ b/iOS/GBMenuButton.m @@ -0,0 +1,23 @@ +#import "GBMenuButton.h" + +@implementation GBMenuButton + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + if (!self.imageView.image) return; + CGSize imageSize = self.imageView.frame.size; + CGSize titleSize = self.titleLabel.frame.size; + + self.imageEdgeInsets = UIEdgeInsetsMake(0, + 0, + 28, + -titleSize.width); + + self.titleEdgeInsets = UIEdgeInsetsMake(36, + -imageSize.width, + 0, + 0); +} + +@end diff --git a/iOS/GBMenuViewController.h b/iOS/GBMenuViewController.h new file mode 100644 index 0000000..26b8253 --- /dev/null +++ b/iOS/GBMenuViewController.h @@ -0,0 +1,5 @@ +#import + +@interface GBMenuViewController : UIAlertController ++ (instancetype)menu; +@end diff --git a/iOS/GBMenuViewController.m b/iOS/GBMenuViewController.m new file mode 100644 index 0000000..1a47011 --- /dev/null +++ b/iOS/GBMenuViewController.m @@ -0,0 +1,164 @@ +#import +#import "GBMenuViewController.h" +#import "GBMenuButton.h" +#import "GBViewController.h" +#import "GBROMManager.h" + +static NSString *const tips[] = { + @"Tip: AirDrop ROM files from a Mac or another device to play them.", + @"Tip: Swipe right on the Game Boy screen to fast forward emulation.", + @"Tip: The D-pad can be replaced with a Swipe-pad in Control Settings. Give it a try!", + @"Tip: Swipe left on the Game Boy screen to rewind.", + @"Tip: Enable Quick Save and Load in Control Settings to use save state gestures.", + @"Tip: The turbo and rewind speeds can be changed in Emulation Settings.", + @"Tip: Change turbo and rewind behavior to locking in Controls Settings.", + @"Tip: Single Touch A+B combo can be enabled in Controls Settings.", + @"Tip: Try different scaling filters in Display Settings.", + @"Tip: Dynamically control turbo and rewind speed by enabling Dynamic Control in Control Settings.", + @"Tip: Rumble can be enabled even for games without rumble support in Control Settings.", + @"Tip: Try different color palettes for monochrome models in Display Settings.", + @"Did you know? The Game Boy uses a Sharp SM83 CPU.", + @"Did you know? The Game Boy Color has 6 different hardware revisions.", + @"Did you know? The Game Boy's frame rate is approximately 59.73 frames per second.", + @"Did you know? The original Super Game Boy runs slightly faster than other Game Boys.", + @"Did you know? The Game Boy generates audio at a sample rate of over 2MHz!", +}; + +@implementation GBMenuViewController +{ + UILabel *_tipLabel; + UIVisualEffectView *_effectView; +} + ++ (instancetype)menu +{ + UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad? + UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet; + GBMenuViewController *ret = [self alertControllerWithTitle:nil + message:nil + preferredStyle:style]; + [ret addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:nil]]; + return ret; +} + +// The redundant sizeof forces the compiler to validate the selector exists +#define SelectorString(x) (sizeof(@selector(x))? @#x : nil) + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:true]; + static const struct { + NSString *label; + NSString *image; + NSString *selector; + } buttons[] = { + {@"Reset", @"arrow.2.circlepath", SelectorString(reset)}, + {@"Library", @"bookmark", SelectorString(openLibrary)}, + {@"Model", @"ModelTemplate", SelectorString(changeModel)}, + {@"States", @"square.stack", SelectorString(openStates)}, + {@"Settings", @"gear", SelectorString(openSettings)}, + {@"About", @"info.circle", SelectorString(showAbout)}, + }; + + double width = self.view.frame.size.width / 3; + double height = 88; + for (unsigned i = 0; i < 6; i++) { + unsigned x = i % 3; + unsigned y = i / 3; + GBMenuButton *button = [GBMenuButton buttonWithType:UIButtonTypeSystem]; + [button setTitle:buttons[i].label forState:UIControlStateNormal]; + if (@available(iOS 13.0, *)) { + UIImage *image = [UIImage imageNamed:buttons[i].image] ?: [UIImage systemImageNamed:buttons[i].image]; + [button setImage:image forState:UIControlStateNormal]; + } + button.frame = CGRectMake(width * x, height * y, width, height); + button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [self.view addSubview:button]; + + if (!buttons[i].selector) { + button.enabled = false; + continue; + } + SEL selector = NSSelectorFromString(buttons[i].selector); + if ((selector == @selector(reset) || selector == @selector(openStates)) + && ![GBROMManager sharedManager].currentROM) { + button.enabled = false; + continue; + } + id block = ^(){ + [self.presentingViewController dismissViewControllerAnimated:true completion:^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + (void)[[UIApplication sharedApplication].delegate performSelector:selector]; +#pragma clang diagnostic pop + }]; + }; + objc_setAssociatedObject(button, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN); + [button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside]; + } + + _effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent]]; + _effectView.layer.cornerRadius = 8; + _effectView.layer.masksToBounds = true; + [self.view.superview addSubview:_effectView]; + _tipLabel = [[UILabel alloc] init]; + unsigned tipIndex = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBTipIndex"]; + _tipLabel.text = tips[tipIndex % (sizeof(tips) / sizeof(tips[0]))]; + if (@available(iOS 13.0, *)) { + _tipLabel.textColor = [UIColor labelColor]; + } + _tipLabel.font = [UIFont systemFontOfSize:14]; + _tipLabel.alpha = 0.8; + [[NSUserDefaults standardUserDefaults] setInteger:tipIndex + 1 forKey:@"GBTipIndex"]; + _tipLabel.lineBreakMode = NSLineBreakByWordWrapping; + _tipLabel.numberOfLines = 3; + [_effectView.contentView addSubview:_tipLabel]; + [self layoutTip]; + _effectView.alpha = 0; + [UIView animateWithDuration:0.25 animations:^{ + _effectView.alpha = 1.0; + }]; +} + +- (void)layoutTip +{ + UIView *view = self.view.superview; + CGSize outerSize = view.frame.size; + CGSize size = [_tipLabel textRectForBounds:(CGRect){{0, 0}, + {outerSize.width - 32, + outerSize.height - 32}} + limitedToNumberOfLines:3].size; + size.width = ceil(size.width); + _tipLabel.frame = (CGRect){{8, 8}, size}; + _effectView.frame = (CGRect) { + {round((outerSize.width - size.width - 16) / 2), view.window.safeAreaInsets.top + 12}, + {size.width + 16, size.height + 16} + }; +} + + +- (void)viewWillDisappear:(BOOL)animated +{ + [UIView animateWithDuration:0.25 animations:^{ + _effectView.alpha = 0; + }]; +} + +- (void)viewDidLayoutSubviews +{ + if (self.view.bounds.size.height < 88 * 2) { + [self.view.heightAnchor constraintEqualToConstant:self.view.bounds.size.height + 88 * 2].active = true; + } + [self layoutTip]; + [super viewDidLayoutSubviews]; +} + +// This is a hack that forces the iPad controller to display the button separator +- (UIViewController *)contentViewController +{ + return [[UIViewController alloc] init]; +} + +@end diff --git a/iOS/GBOptionViewController.h b/iOS/GBOptionViewController.h new file mode 100644 index 0000000..283ae9c --- /dev/null +++ b/iOS/GBOptionViewController.h @@ -0,0 +1,11 @@ +#import + +@interface GBOptionViewController : UITableViewController +- (instancetype)initWithHeader:(NSString *)header; +@property NSString *header; +@property NSString *footer; +@property (getter=isModal) bool modal; +- (void)addOption:(NSString *)title + withCheckmark:(bool)checked + action:(void (^)(void))block; +@end diff --git a/iOS/GBOptionViewController.m b/iOS/GBOptionViewController.m new file mode 100644 index 0000000..85f3910 --- /dev/null +++ b/iOS/GBOptionViewController.m @@ -0,0 +1,74 @@ +#import "GBOptionViewController.h" + +@implementation GBOptionViewController +{ + NSMutableArray *_options; + NSMutableArray *_actions; + NSMutableArray *_checked; +} + +- (instancetype)initWithHeader:(NSString *)header +{ + UITableViewStyle style = UITableViewStyleGrouped; + if (@available(iOS 13.0, *)) { + style = UITableViewStyleInsetGrouped; + } + self = [super initWithStyle:style]; + self.header = header; + _options = [NSMutableArray array]; + _actions = [NSMutableArray array]; + _checked = [NSMutableArray array]; + return self; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section +{ + return self.header; +} + +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section +{ + return self.footer; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView + numberOfRowsInSection:(NSInteger)section +{ + return _options.count; +} + +- (void)addOption:(NSString *)title + withCheckmark:(bool)checked + action:(void (^)(void))block +{ + [_options addObject:title]; + [_actions addObject:block]; + [_checked addObject:@(checked)]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + cell.textLabel.text = _options[[indexPath indexAtPosition:1]]; + cell.accessoryType = _checked[[indexPath indexAtPosition:1]].boolValue? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; + + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [self.presentingViewController dismissViewControllerAnimated:true completion:^{ + _actions[[indexPath indexAtPosition:1]](); + }]; +} + +- (BOOL)isModalInPresentation +{ + return self.isModal; +} +@end diff --git a/iOS/GBROMManager.h b/iOS/GBROMManager.h new file mode 100644 index 0000000..4c71538 --- /dev/null +++ b/iOS/GBROMManager.h @@ -0,0 +1,23 @@ +#import + +@interface GBROMManager : NSObject ++ (instancetype) sharedManager; + +@property (readonly) NSArray *allROMs; +@property (nonatomic) NSString *currentROM; + +@property (readonly) NSString *romFile; +@property (readonly) NSString *batterySaveFile; +@property (readonly) NSString *autosaveStateFile; +- (NSString *)stateFile:(unsigned)index; + +- (NSString *)romFileForROM:(NSString *)rom; +- (NSString *)batterySaveFileForROM:(NSString *)rom; +- (NSString *)autosaveStateFileForROM:(NSString *)rom; +- (NSString *)stateFile:(unsigned)index forROM:(NSString *)rom; +- (NSString *)importROM:(NSString *)romFile keepOriginal:(bool)keep; +- (NSString *)importROM:(NSString *)romFile withName:(NSString *)friendlyName keepOriginal:(bool)keep; +- (NSString *)renameROM:(NSString *)rom toName:(NSString *)newName; +- (NSString *)duplicateROM:(NSString *)rom; +- (void)deleteROM:(NSString *)rom; +@end diff --git a/iOS/GBROMManager.m b/iOS/GBROMManager.m new file mode 100644 index 0000000..6697d09 --- /dev/null +++ b/iOS/GBROMManager.m @@ -0,0 +1,221 @@ +#import "GBROMManager.h" +#import + +@implementation GBROMManager +{ + NSString *_romFile; +} + ++ (instancetype)sharedManager +{ + static dispatch_once_t onceToken; + static GBROMManager *manager; + dispatch_once(&onceToken, ^{ + manager = [[self alloc] init]; + }); + return manager; +} + +- (instancetype)init +{ + self = [super init]; + if (!self) return nil; + self.currentROM = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBLastROM"]; + return self; +} + +- (void)setCurrentROM:(NSString *)currentROM +{ + _romFile = nil; + _currentROM = currentROM; + if (currentROM && !self.romFile) { + _currentROM = nil; + } + + [[NSUserDefaults standardUserDefaults] setObject:_currentROM forKey:@"GBLastROM"]; +} + +- (NSString *)romFileForDirectory:(NSString *)romDirectory +{ + for (NSString *filename in [NSFileManager.defaultManager contentsOfDirectoryAtPath:romDirectory + error:nil]) { + if ([@[@"gb", @"gbc", @"isx"] containsObject:filename.pathExtension.lowercaseString]) { + return [romDirectory stringByAppendingPathComponent:filename]; + } + } + + return nil; +} + +- (NSString *)romFile +{ + if (_romFile) return _romFile; + if (!_currentROM) return nil; + NSString *root = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + NSString *romDirectory = [root stringByAppendingPathComponent:_currentROM]; + return _romFile = [self romFileForDirectory:romDirectory]; +} + +- (NSString *)romFileForROM:(NSString *)rom +{ + if ([rom isEqualToString:@"Inbox"]) return nil; + if ([rom isEqualToString:@"Boot ROMs"]) return nil; + if (rom == _currentROM) { + return self.romFile; + } + NSString *root = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + NSString *romDirectory = [root stringByAppendingPathComponent:rom]; + return [self romFileForDirectory:romDirectory]; +} + +- (NSString *)auxilaryFileForROM:(NSString *)rom withExtension:(NSString *)extension +{ + return [[[self romFileForROM:rom] stringByDeletingPathExtension] stringByAppendingPathExtension:extension]; +} + +- (NSString *)batterySaveFileForROM:(NSString *)rom +{ + return [self auxilaryFileForROM:rom withExtension:@"sav"]; +} + +- (NSString *)batterySaveFile +{ + return [self batterySaveFileForROM:_currentROM]; +} + +- (NSString *)autosaveStateFileForROM:(NSString *)rom +{ + return [self auxilaryFileForROM:rom withExtension:@"auto"]; +} + +- (NSString *)autosaveStateFile +{ + return [self autosaveStateFileForROM:_currentROM]; +} + +- (NSString *)stateFile:(unsigned)index forROM:(NSString *)rom +{ + return [self auxilaryFileForROM:rom withExtension:[NSString stringWithFormat:@"s%u", index]]; +} + +- (NSString *)stateFile:(unsigned)index +{ + return [self stateFile:index forROM:_currentROM]; +} + +- (NSArray *)allROMs +{ + NSMutableArray *ret = [NSMutableArray array]; + NSString *root = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + for (NSString *romDirectory in [NSFileManager.defaultManager contentsOfDirectoryAtPath:root + error:nil]) { + if ([romDirectory hasPrefix:@"."] || [romDirectory isEqualToString:@"Inbox"]) continue; + if ([self romFileForDirectory:[root stringByAppendingPathComponent:romDirectory]]) { + [ret addObject:romDirectory]; + } + } + return [ret sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; +} + +- (NSString *)makeNameUnique:(NSString *)name +{ + NSString *root = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + if (![[NSFileManager defaultManager] fileExistsAtPath:[root stringByAppendingPathComponent:name]]) return name; + + unsigned i = 2; + while (true) { + NSString *attempt = [name stringByAppendingFormat:@" %u", i]; + if ([[NSFileManager defaultManager] fileExistsAtPath:[root stringByAppendingPathComponent:attempt]]) { + i++; + continue; + } + return attempt; + } +} + +- (NSString *)importROM:(NSString *)romFile keepOriginal:(bool)keep +{ + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil]; + NSString *friendlyName = [[romFile lastPathComponent] stringByDeletingPathExtension]; + friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""]; + friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + NSString *root = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + if ([[NSFileManager defaultManager] fileExistsAtPath:[root stringByAppendingPathComponent:friendlyName]]) { + unsigned i = 2; + while (true) { + NSString *attempt = [friendlyName stringByAppendingFormat:@" %u", i]; + if ([[NSFileManager defaultManager] fileExistsAtPath:[root stringByAppendingPathComponent:attempt]]) { + i++; + continue; + } + friendlyName = attempt; + break; + } + } + + return [self importROM:romFile withName:friendlyName keepOriginal:keep]; +} + +- (NSString *)importROM:(NSString *)romFile withName:(NSString *)friendlyName keepOriginal:(bool)keep +{ + NSString *root = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + NSString *romFolder = [root stringByAppendingPathComponent:friendlyName]; + [[NSFileManager defaultManager] createDirectoryAtPath:romFolder + withIntermediateDirectories:false + attributes:nil + error:nil]; + + NSString *newROMPath = [romFolder stringByAppendingPathComponent:romFile.lastPathComponent]; + + if (keep) { + if (copyfile(romFile.fileSystemRepresentation, + newROMPath.fileSystemRepresentation, + NULL, + COPYFILE_CLONE)) { + [[NSFileManager defaultManager] removeItemAtPath:romFolder error:nil]; + return nil; + } + } + else { + if (![[NSFileManager defaultManager] moveItemAtPath:romFile + toPath:newROMPath + error:nil]) { + [[NSFileManager defaultManager] removeItemAtPath:romFolder error:nil]; + return nil; + } + + } + + return friendlyName; +} + +- (NSString *)renameROM:(NSString *)rom toName:(NSString *)newName +{ + newName = [self makeNameUnique:newName]; + if ([rom isEqualToString:_currentROM]) { + _currentROM = newName; + } + NSString *root = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + + [[NSFileManager defaultManager] moveItemAtPath:[root stringByAppendingPathComponent:rom] + toPath:[root stringByAppendingPathComponent:newName] error:nil]; + return newName; +} + +- (NSString *)duplicateROM:(NSString *)rom +{ + NSString *newName = [self makeNameUnique:rom]; + return [self importROM:[self romFileForROM:rom] + withName:newName + keepOriginal:true]; +} + +- (void)deleteROM:(NSString *)rom +{ + NSString *root = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; + NSString *romDirectory = [root stringByAppendingPathComponent:rom]; + [[NSFileManager defaultManager] removeItemAtPath:romDirectory error:nil]; +} + +@end diff --git a/iOS/GBSettingsViewController.h b/iOS/GBSettingsViewController.h new file mode 100644 index 0000000..5583cd6 --- /dev/null +++ b/iOS/GBSettingsViewController.h @@ -0,0 +1,31 @@ +#import +#import +#import "GCExtendedGamepad+AllElements.h" +#import "GBTheme.h" + +typedef enum { + GBRight, + GBLeft, + GBUp, + GBDown, + GBA, + GBB, + GBSelect, + GBStart, + GBTurbo, + GBRewind, + GBUnderclock, + // GBHotkey1, // Todo + // GBHotkey2, // Todo + GBJoypadButtonCount, + GBButtonCount = GBUnderclock + 1, + GBGameBoyButtonCount = GBStart + 1, + GBUnusedButton = 0xFF, +} GBButton; + +@interface GBSettingsViewController : UITableViewController ++ (UIViewController *)settingsViewControllerWithLeftButton:(UIBarButtonItem *)button; ++ (const GB_palette_t *)paletteForTheme:(NSString *)theme; ++ (GBButton)controller:(GCController *)controller convertUsageToButton:(GBControllerUsage)usage; ++ (GBTheme *)themeNamed:(NSString *)name; +@end diff --git a/iOS/GBSettingsViewController.m b/iOS/GBSettingsViewController.m new file mode 100644 index 0000000..5cad05a --- /dev/null +++ b/iOS/GBSettingsViewController.m @@ -0,0 +1,968 @@ +#import "GBSettingsViewController.h" +#import "GBTemperatureSlider.h" +#import "GBViewBase.h" +#import "GBThemesViewController.h" +#import "GBHapticManager.h" +#import "GCExtendedGamepad+AllElements.h" +#import + +static NSString const *typeSubmenu = @"submenu"; +static NSString const *typeOptionSubmenu = @"optionSubmenu"; +static NSString const *typeBlock = @"block"; +static NSString const *typeRadio = @"radio"; +static NSString const *typeCheck = @"check"; +static NSString const *typeDisabled = @"disabled"; +static NSString const *typeSeparator = @"separator"; +static NSString const *typeSlider = @"slider"; +static NSString const *typeLightTemp = @"typeLightTemp"; + +@implementation GBSettingsViewController +{ + NSArray *_structure; + UINavigationController *_detailsNavigation; + NSArray *> *_themes; // For prewarming +} + ++ (const GB_palette_t *)paletteForTheme:(NSString *)theme +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + if ([theme isEqualToString:@"Greyscale"]) { + return &GB_PALETTE_GREY; + } + if ([theme isEqualToString:@"Lime (Game Boy)"]) { + return &GB_PALETTE_DMG; + } + if ([theme isEqualToString:@"Olive (Pocket)"]) { + return &GB_PALETTE_MGB; + } + if ([theme isEqualToString:@"Teal (Light)"]) { + return &GB_PALETTE_GBL; + } + static GB_palette_t customPalette; + NSArray *colors = [defaults dictionaryForKey:@"GBThemes"][theme][@"Colors"]; + if (colors.count != 5) return &GB_PALETTE_DMG; + unsigned i = 0; + for (NSNumber *color in colors) { + uint32_t c = [color unsignedIntValue]; + customPalette.colors[i++] = (struct GB_color_s) {c, c >> 8, c >> 16}; + } + return &customPalette; +} + ++ (UIColor *) colorFromGBColor:(const struct GB_color_s *)color +{ + return [UIColor colorWithRed:color->r / 255.0 + green:color->g / 255.0 + blue:color->b / 255.0 + alpha:1.0]; +} + ++ (UIImage *)previewImageForTheme:(NSString *)theme +{ + const GB_palette_t *palette = [self paletteForTheme:theme]; + UIGraphicsBeginImageContextWithOptions((CGSize){29, 29}, false, [UIScreen mainScreen].scale); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 29, 29) cornerRadius:7]; + [[self colorFromGBColor:&palette->colors[4]] set]; + [path fill]; + + path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(4, 4, 9, 9) cornerRadius:2]; + [[self colorFromGBColor:&palette->colors[0]] set]; + [path fill]; + + path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(16, 4, 9, 9) cornerRadius:2]; + [[self colorFromGBColor:&palette->colors[1]] set]; + [path fill]; + + path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(4, 16, 9, 9) cornerRadius:2]; + [[self colorFromGBColor:&palette->colors[2]] set]; + [path fill]; + + path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(16, 16, 9, 9) cornerRadius:2]; + [[self colorFromGBColor:&palette->colors[3]] set]; + [path fill]; + + UIImage *ret = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return ret; +} + ++ (NSArray *)paletteMenu +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSArray *themes = [@[ + @"Greyscale", + @"Lime (Game Boy)", + @"Olive (Pocket)", + @"Teal (Light)", + ] arrayByAddingObjectsFromArray:[[defaults dictionaryForKey:@"GBThemes"] allKeys]]; + NSMutableArray *themeItems = [NSMutableArray arrayWithCapacity:themes.count]; + for (NSString *theme in themes) { + [themeItems addObject: @{@"type": typeRadio, @"pref": @"GBCurrentTheme", + @"title": theme, @"value": theme, + @"image": [self previewImageForTheme:theme]}]; + } + return @[ + @{ + @"items": [themeItems subarrayWithRange:(NSRange){0, 4}] + }, + @{ + @"items": [themeItems subarrayWithRange:(NSRange){4, themeItems.count - 4}] + } + ]; +} + ++ (NSArray *)rootStructure +{ +#define QUICK_SUBMENU(title, ...) @{@"type": typeOptionSubmenu, @"title": title, @"submenu": @[@{@"items": __VA_ARGS__}]} + NSArray *emulationMenu = @[ + @{ + @"header": @"Rewind Duration", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"Disabled", @"value": @0,}, + @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"10 Seconds", @"value": @10,}, + @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"30 Seconds", @"value": @30,}, + @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"1 Minute", @"value": @60,}, + @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"2 Minutes", @"value": @120,}, + @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"5 Minutes", @"value": @300,}, + @{@"type": typeRadio, @"pref": @"GBRewindLength", @"title": @"10 Minutes", @"value": @600,}, + ] + }, + @{ + @"header": @"Real Time Clock Emulation", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBRTCMode", @"title": @"Accurate", @"value": @(GB_RTC_MODE_ACCURATE),}, + @{@"type": typeRadio, @"pref": @"GBRTCMode", @"title": @"Sync to System Clock", @"value": @(GB_RTC_MODE_SYNC_TO_HOST),}, + ] + }, + @{ + @"header": @"Turbo Speed", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBTurboSpeed", @"title": @"200%", @"value": @2,}, + @{@"type": typeRadio, @"pref": @"GBTurboSpeed", @"title": @"400%", @"value": @4,}, + @{@"type": typeRadio, @"pref": @"GBTurboSpeed", @"title": @"Uncapped", @"value": @1,}, + ], + @"footer": ^NSString *(){ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + return @"This setting will have no effect because horizontal swipes are configured to dynamically control speed in the “Controls” settings"; + } + return @""; + }, + }, + @{ + @"header": @"Rewind Speed", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBRewindSpeed", @"title": @"100%", @"value": @1,}, + @{@"type": typeRadio, @"pref": @"GBRewindSpeed", @"title": @"200%", @"value": @2,}, + @{@"type": typeRadio, @"pref": @"GBRewindSpeed", @"title": @"400%", @"value": @4,}, + ], + @"footer": ^NSString *(){ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + return @"This setting will have no effect because horizontal swipes are configured to dynamically control speed in the “Controls” settings"; + } + return @""; + } + }, + @{ + @"header": @"Boot ROMs", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBCustomBootROMs", @"title": @"Use Built-in Boot ROMs", @"value": @NO,}, + @{@"type": typeRadio, @"pref": @"GBCustomBootROMs", @"title": @"Use Boot ROMs from Files", @"value": @YES,}, + @{@"type": typeBlock, @"title": @"Open Boot ROMs Folder", @"block": ^bool(GBSettingsViewController *controller) { + + NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0]; + path = [path stringByAppendingPathComponent:@"Boot ROMs"]; + [[NSFileManager defaultManager] createDirectoryAtPath:path + withIntermediateDirectories:true + attributes:nil + error:nil]; + + + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"shareddocuments://%@", [path stringByReplacingOccurrencesOfString:@" " + withString:@"%20"]]] + options:nil + completionHandler:nil]; + + return false; + }}, + ], + @"footer": @"Put your boot ROM files (dmg_boot.bin, cgb_boot.bin, etc.) in the Boot ROMs folder to use them" + }, + @{ + @"header": @"Emulated Revisions", + @"items": @[ + QUICK_SUBMENU(@"Game Boy", @[ + @{@"type": typeDisabled, @"title": @"DMG-CPU 0"}, + @{@"type": typeDisabled, @"title": @"DMG-CPU A"}, + @{@"type": typeRadio, @"pref": @"GBDMGModel", @"title": @"DMG-CPU B", @"value": @(GB_MODEL_DMG_B),}, + @{@"type": typeDisabled, @"title": @"DMG-CPU C"}, + ]), + QUICK_SUBMENU(@"Super Game Boy", @[ + @{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy (NTSC)", @"value": @(GB_MODEL_SGB_NTSC),}, + @{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy (PAL)", @"value": @(GB_MODEL_SGB_PAL),}, + @{@"type": typeRadio, @"pref": @"GBSGBModel", @"title": @"Super Game Boy 2", @"value": @(GB_MODEL_SGB2),}, + ]), + QUICK_SUBMENU(@"Game Boy Color", @[ + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB 0 (Experimental)", @"value": @(GB_MODEL_CGB_0),}, + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB A (Experimental)", @"value": @(GB_MODEL_CGB_A),}, + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB B (Experimental)", @"value": @(GB_MODEL_CGB_B),}, + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB C (Experimental)", @"value": @(GB_MODEL_CGB_C),}, + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB D", @"value": @(GB_MODEL_CGB_D),}, + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB E", @"value": @(GB_MODEL_CGB_E),}, + ]), + QUICK_SUBMENU(@"Game Boy Advance", @[ + @{@"type": typeDisabled, @"title": @"CPU AGB 0 (Early GBA)",}, + @{@"type": typeRadio, @"pref": @"GBAGBModel", @"title": @"CPU AGB A (GBA)", @"value": @(GB_MODEL_AGB_A),}, + @{@"type": typeRadio, @"pref": @"GBAGBModel", @"title": @"CPU AGB A (Game Boy Player)", @"value": @(GB_MODEL_GBP_A),}, + @{@"type": typeDisabled, @"title": @"CPU AGB B (GBA SP)",}, + @{@"type": typeDisabled, @"title": @"CPU AGB E (Late GBA SP)",}, + @{@"type": typeDisabled, @"title": @"CPU AGB E (Late Game Boy Player)",}, + ]), + ], + @"footer": @"Changing the emulated revision on active ROMs will take effect after selecting Reset from the menu or changing the emulated model.", + }, + ]; + + NSArray *videoMenu = @[ + @{ + @"items": @[ + @{ + @"title": @"Graphics Filter", + @"type": typeOptionSubmenu, + @"submenu": @[ + @{ + @"header": @"Standard Filters", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Nearest Neighbor (Pixelated)", @"value": @"NearestNeighbor",}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Bilinear (Blurry)", @"value": @"Bilinear",}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Smooth Bilinear (Less blurry)", @"value": @"SmoothBilinear",}, + ] + }, + @{ + @"header": @"Screen Filters", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Monochrome LCD Display", @"value": @"MonoLCD",}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"LCD Display", @"value": @"LCD",}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"CRT Display", @"value": @"CRT",}, + ] + }, + @{ + @"header": @"Upscaling Filters", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Scale2x", @"value": @"Scale2x"}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Scale4x", @"value": @"Scale4x"}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Anti-aliased Scale2x", @"value": @"AAScale2x"}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Anti-aliased Scale4x", @"value": @"AAScale4x"}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"HQ2x", @"value": @"HQ2x"}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"OmniScale", @"value": @"OmniScale"}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"OmniScale Legacy", @"value": @"OmniScaleLegacy"}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"AA OmniScale Legacy", @"value": @"AAOmniScaleLegacy"}, + ] + }, + ] + }, + ] + }, + @{ + @"header": @"Color Correction", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Disabled", @"value": @(GB_COLOR_CORRECTION_DISABLED),}, + @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Correct Color Curves", @"value": @(GB_COLOR_CORRECTION_CORRECT_CURVES),}, + @{@"type": typeSeparator}, + @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern – Balanced", @"value": @(GB_COLOR_CORRECTION_MODERN_BALANCED),}, + @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern – Accurate", @"value": @(GB_COLOR_CORRECTION_MODERN_ACCURATE),}, + @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Modern – Boost contrast", @"value": @(GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST),}, + @{@"type": typeSeparator}, + @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Reduce Contrast", @"value": @(GB_COLOR_CORRECTION_REDUCE_CONTRAST),}, + @{@"type": typeRadio, @"pref": @"GBColorCorrection", @"title": @"Harsh Reality (Low Contrast)", @"value": @(GB_COLOR_CORRECTION_LOW_CONTRAST),}, + ], + @"footer": ^NSString *(){ + return GB_inline_const(NSString *[], { + [GB_COLOR_CORRECTION_DISABLED] = @"Colors are directly interpreted as sRGB, resulting in unbalanced colors and inaccurate hues.", + [GB_COLOR_CORRECTION_CORRECT_CURVES] = @"Colors have their brightness corrected, but hues remain unbalanced.", + [GB_COLOR_CORRECTION_MODERN_BALANCED] = @"Emulates a modern display. Blue contrast is moderately enhanced at the cost of slight hue inaccuracy.", + [GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST] = @"Like Modern – Balanced, but further boosts the contrast of greens and magentas that is lacking on the original hardware.", + [GB_COLOR_CORRECTION_REDUCE_CONTRAST] = @"Slightly reduce the contrast to better represent the tint and contrast of the original display.", + [GB_COLOR_CORRECTION_LOW_CONTRAST] = @"Harshly reduce the contrast to accurately represent the tint and low constrast of the original display.", + [GB_COLOR_CORRECTION_MODERN_ACCURATE] = @"Emulates a modern display. Colors have their hues and brightness corrected.", + })[MIN(GB_COLOR_CORRECTION_MODERN_ACCURATE, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"])]; + }, + }, + @{ + @"header": @"Ambient Light Temperature", + @"items": @[ + @{@"type": typeLightTemp, @"pref": @"GBLightTemperature", @"min": @-1, @"max": @1} + ], + }, + @{ + @"header": @"Frame Blending", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Disabled", @"value": @(GB_FRAME_BLENDING_MODE_DISABLED),}, + @{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Simple", @"value": @(GB_FRAME_BLENDING_MODE_SIMPLE),}, + @{@"type": typeRadio, @"pref": @"GBFrameBlendingMode", @"title": @"Accurate", @"value": @(GB_FRAME_BLENDING_MODE_ACCURATE),}, + ] + }, + @{ + @"items": @[@{ + @"title": @"Monochrome Palette", + @"type": typeOptionSubmenu, + @"submenu": [self paletteMenu] + }], + @"footer": @"This palette will be used when emulating a monochrome model such as the original Game Boy." + } + ]; + + NSArray *audioMenu = @[ + @{ + @"header": @"Enable Audio", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBAudioMode", @"title": @"Never", @"value": @"off",}, + @{@"type": typeRadio, @"pref": @"GBAudioMode", @"title": @"Controlled by Silent Mode", @"value": @"switch",}, + @{@"type": typeRadio, @"pref": @"GBAudioMode", @"title": @"Always", @"value": @"on",}, + ], + + }, + @{ + @"header": @"High-pass Filter", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Disabled (Keep DC Offset)", @"value": @(GB_HIGHPASS_OFF),}, + @{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Accurate (Emulate Hardware)", @"value": @(GB_HIGHPASS_ACCURATE),}, + @{@"type": typeRadio, @"pref": @"GBHighpassFilter", @"title": @"Preserve Waveform", @"value": @(GB_HIGHPASS_REMOVE_DC_OFFSET),}, + ], + @"footer": ^NSString *(){ + return GB_inline_const(NSString *[], { + [GB_HIGHPASS_OFF] = @"No high-pass filter will be applied. DC offset will be kept, pausing and resuming will trigger audio pops.", + [GB_HIGHPASS_ACCURATE] = @"An accurate high-pass filter will be applied, removing the DC offset while somewhat attenuating the bass.", + [GB_HIGHPASS_REMOVE_DC_OFFSET] = @"A high-pass filter will be applied to the DC offset itself, removing the DC offset while preserving the waveform.", + })[MIN(GB_HIGHPASS_REMOVE_DC_OFFSET, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"])]; + }, + }, + @{ + @"header": @"Interference volume", + @"items": @[ + @{@"type": typeSlider, @"pref": @"GBInterferenceVolume", @"min": @0, @"max": @1, @"minImage": @"speaker.fill", @"maxImage": @"speaker.3.fill"} + ], + }, + ]; + + NSArray *controlsMenu = @[ + @{ + @"items": @[ + @{@"type": typeBlock, @"title": @"Configure Game Controllers", @"block": ^bool(GBSettingsViewController *controller){ + return [controller configureGameControllers]; + }}, + ], + }, + @{ + @"header": @"D-pad Style", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBSwipeDpad", @"title": @"Standard", @"value": @NO,}, + @{@"type": typeRadio, @"pref": @"GBSwipeDpad", @"title": @"Swipe", @"value": @YES,}, + ], + @"footer": ^NSString *(){ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeDpad"]) { + return @"Directional input is determined by the swipe direction."; + } + return @"Directional input is determined by the touch position."; + }, + }, + @{ + @"items": @[ + @{@"type": typeCheck, @"pref": @"GBEnableABCombo", @"title": @"Single-Touch A+B"}, + ], + @"footer": @"Enable this option to allow pressing A+B by touching the space between the two buttons", + }, + @{ + @"header": @"Horizontal Swipe Behavior", + @"items": @[ + @{@"type": typeCheck, @"pref": @"GBDynamicSpeed", @"title": @"Dynamically Control Speed"}, + @{@"type": typeCheck, @"pref": @"GBSwipeLock", @"title": @"Lock After Swiping"}, + ], + @"footer": ^NSString *(){ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) { + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + return @"Swipe right on the Game Boy screen to play forward, and swipe left to rewind. Tap on the Game Boy screen to return to normal. The forward and rewind speeds are determinied by the swipe distance."; + } + return @"Swipe right on the Game Boy screen to fast-forward, and swipe left to rewind. Tap on the Game Boy screen to return to normal. The turbo and rewind speeds can be configured under “Emulation” settings."; + } + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + return @"Swipe right on the Game Boy screen to play forward, and swipe left to rewind. Raise the touch to return to normal. The forward and rewind speeds are determinied by the swipe distance."; + } + return @"Swipe right on the Game Boy screen to fast-forward, and swipe left to rewind. Raise the touch to return to normal. The turbo and rewind speeds can be configured under “Emulation” settings."; + }, + }, + @{ + @"header": @"Quick Save and Load", + @"items": @[ + @{@"type": typeCheck, @"pref": @"GBSwipeState", @"title": @"Swipe to Save and Load from Slot 1"}, + ], + @"footer": ^NSString *(){ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) { + return @"Swipe down on the Game Boy to save the state into state slot 1. Swipe up to load the state from state slot 1"; + } + return @" "; // This space is needed, otherwise UITableView spacing breaks + }, + }, + @{ + @"header": @"Enable Rumble", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Never", @"value": @(GB_RUMBLE_DISABLED),}, + @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"For Rumble-Enabled Game Paks", @"value": @(GB_RUMBLE_CARTRIDGE_ONLY),}, + @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Always", @"value": @(GB_RUMBLE_ALL_GAMES),}, + ], + }, + @{ + @"items": @[ + @{@"type": typeCheck, @"pref": @"GBButtonHaptics", @"title": @"Enable Button Haptics"}, + @{@"type": typeSlider, @"pref": @"GBHapticsStrength", @"min": @0.25, @"max": @1, @"minImage": @"waveform.weak", @"maxImage": @"waveform", + @"previewBlock": ^void(void){ + [[GBHapticManager sharedManager] doTapHaptic]; + } + } + ], + }, + ]; + + return @[ + @{ + @"items": @[ + @{ + @"title": @"Emulation", + @"type": typeSubmenu, + @"submenu": emulationMenu, + @"image": [UIImage imageNamed:@"emulationSettings"], + }, + @{ + @"title": @"Video", + @"type": typeSubmenu, + @"submenu": videoMenu, + @"image": [UIImage imageNamed:@"videoSettings"], + }, + @{ + @"title": @"Audio", + @"type": typeSubmenu, + @"submenu": audioMenu, + @"image": [UIImage imageNamed:@"audioSettings"], + }, + @{ + @"title": @"Controls", + @"type": typeSubmenu, + @"submenu": controlsMenu, + @"image": [UIImage imageNamed:@"controlsSettings"], + }, + @{ + @"title": @"Themes", + @"type": typeSubmenu, + @"class": [GBThemesViewController class], + @"image": [UIImage imageNamed:@"themeSettings"], + }, + ] + } + ]; +} + + ++ (UIViewController *)settingsViewControllerWithLeftButton:(UIBarButtonItem *)button +{ + UITableViewStyle style = UITableViewStyleGrouped; + if (@available(iOS 13.0, *)) { + style = UITableViewStyleInsetGrouped; + } + GBSettingsViewController *root = [[self alloc] initWithStructure:[self rootStructure] title:@"Settings" style:style]; + [root preloadThemePreviews]; + UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:root]; + [controller.visibleViewController.navigationItem setLeftBarButtonItem:button]; + if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) { + return controller; + } + + UISplitViewController *split = [[UISplitViewController alloc] init]; + UIViewController *blank = [[UIViewController alloc] init]; + blank.view.backgroundColor = root.view.backgroundColor; + root->_detailsNavigation = [[UINavigationController alloc] initWithRootViewController:blank]; + split.viewControllers = @[controller, root->_detailsNavigation]; + split.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; + return split; +} + +static UIImage *ImageForController(GCController *controller) +{ + if (@available(iOS 13.0, *)) { + + NSString *symbolName = @"gamecontroller.fill"; + UIColor *color = [UIColor grayColor]; + + if (@available(iOS 14.5, *)) { + if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) { + symbolName = @"logo.playstation"; + color = [UIColor colorWithRed:0 green:0x30 / 255.0 blue:0x87 / 255.0 alpha:1.0]; + } + } + if (@available(iOS 14.0, *)) { + if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) { + symbolName = @"logo.playstation"; + color = [UIColor colorWithRed:0 green:0x30 / 255.0 blue:0x87 / 255.0 alpha:1.0]; + } + if ([controller.extendedGamepad isKindOfClass:[GCXboxGamepad class]]) { + symbolName = @"logo.xbox"; + color = [UIColor colorWithRed:0xe / 255.0 green:0x7a / 255.0 blue:0xd / 255.0 alpha:1.0]; + } + } + + UIImage *glyph = [[UIImage systemImageNamed:symbolName] imageWithTintColor:[UIColor whiteColor]]; + if (!glyph) { + glyph = [[UIImage systemImageNamed:@"gamecontroller.fill"] imageWithTintColor:[UIColor whiteColor]]; + } + + UIGraphicsBeginImageContextWithOptions((CGSize){29, 29}, false, [UIScreen mainScreen].scale); + [color setFill]; + [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 29, 29) cornerRadius:7] fill]; + double height = 25 / glyph.size.width * glyph.size.height; + [glyph drawInRect:CGRectMake(2, (29 - height) / 2, 25, height)]; + + UIImage *ret = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return ret; + } + return nil; + +} + ++ (GBButton)controller:(GCController *)controller convertUsageToButton:(GBControllerUsage)usage +{ + bool isSony = false; + if (@available(iOS 14.5, *)) { + if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) { + isSony = true; + } + } + if (@available(iOS 14.0, *)) { + if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) { + isSony = true; + } + } + + NSNumber *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBControllerMappings"][controller.vendorName][[NSString stringWithFormat:@"%u", usage]]; + if (mapping) { + return mapping.intValue; + } + + switch (usage) { + case GBUsageButtonA: return isSony? GBB : GBA; + case GBUsageButtonB: return isSony? GBA : GBB; + case GBUsageButtonX: return isSony? GBSelect : GBStart; + case GBUsageButtonY: return isSony? GBStart : GBSelect; + case GBUsageButtonMenu: return GBStart; + case GBUsageButtonOptions: return GBSelect; + case GBUsageButtonHome: return GBStart; + case GBUsageLeftShoulder: return GBRewind; + case GBUsageRightShoulder: return GBTurbo; + case GBUsageLeftTrigger: return GBUnderclock; + case GBUsageRightTrigger: return GBTurbo; + default: return GBUnusedButton; + } +} + +static NSString *LocalizedNameForElement(GCControllerElement *element, GBControllerUsage usage) +{ + if (@available(iOS 14.0, *)) { + return element.localizedName; + } + switch (usage) { + case GBUsageDpad: return @"D-Pad"; + case GBUsageButtonA: return @"A"; + case GBUsageButtonB: return @"B"; + case GBUsageButtonX: return @"X"; + case GBUsageButtonY: return @"Y"; + case GBUsageButtonMenu: return @"Menu"; + case GBUsageButtonOptions: return @"Options"; + case GBUsageButtonHome: return @"Home"; + case GBUsageLeftThumbstick: return @"Left Thumbstick"; + case GBUsageRightThumbstick: return @"Right Thumbstick"; + case GBUsageLeftShoulder: return @"Left Shoulder"; + case GBUsageRightShoulder: return @"Right Shoulder"; + case GBUsageLeftTrigger: return @"Left Trigger"; + case GBUsageRightTrigger: return @"Right Trigger"; + case GBUsageLeftThumbstickButton: return @"Left Thumbstick Button"; + case GBUsageRightThumbstickButton: return @"Right Thumbstick Button"; + case GBUsageTouchpadButton: return @"Touchpad Button"; + } + + return @"Button"; +} + +- (void)configureGameController:(GCController *)controller +{ + NSMutableArray *items = [NSMutableArray array]; + NSDictionary *elementsDict = controller.extendedGamepad.elementsDictionary; + for (NSNumber *usage in [[elementsDict allKeys] sortedArrayUsingSelector:@selector(compare:)]) { + GCControllerElement *element = elementsDict[usage]; + if (![element isKindOfClass:[GCControllerButtonInput class]]) continue; + + id (^getter)(void) = ^id(void) { + return @([GBSettingsViewController controller:controller convertUsageToButton:usage.intValue]); + }; + + void (^setter)(id) = ^void(id value) { + NSMutableDictionary *mapping = ([[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBControllerMappings"] ?: @{}).mutableCopy; + + NSMutableDictionary *vendorMapping = ((NSDictionary *)mapping[controller.vendorName] ?: @{}).mutableCopy; + vendorMapping[usage.stringValue] = value; + mapping[controller.vendorName] = vendorMapping; + [[NSUserDefaults standardUserDefaults] setObject:mapping forKey:@"GBControllerMappings"]; + }; + + + NSDictionary *item = @{ + @"title": LocalizedNameForElement(element, usage.unsignedIntValue), + @"type": typeOptionSubmenu, + @"submenu": @[@{@"items": @[ + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"None", @"value": @(GBUnusedButton)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Right", @"value": @(GBRight)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Left", @"value": @(GBLeft)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Up", @"value": @(GBUp)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Down", @"value": @(GBDown)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"A", @"value": @(GBA)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"B", @"value": @(GBB)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Select", @"value": @(GBSelect)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Start", @"value": @(GBStart)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Turbo", @"value": @(GBTurbo)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rewind", @"value": @(GBRewind)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Slow-motion", @"value": @(GBUnderclock)}, + ]}], + }; + if (@available(iOS 14.0, *)) { + UIImage *image = [[UIImage systemImageNamed:element.sfSymbolsName] imageWithTintColor:UIColor.labelColor renderingMode:UIImageRenderingModeAlwaysOriginal]; + if (image) { + item = [item mutableCopy]; + ((NSMutableDictionary *)item)[@"image"] = image; + } + } + [items addObject:item]; + } + + UITableViewStyle style = UITableViewStyleGrouped; + if (@available(iOS 13.0, *)) { + style = UITableViewStyleInsetGrouped; + } + + GBSettingsViewController *submenu = [[GBSettingsViewController alloc] initWithStructure:@[@{@"items": items}] + title:controller.vendorName + style:style]; + [self.navigationController pushViewController:submenu animated:true]; +} + +- (bool)configureGameControllers +{ + + NSMutableArray *items = [NSMutableArray array]; + for (GCController *controller in [GCController controllers]) { + if (!controller.extendedGamepad) continue; + NSDictionary *item = @{ + @"title": controller.vendorName, + @"type": typeBlock, + @"block": ^bool(void) { + [self configureGameController:controller]; + return true; + } + }; + UIImage *image = ImageForController(controller); + if (image) { + item = [item mutableCopy]; + ((NSMutableDictionary *)item)[@"image"] = image; + } + + [items addObject:item]; + } + if (items.count) { + UITableViewStyle style = UITableViewStyleGrouped; + if (@available(iOS 13.0, *)) { + style = UITableViewStyleInsetGrouped; + } + + GBSettingsViewController *submenu = [[GBSettingsViewController alloc] initWithStructure:@[@{@"items": items}] + title:@"Configure Game Controllers" + style:style]; + [self.navigationController pushViewController:submenu animated:true]; + } + else { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No Controllers Connected" + message:@"There are no connected game controllers to configure" + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; + return false; + + } + return true; +} + +- (instancetype)initWithStructure:(NSArray *)structure title:(NSString *)title style:(UITableViewStyle)style +{ + self = [super initWithStyle:style]; + if (!self) return nil; + self.title = title; + _structure = structure; + return self; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return _structure.count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return [_structure[section][@"items"] count]; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section +{ + return _structure[section][@"header"]; +} + +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section +{ + if ([_structure[section][@"footer"] respondsToSelector:@selector(invoke)]) { + return ((NSString *(^)(void))_structure[section][@"footer"])(); + } + return _structure[section][@"footer"]; +} + +- (NSDictionary *)itemForIndexPath:(NSIndexPath *)indexPath +{ + return _structure[[indexPath indexAtPosition:0]][@"items"][[indexPath indexAtPosition:1]]; +} + +- (NSDictionary *)followingItemForIndexPath:(NSIndexPath *)indexPath +{ + NSArray *items = _structure[[indexPath indexAtPosition:0]][@"items"]; + if ([indexPath indexAtPosition:1] + 1 >= items.count) { + return nil; + } + return items[[indexPath indexAtPosition:1] + 1]; +} + ++ (void)fixSliderTint:(UIView *)view +{ + if ([view isKindOfClass:[UIImageView class]]) { + view.tintColor = [UIColor systemGrayColor]; + } + for (UIView *subview in view.subviews) { + [self fixSliderTint:subview]; + } +} + +static id ValueForItem(NSDictionary *item) +{ + if (item[@"getter"]) { + return ((id(^)(void))item[@"getter"])(); + } + return [[NSUserDefaults standardUserDefaults] objectForKey:item[@"pref"]] ?: @0; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSDictionary *item = [self itemForIndexPath:indexPath]; + + + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil]; + cell.textLabel.text = item[@"title"]; + if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu || item[@"type"] == typeBlock) { + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + cell.selectionStyle = UITableViewCellSelectionStyleBlue; + if (item[@"type"] == typeOptionSubmenu) { + for (NSDictionary *section in item[@"submenu"]) { + for (NSDictionary *item in section[@"items"]) { + if (item[@"value"] && [ValueForItem(item) isEqual:item[@"value"]]) { + cell.detailTextLabel.text = item[@"title"]; + break; + } + } + } + } + } + else if (item[@"type"] == typeRadio) { + if ([ValueForItem(item) isEqual:item[@"value"]]) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } + } + else if (item[@"type"] == typeCheck) { + UISwitch *button = [[UISwitch alloc] init]; + cell.accessoryView = button; + if ([[NSUserDefaults standardUserDefaults] boolForKey:item[@"pref"]]) { + button.on = true; + } + + __weak typeof(self) weakSelf = self; + id block = ^(){ + [[NSUserDefaults standardUserDefaults] setBool:button.on forKey:item[@"pref"]]; + unsigned section = [indexPath indexAtPosition:0]; + UITableViewHeaderFooterView *view = [weakSelf.tableView footerViewForSection:section]; + view.textLabel.text = [weakSelf tableView:weakSelf.tableView titleForFooterInSection:section]; + [UIView setAnimationsEnabled:false]; + [weakSelf.tableView beginUpdates]; + [view sizeToFit]; + [weakSelf.tableView endUpdates]; + [UIView setAnimationsEnabled:true]; + }; + objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN); + + [button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + } + else if (item[@"type"] == typeDisabled) { + cell.selectionStyle = UITableViewCellSelectionStyleNone; + if (@available(iOS 13.0, *)) { + cell.textLabel.textColor = [UIColor separatorColor]; + } + else { + cell.textLabel.textColor = [UIColor colorWithWhite:0 alpha:0.75]; + } + } + else if (item[@"type"] == typeSeparator) { + cell.backgroundColor = [UIColor clearColor]; + cell.separatorInset = UIEdgeInsetsZero; + } + else if (item[@"type"] == typeSlider || + item[@"type"] == typeLightTemp) { + CGRect rect = cell.contentView.bounds; + rect.size.width -= 24; + rect.size.height -= 24; + rect.origin.x += 12; + rect.origin.y += 12; + UISlider *slider = [item[@"type"] == typeLightTemp? [GBTemperatureSlider alloc] : [UISlider alloc] initWithFrame:rect]; + slider.continuous = true; + slider.minimumValue = [item[@"min"] floatValue]; + slider.maximumValue = [item[@"max"] floatValue]; + slider.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [cell.contentView addSubview:slider]; + slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + if (@available(iOS 13.0, *)) { + if (item[@"minImage"] && item[@"maxImage"]) { + slider.minimumValueImage = [UIImage systemImageNamed:item[@"minImage"]] ?: [[UIImage imageNamed:item[@"minImage"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + slider.maximumValueImage = [UIImage systemImageNamed:item[@"maxImage"]] ?: [[UIImage imageNamed:item[@"maxImage"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [GBSettingsViewController fixSliderTint:slider]; + } + } + + id block = ^(){ + [[NSUserDefaults standardUserDefaults] setDouble:slider.value forKey:item[@"pref"]]; + }; + objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN); + + [slider addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged]; + if (item[@"previewBlock"]) { + [slider addTarget:item[@"previewBlock"] action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchDown]; + } + } + + if ([self followingItemForIndexPath:indexPath][@"type"] == typeSeparator) { + cell.separatorInset = UIEdgeInsetsZero; + } + cell.imageView.image = item[@"image"]; + return cell; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSDictionary *item = [self itemForIndexPath:indexPath]; + if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu) { + UITableViewStyle style = UITableViewStyleGrouped; + if (@available(iOS 13.0, *)) { + style = UITableViewStyleInsetGrouped; + } + UITableViewController *submenu = nil; + + if (item[@"class"]) { + submenu = [(UITableViewController *)[item[@"class"] alloc] initWithStyle:style]; + submenu.title = item[@"title"]; + } + else { + submenu = [[GBSettingsViewController alloc] initWithStructure:item[@"submenu"] + title:item[@"title"] + style:style]; + } + if (_detailsNavigation) { + [_detailsNavigation setViewControllers:@[submenu] animated:false]; + } + else { + [self.navigationController pushViewController:submenu animated:true]; + } + return indexPath; + } + else if (item[@"type"] == typeRadio) { + if (item[@"setter"]) { + ((void(^)(id))item[@"setter"])(item[@"value"]); + } + else { + [[NSUserDefaults standardUserDefaults] setObject:item[@"value"] forKey:item[@"pref"]]; + } + [self.tableView reloadData]; + } + else if (item[@"type"] == typeBlock) { + if (((bool(^)(GBSettingsViewController *))item[@"block"])(self)) { + return indexPath; + } + } + return nil; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSDictionary *item = [self itemForIndexPath:indexPath]; + if (item[@"type"] == typeSeparator) { + return 8; + } + if (item[@"type"] == typeSlider || + item[@"type"] == typeLightTemp) { + return 63; + } + return [super tableView:tableView heightForRowAtIndexPath:indexPath]; + +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [self.tableView reloadData]; +} + +- (void)preloadThemePreviews +{ + /* These take some time to render, preload them when loading the root controller */ + _themes = [GBThemesViewController themes]; + double time = 0; + for (NSArray *section in _themes) { + for (GBTheme *theme in section) { + /* Sadly they can't be safely rendered outside the main thread, but we can + queue each of them individually to not block the main quote for too long. */ + time += 0.1; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, time * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [theme verticalPreview]; + [theme horizontalPreview]; + }); + } + } +} + ++ (GBTheme *)themeNamed:(NSString *)name +{ + NSArray *themes = [GBThemesViewController themes]; + for (NSArray *section in themes) { + for (GBTheme *theme in section) { + if ([theme.name isEqualToString:name]) { + return theme; + } + } + } + + return [themes.firstObject firstObject]; +} + +@end diff --git a/iOS/GBSlotButton.h b/iOS/GBSlotButton.h new file mode 100644 index 0000000..f3e90f0 --- /dev/null +++ b/iOS/GBSlotButton.h @@ -0,0 +1,8 @@ +#import + +@interface GBSlotButton : UIButton ++ (instancetype)buttonWithLabelText:(NSString *)label; +@property (readonly) UILabel *label; +@property (readonly) UILabel *slotSubtitleLabel; +@property (nonatomic, getter=isShowingMenu) bool showingMenu; +@end diff --git a/iOS/GBSlotButton.m b/iOS/GBSlotButton.m new file mode 100644 index 0000000..dc0ba94 --- /dev/null +++ b/iOS/GBSlotButton.m @@ -0,0 +1,150 @@ +#import "GBSlotButton.h" + +@implementation GBSlotButton +{ + UIImageView *_imageView; +} + ++ (instancetype)buttonWithLabelText:(NSString *)labelText +{ + GBSlotButton *ret = [self buttonWithType:UIButtonTypeCustom]; + if (!ret) return nil; + ret.frame = CGRectMake(0, 0, 0x100, 0x100); + + ret->_slotSubtitleLabel = [[UILabel alloc] init]; + ret->_slotSubtitleLabel.text = @"Empty"; + ret->_slotSubtitleLabel.font = [UIFont systemFontOfSize:UIFont.smallSystemFontSize]; + if (@available(iOS 13.0, *)) { + ret->_slotSubtitleLabel.textColor = [UIColor secondaryLabelColor]; + } + else { + ret->_slotSubtitleLabel.textColor = [UIColor systemGrayColor]; + } + [ret->_slotSubtitleLabel sizeToFit]; + ret->_slotSubtitleLabel.textAlignment = NSTextAlignmentCenter; + CGRect slotSubtitleLabelRect = ret->_slotSubtitleLabel.frame; + slotSubtitleLabelRect.size.width = 0x100; + slotSubtitleLabelRect.origin.y = 0x100 - slotSubtitleLabelRect.size.height - 8; + ret->_slotSubtitleLabel.frame = slotSubtitleLabelRect; + ret->_slotSubtitleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + [ret addSubview:ret->_slotSubtitleLabel]; + + ret->_label = [[UILabel alloc] init]; + ret->_label.text = labelText; + [ret->_label sizeToFit]; + ret->_label.textAlignment = NSTextAlignmentCenter; + CGRect labelRect = ret->_label.frame; + labelRect.size.width = 0x100; + labelRect.origin.y = slotSubtitleLabelRect.origin.y - labelRect.size.height - 4; + ret->_label.frame = labelRect; + ret->_label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + [ret addSubview:ret->_label]; + + ret.backgroundColor = [UIColor clearColor]; + + ret->_imageView = [[UIImageView alloc] initWithImage:nil]; + ret.imageView.layer.cornerRadius = 6; + ret.imageView.layer.masksToBounds = true; + if (@available(iOS 13.0, *)) { + ret.imageView.layer.borderColor = [UIColor tertiaryLabelColor] .CGColor; + } + else { + ret.imageView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.5].CGColor; + } + ret.imageView.layer.borderWidth = 1; + ret.imageView.layer.backgroundColor = [UIColor whiteColor].CGColor; + [ret addSubview:ret.imageView]; + + return ret; +} + +- (UIImageView *)imageView +{ + return _imageView; +} + +- (void)setHighlighted:(BOOL)highlighted +{ + if (_showingMenu) return; + if (highlighted == self.isHighlighted) return; + [super setHighlighted:highlighted]; + + [UIView animateWithDuration:0.25 animations:^{ + if (highlighted) { + if (@available(iOS 13.0, *)) { + self.backgroundColor = [UIColor tertiarySystemFillColor]; + } + else { + self.backgroundColor = [UIColor colorWithRed:118 / 255.0 + green:118 / 255.0 + blue:128 / 255.0 + alpha:0.12]; + } + self.imageView.layer.opacity = 11 / 12.0; + } + else { + self.backgroundColor = [UIColor clearColor]; + self.imageView.layer.opacity = 1; + } + }]; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + CGRect screenshotRect = self.bounds; + screenshotRect.size.width -= 8; + screenshotRect.origin.x += 4; + screenshotRect.size.height = _label.frame.origin.y - 16; + screenshotRect.origin.y += 8; + + double scale = [UIApplication sharedApplication].keyWindow.screen.scale; + double nativeWidth = 160.0 / scale; + double nativeHeight = 144.0 / scale; + + if (screenshotRect.size.width > nativeWidth && + screenshotRect.size.height > nativeHeight) { + self.imageView.layer.magnificationFilter = kCAFilterNearest; + double newWidth = floor(screenshotRect.size.width / nativeWidth) * nativeWidth; + screenshotRect.origin.x += (screenshotRect.size.width - newWidth) / 2; + screenshotRect.size.width = newWidth; + + double newHeight = floor(screenshotRect.size.height / nativeHeight) * nativeHeight; + screenshotRect.origin.y += (screenshotRect.size.height - newHeight) / 2; + screenshotRect.size.height = newHeight; + } + else { + self.imageView.layer.magnificationFilter = kCAFilterLinear; + } + + double aspect = screenshotRect.size.width / screenshotRect.size.height; + if (aspect > 160.0 / 144.0) { + // Too wide + double newWidth = screenshotRect.size.height / 144 * 160; + screenshotRect.origin.x += (screenshotRect.size.width - newWidth) / 2; + screenshotRect.size.width = newWidth; + } + else { + // Too narrow + double newHeight = screenshotRect.size.width / 160 * 144; + screenshotRect.origin.y += (screenshotRect.size.height - newHeight) / 2; + screenshotRect.size.height = newHeight; + } + screenshotRect.origin.x = round(screenshotRect.origin.x); + screenshotRect.origin.y = round(screenshotRect.origin.y); + self.imageView.frame = screenshotRect; +} + +- (void)setShowingMenu:(bool)showingMenu +{ + if (showingMenu) { + self.highlighted = true; + _showingMenu = true; + } + else { + _showingMenu = false; + self.highlighted = false; + } +} + +@end diff --git a/iOS/GBStatesViewController.h b/iOS/GBStatesViewController.h new file mode 100644 index 0000000..c32d914 --- /dev/null +++ b/iOS/GBStatesViewController.h @@ -0,0 +1,5 @@ +#import + +@interface GBStatesViewController : UIViewController + +@end diff --git a/iOS/GBStatesViewController.m b/iOS/GBStatesViewController.m new file mode 100644 index 0000000..1576251 --- /dev/null +++ b/iOS/GBStatesViewController.m @@ -0,0 +1,120 @@ +#import "GBStatesViewController.h" +#import "GBSlotButton.h" +#import "GBROMManager.h" +#import "GBViewController.h" + +@implementation GBStatesViewController + +- (void)updateSlotView:(GBSlotButton *)view +{ + NSString *stateFile = [[GBROMManager sharedManager] stateFile:view.tag]; + if ([[NSFileManager defaultManager] fileExistsAtPath:stateFile]) { + NSDate *date = [[[NSFileManager defaultManager] attributesOfItemAtPath:stateFile error:nil] fileModificationDate]; + if (@available(iOS 13.0, *)) { + if ((uint64_t)(date.timeIntervalSince1970) == (uint64_t)([NSDate now].timeIntervalSince1970)) { + view.slotSubtitleLabel.text = @"Just now"; + } + else { + NSRelativeDateTimeFormatter *formatter = [[NSRelativeDateTimeFormatter alloc] init]; + view.slotSubtitleLabel.text = [formatter localizedStringForDate:date relativeToDate:[NSDate now]]; + } + } + else { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.timeStyle = NSDateFormatterShortStyle; + formatter.dateStyle = NSDateFormatterShortStyle; + formatter.doesRelativeDateFormatting = true; + view.slotSubtitleLabel.text = [formatter stringFromDate:date]; + } + + view.imageView.image = [UIImage imageWithContentsOfFile:[stateFile stringByAppendingPathExtension:@"png"]]; + } + else { + view.slotSubtitleLabel.text = @"Empty"; + view.imageView.image = nil; + } +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.view.bounds = CGRectMake(0, 0, 0x300, 0x300); + UIView *root = self.view; + for (unsigned i = 0; i < 9; i++) { + unsigned x = i % 3; + unsigned y = i / 3; + GBSlotButton *slotView = [GBSlotButton buttonWithLabelText:[NSString stringWithFormat:@"Slot %u", i + 1]]; + + slotView.frame = CGRectMake(0x100 * x, + 0x100 * y, + 0x100, + 0x100); + + slotView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | + UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleRightMargin | + UIViewAutoresizingFlexibleTopMargin | + UIViewAutoresizingFlexibleHeight | + UIViewAutoresizingFlexibleBottomMargin; + + slotView.tag = i + 1; + [self updateSlotView:slotView]; + [slotView addTarget:self + action:@selector(slotSelected:) + forControlEvents:UIControlEventTouchUpInside]; + [root addSubview:slotView]; + } + self.edgesForExtendedLayout = 0; +} + +- (void)slotSelected:(GBSlotButton *)slot +{ + + UIAlertController *controller = [UIAlertController alertControllerWithTitle:slot.label.text + message:nil + preferredStyle:UIAlertControllerStyleActionSheet]; + + NSString *stateFile = [[GBROMManager sharedManager] stateFile:slot.tag]; + GBViewController *delegate = (typeof(delegate))[UIApplication sharedApplication].delegate; + + void (^saveState)(UIAlertAction *action) = ^(UIAlertAction *action) { + [delegate saveStateToFile:stateFile]; + [self updateSlotView:slot]; + [self.presentingViewController dismissViewControllerAnimated:true completion:nil]; + slot.showingMenu = false; + }; + + if (![[NSFileManager defaultManager] fileExistsAtPath:stateFile]) { + saveState(nil); + return; + } + + [controller addAction:[UIAlertAction actionWithTitle:@"Save state" + style:UIAlertActionStyleDefault + handler:saveState]]; + + [controller addAction:[UIAlertAction actionWithTitle:@"Load state" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [delegate loadStateFromFile:stateFile]; + [self updateSlotView:slot]; + [self.presentingViewController dismissViewControllerAnimated:true completion:nil]; + slot.showingMenu = false; + }]]; + + [controller addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + slot.showingMenu = false; + }]]; + + slot.showingMenu = true; + controller.popoverPresentationController.sourceView = slot; + [self presentViewController:controller animated:true completion:nil]; +} + +- (NSString *)title +{ + return @"Save States"; +} +@end diff --git a/iOS/GBTemperatureSlider.h b/iOS/GBTemperatureSlider.h new file mode 100644 index 0000000..ccf8d23 --- /dev/null +++ b/iOS/GBTemperatureSlider.h @@ -0,0 +1,5 @@ +#import + +@interface GBTemperatureSlider : UISlider + +@end diff --git a/iOS/GBTemperatureSlider.m b/iOS/GBTemperatureSlider.m new file mode 100644 index 0000000..8e1b68b --- /dev/null +++ b/iOS/GBTemperatureSlider.m @@ -0,0 +1,80 @@ +#import "GBTemperatureSlider.h" + +static inline void temperature_tint(double temperature, double *r, double *g, double *b) +{ + if (temperature >= 0) { + *r = 1; + *g = pow(1 - temperature, 0.375); + if (temperature >= 0.75) { + *b = 0; + } + else { + *b = sqrt(0.75 - temperature) / sqrt(0.75); + } + } + else { + *b = 1; + double squared = pow(temperature, 2); + *g = 0.125 * squared + 0.3 * temperature + 1.0; + *r = 0.21875 * squared + 0.5 * temperature + 1.0; + } +} + +@implementation GBTemperatureSlider + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + [self addTarget:self action:@selector(valueChanged) forControlEvents:UIControlEventValueChanged]; + return self; +} + +- (void)updateTint +{ + double r, g, b; + temperature_tint(self.value, &r, &g, &b); + self.thumbTintColor = [UIColor colorWithRed:r green:g blue:b alpha:1.0]; +} + +- (void)setValue:(float)value +{ + [super setValue:value]; + [self updateTint]; +} + +- (void)valueChanged +{ + if (fabsf(self.value) < 0.05 && self.value != 0) { + self.value = 0; + } + else { + [self updateTint]; + } +} + +-(UIImage *)maximumTrackImageForState:(UIControlState)state +{ + return [[UIImage alloc] init]; +} + + +-(UIImage *)minimumTrackImageForState:(UIControlState)state +{ + return [[UIImage alloc] init]; +} + +- (void)drawRect:(CGRect)rect +{ + CGSize size = self.bounds.size; + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2, round(size.height / 2 - 1.5), size.width - 4, 3) cornerRadius:4]; + [path appendPath:[UIBezierPath bezierPathWithRoundedRect:CGRectMake(round(size.width / 2 - 1.5), 12, 3, size.height - 24) cornerRadius:4]]; + [[UIColor colorWithRed:120 / 255.0 + green:120 / 255.0 + blue:130 / 255.0 + alpha:70 / 255.0] set]; + [path fill]; + + [super drawRect:rect]; +} + +@end diff --git a/iOS/GBTheme.h b/iOS/GBTheme.h new file mode 100644 index 0000000..e35ad59 --- /dev/null +++ b/iOS/GBTheme.h @@ -0,0 +1,26 @@ +#import + +@interface GBTheme : NSObject +@property (readonly, direct) UIColor *brandColor; +@property (readonly, direct) UIColor *backgroundGradientTop; +@property (readonly, direct) UIColor *backgroundGradientBottom; +@property (readonly, direct) UIColor *bezelsGradientTop; +@property (readonly, direct) UIColor *bezelsGradientBottom; + + +@property (readonly, direct) NSString *name; + +@property (readonly, direct) bool renderingPreview; // Kind of a hack + +@property (readonly, direct) UIImage *horizontalPreview; +@property (readonly, direct) UIImage *verticalPreview; + +@property (readonly, direct) bool isDark; + +- (instancetype)initDefaultTheme __attribute__((objc_direct)); +- (instancetype)initDarkTheme __attribute__((objc_direct)); + + +- (UIImage *)imageNamed:(NSString *)name __attribute__((objc_direct)); + +@end diff --git a/iOS/GBTheme.m b/iOS/GBTheme.m new file mode 100644 index 0000000..b015454 --- /dev/null +++ b/iOS/GBTheme.m @@ -0,0 +1,226 @@ +#import "GBTheme.h" +#import "GBVerticalLayout.h" +#import "GBHorizontalLayout.h" +#import "GBBackgroundView.h" + +@interface GBLazyObject : NSProxy +@end + +@implementation GBLazyObject +{ + id _target; + id (^_constructor)(void); +} + + +- (instancetype)initWithConstructor:(id (^)(void))constructor +{ + _constructor = constructor; + return self; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel +{ + if (GB_likely(!_target)) { + _target = _constructor(); + _constructor = nil; + } + return [_target methodSignatureForSelector:sel]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + if (GB_likely(!_target)) { + _target = _constructor(); + _constructor = nil; + } + invocation.target = _target; + [invocation invoke]; +} + +- (instancetype)self +{ + if (GB_likely(!_target)) { + _target = _constructor(); + _constructor = nil; + } + return _target; +} + +@end + +#define MakeColor(r, g, b) [UIColor colorWithRed:(r) / 255.0 green:(g) / 255.0 blue:(b) / 255.0 alpha:1.0] + +__attribute__((objc_direct_members)) +@implementation GBTheme +{ + NSDictionary *_imageOverrides; + UIImage *_horizontalPreview; + UIImage *_verticalPreview; +} + + +// Assumes the image has a purple hue ++ (UIImage *)_recolorImage:(UIImage *)image withColor:(UIColor *)color +{ + double scale = image.scale; + + image = [UIImage imageWithCGImage:image.CGImage scale:1.0 orientation:UIImageOrientationUp]; + + CIImage *ciImage = [CIImage imageWithCGImage:image.CGImage]; + CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix"]; + double r, g, b; + [color getRed:&r green:&g blue:&b alpha:NULL]; + + [filter setDefaults]; + [filter setValue:ciImage forKey:@"inputImage"]; + + [filter setValue:[CIVector vectorWithX:r * 1.34 Y:1 - r Z:0 W:0] forKey:@"inputRVector"]; + [filter setValue:[CIVector vectorWithX:g * 1.34 Y:1 - g Z:0 W:0] forKey:@"inputGVector"]; + [filter setValue:[CIVector vectorWithX:b * 1.34 Y:1 - b Z:0 W:0] forKey:@"inputBVector"]; + + [filter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:1] forKey:@"inputAVector"]; + + + CIContext *context = [CIContext context]; + CGImageRef cgImage = [context createCGImage:filter.outputImage fromRect:filter.outputImage.extent]; + image = [UIImage imageWithCGImage:cgImage scale:scale orientation:0]; + CGImageRelease(cgImage); + + return image; +} + ++ (UIImage *)recolorImage:(UIImage *)image withColor:(UIColor *)color +{ + return (id)[[GBLazyObject alloc] initWithConstructor:^id{ + return [self _recolorImage:image withColor:color]; + }]; +} + +- (instancetype)initDefaultTheme +{ + self = [super init]; + + _brandColor = [UIColor colorWithRed:0 / 255.0 green:70 / 255.0 blue:141 / 255.0 alpha:1.0]; + _backgroundGradientTop = [UIColor colorWithRed:192 / 255.0 green:195 / 255.0 blue:199 / 255.0 alpha:1.0]; + _backgroundGradientBottom = [UIColor colorWithRed:174 / 255.0 green:176 / 255.0 blue:180 / 255.0 alpha:1.0]; + + _bezelsGradientTop = [UIColor colorWithWhite:53 / 255.0 alpha:1.0]; + _bezelsGradientBottom = [UIColor colorWithWhite:45 / 255.0 alpha:1.0]; + + _name = @"SameBoy"; + + return self; +} + +- (void)setupBackgroundWithColor:(uint32_t)color +{ + uint8_t r = color >> 16; + uint8_t g = color >> 8; + uint8_t b = color; + + _backgroundGradientTop = MakeColor(r, g, b); + _backgroundGradientBottom = [UIColor colorWithRed:pow(r / 255.0, 1.125) green:pow(g / 255.0, 1.125) blue:pow(b / 255.0, 1.125) alpha:1.0]; +} + +- (void)setupButtonsWithColor:(UIColor *)color +{ + _imageOverrides = @{ + @"button": [GBTheme recolorImage:[UIImage imageNamed:@"button"] withColor:color], + @"buttonPressed": [GBTheme recolorImage:[UIImage imageNamed:@"buttonPressed"] withColor:color], + + @"dpad": [GBTheme recolorImage:[UIImage imageNamed:@"dpad-tint"] withColor:color], + @"swipepad": [GBTheme recolorImage:[UIImage imageNamed:@"swipepad-tint"] withColor:color], + + @"button2": [GBTheme recolorImage:[UIImage imageNamed:@"button2-tint"] withColor:color], + @"button2Pressed": [GBTheme recolorImage:[UIImage imageNamed:@"button2Pressed-tint"] withColor:color], + }; +} + +- (instancetype)initDarkTheme +{ + self = [super init]; + + [self setupBackgroundWithColor:0x181c23]; + + _brandColor = [UIColor colorWithRed:0 / 255.0 green:70 / 255.0 blue:141 / 255.0 alpha:1.0]; + _bezelsGradientTop = [UIColor colorWithWhite:53 / 255.0 alpha:1.0]; + _bezelsGradientBottom = [UIColor colorWithWhite:45 / 255.0 alpha:1.0]; + + [self setupButtonsWithColor:MakeColor(0x08, 0x0c, 0x12)]; + + _name = @"SameBoy Dark"; + + return self; +} + + +- (bool)isDark +{ + double r, g, b; + [_backgroundGradientTop getRed:&r green:&g blue:&b alpha:NULL]; + if (r > 0.25) return false; + if (g > 0.25) return false; + if (b > 0.25) return false; + return true; +} + +- (UIImage *)imageNamed:(NSString *)name +{ + UIImage *ret = _imageOverrides[name].self ?: [UIImage imageNamed:name]; + if (!ret) { + if ([name isEqual:@"buttonA"] || [name isEqual:@"buttonB"]) { + return [self imageNamed:@"button"]; + } + if ([name isEqual:@"buttonAPressed"] || [name isEqual:@"buttonBPressed"]) { + return [self imageNamed:@"buttonPressed"]; + } + } + return ret; +} + +- (UIImage *)horizontalPreview +{ + if (_horizontalPreview) return _horizontalPreview; + _renderingPreview = true; + GBLayout *layout = [[GBHorizontalLayout alloc] initWithTheme:self]; + _renderingPreview = false; + GBBackgroundView *view = [[GBBackgroundView alloc] initWithLayout:layout]; + [view enterPreviewMode:false]; + view.usesSwipePad = [[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipePad"]; + view.layout = layout; + view.bounds = CGRectMake(0, 0, + MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height), + MIN(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height)); + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){view.bounds.size.width / 8, + view.bounds.size.height / 8, + }]; + return _horizontalPreview = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { + CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1 / 8.0, 1 / 8.0); + [view.layer renderInContext:rendererContext.CGContext]; + }]; +} + +- (UIImage *)verticalPreview +{ + if (_verticalPreview) return _verticalPreview; + _renderingPreview = true; + GBLayout *layout = [[GBVerticalLayout alloc] initWithTheme:self]; + _renderingPreview = false; + GBBackgroundView *view = [[GBBackgroundView alloc] initWithLayout:layout]; + [view enterPreviewMode:false]; + view.usesSwipePad = [[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipePad"]; + view.layout = layout; + view.bounds = CGRectMake(0, 0, + MIN(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height), + MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height)); + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){view.bounds.size.width / 8, + view.bounds.size.height / 8, + }]; + return _verticalPreview = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { + CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1 / 8.0, 1 / 8.0); + [view.layer renderInContext:rendererContext.CGContext]; + }]; +} + +@end diff --git a/iOS/GBThemePreviewController.h b/iOS/GBThemePreviewController.h new file mode 100644 index 0000000..bac864e --- /dev/null +++ b/iOS/GBThemePreviewController.h @@ -0,0 +1,7 @@ +#import +#import "GBTheme.h" + +@interface GBThemePreviewController : UIViewController +- (instancetype)initWithTheme:(GBTheme *)theme; +@end + diff --git a/iOS/GBThemePreviewController.m b/iOS/GBThemePreviewController.m new file mode 100644 index 0000000..d1d99f4 --- /dev/null +++ b/iOS/GBThemePreviewController.m @@ -0,0 +1,116 @@ +#import "GBThemePreviewController.h" +#import "GBVerticalLayout.h" +#import "GBHorizontalLayout.h" +#import "GBBackgroundView.h" + +@implementation GBThemePreviewController +{ + GBHorizontalLayout *_horizontalLayout; + GBVerticalLayout *_verticalLayout; + GBBackgroundView *_backgroundView; +} + +- (instancetype)initWithTheme:(GBTheme *)theme +{ + self = [super init]; + _horizontalLayout = [[GBHorizontalLayout alloc] initWithTheme:theme]; + _verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme]; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + _backgroundView = [[GBBackgroundView alloc] initWithLayout:_verticalLayout]; + self.view.backgroundColor = _verticalLayout.theme.backgroundGradientBottom; + [self.view addSubview:_backgroundView]; + [_backgroundView enterPreviewMode:true]; + _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; + + [self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0]; + [self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(showPopup)]]; +} + +- (UIModalPresentationStyle)modalPresentationStyle +{ + return UIModalPresentationFullScreen; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration +{ + GBLayout *layout = _horizontalLayout; + if (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) { + layout = _verticalLayout; + } + _backgroundView.frame = [layout viewRectForOrientation:orientation]; + _backgroundView.layout = layout; +} + +- (void)showPopup +{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Apply “%@” as the current theme?", _verticalLayout.theme.name] + message:nil + preferredStyle:[UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad? + UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet]; + if (false) { + // No supporter-only themes outside the App Store release + } + else { + [alert addAction:[UIAlertAction actionWithTitle:@"Apply Theme" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [[NSUserDefaults standardUserDefaults] setObject:_verticalLayout.theme.name forKey:@"GBInterfaceTheme"]; + [[self presentingViewController] dismissViewControllerAnimated:true completion:nil]; + }]]; + } + [alert addAction:[UIAlertAction actionWithTitle:@"Exit Preview" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [[self presentingViewController] dismissViewControllerAnimated:true completion:nil]; + }]]; + [self presentViewController:alert animated:true completion:^{ + alert.view.superview.userInteractionEnabled = true; + [alert.view.superview addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(dismissPopup)]]; + for (UIView *view in alert.view.superview.subviews) { + if (view.backgroundColor) { + view.userInteractionEnabled = true; + [view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(dismissPopup)]]; + + } + } + }]; +} + +- (void)dismissViewController +{ + [self dismissViewControllerAnimated:true completion:nil]; +} + +- (void)dismissPopup +{ + [self dismissViewControllerAnimated:true completion:nil]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + if (@available(iOS 13.0, *)) { + return _verticalLayout.theme.isDark? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent; + } + return _verticalLayout.theme.isDark? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { + return UIInterfaceOrientationMaskAll; + } + if (MAX([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width) <= 568) { + return UIInterfaceOrientationMaskLandscape; + } + return UIInterfaceOrientationMaskAllButUpsideDown; +} + +@end diff --git a/iOS/GBThemesViewController.h b/iOS/GBThemesViewController.h new file mode 100644 index 0000000..348597e --- /dev/null +++ b/iOS/GBThemesViewController.h @@ -0,0 +1,7 @@ +#import +#import "GBTheme.h" + +@interface GBThemesViewController : UITableViewController ++ (NSArray *> *)themes; +@end + diff --git a/iOS/GBThemesViewController.m b/iOS/GBThemesViewController.m new file mode 100644 index 0000000..5d0e223 --- /dev/null +++ b/iOS/GBThemesViewController.m @@ -0,0 +1,104 @@ +#import "GBThemesViewController.h" +#import "GBThemePreviewController.h" +#import "GBTheme.h" + +@interface GBThemesViewController () + +@end + +@implementation GBThemesViewController +{ + NSArray *> *_themes; +} + ++ (NSArray *> *)themes +{ + static __weak NSArray *> *cache = nil; + if (cache) return cache; + id ret = @[ + @[ + [[GBTheme alloc] initDefaultTheme], + [[GBTheme alloc] initDarkTheme], + ], + ]; + cache = ret; + return ret; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + _themes = [[self class] themes]; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return _themes.count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return _themes[section].count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 60; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + GBTheme *theme = _themes[indexPath.section][indexPath.row]; + cell.textLabel.text = theme.name; + + cell.accessoryType = [[[NSUserDefaults standardUserDefaults] stringForKey:@"GBInterfaceTheme"] isEqual:theme.name]? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone; + bool horizontal = self.interfaceOrientation >= UIInterfaceOrientationLandscapeRight; + UIImage *preview = horizontal? [theme horizontalPreview] : [theme verticalPreview]; + UIGraphicsBeginImageContextWithOptions((CGSize){60, 60}, false, self.view.window.screen.scale); + unsigned width = 60; + unsigned height = 56; + if (horizontal) { + height = round(preview.size.height / preview.size.width * 60); + } + else { + width = round(preview.size.width / preview.size.height * 56); + } + UIBezierPath *mask = [UIBezierPath bezierPathWithRoundedRect:CGRectMake((60 - width) / 2, (60 - height) / 2, width, height) cornerRadius:4]; + [mask addClip]; + [preview drawInRect:mask.bounds]; + if (@available(iOS 13.0, *)) { + [[UIColor tertiaryLabelColor] set]; + } + else { + [[UIColor colorWithWhite:0 alpha:0.5] set]; + } + [mask stroke]; + cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + + return cell; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +{ + [self.tableView reloadData]; + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + GBTheme *theme = _themes[indexPath.section][indexPath.row]; + GBThemePreviewController *preview = [[GBThemePreviewController alloc] initWithTheme:theme]; + [self presentViewController:preview animated:true completion:nil]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self.tableView reloadData]; +} + +@end diff --git a/iOS/GBVerticalLayout.h b/iOS/GBVerticalLayout.h new file mode 100644 index 0000000..a5720ea --- /dev/null +++ b/iOS/GBVerticalLayout.h @@ -0,0 +1,5 @@ +#import "GBLayout.h" + +@interface GBVerticalLayout : GBLayout + +@end diff --git a/iOS/GBVerticalLayout.m b/iOS/GBVerticalLayout.m new file mode 100644 index 0000000..44797d4 --- /dev/null +++ b/iOS/GBVerticalLayout.m @@ -0,0 +1,86 @@ +#define GBLayoutInternal +#import "GBVerticalLayout.h" + +@implementation GBVerticalLayout + +- (instancetype)initWithTheme:(GBTheme *)theme +{ + self = [super initWithTheme:theme]; + if (!self) return nil; + + CGSize resolution = self.resolution; + + CGRect screenRect = {0,}; + screenRect.size.width = self.hasFractionalPixels? resolution.width : floor(resolution.width / 160) * 160; + screenRect.size.height = screenRect.size.width / 160 * 144; + + double screenBorderWidth = screenRect.size.width / 40; + screenRect.origin.x = (resolution.width - screenRect.size.width) / 2; + screenRect.origin.y = self.minY + screenBorderWidth * 2; + self.screenRect = screenRect; + + double controlAreaStart = screenRect.origin.y + screenRect.size.height + screenBorderWidth * 2; + + self.selectLocation = (CGPoint){ + MIN(resolution.width / 4, 120 * self.factor), + MIN(resolution.height - 80 * self.factor, (resolution.height - controlAreaStart) * 0.75 + controlAreaStart) + }; + + self.startLocation = (CGPoint){ + resolution.width - self.selectLocation.x, + self.selectLocation.y + }; + + double buttonRadius = 36 * self.factor; + CGSize buttonsDelta = [self buttonDeltaForMaxHorizontalDistance:resolution.width / 2 - buttonRadius * 2 - screenBorderWidth * 2]; + + self.dpadLocation = (CGPoint) { + self.selectLocation.x, + self.selectLocation.y - 140 * self.factor + }; + + CGPoint buttonsCenter = { + resolution.width - self.dpadLocation.x, + self.dpadLocation.y, + }; + + self.aLocation = (CGPoint) { + round(buttonsCenter.x + buttonsDelta.width / 2), + round(buttonsCenter.y - buttonsDelta.height / 2) + }; + + self.bLocation = (CGPoint) { + round(buttonsCenter.x - buttonsDelta.width / 2), + round(buttonsCenter.y + buttonsDelta.height / 2) + }; + + self.abComboLocation = buttonsCenter; + + double controlsTop = self.dpadLocation.y - 80 * self.factor; + double middleSpace = self.bLocation.x - buttonRadius - (self.dpadLocation.x + 80 * self.factor); + + if (theme.renderingPreview) { + UIGraphicsBeginImageContextWithOptions((CGSize){resolution.width / 8, resolution.height / 8}, true, 1); + CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1 / 8.0, 1 / 8.0); + } + else { + UIGraphicsBeginImageContextWithOptions(resolution, true, 1); + } + [self drawBackground]; + [self drawScreenBezels]; + + [self drawThemedLabelsWithBlock:^{ + if (controlsTop - controlAreaStart > 24 * self.factor + screenBorderWidth * 2 || + middleSpace > 160 * self.factor) { + [self drawLogoInVerticalRange:(NSRange){controlAreaStart + screenBorderWidth, 24 * self.factor}]; + } + + [self drawLabels]; + }]; + + self.background = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return self; +} + +@end diff --git a/iOS/GBView.h b/iOS/GBView.h new file mode 100644 index 0000000..f30cf5a --- /dev/null +++ b/iOS/GBView.h @@ -0,0 +1,5 @@ +#import "GBViewBase.h" + +@interface GBView : GBViewBase + +@end diff --git a/iOS/GBView.m b/iOS/GBView.m new file mode 100644 index 0000000..40e08b3 --- /dev/null +++ b/iOS/GBView.m @@ -0,0 +1,12 @@ +#import "GBView.h" + +@implementation GBView +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + [self createInternalView]; + [self addSubview:self.internalView]; + self.internalView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + return self; +} +@end diff --git a/iOS/GBViewController.h b/iOS/GBViewController.h new file mode 100644 index 0000000..3dd1b85 --- /dev/null +++ b/iOS/GBViewController.h @@ -0,0 +1,28 @@ +#import +#import +#import + +typedef enum { + GBRunModeNormal, + GBRunModeTurbo, + GBRunModeRewind, + GBRunModePaused, + GBRunModeUnderclock, +} GBRunMode; + +@interface GBViewController : UIViewController +@property (nonatomic, strong) UIWindow *window; +- (void)reset; +- (void)openLibrary; +- (void)start; +- (void)stop; +- (void)changeModel; +- (void)openStates; +- (void)openSettings; +- (void)showAbout; +- (void)saveStateToFile:(NSString *)file; +- (bool)loadStateFromFile:(NSString *)file; +@property (nonatomic) GBRunMode runMode; +@end diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m new file mode 100644 index 0000000..ef4819b --- /dev/null +++ b/iOS/GBViewController.m @@ -0,0 +1,1421 @@ +#import "GBViewController.h" +#import "GBHorizontalLayout.h" +#import "GBVerticalLayout.h" +#import "GBViewMetal.h" +#import "GBAudioClient.h" +#import "GBROMManager.h" +#import "GBLoadROMTableViewController.h" +#import "GBBackgroundView.h" +#import "GBHapticManager.h" +#import "GBMenuViewController.h" +#import "GBOptionViewController.h" +#import "GBAboutController.h" +#import "GBSettingsViewController.h" +#import "GBStatesViewController.h" +#import "GCExtendedGamepad+AllElements.h" +#import +#import + +@implementation GBViewController +{ + GB_gameboy_t _gb; + GBView *_gbView; + + volatile bool _running; + volatile bool _stopping; + bool _rewind; + bool _rewindOver; + bool _romLoaded; + bool _swappingROM; + + UIInterfaceOrientation _orientation; + GBHorizontalLayout *_horizontalLayout; + GBVerticalLayout *_verticalLayout; + GBBackgroundView *_backgroundView; + + NSCondition *_audioLock; + GB_sample_t *_audioBuffer; + size_t _audioBufferSize; + size_t _audioBufferPosition; + size_t _audioBufferNeeded; + GBAudioClient *_audioClient; + + NSMutableSet *_defaultsObservers; + GB_palette_t _palette; + CMMotionManager *_motionManager; + + CVImageBufferRef _cameraImage; + AVCaptureSession *_cameraSession; + AVCaptureConnection *_cameraConnection; + AVCaptureVideoDataOutput *_cameraOutput; + bool _cameraNeedsUpdate; + NSTimer *_disableCameraTimer; + AVCaptureDevicePosition _cameraPosition; + UIButton *_cameraPositionButton; + UIButton *_changeCameraButton; + NSArray *_allCaptureDevices; + NSArray *_backCaptureDevices; + AVCaptureDevice *_selectedBackCaptureDevice; + + __weak GCController *_lastController; + + dispatch_queue_t _cameraQueue; + + bool _runModeFromController; +} + +static void loadBootROM(GB_gameboy_t *gb, GB_boot_rom_t type) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self loadBootROM:type]; +} + +static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self vblankWithType:type]; +} + +static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +{ + static NSString *buffer = @""; + buffer = [buffer stringByAppendingString:@(string)]; + if ([buffer containsString:@"\n"]) { + NSLog(@"%@", buffer); + buffer = @""; + } +} + +static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + return (r << 0) | (g << 8) | (b << 16) | 0xFF000000; +} + +static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self gotNewSample:sample]; +} + +static void cameraRequestUpdate(GB_gameboy_t *gb) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self cameraRequestUpdate]; +} + +static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + return [self cameraGetPixelAtX:x andY:y]; +} + + +static void rumbleCallback(GB_gameboy_t *gb, double amp) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self rumbleChanged:amp]; +} + +- (void)initGameBoy +{ + GB_gameboy_t *gb = &_gb; + GB_init(gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]); + GB_set_user_data(gb, (__bridge void *)(self)); + GB_set_boot_rom_load_callback(gb, (GB_boot_rom_load_callback_t)loadBootROM); + GB_set_vblank_callback(gb, (GB_vblank_callback_t) vblank); + GB_set_log_callback(gb, (GB_log_callback_t) consoleLog); + GB_set_camera_get_pixel_callback(gb, cameraGetPixel); + GB_set_camera_update_request_callback(gb, cameraRequestUpdate); + [self addDefaultObserver:^(id newValue) { + GB_set_color_correction_mode(gb, (GB_color_correction_mode_t)[newValue integerValue]); + } forKey:@"GBColorCorrection"]; + [self addDefaultObserver:^(id newValue) { + GB_set_light_temperature(gb, [newValue doubleValue]); + } forKey:@"GBLightTemperature"]; + GB_set_border_mode(gb, GB_BORDER_NEVER); + __weak typeof(self) weakSelf = self; + [self addDefaultObserver:^(id newValue) { + [weakSelf updatePalette]; + } forKey:@"GBCurrentTheme"]; + GB_set_rgb_encode_callback(gb, rgbEncode); + [self addDefaultObserver:^(id newValue) { + GB_set_highpass_filter_mode(gb, (GB_highpass_mode_t)[newValue integerValue]); + } forKey:@"GBHighpassFilter"]; + [self addDefaultObserver:^(id newValue) { + GB_set_rtc_mode(gb, [newValue integerValue]); + } forKey:@"GBRTCMode"]; + GB_apu_set_sample_callback(gb, audioCallback); + GB_set_rumble_callback(gb, rumbleCallback); + [self addDefaultObserver:^(id newValue) { + GB_set_rumble_mode(gb, [newValue integerValue]); + } forKey:@"GBRumbleMode"]; + [self addDefaultObserver:^(id newValue) { + GB_set_interference_volume(gb, [newValue doubleValue]); + } forKey:@"GBInterferenceVolume"]; + [self addDefaultObserver:^(id newValue) { + GB_set_rewind_length(gb, [newValue unsignedIntValue]); + } forKey:@"GBRewindLength"]; + [self addDefaultObserver:^(id newValue) { + [[AVAudioSession sharedInstance] setCategory:[newValue isEqual:@"on"]? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient + mode:AVAudioSessionModeMeasurement // Reduces latency on BT + routeSharingPolicy:AVAudioSessionRouteSharingPolicyDefault + options:AVAudioSessionCategoryOptionAllowBluetoothA2DP | AVAudioSessionCategoryOptionAllowAirPlay + error:nil]; + } forKey:@"GBAudioMode"]; +} + +- (void)addDefaultObserver:(void(^)(id newValue))block forKey:(NSString *)key +{ + if (!_defaultsObservers) { + _defaultsObservers = [NSMutableSet set]; + } + block = [block copy]; + [_defaultsObservers addObject:block]; + [[NSUserDefaults standardUserDefaults] addObserver:self + forKeyPath:key + options:NSKeyValueObservingOptionNew + context:(void *)block]; + block([[NSUserDefaults standardUserDefaults] objectForKey:key]); +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + ((__bridge void(^)(id))context)(change[NSKeyValueChangeNewKey]); +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + _window = [[UIWindow alloc] init]; + _window.rootViewController = self; + [_window makeKeyAndVisible]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + [self addDefaultObserver:^(id newValue) { + GBTheme *theme = [GBSettingsViewController themeNamed:newValue]; + _horizontalLayout = [[GBHorizontalLayout alloc] initWithTheme:theme]; + _verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme]; + + [self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation + duration:0]; + [_backgroundView reloadThemeImages]; + + [self setNeedsStatusBarAppearanceUpdate]; + } forKey:@"GBInterfaceTheme"]; +#pragma clang diagnostic pop + + _backgroundView = [[GBBackgroundView alloc] initWithLayout:_verticalLayout]; + [_window addSubview:_backgroundView]; + self.view = _backgroundView; + + + [self initGameBoy]; + _gbView = _backgroundView.gbView; + _gbView.hidden = true; + _gbView.gb = &_gb; + [_gbView screenSizeChanged]; + + [self addDefaultObserver:^(id newValue) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil]; + } forKey:@"GBFilter"]; + + __weak GBView *gbview = _gbView; + [self addDefaultObserver:^(id newValue) { + gbview.frameBlendingMode = [newValue integerValue]; + } forKey:@"GBFrameBlendingMode"]; + + __weak GBBackgroundView *backgroundView = _backgroundView; + [self addDefaultObserver:^(id newValue) { + backgroundView.usesSwipePad = [newValue boolValue]; + } forKey:@"GBSwipeDpad"]; + + + [self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation + duration:0]; + + + _audioLock = [[NSCondition alloc] init]; + + [self loadROM]; + [[NSNotificationCenter defaultCenter] addObserverForName:@"GBROMChanged" + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + [self loadROM]; + [self start]; + }]; + + _motionManager = [[CMMotionManager alloc] init]; + _cameraPosition = AVCaptureDevicePositionBack; + _selectedBackCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; + + // Back camera setup + NSArray *deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera, + AVCaptureDeviceTypeBuiltInTelephotoCamera]; + if (@available(iOS 13.0, *)) { + // AVCaptureDeviceTypeBuiltInUltraWideCamera is only available in iOS 13+ + deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera, + AVCaptureDeviceTypeBuiltInUltraWideCamera, + AVCaptureDeviceTypeBuiltInTelephotoCamera]; + } + + // Use a discovery session to gather the capture devices (all back cameras as well as the front camera) + AVCaptureDeviceDiscoverySession *cameraDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]; + _allCaptureDevices = cameraDiscoverySession.devices; + + // Filter only the back cameras into a list used for switching between them + NSMutableArray *filteredBackCameras = [NSMutableArray array]; + for (AVCaptureDevice *device in _allCaptureDevices) { + if ([device position] == AVCaptureDevicePositionBack) { + [filteredBackCameras addObject:device]; + } + } + _backCaptureDevices = filteredBackCameras; + + UIEdgeInsets insets = self.window.safeAreaInsets; + _cameraPositionButton = [[UIButton alloc] initWithFrame:CGRectMake(insets.left + 8, + _backgroundView.bounds.size.height - 8 - insets.bottom - 32, + 32, + 32)]; + [self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation]; + if (@available(iOS 13.0, *)) { + [_cameraPositionButton setImage:[UIImage systemImageNamed:@"camera.rotate" + withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]] + forState:UIControlStateNormal]; + _cameraPositionButton.backgroundColor = [UIColor systemBackgroundColor]; + + // Configure the change camera button stacked on top of the camera position button + _changeCameraButton = [[UIButton alloc] initWithFrame:CGRectMake(insets.left + 8, + _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - 32 - 8, + 32, + 32)]; + [_changeCameraButton setImage:[UIImage systemImageNamed:@"camera.aperture" + withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]] + forState:UIControlStateNormal]; + _changeCameraButton.backgroundColor = [UIColor systemBackgroundColor]; + _changeCameraButton.layer.cornerRadius = 6; + _changeCameraButton.alpha = 0; + [_changeCameraButton addTarget:self + action:@selector(changeCamera) + forControlEvents:UIControlEventTouchUpInside]; + // Only show the change camera button if we have more than one back camera to swap between. + if ([_backCaptureDevices count] > 1) { + [_backgroundView addSubview:_changeCameraButton]; + } + } + else { + UIImage *rotateImage = [[UIImage imageNamed:@"CameraRotateTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [_cameraPositionButton setImage:rotateImage + forState:UIControlStateNormal]; + _cameraPositionButton.backgroundColor = [UIColor whiteColor]; + } + + _cameraPositionButton.layer.cornerRadius = 6; + _cameraPositionButton.alpha = 0; + [_cameraPositionButton addTarget:self + action:@selector(rotateCamera) + forControlEvents:UIControlEventTouchUpInside]; + + [_backgroundView addSubview:_cameraPositionButton]; + + _cameraQueue = dispatch_queue_create("SameBoy Camera Queue", NULL); + + [UNUserNotificationCenter currentNotificationCenter].delegate = self; + [self verifyEntitlements]; + + [self setControllerHandlers]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(setControllerHandlers) + name:GCControllerDidConnectNotification + object:nil]; + + return true; +} + + +- (void)setControllerHandlers +{ + for (GCController *controller in [GCController controllers]) { + __weak GCController *weakController = controller; + if (controller.extendedGamepad) { + [[controller.extendedGamepad elementsDictionary] enumerateKeysAndObjectsUsingBlock:^(NSNumber *usage, GCControllerElement *element, BOOL *stop) { + if ([element isKindOfClass:[GCControllerButtonInput class]]) { + [(GCControllerButtonInput *)element setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) { + [self controller:weakController buttonChanged:button usage:usage.unsignedIntValue]; + }]; + } + else if ([element isKindOfClass:[GCControllerDirectionPad class]]) { + [(GCControllerDirectionPad *)element setValueChangedHandler:^(GCControllerDirectionPad *dpad, float xValue, float yValue) { + [self controller:weakController axisChanged:dpad usage:usage.unsignedIntValue]; + }]; + } + }]; + + if (controller.motion) { + [controller.motion setValueChangedHandler:^(GCMotion *motion) { + [self controller:weakController motionChanged:motion]; + }]; + } + } + } +} + +- (void)updateLastController:(GCController *)controller +{ + if (_lastController == controller) return; + _lastController = controller; + [GBHapticManager sharedManager].controller = controller; +} + +- (void)controller:(GCController *)controller buttonChanged:(GCControllerButtonInput *)button usage:(GBControllerUsage)usage +{ + [self updateLastController:controller]; + + GBButton gbButton = [GBSettingsViewController controller:controller convertUsageToButton:usage]; + static const double analogThreshold = 0.0625; + switch (gbButton) { + case GBRight: + case GBLeft: + case GBUp: + case GBDown: + case GBA: + case GBB: + case GBSelect: + case GBStart: + GB_set_key_state(&_gb, (GB_key_t)gbButton, button.value > 0.25); + break; + case GBTurbo: + if (button.value > analogThreshold) { + [self setRunMode:GBRunModeTurbo ignoreDynamicSpeed:!button.isAnalog]; + if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + GB_set_clock_multiplier(&_gb, (button.value - analogThreshold) / (1 - analogThreshold) * 3 + 1); + } + _runModeFromController = true; + [_backgroundView fadeOverlayOut]; + } + else { + if (self.runMode == GBRunModeTurbo && _runModeFromController) { + [self setRunMode:GBRunModeNormal]; + _runModeFromController = false; + } + } + break; + case GBRewind: + if (button.value > analogThreshold) { + [self setRunMode:GBRunModeRewind ignoreDynamicSpeed:!button.isAnalog]; + if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + GB_set_clock_multiplier(&_gb, (button.value - analogThreshold) / (1 - analogThreshold) * 4); + } + _runModeFromController = true; + [_backgroundView fadeOverlayOut]; + } + else { + if (self.runMode == GBRunModeRewind && _runModeFromController) { + [self setRunMode:GBRunModeNormal]; + _runModeFromController = false; + } + } + break; + case GBUnderclock: + if (button.value > analogThreshold) { + [self setRunMode:GBRunModeUnderclock ignoreDynamicSpeed:!button.isAnalog]; + if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + GB_set_clock_multiplier(&_gb, 1 - ((button.value - analogThreshold) / (1 - analogThreshold) * 0.75)); + } + _runModeFromController = true; + [_backgroundView fadeOverlayOut]; + } + else { + if (self.runMode == GBRunModeUnderclock && _runModeFromController) { + [self setRunMode:GBRunModeNormal]; + _runModeFromController = false; + } + } + break; + default: break; + } +} + +- (void)controller:(GCController *)controller axisChanged:(GCControllerDirectionPad *)axis usage:(GBControllerUsage)usage +{ + [self updateLastController:controller]; + + GB_set_key_state(&_gb, GB_KEY_LEFT, axis.left.value > 0.5); + GB_set_key_state(&_gb, GB_KEY_RIGHT, axis.right.value > 0.5); + GB_set_key_state(&_gb, GB_KEY_UP, axis.up.value > 0.5); + GB_set_key_state(&_gb, GB_KEY_DOWN, axis.down.value > 0.5); +} + +- (void)controller:(GCController *)controller motionChanged:(GCMotion *)motion +{ + if (controller != _lastController) return; + GCAcceleration gravity = {0,}; + GCAcceleration userAccel = {0,}; + if (@available(iOS 14.0, *)) { + if (motion.hasGravityAndUserAcceleration) { + gravity = motion.gravity; + userAccel = motion.userAcceleration; + } + else { + gravity = motion.acceleration; + } + } + else { + gravity = motion.gravity; + userAccel = motion.userAcceleration; + } + GB_set_accelerometer_values(&_gb, -(gravity.x + userAccel.x), gravity.y + userAccel.y); +} + + +- (void)verifyEntitlements +{ + /* + Make sure SameBoy is properly signed. If the bundle identifier the Info.plist file does not match the bundle + identifier in the application-identifier entitlement, iOS will not allow SameBoy to open files. + */ + typedef void *xpc_object_t; + void *libxpc = dlopen("/usr/lib/system/libxpc.dylib", RTLD_NOW); + + extern xpc_object_t xpc_copy_entitlements_for_self$(void); + extern const char *xpc_dictionary_get_string$(xpc_object_t *object, const char *key); + + typeof(xpc_copy_entitlements_for_self$) *xpc_copy_entitlements_for_self = dlsym(libxpc, "xpc_copy_entitlements_for_self"); + typeof(xpc_dictionary_get_string$) *xpc_dictionary_get_string = dlsym(libxpc, "xpc_dictionary_get_string"); + + if (!xpc_copy_entitlements_for_self || !xpc_dictionary_get_string) return; + + xpc_object_t entitlements = xpc_copy_entitlements_for_self(); + if (!entitlements) return; + const char *_entIdentifier = xpc_dictionary_get_string(entitlements, "application-identifier"); + NSString *entIdentifier = _entIdentifier? @(_entIdentifier) : nil; + + const char *_teamIdentifier = xpc_dictionary_get_string(entitlements, "com.apple.developer.team-identifier"); + NSString *teamIdentifier = _teamIdentifier? @(_teamIdentifier) : nil; + + CFRelease(entitlements); + + if (!entIdentifier) { // No identifier. Installed using a jailbreak, we're fine. + return; + } + + + if (teamIdentifier && [entIdentifier hasPrefix:[teamIdentifier stringByAppendingString:@"."]]) { + entIdentifier = [entIdentifier substringFromIndex:teamIdentifier.length + 1]; + } + + NSString *plistIdentifier = [NSBundle mainBundle].bundleIdentifier; + + if (![entIdentifier isEqual:plistIdentifier]) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SameBoy is not properly signed and might not be able to open ROMs" + message:[NSString stringWithFormat:@"The bundle identifier in the Info.plist file (“%@”) does not match the one in the entitlements (“%@”)", plistIdentifier, entIdentifier] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; + } +} + +- (void)saveStateToFile:(NSString *)file +{ + GB_save_state(&_gb, file.fileSystemRepresentation); + NSData *data = [NSData dataWithBytes:_gbView.previousBuffer + length:GB_get_screen_width(&_gb) * + GB_get_screen_height(&_gb) * + sizeof(*_gbView.previousBuffer)]; + UIImage *screenshot = [self imageFromData:data width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb)]; + [UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false]; +} + +- (bool)loadStateFromFile:(NSString *)file +{ + [self stop]; + GB_model_t model; + if (!GB_get_state_model(file.fileSystemRepresentation, &model)) { + if (GB_get_model(&_gb) != model) { + GB_switch_model_and_reset(&_gb, model); + } + return GB_load_state(&_gb, file.fileSystemRepresentation) == 0; + } + return false; +} + +- (void)loadROM +{ + _swappingROM = true; + [self stop]; + GBROMManager *romManager = [GBROMManager sharedManager]; + if (romManager.romFile) { + // Todo: display errors and warnings + if ([romManager.romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) { + _romLoaded = GB_load_isx(&_gb, romManager.romFile.fileSystemRepresentation) == 0; + } + else { + _romLoaded = GB_load_rom(&_gb, romManager.romFile.fileSystemRepresentation) == 0; + } + if (_romLoaded) { + GB_reset(&_gb); + GB_load_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation); + if (![self loadStateFromFile:[GBROMManager sharedManager].autosaveStateFile]) { + // Newly played ROM, pick the best model + uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL); + + if ((rom[0x143] & 0x80)) { + if (!GB_is_cgb(&_gb)) { + GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]); + } + } + else if ((rom[0x146] == 3) && !GB_is_sgb(&_gb)) { + GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]); + } + } + } + GB_rewind_reset(&_gb); + } + else { + _romLoaded = false; + } + _gbView.hidden = !_romLoaded; + _swappingROM = false; +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + if (self.presentedViewController) return; + [self start]; +} + +-(void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion +{ + [self stop]; + [super presentViewController:viewControllerToPresent + animated:flag + completion:completion]; +} + +- (void)reset +{ + [self stop]; + GB_reset(&_gb); + [self start]; +} + +- (void)openLibrary +{ + UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBLoadROMTableViewController alloc] init]]; + UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStylePlain + target:self + action:@selector(dismissViewController)]; + [controller.visibleViewController.navigationItem setLeftBarButtonItem:close]; + [self presentViewController:controller + animated:true + completion:nil]; +} + +- (void)changeModel +{ + GBOptionViewController *controller = [[GBOptionViewController alloc] initWithHeader:@"Select a model to emulate"]; + controller.footer = @"Changing the emulated model will reset the emulator"; + + GB_model_t currentModel = GB_get_model(&_gb); + struct { + NSString *title; + NSString *settingKey; + bool checked; + } items[] = { + {@"Game Boy", @"GBDMGModel", currentModel < GB_MODEL_SGB}, + {@"Game Boy Pocket/Light", nil, currentModel == GB_MODEL_MGB}, + {@"Super Game Boy", @"GBSGBModel", GB_is_sgb(&_gb)}, + {@"Game Boy Color", @"GBCGBModel", GB_is_cgb(&_gb) && currentModel <= GB_MODEL_CGB_E}, + {@"Game Boy Advance", @"GBAGBModel", currentModel > GB_MODEL_CGB_E}, + }; + + for (unsigned i = 0; i < sizeof(items) / sizeof(items[0]); i++) { + GB_model_t model = GB_MODEL_MGB; + if (items[i].settingKey) { + model = [[NSUserDefaults standardUserDefaults] integerForKey:items[i].settingKey]; + } + [controller addOption:items[i].title withCheckmark:items[i].checked action:^{ + [self stop]; + GB_switch_model_and_reset(&_gb, model); + if (model > GB_MODEL_CGB_E && ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBShownGBAWarning"]) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SameBoy is not a Game Boy Advance Emulator" + message:@"SameBoy cannot play GBA games. Changing the model to Game Boy Advance lets you play Game Boy games as if on a Game Boy Advance in Game Boy Color mode." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + [self start]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBShownGBAWarning"]; + }]]; + [self presentViewController:alert animated:true completion:nil]; + } + else { + [self start]; + } + }]; + } + controller.title = @"Change Model"; + + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller]; + UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStylePlain + target:self + action:@selector(dismissViewController)]; + [navController.visibleViewController.navigationItem setLeftBarButtonItem:close]; + + [self presentViewController:navController animated:true completion:nil]; +} + +- (void)openStates +{ + UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBStatesViewController alloc] init]]; + UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleProminent]; + UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; + effectView.frame = controller.view.bounds; + effectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [controller.view insertSubview:effectView atIndex:0]; + UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStylePlain + target:self + action:@selector(dismissViewController)]; + [controller.visibleViewController.navigationItem setLeftBarButtonItem:close]; + [self presentViewController:controller + animated:true + completion:nil]; +} + +- (void)openSettings +{ + UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStylePlain + target:self + action:@selector(dismissViewController)]; + [self presentViewController:[GBSettingsViewController settingsViewControllerWithLeftButton:close] + animated:true + completion:nil]; +} + +- (void)showAbout +{ + [self presentViewController:[[GBAboutController alloc] init] animated:true completion:nil]; +} + +- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion +{ + [super dismissViewControllerAnimated:flag completion:^() { + if (completion) { + completion(); + } + if (!self.presentedViewController) { + [self start]; + } + }]; +} + +- (void)setNeedsUpdateOfSupportedInterfaceOrientations +{ + /* Hack. Some view controllers dismiss without calling the method above. */ + [super setNeedsUpdateOfSupportedInterfaceOrientations]; + if (!self.presentedViewController) { + [self start]; + } +} + +- (void)dismissViewController +{ + [self dismissViewControllerAnimated:true completion:nil]; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + [self stop]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration +{ + GBLayout *layout = _horizontalLayout; + _orientation = orientation; + if (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) { + layout = _verticalLayout; + } + _backgroundView.frame = [layout viewRectForOrientation:orientation]; + _backgroundView.layout = layout; + if (!self.presentedViewController) { + _window.backgroundColor = layout.theme.backgroundGradientBottom; + } +} + +- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation +{ + UIEdgeInsets insets = self.window.safeAreaInsets; + _cameraPositionButton.frame = CGRectMake(insets.left + 8, + _backgroundView.bounds.size.height - 8 - insets.bottom - 32, + 32, + 32); + if (_changeCameraButton) { + _changeCameraButton.frame = CGRectMake(insets.left + 8, + _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - 32 - 8, + 32, + 32); + } +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { + return UIInterfaceOrientationMaskAll; + } + if (MAX([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width) <= 568) { + return UIInterfaceOrientationMaskLandscape; + } + return UIInterfaceOrientationMaskAllButUpsideDown; +} + +- (BOOL)shouldAutorotate +{ + if (_running && GB_has_accelerometer(&_gb)) { + return false; + } + return true; +} + +- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures +{ + return UIRectEdgeTop; +} + +- (BOOL)prefersStatusBarHidden +{ + switch (_orientation) { + case UIInterfaceOrientationLandscapeRight: + case UIInterfaceOrientationLandscapeLeft: + return true; + default: + return false; + } +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + if (@available(iOS 13.0, *)) { + return _verticalLayout.theme.isDark? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent; + } + return _verticalLayout.theme.isDark? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; +} + + +- (void)preRun +{ + dispatch_async(dispatch_get_main_queue(), ^{ + UIApplication.sharedApplication.idleTimerDisabled = true; + }); + GB_set_pixels_output(&_gb, _gbView.pixels); + GB_set_sample_rate(&_gb, 96000); + if (![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBAudioMode"] isEqual:@"off"]) { + _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { + [_audioLock lock]; + + if (_audioBufferPosition < nFrames) { + _audioBufferNeeded = nFrames; + [_audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:(double)(_audioBufferNeeded - _audioBufferPosition) / sampleRate]]; + _audioBufferNeeded = 0; + } + + if (_stopping) { + memset(buffer, 0, nFrames * sizeof(*buffer)); + [_audioLock unlock]; + return; + } + + if (_audioBufferPosition < nFrames) { + // Not enough audio + memset(buffer, 0, (nFrames - _audioBufferPosition) * sizeof(*buffer)); + memcpy(buffer, _audioBuffer, _audioBufferPosition * sizeof(*buffer)); + // Do not reset the audio position to avoid more underflows + } + else if (_audioBufferPosition < nFrames + 4800) { + memcpy(buffer, _audioBuffer, nFrames * sizeof(*buffer)); + memmove(_audioBuffer, _audioBuffer + nFrames, (_audioBufferPosition - nFrames) * sizeof(*buffer)); + _audioBufferPosition = _audioBufferPosition - nFrames; + } + else { + memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer)); + _audioBufferPosition = 0; + } + [_audioLock unlock]; + } andSampleRate:96000]; + } + + [_audioClient start]; + if (GB_has_accelerometer(&_gb)) { + if (@available(iOS 14.0, *)) { + for (GCController *controller in [GCController controllers]) { + if (controller.motion.sensorsRequireManualActivation) { + [controller.motion setSensorsActive:true]; + } + } + } + [_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] + withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) { + if (_lastController.motion) return; + CMAcceleration data = accelerometerData.acceleration; + UIInterfaceOrientation orientation = _orientation; + switch (orientation) { + case UIInterfaceOrientationUnknown: + case UIInterfaceOrientationPortrait: + break; + case UIInterfaceOrientationPortraitUpsideDown: + data.x = -data.x; + data.y = -data.y; + break; + case UIInterfaceOrientationLandscapeLeft: { + double tempX = data.x; + data.x = data.y; + data.y = -tempX; + break; + } + case UIInterfaceOrientationLandscapeRight:{ + double tempX = data.x; + data.x = -data.y; + data.y = tempX; + break; + } + } + GB_set_accelerometer_values(&_gb, -data.x, data.y); + }]; + } + + /* Clear pending alarms, don't play alarms while playing */ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center removeDeliveredNotificationsWithIdentifiers:@[[GBROMManager sharedManager].romFile]]; + [center removePendingNotificationRequestsWithIdentifiers:@[[GBROMManager sharedManager].romFile]]; + } + + if (GB_rom_supports_alarms(&_gb)) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center requestAuthorizationWithOptions:UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert completionHandler:nil]; + } +} + +- (void)run +{ + [self preRun]; + while (_running) { + if (_rewind) { + _rewind = false; + GB_rewind_pop(&_gb); + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + if (!GB_rewind_pop(&_gb)) { + self.runMode = GBRunModePaused; + _rewindOver = true; + } + } + else { + for (unsigned i = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindSpeed"]; i--;) { + if (!GB_rewind_pop(&_gb)) { + self.runMode = GBRunModePaused; + _rewindOver = true; + } + } + } + } + if (_runMode != GBRunModePaused) { + GB_run(&_gb); + } + } + [self postRun]; + _stopping = false; +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center +didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler +{ + if (![response.notification.request.identifier isEqual:[GBROMManager sharedManager].currentROM]) { + [self application:[UIApplication sharedApplication] + openURL:[NSURL fileURLWithPath:response.notification.request.identifier] + options:@{}]; + } + completionHandler(); +} + +- (UIImage *)imageFromData:(NSData *)data width:(unsigned)width height:(unsigned)height +{ + /* Convert the screenshot to a CGImageRef */ + CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, data.bytes, data.length, NULL); + CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; + CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; + + CGImageRef iref = CGImageCreate(width, + height, + 8, + 32, + 4 * width, + colorSpaceRef, + bitmapInfo, + provider, + NULL, + true, + renderingIntent); + + UIImage *ret = [[UIImage alloc] initWithCGImage:iref]; + CGColorSpaceRelease(colorSpaceRef); + CGDataProviderRelease(provider); + CGImageRelease(iref); + return ret; +} + +- (void)postRun +{ + [_audioLock lock]; + _audioBufferPosition = _audioBufferNeeded = 0; + [_audioLock signal]; + [_audioLock unlock]; + [_audioClient stop]; + _audioClient = nil; + + if (!_swappingROM) { + GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation); + [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile]; + } + [[GBHapticManager sharedManager] setRumbleStrength:0]; + if (@available(iOS 14.0, *)) { + for (GCController *controller in [GCController controllers]) { + if (controller.motion.sensorsRequireManualActivation) { + [controller.motion setSensorsActive:false]; + } + } + } + [_motionManager stopAccelerometerUpdates]; + + unsigned timeToAlarm = GB_time_to_alarm(&_gb); + + if (timeToAlarm) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + + UNMutableNotificationContent *notificationContent = [[UNMutableNotificationContent alloc] init]; + NSString *friendlyName = [[[GBROMManager sharedManager].romFile lastPathComponent] stringByDeletingPathExtension]; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil]; + friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""]; + friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + notificationContent.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName]; + notificationContent.body = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName]; + notificationContent.sound = UNNotificationSound.defaultSound; + + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[GBROMManager sharedManager].romFile + content:notificationContent + trigger:[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timeToAlarm repeats:false]]; + + + [center addNotificationRequest:request withCompletionHandler:nil]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + UIApplication.sharedApplication.idleTimerDisabled = false; + }); +} + +- (void)start +{ + if (!_romLoaded) return; + if (_running) return; + _running = true; + [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start]; +} + +- (void)stop +{ + if (!_running) return; + [_audioLock lock]; + _stopping = true; + [_audioLock signal]; + [_audioLock unlock]; + _running = false; + while (_stopping) { + [_audioLock lock]; + [_audioLock signal]; + [_audioLock unlock]; + } + dispatch_async(dispatch_get_main_queue(), ^{ + self.runMode = GBRunModeNormal; + [_backgroundView fadeOverlayOut]; + }); +} + + +- (NSString *)bootROMPathForName:(NSString *)name +{ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBCustomBootROMs"]) { + NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0]; + path = [path stringByAppendingPathComponent:@"Boot ROMs"]; + path = [path stringByAppendingPathComponent:name]; + path = [path stringByAppendingPathExtension:@"bin"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + return path; + } + } + + return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"]; +} + +- (void)loadBootROM: (GB_boot_rom_t)type +{ + static NSString *const names[] = { + [GB_BOOT_ROM_DMG_0] = @"dmg0_boot", + [GB_BOOT_ROM_DMG] = @"dmg_boot", + [GB_BOOT_ROM_MGB] = @"mgb_boot", + [GB_BOOT_ROM_SGB] = @"sgb_boot", + [GB_BOOT_ROM_SGB2] = @"sgb2_boot", + [GB_BOOT_ROM_CGB_0] = @"cgb0_boot", + [GB_BOOT_ROM_CGB] = @"cgb_boot", + [GB_BOOT_ROM_CGB_E] = @"cgbE_boot", + [GB_BOOT_ROM_AGB_0] = @"agb0_boot", + [GB_BOOT_ROM_AGB] = @"agb_boot", + }; + NSString *name = names[type]; + NSString *path = [self bootROMPathForName:name]; + /* These boot types are not commonly available, and they are indentical + from an emulator perspective, so fall back to the more common variants + if they can't be found. */ + if (!path && type == GB_BOOT_ROM_CGB_E) { + [self loadBootROM:GB_BOOT_ROM_CGB]; + return; + } + if (!path && type == GB_BOOT_ROM_AGB_0) { + [self loadBootROM:GB_BOOT_ROM_AGB]; + return; + } + GB_load_boot_rom(&_gb, [path UTF8String]); +} + +- (void)vblankWithType:(GB_vblank_type_t)type +{ + if (type != GB_VBLANK_TYPE_REPEAT) { + [_gbView flip]; + GB_set_pixels_output(&_gb, _gbView.pixels); + } + _rewind = _runMode == GBRunModeRewind; +} + +- (void)gotNewSample:(GB_sample_t *)sample +{ + [_audioLock lock]; + if (_audioClient.isPlaying) { + if (_audioBufferPosition == _audioBufferSize) { + if (_audioBufferSize >= 0x4000) { + _audioBufferPosition = 0; + [_audioLock unlock]; + return; + } + + if (_audioBufferSize == 0) { + _audioBufferSize = 512; + } + else { + _audioBufferSize += _audioBufferSize >> 2; + } + _audioBuffer = realloc(_audioBuffer, sizeof(*sample) * _audioBufferSize); + } + _audioBuffer[_audioBufferPosition++] = *sample; + } + if (_audioBufferPosition == _audioBufferNeeded) { + [_audioLock signal]; + _audioBufferNeeded = 0; + } + [_audioLock unlock]; +} + +- (void)rumbleChanged:(double)amp +{ + [[GBHapticManager sharedManager] setRumbleStrength:amp]; +} + +- (void)updatePalette +{ + memcpy(&_palette, + [GBSettingsViewController paletteForTheme:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]], + sizeof(_palette)); + GB_set_palette(&_gb, &_palette); +} + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options +{ + [self stop]; + NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; + if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { + [GBROMManager sharedManager].currentROM = potentialROM; + } + else { + [url startAccessingSecurityScopedResource]; + [GBROMManager sharedManager].currentROM = + [[GBROMManager sharedManager] importROM:url.path + keepOriginal:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]]; + [url stopAccessingSecurityScopedResource]; + } + [self loadROM]; + [self start]; + return [GBROMManager sharedManager].currentROM != nil; +} + +- (void)setRunMode:(GBRunMode)runMode ignoreDynamicSpeed:(bool)ignoreDynamicSpeed +{ + if (runMode == GBRunModeRewind && _rewindOver) { + runMode = GBRunModePaused; + } + if (runMode == _runMode) return; + if (_runMode == GBRunModePaused) { + [_audioClient start]; + } + _runMode = runMode; + if (_runMode == GBRunModePaused) { + [_audioClient stop]; + } + + if (_runMode == GBRunModeNormal || _runMode == GBRunModeTurbo || _runMode == GBRunModeUnderclock) { + _rewindOver = false; + } + + if (_runMode == GBRunModeNormal || _runMode == GBRunModeUnderclock || !([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"] && !ignoreDynamicSpeed)) { + if (_runMode == GBRunModeTurbo) { + double multiplier = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboSpeed"]; + GB_set_turbo_mode(&_gb, multiplier == 1, false); + GB_set_clock_multiplier(&_gb, multiplier); + } + else if (_runMode == GBRunModeUnderclock) { + GB_set_clock_multiplier(&_gb, 0.5); + } + else { + GB_set_turbo_mode(&_gb, false, false); + GB_set_clock_multiplier(&_gb, 1.0); + } + } +} + +- (void)setRunMode:(GBRunMode)runMode +{ + [self setRunMode:runMode ignoreDynamicSpeed:false]; +} + +- (AVCaptureDevice *)captureDevice +{ + for (AVCaptureDevice *device in _allCaptureDevices) { + if ([device position] == _cameraPosition) { + // There is only one front camera, return it + if (_cameraPosition == AVCaptureDevicePositionFront) { + return device; + } + + // There may be several back cameras, return the one with the matching type + if ([device deviceType] == [_selectedBackCaptureDevice deviceType]) { + return device; + } + } + } + // Return the default camera + return [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; +} + +- (void)cameraRequestUpdate +{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (!_cameraSession) { + dispatch_async(_cameraQueue, ^{ + @try { + if (!_cameraSession) { + NSError *error; + AVCaptureDevice *device = [self captureDevice]; + AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error]; + + if (!input) { + GB_camera_updated(&_gb); + return; + } + + _cameraOutput = [[AVCaptureVideoDataOutput alloc] init]; + [_cameraOutput setVideoSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}]; + [_cameraOutput setSampleBufferDelegate:self + queue:_cameraQueue]; + + + + _cameraSession = [AVCaptureSession new]; + _cameraSession.sessionPreset = AVCaptureSessionPreset352x288; + + [_cameraSession addInput: input]; + [_cameraSession addOutput: _cameraOutput]; + _cameraConnection = [_cameraOutput connectionWithMediaType: AVMediaTypeVideo]; + _cameraConnection.videoOrientation = AVCaptureVideoOrientationPortrait; + [_cameraSession startRunning]; + } + } + @catch (NSException *exception) { + /* I have not tested camera support on many devices, so we catch exceptions just in case. */ + GB_camera_updated(&_gb); + } + }); + } + + _cameraNeedsUpdate = true; + [_disableCameraTimer invalidate]; + if (!_cameraPositionButton.alpha) { + [UIView animateWithDuration:0.25 animations:^{ + _cameraPositionButton.alpha = 1; + }]; + } + if (_changeCameraButton) { + // The change camera button is only available when we are using a capture device on the back of the device + double changeCameraButtonAlpha = (_cameraPosition == AVCaptureDevicePositionFront) ? 0 : 1; + if (changeCameraButtonAlpha != _changeCameraButton.alpha) { + [UIView animateWithDuration:0.25 animations:^{ + _changeCameraButton.alpha = changeCameraButtonAlpha; + }]; + } + } + + _disableCameraTimer = [NSTimer scheduledTimerWithTimeInterval:1 + repeats:false + block:^(NSTimer *timer) { + if (_cameraPositionButton.alpha) { + [UIView animateWithDuration:0.25 animations:^{ + _cameraPositionButton.alpha = 0; + }]; + } + if (_changeCameraButton.alpha) { + [UIView animateWithDuration:0.25 animations:^{ + _changeCameraButton.alpha = 0; + }]; + } + dispatch_async(_cameraQueue, ^{ + [_cameraSession stopRunning]; + _cameraSession = nil; + _cameraConnection = nil; + _cameraOutput = nil; + }); + }]; + }); +} + +- (uint8_t)cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y +{ + if (!_cameraImage) { + return 0; + } + if (_cameraNeedsUpdate) { + return 0; + } + + y += 8; // Equalize X and Y for rotation as a 128x128 image + + if (_cameraPosition == AVCaptureDevicePositionFront) { + x = 127 - x; + } + + switch (_orientation) { + case UIInterfaceOrientationUnknown: + case UIInterfaceOrientationPortrait: + break; + case UIInterfaceOrientationPortraitUpsideDown: + x = 127 - x; + y = 127 - y; + break; + case UIInterfaceOrientationLandscapeLeft: { + uint8_t tempX = x; + x = y; + y = 127 - tempX; + break; + } + case UIInterfaceOrientationLandscapeRight:{ + uint8_t tempX = x; + x = 127 - y; + y = tempX; + break; + } + } + + if (_cameraPosition == AVCaptureDevicePositionFront) { + x = 127 - x; + } + + // Center the 128*128 image on the 130*XXX (or XXX*130) captured image + unsigned offsetX = (CVPixelBufferGetWidth(_cameraImage) - 128) / 2; + unsigned offsetY = (CVPixelBufferGetHeight(_cameraImage) - 112) / 2; + + uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(_cameraImage); + size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_cameraImage); + uint8_t ret = baseAddress[(x + offsetX) * 4 + (y + offsetY) * bytesPerRow + 2]; + + return ret; +} + +- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection +{ + if (!_cameraNeedsUpdate) return; + CVImageBufferRef buffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CIImage *image = [CIImage imageWithCVPixelBuffer:buffer + options:[NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], kCIImageColorSpace, nil]]; + double scale = MAX(130.0 / CVPixelBufferGetWidth(buffer), 130.0 / CVPixelBufferGetHeight(buffer)); + image = [image imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)]; + if (_cameraImage) { + CVPixelBufferUnlockBaseAddress(_cameraImage, 0); + CVBufferRelease(_cameraImage); + _cameraImage = NULL; + } + CGSize size = image.extent.size; + CVPixelBufferCreate(kCFAllocatorDefault, + size.width, + size.height, + kCVPixelFormatType_32BGRA, + NULL, + &_cameraImage); + [[[CIContext alloc] init] render:image toCVPixelBuffer:_cameraImage]; + CVPixelBufferLockBaseAddress(_cameraImage, 0); + + GB_camera_updated(&_gb); + + _cameraNeedsUpdate = false; +} + +- (void)rotateCamera +{ + dispatch_async(_cameraQueue, ^{ + _cameraPosition ^= AVCaptureDevicePositionBack ^ AVCaptureDevicePositionFront; + [_cameraSession stopRunning]; + _cameraSession = nil; + _cameraConnection = nil; + _cameraOutput = nil; + if (_cameraNeedsUpdate) { + _cameraNeedsUpdate = false; + GB_camera_updated(&_gb); + } + }); +} + +- (void)changeCamera +{ + dispatch_async(_cameraQueue, ^{ + // Get index of selected camera and select the next one, wrapping to the beginning + NSUInteger i = [_backCaptureDevices indexOfObject:_selectedBackCaptureDevice]; + int nextIndex = (i + 1) % _backCaptureDevices.count; + _selectedBackCaptureDevice = _backCaptureDevices[nextIndex]; + + [_cameraSession stopRunning]; + _cameraSession = nil; + _cameraConnection = nil; + _cameraOutput = nil; + if (_cameraNeedsUpdate) { + _cameraNeedsUpdate = false; + GB_camera_updated(&_gb); + } + }); +} + +@end diff --git a/iOS/GCExtendedGamepad+AllElements.h b/iOS/GCExtendedGamepad+AllElements.h new file mode 100644 index 0000000..427d5eb --- /dev/null +++ b/iOS/GCExtendedGamepad+AllElements.h @@ -0,0 +1,25 @@ +#import + +typedef enum { + GBUsageDpad, + GBUsageButtonA, + GBUsageButtonB, + GBUsageButtonX, + GBUsageButtonY, + GBUsageButtonMenu, + GBUsageButtonOptions, + GBUsageButtonHome, + GBUsageLeftThumbstick, + GBUsageRightThumbstick, + GBUsageLeftShoulder, + GBUsageRightShoulder, + GBUsageLeftTrigger, + GBUsageRightTrigger, + GBUsageLeftThumbstickButton, + GBUsageRightThumbstickButton, + GBUsageTouchpadButton, +} GBControllerUsage; + +@interface GCExtendedGamepad (AllElements) +- (NSDictionary *)elementsDictionary; +@end diff --git a/iOS/GCExtendedGamepad+AllElements.m b/iOS/GCExtendedGamepad+AllElements.m new file mode 100644 index 0000000..006ea3a --- /dev/null +++ b/iOS/GCExtendedGamepad+AllElements.m @@ -0,0 +1,48 @@ +#import "GCExtendedGamepad+AllElements.h" +#import + +@implementation GCExtendedGamepad (AllElements) + +- (NSDictionary *)elementsDictionary; +{ + NSMutableDictionary *ret = [NSMutableDictionary dictionary]; + if (self.dpad) ret[@(GBUsageDpad)] = self.dpad; + if (self.buttonA) ret[@(GBUsageButtonA)] = self.buttonA; + if (self.buttonB) ret[@(GBUsageButtonB)] = self.buttonB; + if (self.buttonX) ret[@(GBUsageButtonX)] = self.buttonX; + if (self.buttonY) ret[@(GBUsageButtonY)] = self.buttonY; + if (@available(iOS 13.0, *)) { + if (self.buttonMenu) ret[@(GBUsageButtonMenu)] = self.buttonMenu; + if (self.buttonOptions) ret[@(GBUsageButtonOptions)] = self.buttonOptions; + } + // Can't be used + /* if (@available(iOS 14.0, *)) { + if (self.buttonHome) ret[@(GBUsageButtonHome)] = self.buttonHome; + } */ + if (self.leftThumbstick) ret[@(GBUsageLeftThumbstick)] = self.leftThumbstick; + if (self.rightThumbstick) ret[@(GBUsageRightThumbstick)] = self.rightThumbstick; + if (self.leftShoulder) ret[@(GBUsageLeftShoulder)] = self.leftShoulder; + if (self.rightShoulder) ret[@(GBUsageRightShoulder)] = self.rightShoulder; + if (self.leftTrigger) ret[@(GBUsageLeftTrigger)] = self.leftTrigger; + if (self.rightTrigger) ret[@(GBUsageRightTrigger)] = self.rightTrigger; + if (@available(iOS 12.1, *)) { + if (self.leftThumbstickButton) ret[@(GBUsageLeftThumbstickButton)] = self.leftThumbstickButton; + if (self.rightThumbstickButton) ret[@(GBUsageRightThumbstickButton)] = self.rightThumbstickButton; + } + + if (@available(iOS 14.0, *)) { + if ([self isKindOfClass:[GCDualShockGamepad class]]) { + GCDualShockGamepad *dualShock = (GCDualShockGamepad *)self; + if (dualShock.touchpadButton) ret[@(GBUsageTouchpadButton)] = dualShock.touchpadButton; + } + } + + if (@available(iOS 14.5, *)) { + if ([self isKindOfClass:[GCDualSenseGamepad class]]) { + GCDualSenseGamepad *dualSense = (GCDualSenseGamepad *)self; + if (dualSense.touchpadButton) ret[@(GBUsageTouchpadButton)] = dualSense.touchpadButton; + } + } + return ret; +} +@end diff --git a/iOS/Info.plist b/iOS/Info.plist new file mode 100644 index 0000000..f988a81 --- /dev/null +++ b/iOS/Info.plist @@ -0,0 +1,197 @@ + + + + + CFBundleExecutable + SameBoy + CFBundleIdentifier + com.github.liji32.sameboy.ios + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + SameBoy + CFBundlePackageType + APPL + CFBundleShortVersionString + @VERSION + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSHumanReadableCopyright + Copyright © 2015-@COPYRIGHT_YEAR Lior Halphon + CFBundleIcons + + CFBundlePrimaryIcon + + CFBundleIconFiles + + AppIcon60x60@2x.png + AppIcon60x60@3x.png + AppIcon76x76@2x.png + AppIcon83.5x83.5@2x.png + + + + LSApplicationCategoryType + public.app-category.games + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + + UISupportsDocumentBrowser + + NSCameraUsageDescription + SameBoy needs to access your device's camera to emulate the Game Boy Camera + CFBundleDocumentTypes + + + CFBundleTypeIconFiles + + Cartridge.png + + CFBundleTypeExtensions + + gb + + CFBundleTypeName + Game Boy Game + LSItemContentTypes + + com.github.liji32.sameboy.gb + + LSHandlerRank + Owner + + + CFBundleTypeIconFiles + + ColorCartridge.png + + CFBundleTypeExtensions + + gbc + + CFBundleTypeName + Game Boy Color Game + LSItemContentTypes + + com.github.liji32.sameboy.gbc + + LSHandlerRank + Owner + + + CFBundleTypeIconFiles + + ColorCartridge.png + + CFBundleTypeExtensions + + isx + + CFBundleTypeName + Game Boy ISX File + LSItemContentTypes + + com.github.liji32.sameboy.isx + + LSHandlerRank + Owner + + + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy Game + UTTypeIdentifier + com.github.liji32.sameboy.gb + UTTypeTagSpecification + + public.filename-extension + + gb + + + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy Color Game + UTTypeIdentifier + com.github.liji32.sameboy.gbc + UTTypeTagSpecification + + public.filename-extension + + gbc + + + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy ISX File + UTTypeIdentifier + com.github.liji32.sameboy.isx + UTTypeTagSpecification + + public.filename-extension + + isx + + + + + UIDeviceFamily + + 1 + 2 + + CFBundleSupportedPlatforms + + iPhoneOS + + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + GCSupportedGameControllers + + + ProfileName + ExtendedGamepad + + + GCSupportsControllerUserInteraction + + + diff --git a/iOS/LaunchScreen.storyboard b/iOS/LaunchScreen.storyboard new file mode 100644 index 0000000..605cefa --- /dev/null +++ b/iOS/LaunchScreen.storyboard @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/License.html b/iOS/License.html new file mode 100644 index 0000000..488c8ca --- /dev/null +++ b/iOS/License.html @@ -0,0 +1,51 @@ + + + + + + + + + +

SameBoy

+

Copyright © 2015-@COPYRIGHT_YEAR Lior Halphon

+ +

Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:

+ +

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

+ +

A written permission from Lior Halphon is required to distribute copies or +substantial portions of the Software in a digital marketplace, such as +Apple's App Store.

+ +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.

+ + \ No newline at end of file diff --git a/iOS/ModelTemplate@2x.png b/iOS/ModelTemplate@2x.png new file mode 100644 index 0000000..c08c9a3 Binary files /dev/null and b/iOS/ModelTemplate@2x.png differ diff --git a/iOS/ModelTemplate@3x.png b/iOS/ModelTemplate@3x.png new file mode 100644 index 0000000..2bd77a5 Binary files /dev/null and b/iOS/ModelTemplate@3x.png differ diff --git a/iOS/UILabel+TapLocation.h b/iOS/UILabel+TapLocation.h new file mode 100644 index 0000000..168bdeb --- /dev/null +++ b/iOS/UILabel+TapLocation.h @@ -0,0 +1,5 @@ +#import + +@interface UILabel (TapLocation) +- (unsigned)characterAtTap:(UITapGestureRecognizer *)tap; +@end diff --git a/iOS/UILabel+TapLocation.m b/iOS/UILabel+TapLocation.m new file mode 100644 index 0000000..d797568 --- /dev/null +++ b/iOS/UILabel+TapLocation.m @@ -0,0 +1,27 @@ +#import "UILabel+TapLocation.h" + +@implementation UILabel (TapLocation) + +- (unsigned)characterAtTap:(UITapGestureRecognizer *)tap +{ + CGPoint tapLocation = [tap locationInView:self]; + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText]; + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; + [textStorage addLayoutManager:layoutManager]; + + NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(self.frame.size.width, + self.frame.size.height + 256)]; + textContainer.lineFragmentPadding = 0; + textContainer.maximumNumberOfLines = 256; + textContainer.lineBreakMode = self.lineBreakMode; + + [layoutManager addTextContainer:textContainer]; + + return [layoutManager characterIndexForPoint:tapLocation + inTextContainer:textContainer + fractionOfDistanceBetweenInsertionPoints:NULL]; + +} + +@end diff --git a/iOS/audioSettings@2x.png b/iOS/audioSettings@2x.png new file mode 100644 index 0000000..482f12a Binary files /dev/null and b/iOS/audioSettings@2x.png differ diff --git a/iOS/audioSettings@3x.png b/iOS/audioSettings@3x.png new file mode 100644 index 0000000..1fe0317 Binary files /dev/null and b/iOS/audioSettings@3x.png differ diff --git a/iOS/button2-tint@2x.png b/iOS/button2-tint@2x.png new file mode 100644 index 0000000..88469b2 Binary files /dev/null and b/iOS/button2-tint@2x.png differ diff --git a/iOS/button2-tint@3x.png b/iOS/button2-tint@3x.png new file mode 100644 index 0000000..8d8fbfe Binary files /dev/null and b/iOS/button2-tint@3x.png differ diff --git a/iOS/button2@2x.png b/iOS/button2@2x.png new file mode 100644 index 0000000..ccb6a4e Binary files /dev/null and b/iOS/button2@2x.png differ diff --git a/iOS/button2@3x.png b/iOS/button2@3x.png new file mode 100644 index 0000000..8feb468 Binary files /dev/null and b/iOS/button2@3x.png differ diff --git a/iOS/button2Pressed-tint@2x.png b/iOS/button2Pressed-tint@2x.png new file mode 100644 index 0000000..38084ca Binary files /dev/null and b/iOS/button2Pressed-tint@2x.png differ diff --git a/iOS/button2Pressed-tint@3x.png b/iOS/button2Pressed-tint@3x.png new file mode 100644 index 0000000..4bb86a7 Binary files /dev/null and b/iOS/button2Pressed-tint@3x.png differ diff --git a/iOS/button2Pressed@2x.png b/iOS/button2Pressed@2x.png new file mode 100644 index 0000000..45b19ab Binary files /dev/null and b/iOS/button2Pressed@2x.png differ diff --git a/iOS/button2Pressed@3x.png b/iOS/button2Pressed@3x.png new file mode 100644 index 0000000..f86440d Binary files /dev/null and b/iOS/button2Pressed@3x.png differ diff --git a/iOS/button@2x.png b/iOS/button@2x.png new file mode 100644 index 0000000..5737256 Binary files /dev/null and b/iOS/button@2x.png differ diff --git a/iOS/button@3x.png b/iOS/button@3x.png new file mode 100644 index 0000000..3d1c6ca Binary files /dev/null and b/iOS/button@3x.png differ diff --git a/iOS/buttonPressed@2x.png b/iOS/buttonPressed@2x.png new file mode 100644 index 0000000..5802cda Binary files /dev/null and b/iOS/buttonPressed@2x.png differ diff --git a/iOS/buttonPressed@3x.png b/iOS/buttonPressed@3x.png new file mode 100644 index 0000000..7ae742b Binary files /dev/null and b/iOS/buttonPressed@3x.png differ diff --git a/iOS/controlsSettings@2x.png b/iOS/controlsSettings@2x.png new file mode 100644 index 0000000..dd832ab Binary files /dev/null and b/iOS/controlsSettings@2x.png differ diff --git a/iOS/controlsSettings@3x.png b/iOS/controlsSettings@3x.png new file mode 100644 index 0000000..f4571a0 Binary files /dev/null and b/iOS/controlsSettings@3x.png differ diff --git a/iOS/deb-control b/iOS/deb-control new file mode 100644 index 0000000..4c92a80 --- /dev/null +++ b/iOS/deb-control @@ -0,0 +1,10 @@ +Package: com.github.liji32.sameboy.ios +Name: SameBoy +Depends: firmware (>= 11.0) +Architecture: all +Description: A Game Boy emulator for iOS +Maintainer: Lior Halphon +Author: Lior Halphon +Section: Games +Icon: file:///Applications/SameBoy-iOS.app/AppIcon60x60@2x.png +Version: @VERSION diff --git a/iOS/deb-postinst b/iOS/deb-postinst new file mode 100755 index 0000000..13dc0e2 --- /dev/null +++ b/iOS/deb-postinst @@ -0,0 +1,2 @@ +#!/bin/bash +/private/var/containers/SameBoy-iOS.app/installer install diff --git a/iOS/deb-prerm b/iOS/deb-prerm new file mode 100755 index 0000000..849f266 --- /dev/null +++ b/iOS/deb-prerm @@ -0,0 +1,2 @@ +#!/bin/bash +/Applications/SameBoy-iOS.app/installer uninstall || /var/jb/Applications/SameBoy-iOS.app/installer uninstall diff --git a/iOS/dpad-tint@2x.png b/iOS/dpad-tint@2x.png new file mode 100644 index 0000000..f4b5fac Binary files /dev/null and b/iOS/dpad-tint@2x.png differ diff --git a/iOS/dpad-tint@3x.png b/iOS/dpad-tint@3x.png new file mode 100644 index 0000000..512a89e Binary files /dev/null and b/iOS/dpad-tint@3x.png differ diff --git a/iOS/dpad@2x.png b/iOS/dpad@2x.png new file mode 100644 index 0000000..3ead7c5 Binary files /dev/null and b/iOS/dpad@2x.png differ diff --git a/iOS/dpad@3x.png b/iOS/dpad@3x.png new file mode 100644 index 0000000..401c4cc Binary files /dev/null and b/iOS/dpad@3x.png differ diff --git a/iOS/dpadShadow@2x.png b/iOS/dpadShadow@2x.png new file mode 100644 index 0000000..cdbf9c3 Binary files /dev/null and b/iOS/dpadShadow@2x.png differ diff --git a/iOS/dpadShadow@3x.png b/iOS/dpadShadow@3x.png new file mode 100644 index 0000000..a37d7fc Binary files /dev/null and b/iOS/dpadShadow@3x.png differ diff --git a/iOS/dpadShadowDiagonal@2x.png b/iOS/dpadShadowDiagonal@2x.png new file mode 100644 index 0000000..3e9d6e6 Binary files /dev/null and b/iOS/dpadShadowDiagonal@2x.png differ diff --git a/iOS/dpadShadowDiagonal@3x.png b/iOS/dpadShadowDiagonal@3x.png new file mode 100644 index 0000000..b4cab27 Binary files /dev/null and b/iOS/dpadShadowDiagonal@3x.png differ diff --git a/iOS/emulationSettings@2x.png b/iOS/emulationSettings@2x.png new file mode 100644 index 0000000..0f22751 Binary files /dev/null and b/iOS/emulationSettings@2x.png differ diff --git a/iOS/emulationSettings@3x.png b/iOS/emulationSettings@3x.png new file mode 100644 index 0000000..23cd4b6 Binary files /dev/null and b/iOS/emulationSettings@3x.png differ diff --git a/iOS/installer.entitlements b/iOS/installer.entitlements new file mode 100644 index 0000000..5dd0f8a --- /dev/null +++ b/iOS/installer.entitlements @@ -0,0 +1,21 @@ + + + + +com.apple.private.mobileinstall.allowedSPI + + InstallForLaunchServices + UninstallForLaunchServices + +com.apple.lsapplicationworkspace.rebuildappdatabases + +com.apple.private.MobileContainerManager.allowed + +com.apple.frontboard.launchapplications + +platform-application + +com.apple.private.security.no-container + + + \ No newline at end of file diff --git a/iOS/installer.m b/iOS/installer.m new file mode 100644 index 0000000..2fa2ff6 --- /dev/null +++ b/iOS/installer.m @@ -0,0 +1,105 @@ +#import +#import +#import + +@interface LSApplicationProxy : NSObject +@property (readonly, getter=isContainerized) bool containerized; +@property (readonly) NSString *bundleIdentifier; +@property (readonly) NSURL * bundleURL; +@end + +@interface LSApplicationWorkspace : NSObject ++ (instancetype)defaultWorkspace; +- (NSArray *)allInstalledApplications; +- (bool)unregisterApplication:(NSURL *)url; +- (bool)registerApplicationDictionary:(NSDictionary *)dict; +@end + +@interface MCMAppDataContainer : NSObject ++ (MCMAppDataContainer *)containerWithIdentifier:(NSString *)identifier + createIfNecessary:(bool)create + existed:(bool *)existed + error:(NSError **)error; +@property(readonly) NSURL *url; +@end + + +int main(int argc, char **argv) +{ + if (argc != 2) return 1; + // Make sure MobileContainerManager is loaded + if (!dlopen("/System/Library/PrivateFrameworks/MobileContainerManager.framework/MobileContainerManager", RTLD_NOW)) return 1; + + bool uninstall = false; + if (strcmp(argv[1], "uninstall") == 0) { + uninstall = true; + } + else if (strcmp(argv[1], "install") != 0) { + return 1; + } + + NSString *installPath = @"/var/jb/Applications/SameBoy-iOS.app"; + if (access("/Applications/", W_OK) == 0) { + installPath = @"/Applications/SameBoy-iOS.app"; + } + NSLog(@"Install path is %@", installPath); + + for (LSApplicationProxy *app in [[LSApplicationWorkspace defaultWorkspace] allInstalledApplications]) { + if (![app.bundleIdentifier isEqualToString:[NSBundle mainBundle].bundleIdentifier]) continue; + if (![app.bundleURL.path.stringByResolvingSymlinksInPath isEqual:installPath.stringByResolvingSymlinksInPath]) { + // Already installed elsewhere + NSLog(@"Already installed at %@", app.bundleURL.path); + return 1; + } + + NSLog(@"Unregistering previous installation"); + // We're registered but not containerized (or just uninstalling), unregister ourselves first + if (![[LSApplicationWorkspace defaultWorkspace] unregisterApplication:app.bundleURL]) return 1; + + break; + } + + // Don't modify files if we're at the correct path already + if (uninstall || ![[NSBundle mainBundle].bundlePath.stringByResolvingSymlinksInPath isEqual:installPath.stringByResolvingSymlinksInPath]) { + // Remove any previous copy + NSError *error = nil; + if (!access(installPath.UTF8String, F_OK)) { + NSLog(@"Removing previous installation"); + [[NSFileManager defaultManager] removeItemAtPath:installPath error:&error]; + if (error) { + NSLog(@"Error: %@", error); + return 1; + } + } + + // If we're uninstalling, we're done + if (uninstall) return 0; + + NSLog(@"Installing..."); + + [[NSFileManager defaultManager] moveItemAtPath:[NSBundle mainBundle].bundlePath toPath:installPath error:&error]; + if (error) { + NSLog(@"Error: %@", error); + return 1; + } + } + + + NSLog(@"Registering..."); + + NSString *container = [objc_getClass("MCMAppDataContainer") containerWithIdentifier:[NSBundle mainBundle].bundleIdentifier + createIfNecessary:true + existed:nil + error:nil].url.path; + + return ![[LSApplicationWorkspace defaultWorkspace] registerApplicationDictionary:@{ + @"ApplicationType": @"System", + @"CFBundleIdentifier": [NSBundle mainBundle].bundleIdentifier, + @"CompatibilityState": @NO, + @"Container": container, + @"IsDeletable": @NO, + @"Path": installPath, + @"_LSBundlePlugins": @{}, + @"IsContainerized": @YES, + }]; +} diff --git a/iOS/jailbreak.entitlements b/iOS/jailbreak.entitlements new file mode 100644 index 0000000..ce0a662 --- /dev/null +++ b/iOS/jailbreak.entitlements @@ -0,0 +1,8 @@ + + + + +com.apple.private.security.container-required + + + \ No newline at end of file diff --git a/iOS/logo@2x.png b/iOS/logo@2x.png new file mode 100644 index 0000000..1a47ce7 Binary files /dev/null and b/iOS/logo@2x.png differ diff --git a/iOS/logo@3x.png b/iOS/logo@3x.png new file mode 100644 index 0000000..0f9f0b5 Binary files /dev/null and b/iOS/logo@3x.png differ diff --git a/iOS/main.m b/iOS/main.m new file mode 100644 index 0000000..612a8c7 --- /dev/null +++ b/iOS/main.m @@ -0,0 +1,93 @@ +#import +#import +#import "GBViewController.h" +#import "GBView.h" + +int main(int argc, char * argv[]) +{ + @autoreleasepool { + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"GBFilter": @"NearestNeighbor", + @"GBColorCorrection": @(GB_COLOR_CORRECTION_MODERN_BALANCED), + @"GBAudioMode": @"switch", + @"GBHighpassFilter": @(GB_HIGHPASS_ACCURATE), + @"GBRewindLength": @(120), + @"GBFrameBlendingMode": @(GB_FRAME_BLENDING_MODE_ACCURATE), + + @"GBDMGModel": @(GB_MODEL_DMG_B), + @"GBCGBModel": @(GB_MODEL_CGB_E), + @"GBAGBModel": @(GB_MODEL_AGB_A), + @"GBSGBModel": @(GB_MODEL_SGB2), + @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY), + @"GBButtonHaptics": @YES, + @"GBHapticsStrength": @0.75, + @"GBTurboSpeed": @1, + @"GBRewindSpeed": @1, + @"GBDynamicSpeed": @NO, + + @"GBInterfaceTheme": @"SameBoy", + + @"GBCurrentTheme": @"Lime (Game Boy)", + // Default themes + @"GBThemes": @{ + @"Desert": @{ + @"BrightnessBias": @0.0, + @"Colors": @[@0xff302f3e, @0xff576674, @0xff839ba4, @0xffb1d0d2, @0xffb7d7d8], + @"DisabledLCDColor": @YES, + @"HueBias": @0.10087773904382469, + @"HueBiasStrength": @0.062142056772908363, + @"Manual": @NO, + }, + @"Evening": @{ + @"BrightnessBias": @-0.10168700106441975, + @"Colors": @[@0xff362601, @0xff695518, @0xff899853, @0xffa6e4ae, @0xffa9eebb], + @"DisabledLCDColor": @YES, + @"HueBias": @0.60027079191058874, + @"HueBiasStrength": @0.33816297305747867, + @"Manual": @NO, + }, + @"Fog": @{ + @"BrightnessBias": @0.0, + @"Colors": @[@0xff373c34, @0xff737256, @0xff9da386, @0xffc3d2bf, @0xffc7d8c6], + @"DisabledLCDColor": @YES, + @"HueBias": @0.55750435756972117, + @"HueBiasStrength": @0.18424738545816732, + @"Manual": @NO, + }, + @"Magic Eggplant": @{ + @"BrightnessBias": @0.0, + @"Colors": @[@0xff3c2136, @0xff942e84, @0xffc7699d, @0xfff1e4b0, @0xfff6f9b2], + @"DisabledLCDColor": @YES, + @"HueBias": @0.87717878486055778, + @"HueBiasStrength": @0.65018052788844627, + @"Manual": @NO, + }, + @"Radioactive Pea": @{ + @"BrightnessBias": @-0.48079556772908372, + @"Colors": @[@0xff215200, @0xff1f7306, @0xff169e34, @0xff03ceb8, @0xff00d4d1], + @"DisabledLCDColor": @YES, + @"HueBias": @0.3795131972111554, + @"HueBiasStrength": @0.34337649402390436, + @"Manual": @NO, + }, + @"Seaweed": @{ + @"BrightnessBias": @-0.28532744023904377, + @"Colors": @[@0xff3f0015, @0xff426532, @0xff58a778, @0xff95e0df, @0xffa0e7ee], + @"DisabledLCDColor": @YES, + @"HueBias": @0.2694067480079681, + @"HueBiasStrength": @0.51565612549800799, + @"Manual": @NO, + }, + @"Twilight": @{ + @"BrightnessBias": @-0.091789093625498031, + @"Colors": @[@0xff3f0015, @0xff461286, @0xff6254bd, @0xff97d3e9, @0xffa0e7ee], + @"DisabledLCDColor": @YES, + @"HueBias": @0.0, + @"HueBiasStrength": @0.49710532868525897, + @"Manual": @NO, + }, + }, + }]; + } + return UIApplicationMain(argc, argv, nil, NSStringFromClass([GBViewController class])); +} diff --git a/iOS/sideload.entitlements b/iOS/sideload.entitlements new file mode 100644 index 0000000..5117894 --- /dev/null +++ b/iOS/sideload.entitlements @@ -0,0 +1,12 @@ + + + + + application-identifier + SAMEBOY.com.github.liji32.sameboy.ios + com.apple.developer.team-identifier + SAMEBOY + com.apple.private.security.container-required + com.github.liji32.sameboy.ios + + \ No newline at end of file diff --git a/iOS/swipepad-tint@2x.png b/iOS/swipepad-tint@2x.png new file mode 100644 index 0000000..62a9d99 Binary files /dev/null and b/iOS/swipepad-tint@2x.png differ diff --git a/iOS/swipepad-tint@3x.png b/iOS/swipepad-tint@3x.png new file mode 100644 index 0000000..2017aca Binary files /dev/null and b/iOS/swipepad-tint@3x.png differ diff --git a/iOS/swipepad@2x.png b/iOS/swipepad@2x.png new file mode 100644 index 0000000..79c41b8 Binary files /dev/null and b/iOS/swipepad@2x.png differ diff --git a/iOS/swipepad@3x.png b/iOS/swipepad@3x.png new file mode 100644 index 0000000..0f727d4 Binary files /dev/null and b/iOS/swipepad@3x.png differ diff --git a/iOS/swipepadShadow@2x.png b/iOS/swipepadShadow@2x.png new file mode 100644 index 0000000..8840d24 Binary files /dev/null and b/iOS/swipepadShadow@2x.png differ diff --git a/iOS/swipepadShadow@3x.png b/iOS/swipepadShadow@3x.png new file mode 100644 index 0000000..115a58a Binary files /dev/null and b/iOS/swipepadShadow@3x.png differ diff --git a/iOS/swipepadShadowDiagonal@2x.png b/iOS/swipepadShadowDiagonal@2x.png new file mode 100644 index 0000000..7afe195 Binary files /dev/null and b/iOS/swipepadShadowDiagonal@2x.png differ diff --git a/iOS/swipepadShadowDiagonal@3x.png b/iOS/swipepadShadowDiagonal@3x.png new file mode 100644 index 0000000..fa8d576 Binary files /dev/null and b/iOS/swipepadShadowDiagonal@3x.png differ diff --git a/iOS/themeSettings@2x.png b/iOS/themeSettings@2x.png new file mode 100644 index 0000000..668b453 Binary files /dev/null and b/iOS/themeSettings@2x.png differ diff --git a/iOS/themeSettings@3x.png b/iOS/themeSettings@3x.png new file mode 100644 index 0000000..3e52ae4 Binary files /dev/null and b/iOS/themeSettings@3x.png differ diff --git a/iOS/videoSettings@2x.png b/iOS/videoSettings@2x.png new file mode 100644 index 0000000..94921b5 Binary files /dev/null and b/iOS/videoSettings@2x.png differ diff --git a/iOS/videoSettings@3x.png b/iOS/videoSettings@3x.png new file mode 100644 index 0000000..12a673d Binary files /dev/null and b/iOS/videoSettings@3x.png differ diff --git a/iOS/waveform.weak@2x.png b/iOS/waveform.weak@2x.png new file mode 100644 index 0000000..e206b66 Binary files /dev/null and b/iOS/waveform.weak@2x.png differ diff --git a/iOS/waveform.weak@3x.png b/iOS/waveform.weak@3x.png new file mode 100644 index 0000000..be46c96 Binary files /dev/null and b/iOS/waveform.weak@3x.png differ diff --git a/libretro/Makefile b/libretro/Makefile index ada200d..7459908 100644 --- a/libretro/Makefile +++ b/libretro/Makefile @@ -41,6 +41,9 @@ else ifneq ($(findstring Darwin,$(shell uname -a)),) ifeq ($(shell uname -p),powerpc) arch = ppc endif + ifeq ($(shell uname -p),arm) + arch = arm + endif else ifneq ($(findstring MINGW,$(shell uname -a)),) system_platform = win endif @@ -49,18 +52,11 @@ ifeq ($(platform), win) INCFLAGS += -I Windows endif -CORE_DIR = ../ +CORE_DIR = .. TARGET_NAME = sameboy -LIBM = -lm +LIBM = -lm -ifeq ($(ARCHFLAGS),) -ifeq ($(archs),ppc) - ARCHFLAGS = -arch ppc -arch ppc64 -else - ARCHFLAGS = -arch i386 -arch x86_64 -endif -endif ifneq ($(SANITIZER),) CFLAGS := -fsanitize=$(SANITIZER) $(CFLAGS) @@ -68,13 +64,6 @@ ifneq ($(SANITIZER),) LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS) -lasan endif -ifeq ($(platform), osx) -ifndef ($(NOUNIVERSAL)) - CFLAGS += $(ARCHFLAGS) - LFLAGS += $(ARCHFLAGS) -endif -endif - ifeq ($(STATIC_LINKING), 1) EXT := a endif @@ -91,7 +80,7 @@ else ifeq ($(platform), linux-portable) LIBM := # (armv7 a7, hard point, neon based) ### # NESC, SNESC, C64 mini -else ifeq ($(platform), classic_armv7_a7) +else ifeq ($(platform),$(filter $(platform),classic_armv7_a7 unix-armv7-hardfloat-neon)) TARGET := $(TARGET_NAME)_libretro.so fpic := -fPIC SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined @@ -149,8 +138,8 @@ else ifeq ($(platform), libnx) # Nintendo WiiU else ifeq ($(platform), wiiu) TARGET := $(TARGET_NAME)_libretro_$(platform).a - CC ?= $(DEVKITPPC)/bin/powerpc-eabi-gcc$(EXE_EXT) - AR ?= $(DEVKITPPC)/bin/powerpc-eabi-ar$(EXE_EXT) + CC = $(DEVKITPPC)/bin/powerpc-eabi-gcc$(EXE_EXT) + AR = $(DEVKITPPC)/bin/powerpc-eabi-ar$(EXE_EXT) CFLAGS += -DGEKKO -DHW_RVL -DWIIU -mwup -mcpu=750 -meabi -mhard-float -D__ppc__ -DMSB_FIRST -I$(DEVKITPRO)/libogc/include CFLAGS += -U__INT32_TYPE__ -U __UINT32_TYPE__ -D__INT32_TYPE__=int STATIC_LINKING = 1 @@ -158,10 +147,40 @@ else ifneq (,$(findstring osx,$(platform))) TARGET := $(TARGET_NAME)_libretro.dylib fpic := -fPIC SHARED := -dynamiclib + MACSOSVER = `sw_vers -productVersion | cut -d. -f 1` + OSXVER = `sw_vers -productVersion | cut -d. -f 2` + OSX_LT_MAVERICKS = `(( $(OSXVER) <= 9)) && echo "YES"` + MINVERSION := + +ifeq ($(UNIVERSAL),1) + ifeq ($(archs),ppc) + ARCHFLAGS = -arch ppc -arch ppc64 + else ifeq ($(archs),arm64) + ARCHFLAGS = -arch x86_64 -arch arm64 + else + ARCHFLAGS = -arch i386 -arch x86_64 + endif + CFLAGS += $(ARCHFLAGS) + LFLAGS += $(ARCHFLAGS) + endif + + ifeq ($(CROSS_COMPILE),1) + TARGET_RULE = -target $(LIBRETRO_APPLE_PLATFORM) -isysroot $(LIBRETRO_APPLE_ISYSROOT) + CFLAGS += $(TARGET_RULE) + CPPFLAGS += $(TARGET_RULE) + CXXFLAGS += $(TARGET_RULE) + LDFLAGS += $(TARGET_RULE) + endif + + CFLAGS += $(ARCHFLAGS) + CXXFLAGS += $(ARCHFLAGS) + LDFLAGS += $(ARCHFLAGS) + else ifneq (,$(findstring ios,$(platform))) - TARGET := $(TARGET_NAME)_libretro_ios.dylib + TARGET := $(TARGET_NAME)_libretro_ios.dylib fpic := -fPIC SHARED := -dynamiclib + MINVERSION := ifeq ($(IOSSDK),) IOSSDK := $(shell xcodebuild -version -sdk iphoneos Path) @@ -169,19 +188,30 @@ endif DEFINES := -DIOS ifeq ($(platform),ios-arm64) - CC = cc -arch armv64 -isysroot $(IOSSDK) + CC = cc -arch arm64 -isysroot $(IOSSDK) else - CC = cc -arch armv7 -isysroot $(IOSSDK) + CC = cc -arch armv7 -isysroot $(IOSSDK) endif ifeq ($(platform),$(filter $(platform),ios9 ios-arm64)) -CC += -miphoneos-version-min=8.0 -CFLAGS += -miphoneos-version-min=8.0 + MINVERSION = -miphoneos-version-min=8.0 else -CC += -miphoneos-version-min=5.0 -CFLAGS += -miphoneos-version-min=5.0 + MINVERSION = -miphoneos-version-min=5.0 endif + CFLAGS += $(MINVERSION) + +else ifeq ($(platform), tvos-arm64) + EXT?=dylib + TARGET := $(TARGET_NAME)_libretro_tvos.$(EXT) + fpic := -fPIC + SHARED := -dynamiclib + DEFINES := -DIOS +ifeq ($(IOSSDK),) + IOSSDK := $(shell xcodebuild -version -sdk appletvos Path) +endif + CC = cc -arch arm64 -isysroot $(IOSSDK) + else ifneq (,$(findstring qnx,$(platform))) - TARGET := $(TARGET_NAME)_libretro_qnx.so + TARGET := $(TARGET_NAME)_libretro_qnx.so fpic := -fPIC SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined else ifeq ($(platform), emscripten) @@ -296,7 +326,14 @@ else SHARED := -shared -static-libgcc -static-libstdc++ -s -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined endif -TARGET := $(CORE_DIR)/build/bin/$(TARGET) +ifeq ($(STATIC_LINKING), 1) +# For some reason libretro's buildbot expects the output to be at ./libretro/ for static targets +BIN ?= $(abspath $(CORE_DIR)/libretro) +else +BIN ?= $(abspath $(CORE_DIR)) +endif +TARGET := $(BIN)/$(TARGET) + # To force use of the Unix version instead of the Windows version MKDIR := $(shell which mkdir) @@ -335,7 +372,7 @@ CFLAGS += -D__LIBRETRO__ $(fpic) $(INCFLAGS) -std=gnu11 -D_GNU_SOURCE -D_USE_M all: $(TARGET) -$(CORE_DIR)/libretro/%_boot.c: $(CORE_DIR)/build/bin/BootROMs/%_boot.bin +$(CORE_DIR)/libretro/%_boot.c: $(BOOTROMS_DIR)/%_boot.bin echo "/* AUTO-GENERATED */" > $@ echo "const unsigned char $(notdir $(@:%.c=%))[] = {" >> $@ ifneq ($(findstring Haiku,$(shell uname -s)),) @@ -348,8 +385,8 @@ endif echo "};" >> $@ echo "const unsigned $(notdir $(@:%.c=%))_length = sizeof($(notdir $(@:%.c=%)));" >> $@ -$(CORE_DIR)/build/bin/BootROMs/%_boot.bin: - $(MAKE) -C $(CORE_DIR) $(patsubst $(CORE_DIR)/%,%,$@) +$(abspath $(CORE_DIR))/build/bin/BootROMs/%_boot.bin: + $(MAKE) -C $(CORE_DIR) $(patsubst $(abspath $(CORE_DIR))/%,%,$@) $(TARGET): $(OBJECTS) -@$(MKDIR) -p $(dir $@) diff --git a/libretro/Makefile.common b/libretro/Makefile.common index fabe3ad..72fcd38 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -9,7 +9,6 @@ SOURCES_C := $(CORE_DIR)/Core/gb.c \ $(CORE_DIR)/Core/mbc.c \ $(CORE_DIR)/Core/timing.c \ $(CORE_DIR)/Core/display.c \ - $(CORE_DIR)/Core/symbol_hash.c \ $(CORE_DIR)/Core/camera.c \ $(CORE_DIR)/Core/sm83_cpu.c \ $(CORE_DIR)/Core/joypad.c \ @@ -18,6 +17,8 @@ SOURCES_C := $(CORE_DIR)/Core/gb.c \ $(CORE_DIR)/Core/rumble.c \ $(CORE_DIR)/libretro/agb_boot.c \ $(CORE_DIR)/libretro/cgb_boot.c \ + $(CORE_DIR)/libretro/cgb0_boot.c \ + $(CORE_DIR)/libretro/mgb_boot.c \ $(CORE_DIR)/libretro/dmg_boot.c \ $(CORE_DIR)/libretro/sgb_boot.c \ $(CORE_DIR)/libretro/sgb2_boot.c \ diff --git a/libretro/gitlab-ci.yml b/libretro/gitlab-ci.yml new file mode 100644 index 0000000..9cb683b --- /dev/null +++ b/libretro/gitlab-ci.yml @@ -0,0 +1,182 @@ +# DESCRIPTION: GitLab CI/CD for libRetro (NOT FOR GitLab-proper) + +############################################################################## +################################# BOILERPLATE ################################ +############################################################################## + +# Core definitions +.core-defs: + variables: + JNI_PATH: libretro + MAKEFILE_PATH: libretro + CORENAME: sameboy + before_script: + - export BOOTROMS_DIR=$(pwd)/BootROMs/prebuilt + +# Inclusion templates, required for the build to work +include: + ################################## DESKTOPS ################################ + # Windows 64-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/windows-x64-mingw.yml' + + # Windows 32-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/windows-i686-mingw.yml' + + # Linux 64-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/linux-x64.yml' + + # Linux 32-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/linux-i686.yml' + + # MacOS 64-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/osx-x64.yml' + + # MacOS ARM 64-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/osx-arm64.yml' + + ################################## CELLULAR ################################ + # Android + - project: 'libretro-infrastructure/ci-templates' + file: '/android-jni.yml' + + # iOS + - project: 'libretro-infrastructure/ci-templates' + file: '/ios-arm64.yml' + + # iOS (armv7) + - project: 'libretro-infrastructure/ci-templates' + file: '/ios9.yml' + + ################################## CONSOLES ################################ + # Nintendo WiiU + - project: 'libretro-infrastructure/ci-templates' + file: '/wiiu-static.yml' + + # Nintendo Switch + - project: 'libretro-infrastructure/ci-templates' + file: '/libnx-static.yml' + + # PlayStation Vita + - project: 'libretro-infrastructure/ci-templates' + file: '/vita-static.yml' + + # tvOS (AppleTV) + - project: 'libretro-infrastructure/ci-templates' + file: '/tvos-arm64.yml' + + #################################### MISC ################################## + +# Stages for building +stages: + - build-prepare + - build-shared + - build-static + +############################################################################## +#################################### STAGES ################################## +############################################################################## +# +################################### DESKTOPS ################################# +# Windows 64-bit +libretro-build-windows-x64: + extends: + - .libretro-windows-x64-mingw-make-default + - .core-defs + +# Windows 32-bit +libretro-build-windows-i686: + extends: + - .libretro-windows-i686-mingw-make-default + - .core-defs + +# Linux 64-bit +libretro-build-linux-x64: + extends: + - .libretro-linux-x64-make-default + - .core-defs + +# Linux 32-bit +libretro-build-linux-i686: + extends: + - .libretro-linux-i686-make-default + - .core-defs + +# MacOS 64-bit +libretro-build-osx-x64: + extends: + - .libretro-osx-x64-make-10-7 + - .core-defs + +# MacOS ARM 64-bit +libretro-build-osx-arm64: + extends: + - .libretro-osx-arm64-make-default + - .core-defs + +################################### CELLULAR ################################# +# Android ARMv7a +android-armeabi-v7a: + extends: + - .libretro-android-jni-armeabi-v7a + - .core-defs + +# Android ARMv8a +android-arm64-v8a: + extends: + - .libretro-android-jni-arm64-v8a + - .core-defs + +# Android 64-bit x86 +android-x86_64: + extends: + - .libretro-android-jni-x86_64 + - .core-defs + +# Android 32-bit x86 +android-x86: + extends: + - .libretro-android-jni-x86 + - .core-defs + +# iOS +libretro-build-ios-arm64: + extends: + - .libretro-ios-arm64-make-default + - .core-defs + +# iOS (armv7) [iOS 9 and up] +libretro-build-ios9: + extends: + - .libretro-ios9-make-default + - .core-defs + +# tvOS +libretro-build-tvos-arm64: + extends: + - .libretro-tvos-arm64-make-default + - .core-defs + +################################### CONSOLES ################################# +# Nintendo WiiU +libretro-build-wiiu: + extends: + - .libretro-wiiu-static-retroarch-master + - .core-defs + +# Nintendo Switch +libretro-build-libnx-aarch64: + extends: + - .libretro-libnx-static-retroarch-master + - .core-defs + +# PlayStation Vita +libretro-build-vita: + extends: + - .libretro-vita-static-retroarch-master + - .core-defs diff --git a/libretro/jni/Android.mk b/libretro/jni/Android.mk index 8ac1b3b..d1b7f67 100644 --- a/libretro/jni/Android.mk +++ b/libretro/jni/Android.mk @@ -22,7 +22,9 @@ LOCAL_CFLAGS := -std=c99 $(COREFLAGS) $(CFLAGS) LOCAL_LDFLAGS := -Wl,-version-script=$(CORE_DIR)/libretro/link.T include $(BUILD_SHARED_LIBRARY) -$(CORE_DIR)/libretro/%_boot.c: $(CORE_DIR)/build/bin/BootROMs/%_boot.bin +override BOOTROMS_DIR := $(shell cd ../.. && realpath -m $(BOOTROMS_DIR)) + +$(CORE_DIR)/libretro/%_boot.c: $(BOOTROMS_DIR)/%_boot.bin echo "/* AUTO-GENERATED */" > $@ echo "const unsigned char $(notdir $(@:%.c=%))[] = {" >> $@ hexdump -v -e '/1 "0x%02x, "' $< >> $@ diff --git a/libretro/libretro.c b/libretro/libretro.c index f793eb4..ffb9b0e 100644 --- a/libretro/libretro.c +++ b/libretro/libretro.c @@ -34,7 +34,6 @@ static const char slash = '/'; #define MAX_VIDEO_HEIGHT 224 #define MAX_VIDEO_PIXELS (MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT) - #define RETRO_MEMORY_GAMEBOY_1_SRAM ((1 << 8) | RETRO_MEMORY_SAVE_RAM) #define RETRO_MEMORY_GAMEBOY_1_RTC ((2 << 8) | RETRO_MEMORY_RTC) #define RETRO_MEMORY_GAMEBOY_2_SRAM ((3 << 8) | RETRO_MEMORY_SAVE_RAM) @@ -42,30 +41,14 @@ static const char slash = '/'; #define RETRO_GAME_TYPE_GAMEBOY_LINK_2P 0x101 -char battery_save_path[512]; -char symbols_path[512]; - -enum model { - MODEL_DMG_B, - MODEL_CGB_C, - MODEL_CGB_E, - MODEL_AGB, - MODEL_SGB_PAL, - MODEL_SGB_NTSC, - MODEL_SGB2, - MODEL_AUTO +enum rom_type { + ROM_TYPE_INVALID, + ROM_TYPE_DMG, + ROM_TYPE_SGB, + ROM_TYPE_CGB }; -static const GB_model_t libretro_to_internal_model[] = -{ - [MODEL_DMG_B] = GB_MODEL_DMG_B, - [MODEL_CGB_C] = GB_MODEL_CGB_C, - [MODEL_CGB_E] = GB_MODEL_CGB_E, - [MODEL_AGB] = GB_MODEL_AGB_A, - [MODEL_SGB_PAL] = GB_MODEL_SGB_PAL, - [MODEL_SGB_NTSC] = GB_MODEL_SGB_NTSC, - [MODEL_SGB2] = GB_MODEL_SGB2 -}; +#define GB_MODEL_AUTO (-1) enum screen_layout { LAYOUT_TOP_DOWN, @@ -77,16 +60,32 @@ enum audio_out { GB_2 }; -static enum model model[2]; -static enum model auto_model = MODEL_CGB_E; +static GB_model_t model[2] = { + GB_MODEL_DMG_B, + GB_MODEL_DMG_B +}; +static GB_model_t auto_model[2] = { + GB_MODEL_CGB_E, + GB_MODEL_CGB_E +}; +static GB_model_t auto_sgb_model[2] = { + GB_MODEL_SGB_NTSC, + GB_MODEL_SGB_NTSC +}; +static bool auto_sgb_enabled[2] = { + false, + false +}; static uint32_t *frame_buf = NULL; static uint32_t *frame_buf_copy = NULL; +static uint32_t retained_frame_1[256 * 224]; +static uint32_t retained_frame_2[256 * 224]; static struct retro_log_callback logging; static retro_log_printf_t log_cb; static retro_video_refresh_t video_cb; -static retro_audio_sample_t audio_sample_cb; +static retro_audio_sample_batch_t audio_batch_cb; static retro_input_poll_t input_poll_cb; static retro_input_state_t input_state_cb; @@ -101,16 +100,18 @@ static bool geometry_updated = false; static bool link_cable_emulation = false; /*static bool infrared_emulation = false;*/ -signed short soundbuf[1024 * 2]; +static struct { + int16_t *data; + int32_t size; + int32_t capacity; +} output_audio_buffer = {NULL, 0, 0}; char retro_system_directory[4096]; -char retro_save_directory[4096]; -char retro_game_path[4096]; GB_gameboy_t gameboy[2]; -extern const unsigned char dmg_boot[], cgb_boot[], agb_boot[], sgb_boot[], sgb2_boot[]; -extern const unsigned dmg_boot_length, cgb_boot_length, agb_boot_length, sgb_boot_length, sgb2_boot_length; +extern const unsigned char dmg_boot[], mgb_boot[], cgb0_boot[], cgb_boot[], agb_boot[], sgb_boot[], sgb2_boot[]; +extern const unsigned dmg_boot_length, mgb_boot_length, cgb0_boot_length, cgb_boot_length, agb_boot_length, sgb_boot_length, sgb2_boot_length; bool vblank1_occurred = false, vblank2_occurred = false; static void fallback_log(enum retro_log_level level, const char *fmt, ...) @@ -174,24 +175,99 @@ static void rumble_callback(GB_gameboy_t *gb, double amplitude) } } -static void audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) +static void ensure_output_audio_buffer_capacity(int32_t capacity) { - if ((audio_out == GB_1 && gb == &gameboy[0]) || - (audio_out == GB_2 && gb == &gameboy[1])) { - audio_sample_cb(sample->left, sample->right); + if (capacity <= output_audio_buffer.capacity) { + return; } + output_audio_buffer.data = realloc( + output_audio_buffer.data, capacity * sizeof(*output_audio_buffer.data)); + output_audio_buffer.capacity = capacity; + log_cb(RETRO_LOG_DEBUG, "Output audio buffer capacity set to %d\n", capacity); } -static void vblank1(GB_gameboy_t *gb) +static void init_output_audio_buffer(int32_t capacity) { + output_audio_buffer.data = NULL; + output_audio_buffer.size = 0; + output_audio_buffer.capacity = 0; + ensure_output_audio_buffer_capacity(capacity); +} + +static void free_output_audio_buffer() +{ + free(output_audio_buffer.data); + output_audio_buffer.data = NULL; + output_audio_buffer.size = 0; + output_audio_buffer.capacity = 0; +} + +static void upload_output_audio_buffer() +{ + int32_t remaining_frames = output_audio_buffer.size / 2; + int16_t *buf_pos = output_audio_buffer.data; + + while (remaining_frames > 0) { + size_t uploaded_frames = audio_batch_cb(buf_pos, remaining_frames); + buf_pos += uploaded_frames * 2; + remaining_frames -= uploaded_frames; + } + output_audio_buffer.size = 0; +} + +static void audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) +{ + if (!(audio_out == GB_1 && gb == &gameboy[0]) && + !(audio_out == GB_2 && gb == &gameboy[1])) { + return; + } + + if (output_audio_buffer.capacity - output_audio_buffer.size < 2) { + ensure_output_audio_buffer_capacity(output_audio_buffer.capacity * 1.5); + } + + output_audio_buffer.data[output_audio_buffer.size++] = sample->left; + output_audio_buffer.data[output_audio_buffer.size++] = sample->right; +} + +static void vblank1(GB_gameboy_t *gb, GB_vblank_type_t type) +{ + if (type == GB_VBLANK_TYPE_REPEAT) { + memcpy(GB_get_pixels_output(gb), + retained_frame_1, + GB_get_screen_width(gb) * GB_get_screen_height(gb) * sizeof(uint32_t)); + } vblank1_occurred = true; } -static void vblank2(GB_gameboy_t *gb) +static void vblank2(GB_gameboy_t *gb, GB_vblank_type_t type) { + if (type == GB_VBLANK_TYPE_REPEAT) { + memcpy(GB_get_pixels_output(gb), + retained_frame_2, + GB_get_screen_width(gb) * GB_get_screen_height(gb) * sizeof(uint32_t)); + } vblank2_occurred = true; } +static void lcd_status_change_1(GB_gameboy_t *gb, bool on) +{ + if (!on) { + memcpy(retained_frame_1, + GB_get_pixels_output(gb), + GB_get_screen_width(gb) * GB_get_screen_height(gb) * sizeof(uint32_t)); + } +} + +static void lcd_status_change_2(GB_gameboy_t *gb, bool on) +{ + if (!on) { + memcpy(retained_frame_2, + GB_get_pixels_output(gb), + GB_get_screen_width(gb) * GB_get_screen_height(gb) * sizeof(uint32_t)); + } +} + static bool bit_to_send1 = true, bit_to_send2 = true; static void serial_start1(GB_gameboy_t *gb, bool bit_received) @@ -263,6 +339,7 @@ static void set_variable_visibility(void) for (i = 0; i < num_options; i++) { const char *key = option_defs_us[i].key; if ((strcmp(key, "sameboy_model") == 0) || + (strcmp(key, "sameboy_auto_sgb_model") == 0) || (strcmp(key, "sameboy_rtc") == 0) || (strcmp(key, "sameboy_scaling_filter") == 0) || (strcmp(key, "sameboy_mono_palette") == 0) || @@ -275,11 +352,13 @@ static void set_variable_visibility(void) option_display_singlecart.key = key; environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display_singlecart); } - else if ((strcmp(key, "sameboy_link") == 0) || + else if ((strcmp(key, "sameboy_link") == 0) || (strcmp(key, "sameboy_screen_layout") == 0) || (strcmp(key, "sameboy_audio_output") == 0) || (strcmp(key, "sameboy_model_1") == 0) || + (strcmp(key, "sameboy_auto_sgb_model_1") == 0) || (strcmp(key, "sameboy_model_2") == 0) || + (strcmp(key, "sameboy_auto_sgb_model_2") == 0) || (strcmp(key, "sameboy_mono_palette_1") == 0) || (strcmp(key, "sameboy_mono_palette_2") == 0) || (strcmp(key, "sameboy_color_correction_mode_1") == 0) || @@ -309,8 +388,8 @@ static const struct retro_subsystem_memory_info gb2_memory[] = { }; static const struct retro_subsystem_rom_info gb_roms[] = { - { "GameBoy #1", "gb|gbc", true, false, true, gb1_memory, 1 }, - { "GameBoy #2", "gb|gbc", true, false, true, gb2_memory, 1 }, + { "GameBoy #1", "gb|gbc", false, false, true, gb1_memory, 1 }, + { "GameBoy #2", "gb|gbc", false, false, true, gb2_memory, 1 }, }; static const struct retro_subsystem_info subsystems[] = { @@ -425,16 +504,18 @@ static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) [GB_BOOT_ROM_SGB2] = "sgb2", [GB_BOOT_ROM_CGB_0] = "cgb0", [GB_BOOT_ROM_CGB] = "cgb", + [GB_BOOT_ROM_CGB_E] = "cgbE", + [GB_BOOT_ROM_AGB_0] = "agb0", [GB_BOOT_ROM_AGB] = "agb", }[type]; const uint8_t *boot_code = (const unsigned char *[]) { [GB_BOOT_ROM_DMG_0] = dmg_boot, // DMG_0 not implemented yet [GB_BOOT_ROM_DMG] = dmg_boot, - [GB_BOOT_ROM_MGB] = dmg_boot, // mgb not implemented yet + [GB_BOOT_ROM_MGB] = mgb_boot, [GB_BOOT_ROM_SGB] = sgb_boot, [GB_BOOT_ROM_SGB2] = sgb2_boot, - [GB_BOOT_ROM_CGB_0] = cgb_boot, // CGB_0 not implemented yet + [GB_BOOT_ROM_CGB_0] = cgb0_boot, [GB_BOOT_ROM_CGB] = cgb_boot, [GB_BOOT_ROM_AGB] = agb_boot, }[type]; @@ -442,20 +523,28 @@ static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) unsigned boot_length = (unsigned []) { [GB_BOOT_ROM_DMG_0] = dmg_boot_length, // DMG_0 not implemented yet [GB_BOOT_ROM_DMG] = dmg_boot_length, - [GB_BOOT_ROM_MGB] = dmg_boot_length, // mgb not implemented yet + [GB_BOOT_ROM_MGB] = mgb_boot_length, [GB_BOOT_ROM_SGB] = sgb_boot_length, [GB_BOOT_ROM_SGB2] = sgb2_boot_length, - [GB_BOOT_ROM_CGB_0] = cgb_boot_length, // CGB_0 not implemented yet + [GB_BOOT_ROM_CGB_0] = cgb0_boot_length, [GB_BOOT_ROM_CGB] = cgb_boot_length, [GB_BOOT_ROM_AGB] = agb_boot_length, }[type]; - char buf[256]; + char buf[4096 + 1 + 4 + 9 + 1]; snprintf(buf, sizeof(buf), "%s%c%s_boot.bin", retro_system_directory, slash, model_name); log_cb(RETRO_LOG_INFO, "Initializing as model: %s\n", model_name); log_cb(RETRO_LOG_INFO, "Loading boot image: %s\n", buf); if (GB_load_boot_rom(gb, buf)) { + if (type == GB_BOOT_ROM_CGB_E) { + boot_rom_load(gb, GB_BOOT_ROM_CGB); + return; + } + if (type == GB_BOOT_ROM_AGB_0) { + boot_rom_load(gb, GB_BOOT_ROM_AGB); + return; + } GB_load_boot_rom_from_buffer(gb, boot_code, boot_length); } } @@ -530,20 +619,21 @@ static void retro_set_memory_maps(void) static void init_for_current_model(unsigned id) { unsigned i = id; - enum model effective_model; + GB_model_t effective_model; effective_model = model[i]; - if (effective_model == MODEL_AUTO) { - effective_model = auto_model; + if (effective_model == GB_MODEL_AUTO) { + effective_model = auto_model[i]; } - if (GB_is_inited(&gameboy[i])) { - GB_switch_model_and_reset(&gameboy[i], libretro_to_internal_model[effective_model]); + GB_switch_model_and_reset(&gameboy[i], effective_model); + retro_set_memory_maps(); } else { - GB_init(&gameboy[i], libretro_to_internal_model[effective_model]); + GB_init(&gameboy[i], effective_model); } + geometry_updated = true; GB_set_boot_rom_load_callback(&gameboy[i], boot_rom_load); @@ -558,15 +648,17 @@ static void init_for_current_model(unsigned id) /* todo: attempt to make these more generic */ GB_set_vblank_callback(&gameboy[0], (GB_vblank_callback_t) vblank1); + GB_set_lcd_status_callback(&gameboy[0], lcd_status_change_1); if (emulated_devices == 2) { GB_set_vblank_callback(&gameboy[1], (GB_vblank_callback_t) vblank2); + GB_set_lcd_status_callback(&gameboy[1], lcd_status_change_2); if (link_cable_emulation) { set_link_cable_state(true); } } /* Let's be extremely nitpicky about how devices and descriptors are set */ - if (emulated_devices == 1 && (model[0] == MODEL_SGB_PAL || model[0] == MODEL_SGB_NTSC || model[0] == MODEL_SGB2)) { + if (emulated_devices == 1 && (model[0] == GB_MODEL_SGB_PAL || model[0] == GB_MODEL_SGB_NTSC || model[0] == GB_MODEL_SGB2)) { static const struct retro_controller_info ports[] = { { controllers_sgb, 1 }, { controllers_sgb, 1 }, @@ -596,6 +688,34 @@ static void init_for_current_model(unsigned id) } } +static GB_model_t string_to_model(const char *string) +{ + static const struct { + const char *name; + GB_model_t model; + } models[] = { + { "Game Boy", GB_MODEL_DMG_B}, + { "Game Boy Pocket", GB_MODEL_MGB}, + { "Game Boy Color 0", GB_MODEL_CGB_0}, + { "Game Boy Color A", GB_MODEL_CGB_A}, + { "Game Boy Color B", GB_MODEL_CGB_B}, + { "Game Boy Color C", GB_MODEL_CGB_C}, + { "Game Boy Color D", GB_MODEL_CGB_D}, + { "Game Boy Color", GB_MODEL_CGB_E}, + { "Game Boy Advance", GB_MODEL_AGB_A}, + { "Game Boy Player", GB_MODEL_GBP_A}, + { "Super Game Boy", GB_MODEL_SGB_NTSC}, + { "Super Game Boy PAL", GB_MODEL_SGB_PAL}, + { "Super Game Boy 2", GB_MODEL_SGB2}, + }; + for (unsigned i = 0; i < sizeof(models) / sizeof(models[0]); i++) { + if (strcmp(models[i].name, string) == 0) { + return models[i].model; + } + } + return GB_MODEL_AUTO; +} + static void check_variables() { struct retro_variable var = {0}; @@ -603,36 +723,43 @@ static void check_variables() var.key = "sameboy_model"; var.value = NULL; + + model[0] = GB_MODEL_AUTO; + auto_sgb_enabled[0] = false; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - enum model new_model = model[0]; - if (strcmp(var.value, "Game Boy") == 0) { - new_model = MODEL_DMG_B; - } - else if (strcmp(var.value, "Game Boy Color C") == 0) { - new_model = MODEL_CGB_C; - } - else if (strcmp(var.value, "Game Boy Color") == 0) { - new_model = MODEL_CGB_E; - } - else if (strcmp(var.value, "Game Boy Advance") == 0) { - new_model = MODEL_AGB; - } - else if (strcmp(var.value, "Super Game Boy") == 0) { - new_model = MODEL_SGB_NTSC; - } - else if (strcmp(var.value, "Super Game Boy PAL") == 0) { - new_model = MODEL_SGB_PAL; - } - else if (strcmp(var.value, "Super Game Boy 2") == 0) { - new_model = MODEL_SGB2; - } - else { - new_model = MODEL_AUTO; + GB_model_t new_model = model[0]; + new_model = string_to_model(var.value); + if (new_model == GB_MODEL_AUTO) { + if (strcmp(var.value, "Auto (SGB)") == 0) { + new_model = GB_MODEL_AUTO; + auto_sgb_enabled[0] = true; + } } model[0] = new_model; } + var.key = "sameboy_auto_sgb_model"; + var.value = NULL; + + auto_sgb_model[0] = GB_MODEL_SGB_NTSC; + + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + GB_model_t new_model = auto_sgb_model[0]; + if (strcmp(var.value, "Super Game Boy PAL") == 0) { + new_model = GB_MODEL_SGB_PAL; + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { + new_model = GB_MODEL_SGB2; + } + else { + new_model = GB_MODEL_SGB_NTSC; + } + + auto_sgb_model[0] = new_model; + } + var.key = "sameboy_rtc"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { @@ -671,10 +798,10 @@ static void check_variables() GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_CORRECT_CURVES); } else if (strcmp(var.value, "emulate hardware") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_EMULATE_HARDWARE); + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_MODERN_BALANCED); } else if (strcmp(var.value, "preserve brightness") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST); } else if (strcmp(var.value, "reduce contrast") == 0) { GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_REDUCE_CONTRAST); @@ -682,6 +809,9 @@ static void check_variables() else if (strcmp(var.value, "harsh reality") == 0) { GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_LOW_CONTRAST); } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_MODERN_ACCURATE); + } } var.key = "sameboy_light_temperature"; @@ -790,66 +920,81 @@ static void check_variables() var.key = "sameboy_model_1"; var.value = NULL; + + model[0] = GB_MODEL_AUTO; + auto_sgb_enabled[0] = false; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - enum model new_model = model[0]; - if (strcmp(var.value, "Game Boy") == 0) { - new_model = MODEL_DMG_B; + GB_model_t new_model = model[0]; + new_model = string_to_model(var.value); + if (new_model == GB_MODEL_AUTO) { + if (strcmp(var.value, "Auto (SGB)") == 0) { + new_model = GB_MODEL_AUTO; + auto_sgb_enabled[0] = true; + } } - else if (strcmp(var.value, "Game Boy Color C") == 0) { - new_model = MODEL_CGB_C; - } - else if (strcmp(var.value, "Game Boy Color") == 0) { - new_model = MODEL_CGB_E; - } - else if (strcmp(var.value, "Game Boy Advance") == 0) { - new_model = MODEL_AGB; - } - else if (strcmp(var.value, "Super Game Boy") == 0) { - new_model = MODEL_SGB_NTSC; - } - else if (strcmp(var.value, "Super Game Boy PAL") == 0) { - new_model = MODEL_SGB_PAL; + + model[0] = new_model; + } + + + var.key = "sameboy_auto_sgb_model_1"; + var.value = NULL; + + auto_sgb_model[0] = GB_MODEL_SGB_NTSC; + + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + GB_model_t new_model = auto_sgb_model[0]; + if (strcmp(var.value, "Super Game Boy PAL") == 0) { + new_model = GB_MODEL_SGB_PAL; } else if (strcmp(var.value, "Super Game Boy 2") == 0) { - new_model = MODEL_SGB2; + new_model = GB_MODEL_SGB2; } else { - new_model = MODEL_AUTO; + new_model = GB_MODEL_SGB_NTSC; } - model[0] = new_model; + auto_sgb_model[0] = new_model; } var.key = "sameboy_model_2"; var.value = NULL; + + model[1] = GB_MODEL_AUTO; + auto_sgb_enabled[1] = false; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - enum model new_model = model[1]; - if (strcmp(var.value, "Game Boy") == 0) { - new_model = MODEL_DMG_B; + GB_model_t new_model = model[1]; + new_model = string_to_model(var.value); + if (new_model == GB_MODEL_AUTO) { + if (strcmp(var.value, "Auto (SGB)") == 0) { + new_model = GB_MODEL_AUTO; + auto_sgb_enabled[0] = true; + } } - else if (strcmp(var.value, "Game Boy Color C") == 0) { - new_model = MODEL_CGB_C; - } - else if (strcmp(var.value, "Game Boy Color") == 0) { - new_model = MODEL_CGB_E; - } - else if (strcmp(var.value, "Game Boy Advance") == 0) { - new_model = MODEL_AGB; - } - else if (strcmp(var.value, "Super Game Boy") == 0) { - new_model = MODEL_SGB_NTSC; - } - else if (strcmp(var.value, "Super Game Boy PAL") == 0) { - new_model = MODEL_SGB_PAL; + + model[1] = new_model; + } + + var.key = "sameboy_auto_sgb_model_2"; + var.value = NULL; + + auto_sgb_model[1] = GB_MODEL_SGB_NTSC; + + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + GB_model_t new_model = auto_sgb_model[1]; + if (strcmp(var.value, "Super Game Boy PAL") == 0) { + new_model = GB_MODEL_SGB_PAL; } else if (strcmp(var.value, "Super Game Boy 2") == 0) { - new_model = MODEL_SGB2; + new_model = GB_MODEL_SGB2; } else { - new_model = MODEL_AUTO; + new_model = GB_MODEL_SGB_NTSC; } - model[1] = new_model; + auto_sgb_model[1] = new_model; } var.key = "sameboy_mono_palette_1"; @@ -896,10 +1041,10 @@ static void check_variables() GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_CORRECT_CURVES); } else if (strcmp(var.value, "emulate hardware") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_EMULATE_HARDWARE); + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_MODERN_BALANCED); } else if (strcmp(var.value, "preserve brightness") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST); } else if (strcmp(var.value, "reduce contrast") == 0) { GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_REDUCE_CONTRAST); @@ -907,6 +1052,9 @@ static void check_variables() else if (strcmp(var.value, "harsh reality") == 0) { GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_LOW_CONTRAST); } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_MODERN_ACCURATE); + } } var.key = "sameboy_color_correction_mode_2"; @@ -919,10 +1067,10 @@ static void check_variables() GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_CORRECT_CURVES); } else if (strcmp(var.value, "emulate hardware") == 0) { - GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_EMULATE_HARDWARE); + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_MODERN_BALANCED); } else if (strcmp(var.value, "preserve brightness") == 0) { - GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST); } else if (strcmp(var.value, "reduce contrast") == 0) { GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_REDUCE_CONTRAST); @@ -930,6 +1078,9 @@ static void check_variables() else if (strcmp(var.value, "harsh reality") == 0) { GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_LOW_CONTRAST); } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_MODERN_ACCURATE); + } } var.key = "sameboy_light_temperature_1"; @@ -1027,13 +1178,6 @@ void retro_init(void) snprintf(retro_system_directory, sizeof(retro_system_directory), "%s", "."); } - if (environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &dir) && dir) { - snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", dir); - } - else { - snprintf(retro_save_directory, sizeof(retro_save_directory), "%s", "."); - } - if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &logging)) { log_cb = logging.log; } @@ -1044,6 +1188,8 @@ void retro_init(void) if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL)) { libretro_supports_bitmasks = true; } + + init_output_audio_buffer(16384); } void retro_deinit(void) @@ -1053,6 +1199,8 @@ void retro_deinit(void) frame_buf = NULL; frame_buf_copy = NULL; + free_output_audio_buffer(); + libretro_supports_bitmasks = false; } @@ -1075,7 +1223,7 @@ void retro_get_system_info(struct retro_system_info *info) #else info->library_version = GB_VERSION; #endif - info->need_fullpath = true; + info->need_fullpath = false; info->valid_extensions = "gb|gbc"; } @@ -1109,23 +1257,25 @@ void retro_get_system_av_info(struct retro_system_av_info *info) info->timing = timing; } - void retro_set_environment(retro_environment_t cb) { + bool categories_supported; + environ_cb = cb; - libretro_set_core_options(environ_cb); + libretro_set_core_options(environ_cb, &categories_supported); - cb(RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO, (void*)subsystems); + environ_cb(RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO, (void*)subsystems); } void retro_set_audio_sample(retro_audio_sample_t cb) { - audio_sample_cb = cb; + (void)cb; } void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { + audio_batch_cb = cb; } void retro_set_input_poll(retro_input_poll_t cb) @@ -1183,7 +1333,7 @@ void retro_run(void) GB_update_keys_status(&gameboy[0], 0); GB_update_keys_status(&gameboy[1], 1); } - else if (emulated_devices == 1 && (model[0] == MODEL_SGB_PAL || model[0] == MODEL_SGB_NTSC || model[0] == MODEL_SGB2)) { + else if (emulated_devices == 1 && (model[0] == GB_MODEL_SGB_PAL || model[0] == GB_MODEL_SGB_NTSC || model[0] == GB_MODEL_SGB2)) { for (unsigned i = 0; i < 4; i++) { GB_update_keys_status(&gameboy[0], i); } @@ -1236,32 +1386,80 @@ void retro_run(void) GB_get_screen_width(&gameboy[0]) * sizeof(uint32_t)); } - + upload_output_audio_buffer(); initialized = true; } +static enum rom_type check_rom_header(const uint8_t *data, size_t size) +{ + enum rom_type type; + uint8_t cgb_flag; + uint8_t sgb_flag; + + if (!data || (size < 0x146 + 1)) { + return ROM_TYPE_INVALID; + } + + type = ROM_TYPE_DMG; + cgb_flag = data[0x143]; + sgb_flag = data[0x146]; + + if ((cgb_flag == 0x80) || (cgb_flag == 0xC0)) { + type = ROM_TYPE_CGB; + } + + if ((type == ROM_TYPE_DMG) && (sgb_flag == 0x03)) { + type = ROM_TYPE_SGB; + } + + return type; +} + bool retro_load_game(const struct retro_game_info *info) { + enum rom_type content_type = ROM_TYPE_INVALID; + const uint8_t *content_data = NULL; + size_t content_size; + + if (info) { + content_data = (const uint8_t *)info->data; + content_size = info->size; + content_type = check_rom_header(content_data, content_size); + } + check_variables(); + switch (content_type) { + case ROM_TYPE_DMG: + auto_model[0] = GB_MODEL_DMG_B; + auto_model[1] = GB_MODEL_DMG_B; + break; + case ROM_TYPE_SGB: + auto_model[0] = auto_sgb_enabled[0] ? auto_sgb_model[0] : GB_MODEL_DMG_B; + auto_model[1] = auto_sgb_enabled[1] ? auto_sgb_model[1] : GB_MODEL_DMG_B; + break; + case ROM_TYPE_CGB: + auto_model[0] = GB_MODEL_CGB_E; + auto_model[1] = GB_MODEL_CGB_E; + break; + case ROM_TYPE_INVALID: + default: + log_cb(RETRO_LOG_ERROR, "Invalid content\n"); + return false; + } + frame_buf = (uint32_t *)malloc(MAX_VIDEO_PIXELS * emulated_devices * sizeof(uint32_t)); memset(frame_buf, 0, MAX_VIDEO_PIXELS * emulated_devices * sizeof(uint32_t)); enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888; if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { - log_cb(RETRO_LOG_INFO, "XRGB8888 is not supported\n"); + log_cb(RETRO_LOG_ERROR, "XRGB8888 is not supported\n"); return false; } - auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB_E : MODEL_DMG_B; - snprintf(retro_game_path, sizeof(retro_game_path), "%s", info->path); - for (int i = 0; i < emulated_devices; i++) { init_for_current_model(i); - if (GB_load_rom(&gameboy[i], info->path)) { - log_cb(RETRO_LOG_INFO, "Failed to load ROM at %s\n", info->path); - return false; - } + GB_load_rom_from_buffer(&gameboy[i], content_data, content_size); } bool achievements = true; @@ -1296,8 +1494,7 @@ unsigned retro_get_region(void) bool retro_load_game_special(unsigned type, const struct retro_game_info *info, size_t num_info) { - - if (type == RETRO_GAME_TYPE_GAMEBOY_LINK_2P) { + if ((type == RETRO_GAME_TYPE_GAMEBOY_LINK_2P) && (num_info >= 2)) { emulated_devices = 2; } else { @@ -1314,19 +1511,35 @@ bool retro_load_game_special(unsigned type, const struct retro_game_info *info, enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888; if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { - log_cb(RETRO_LOG_INFO, "XRGB8888 is not supported\n"); + log_cb(RETRO_LOG_ERROR, "XRGB8888 is not supported\n"); return false; } - auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB_E : MODEL_DMG_B; - snprintf(retro_game_path, sizeof(retro_game_path), "%s", info->path); - for (int i = 0; i < emulated_devices; i++) { - init_for_current_model(i); - if (GB_load_rom(&gameboy[i], info[i].path)) { - log_cb(RETRO_LOG_INFO, "Failed to load ROM\n"); - return false; + enum rom_type content_type = ROM_TYPE_INVALID; + const uint8_t *content_data = info[i].data; + size_t content_size = info[i].size; + + content_type = check_rom_header(content_data, content_size); + + switch (content_type) { + case ROM_TYPE_DMG: + auto_model[i] = GB_MODEL_DMG_B; + break; + case ROM_TYPE_SGB: + auto_model[i] = auto_sgb_enabled[i] ? auto_sgb_model[i] : GB_MODEL_DMG_B; + break; + case ROM_TYPE_CGB: + auto_model[i] = GB_MODEL_CGB_E; + break; + case ROM_TYPE_INVALID: + default: + log_cb(RETRO_LOG_ERROR, "Invalid content\n"); + return false; } + + init_for_current_model(i); + GB_load_rom_from_buffer(&gameboy[i], content_data, content_size); } bool achievements = true; @@ -1559,4 +1772,3 @@ void retro_cheat_set(unsigned index, bool enabled, const char *code) (void)enabled; (void)code; } - diff --git a/libretro/libretro.h b/libretro/libretro.h index e6ee626..4f4db1c 100644 --- a/libretro/libretro.h +++ b/libretro/libretro.h @@ -1388,6 +1388,363 @@ enum retro_mod * fastforwarding state will occur in this case). */ +#define RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE 65 + /* const struct retro_system_content_info_override * -- + * Allows an implementation to override 'global' content + * info parameters reported by retro_get_system_info(). + * Overrides also affect subsystem content info parameters + * set via RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO. + * This function must be called inside retro_set_environment(). + * If callback returns false, content info overrides + * are unsupported by the frontend, and will be ignored. + * If callback returns true, extended game info may be + * retrieved by calling RETRO_ENVIRONMENT_GET_GAME_INFO_EXT + * in retro_load_game() or retro_load_game_special(). + * + * 'data' points to an array of retro_system_content_info_override + * structs terminated by a { NULL, false, false } element. + * If 'data' is NULL, no changes will be made to the frontend; + * a core may therefore pass NULL in order to test whether + * the RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE and + * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT callbacks are supported + * by the frontend. + * + * For struct member descriptions, see the definition of + * struct retro_system_content_info_override. + * + * Example: + * + * - struct retro_system_info: + * { + * "My Core", // library_name + * "v1.0", // library_version + * "m3u|md|cue|iso|chd|sms|gg|sg", // valid_extensions + * true, // need_fullpath + * false // block_extract + * } + * + * - Array of struct retro_system_content_info_override: + * { + * { + * "md|sms|gg", // extensions + * false, // need_fullpath + * true // persistent_data + * }, + * { + * "sg", // extensions + * false, // need_fullpath + * false // persistent_data + * }, + * { NULL, false, false } + * } + * + * Result: + * - Files of type m3u, cue, iso, chd will not be + * loaded by the frontend. Frontend will pass a + * valid path to the core, and core will handle + * loading internally + * - Files of type md, sms, gg will be loaded by + * the frontend. A valid memory buffer will be + * passed to the core. This memory buffer will + * remain valid until retro_deinit() returns + * - Files of type sg will be loaded by the frontend. + * A valid memory buffer will be passed to the core. + * This memory buffer will remain valid until + * retro_load_game() (or retro_load_game_special()) + * returns + * + * NOTE: If an extension is listed multiple times in + * an array of retro_system_content_info_override + * structs, only the first instance will be registered + */ + +#define RETRO_ENVIRONMENT_GET_GAME_INFO_EXT 66 + /* const struct retro_game_info_ext ** -- + * Allows an implementation to fetch extended game + * information, providing additional content path + * and memory buffer status details. + * This function may only be called inside + * retro_load_game() or retro_load_game_special(). + * If callback returns false, extended game information + * is unsupported by the frontend. In this case, only + * regular retro_game_info will be available. + * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT is guaranteed + * to return true if RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE + * returns true. + * + * 'data' points to an array of retro_game_info_ext structs. + * + * For struct member descriptions, see the definition of + * struct retro_game_info_ext. + * + * - If function is called inside retro_load_game(), + * the retro_game_info_ext array is guaranteed to + * have a size of 1 - i.e. the returned pointer may + * be used to access directly the members of the + * first retro_game_info_ext struct, for example: + * + * struct retro_game_info_ext *game_info_ext; + * if (environ_cb(RETRO_ENVIRONMENT_GET_GAME_INFO_EXT, &game_info_ext)) + * printf("Content Directory: %s\n", game_info_ext->dir); + * + * - If the function is called inside retro_load_game_special(), + * the retro_game_info_ext array is guaranteed to have a + * size equal to the num_info argument passed to + * retro_load_game_special() + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 67 + /* const struct retro_core_options_v2 * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION + * returns an API version of >= 2. + * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * If RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION returns an API + * version of >= 2, this callback is guaranteed to succeed + * (i.e. callback return value does not indicate success) + * If callback returns true, frontend has core option category + * support. + * If callback returns false, frontend does not have core option + * category support. + * + * 'data' points to a retro_core_options_v2 struct, containing + * of two pointers: + * - retro_core_options_v2::categories is an array of + * retro_core_option_v2_category structs terminated by a + * { NULL, NULL, NULL } element. If retro_core_options_v2::categories + * is NULL, all core options will have no category and will be shown + * at the top level of the frontend core option interface. If frontend + * does not have core option category support, categories array will + * be ignored. + * - retro_core_options_v2::definitions is an array of + * retro_core_option_v2_definition structs terminated by a + * { NULL, NULL, NULL, NULL, NULL, NULL, {{0}}, NULL } + * element. + * + * >> retro_core_option_v2_category notes: + * + * - retro_core_option_v2_category::key should contain string + * that uniquely identifies the core option category. Valid + * key characters are [a-z, A-Z, 0-9, _, -] + * Namespace collisions with other implementations' category + * keys are permitted. + * - retro_core_option_v2_category::desc should contain a human + * readable description of the category key. + * - retro_core_option_v2_category::info should contain any + * additional human readable information text that a typical + * user may need to understand the nature of the core option + * category. + * + * Example entry: + * { + * "advanced_settings", + * "Advanced", + * "Options affecting low-level emulation performance and accuracy." + * } + * + * >> retro_core_option_v2_definition notes: + * + * - retro_core_option_v2_definition::key should be namespaced to not + * collide with other implementations' keys. e.g. A core called + * 'foo' should use keys named as 'foo_option'. Valid key characters + * are [a-z, A-Z, 0-9, _, -]. + * - retro_core_option_v2_definition::desc should contain a human readable + * description of the key. Will be used when the frontend does not + * have core option category support. Examples: "Aspect Ratio" or + * "Video > Aspect Ratio". + * - retro_core_option_v2_definition::desc_categorized should contain a + * human readable description of the key, which will be used when + * frontend has core option category support. Example: "Aspect Ratio", + * where associated retro_core_option_v2_category::desc is "Video". + * If empty or NULL, the string specified by + * retro_core_option_v2_definition::desc will be used instead. + * retro_core_option_v2_definition::desc_categorized will be ignored + * if retro_core_option_v2_definition::category_key is empty or NULL. + * - retro_core_option_v2_definition::info should contain any additional + * human readable information text that a typical user may need to + * understand the functionality of the option. + * - retro_core_option_v2_definition::info_categorized should contain + * any additional human readable information text that a typical user + * may need to understand the functionality of the option, and will be + * used when frontend has core option category support. This is provided + * to accommodate the case where info text references an option by + * name/desc, and the desc/desc_categorized text for that option differ. + * If empty or NULL, the string specified by + * retro_core_option_v2_definition::info will be used instead. + * retro_core_option_v2_definition::info_categorized will be ignored + * if retro_core_option_v2_definition::category_key is empty or NULL. + * - retro_core_option_v2_definition::category_key should contain a + * category identifier (e.g. "video" or "audio") that will be + * assigned to the core option if frontend has core option category + * support. A categorized option will be shown in a subsection/ + * submenu of the frontend core option interface. If key is empty + * or NULL, or if key does not match one of the + * retro_core_option_v2_category::key values in the associated + * retro_core_option_v2_category array, option will have no category + * and will be shown at the top level of the frontend core option + * interface. + * - retro_core_option_v2_definition::values is an array of + * retro_core_option_value structs terminated by a { NULL, NULL } + * element. + * --> retro_core_option_v2_definition::values[index].value is an + * expected option value. + * --> retro_core_option_v2_definition::values[index].label is a + * human readable label used when displaying the value on screen. + * If NULL, the value itself is used. + * - retro_core_option_v2_definition::default_value is the default + * core option setting. It must match one of the expected option + * values in the retro_core_option_v2_definition::values array. If + * it does not, or the default value is NULL, the first entry in the + * retro_core_option_v2_definition::values array is treated as the + * default. + * + * The number of possible option values should be very limited, + * and must be less than RETRO_NUM_CORE_OPTION_VALUES_MAX. + * i.e. it should be feasible to cycle through options + * without a keyboard. + * + * Example entries: + * + * - Uncategorized: + * + * { + * "foo_option", + * "Speed hack coprocessor X", + * NULL, + * "Provides increased performance at the expense of reduced accuracy.", + * NULL, + * NULL, + * { + * { "false", NULL }, + * { "true", NULL }, + * { "unstable", "Turbo (Unstable)" }, + * { NULL, NULL }, + * }, + * "false" + * } + * + * - Categorized: + * + * { + * "foo_option", + * "Advanced > Speed hack coprocessor X", + * "Speed hack coprocessor X", + * "Setting 'Advanced > Speed hack coprocessor X' to 'true' or 'Turbo' provides increased performance at the expense of reduced accuracy", + * "Setting 'Speed hack coprocessor X' to 'true' or 'Turbo' provides increased performance at the expense of reduced accuracy", + * "advanced_settings", + * { + * { "false", NULL }, + * { "true", NULL }, + * { "unstable", "Turbo (Unstable)" }, + * { NULL, NULL }, + * }, + * "false" + * } + * + * Only strings are operated on. The possible values will + * generally be displayed and stored as-is by the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL 68 + /* const struct retro_core_options_v2_intl * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION + * returns an API version of >= 2. + * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * If RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION returns an API + * version of >= 2, this callback is guaranteed to succeed + * (i.e. callback return value does not indicate success) + * If callback returns true, frontend has core option category + * support. + * If callback returns false, frontend does not have core option + * category support. + * + * This is fundamentally the same as RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, + * with the addition of localisation support. The description of the + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 callback should be consulted + * for further details. + * + * 'data' points to a retro_core_options_v2_intl struct. + * + * - retro_core_options_v2_intl::us is a pointer to a + * retro_core_options_v2 struct defining the US English + * core options implementation. It must point to a valid struct. + * + * - retro_core_options_v2_intl::local is a pointer to a + * retro_core_options_v2 struct defining core options for + * the current frontend language. It may be NULL (in which case + * retro_core_options_v2_intl::us is used by the frontend). Any items + * missing from this struct will be read from + * retro_core_options_v2_intl::us instead. + * + * NOTE: Default core option values are always taken from the + * retro_core_options_v2_intl::us struct. Any default values in + * the retro_core_options_v2_intl::local struct will be ignored. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK 69 + /* const struct retro_core_options_update_display_callback * -- + * Allows a frontend to signal that a core must update + * the visibility of any dynamically hidden core options, + * and enables the frontend to detect visibility changes. + * Used by the frontend to update the menu display status + * of core options without requiring a call of retro_run(). + * Must be called in retro_set_environment(). + */ + +#define RETRO_ENVIRONMENT_SET_VARIABLE 70 + /* const struct retro_variable * -- + * Allows an implementation to notify the frontend + * that a core option value has changed. + * + * retro_variable::key and retro_variable::value + * must match strings that have been set previously + * via one of the following: + * + * - RETRO_ENVIRONMENT_SET_VARIABLES + * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS + * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL + * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 + * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL + * + * After changing a core option value via this + * callback, RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE + * will return true. + * + * If data is NULL, no changes will be registered + * and the callback will return true; an + * implementation may therefore pass NULL in order + * to test whether the callback is supported. + */ + +#define RETRO_ENVIRONMENT_GET_THROTTLE_STATE (71 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_throttle_state * -- + * Allows an implementation to get details on the actual rate + * the frontend is attempting to call retro_run(). + */ + /* VFS functionality */ /* File paths: @@ -2902,6 +3259,143 @@ struct retro_core_options_intl struct retro_core_option_definition *local; }; +struct retro_core_option_v2_category +{ + /* Variable uniquely identifying the + * option category. Valid key characters + * are [a-z, A-Z, 0-9, _, -] */ + const char *key; + + /* Human-readable category description + * > Used as category menu label when + * frontend has core option category + * support */ + const char *desc; + + /* Human-readable category information + * > Used as category menu sublabel when + * frontend has core option category + * support + * > Optional (may be NULL or an empty + * string) */ + const char *info; +}; + +struct retro_core_option_v2_definition +{ + /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. + * Valid key characters are [a-z, A-Z, 0-9, _, -] */ + const char *key; + + /* Human-readable core option description + * > Used as menu label when frontend does + * not have core option category support + * e.g. "Video > Aspect Ratio" */ + const char *desc; + + /* Human-readable core option description + * > Used as menu label when frontend has + * core option category support + * e.g. "Aspect Ratio", where associated + * retro_core_option_v2_category::desc + * is "Video" + * > If empty or NULL, the string specified by + * desc will be used as the menu label + * > Will be ignored (and may be set to NULL) + * if category_key is empty or NULL */ + const char *desc_categorized; + + /* Human-readable core option information + * > Used as menu sublabel */ + const char *info; + + /* Human-readable core option information + * > Used as menu sublabel when frontend + * has core option category support + * (e.g. may be required when info text + * references an option by name/desc, + * and the desc/desc_categorized text + * for that option differ) + * > If empty or NULL, the string specified by + * info will be used as the menu sublabel + * > Will be ignored (and may be set to NULL) + * if category_key is empty or NULL */ + const char *info_categorized; + + /* Variable specifying category (e.g. "video", + * "audio") that will be assigned to the option + * if frontend has core option category support. + * > Categorized options will be displayed in a + * subsection/submenu of the frontend core + * option interface + * > Specified string must match one of the + * retro_core_option_v2_category::key values + * in the associated retro_core_option_v2_category + * array; If no match is not found, specified + * string will be considered as NULL + * > If specified string is empty or NULL, option will + * have no category and will be shown at the top + * level of the frontend core option interface */ + const char *category_key; + + /* Array of retro_core_option_value structs, terminated by NULL */ + struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX]; + + /* Default core option value. Must match one of the values + * in the retro_core_option_value array, otherwise will be + * ignored */ + const char *default_value; +}; + +struct retro_core_options_v2 +{ + /* Array of retro_core_option_v2_category structs, + * terminated by NULL + * > If NULL, all entries in definitions array + * will have no category and will be shown at + * the top level of the frontend core option + * interface + * > Will be ignored if frontend does not have + * core option category support */ + struct retro_core_option_v2_category *categories; + + /* Array of retro_core_option_v2_definition structs, + * terminated by NULL */ + struct retro_core_option_v2_definition *definitions; +}; + +struct retro_core_options_v2_intl +{ + /* Pointer to a retro_core_options_v2 struct + * > US English implementation + * > Must point to a valid struct */ + struct retro_core_options_v2 *us; + + /* Pointer to a retro_core_options_v2 struct + * - Implementation for current frontend language + * - May be NULL */ + struct retro_core_options_v2 *local; +}; + +/* Used by the frontend to monitor changes in core option + * visibility. May be called each time any core option + * value is set via the frontend. + * - On each invocation, the core must update the visibility + * of any dynamically hidden options using the + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY environment + * callback. + * - On the first invocation, returns 'true' if the visibility + * of any core option has changed since the last call of + * retro_load_game() or retro_load_game_special(). + * - On each subsequent invocation, returns 'true' if the + * visibility of any core option has changed since the last + * time the function was called. */ +typedef bool (RETRO_CALLCONV *retro_core_options_update_display_callback_t)(void); +struct retro_core_options_update_display_callback +{ + retro_core_options_update_display_callback_t callback; +}; + struct retro_game_info { const char *path; /* Path to game, UTF-8 encoded. diff --git a/libretro/libretro_core_options.inc b/libretro/libretro_core_options.inc index 1185763..8646b8a 100644 --- a/libretro/libretro_core_options.inc +++ b/libretro/libretro_core_options.inc @@ -9,9 +9,10 @@ /* ******************************** - * VERSION: 1.3 + * VERSION: 2.0 ******************************** * + * - 2.0: Add support for core options v2 interface * - 1.3: Move translations to libretro_core_options_intl.h * - libretro_core_options_intl.h includes BOM and utf-8 * fix for MSVC 2010-2013 @@ -44,20 +45,54 @@ extern "C" { * - Will be used as a fallback for any missing entries in * frontend language definition */ -struct retro_core_option_definition option_defs_us[] = { - +struct retro_core_option_v2_category option_cats_us[] = { + { + "system", + "System", + "Configure base hardware selection." + }, + { + "video", + "Video", + "Configure display parameters: palette selection, colour correction, screen border." + }, + { + "audio", + "Audio", + "Configure audio emulation: highpass filter, electrical interference." + }, + { + "input", + "Input", + "Configure input parameters: rumble support." + }, + { NULL, NULL }, +}; + +struct retro_core_option_v2_definition option_defs_us[] = { + /* Core options used in single cart mode */ - + { "sameboy_model", "System - Emulated Model (Requires Restart)", + "Emulated Model (Requires Restart)", "Chooses which system model the content should be started on. Certain games may activate special in-game features when ran on specific models. This option requires a content restart in order to take effect.", + NULL, + "system", { - { "Auto", "Detect automatically" }, + { "Auto", "Auto Detect DMG/CGB" }, + { "Auto (SGB)", "Auto Detect DMG/SGB/CGB" }, { "Game Boy", "Game Boy (DMG-CPU B)" }, - { "Game Boy Color C", "Game Boy Color (CPU-CGB C) (Experimental)" }, - { "Game Boy Color", "Game Boy Color (CPU-CGB E)" }, - { "Game Boy Advance", NULL }, + { "Game Boy Pocket", "Game Boy Pocket/Light" }, + { "Game Boy Color 0", "Game Boy Color (CPU CGB 0) (Experimental)" }, + { "Game Boy Color A", "Game Boy Color (CPU CGB A) (Experimental)" }, + { "Game Boy Color B", "Game Boy Color (CPU CGB B) (Experimental)" }, + { "Game Boy Color C", "Game Boy Color (CPU CGB C) (Experimental)" }, + { "Game Boy Color D", "Game Boy Color (CPU CGB D)" }, + { "Game Boy Color", "Game Boy Color (CPU CGB E)" }, + { "Game Boy Advance", "Game Boy Advance (CPU AGB A)" }, + { "Game Boy Player", "Game Boy Player (CPU AGB A)" }, { "Super Game Boy", "Super Game Boy NTSC" }, { "Super Game Boy PAL", NULL }, { "Super Game Boy 2", NULL }, @@ -65,10 +100,28 @@ struct retro_core_option_definition option_defs_us[] = { }, "Auto" }, + { + "sameboy_auto_sgb_model", + "System - Auto Detected SGB Model (Requires Restart)", + "Auto Detected SGB Model (Requires Restart)", + "Specifies which model of Super Game Boy hardware to emulate when SGB content is automatically detected. This option requires a content restart in order to take effect.", + NULL, + "system", + { + { "Super Game Boy", "Super Game Boy NTSC" }, + { "Super Game Boy PAL", NULL }, + { "Super Game Boy 2", NULL }, + { NULL, NULL }, + }, + "Super Game Boy" + }, { "sameboy_rtc", "System - Real Time Clock Emulation", + "Real Time Clock Emulation", "Specifies how the emulation of the real-time clock feature used in certain Game Boy and Game Boy Color games should function.", + NULL, + "system", { { "sync to system clock", "Sync to System Clock" }, { "accurate", "Accurate" }, @@ -76,11 +129,13 @@ struct retro_core_option_definition option_defs_us[] = { }, "sync to system clock" }, - { "sameboy_mono_palette", "Video - GB Mono Palette", + "GB Mono Palette", "Selects the color palette that should be used when playing Game Boy games.", + NULL, + "video", { { "greyscale", "Greyscale" }, { "lime", "Lime (Game Boy)" }, @@ -93,10 +148,14 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_color_correction_mode", "Video - GBC Color Correction", + "GBC Color Correction", "Defines which type of color correction should be applied when playing Game Boy Color games.", + NULL, + "video", { - { "emulate hardware", "Emulate Hardware" }, - { "preserve brightness", "Preserve Brightness" }, + { "emulate hardware", "Modern – Balanced" }, + { "accurate", "Modern – Accurate" }, + { "preserve brightness", "Modern – Boost Contrast" }, { "reduce contrast", "Reduce Contrast" }, { "correct curves", "Correct Color Curves" }, { "harsh reality", "Harsh Reality (Low Contrast)" }, @@ -108,7 +167,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_light_temperature", "Video - Ambient Light Temperature", - "Simulates an ambient light’s effect on non-backlit Game Boy screens, by setting a user-controlled color temperature. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + "Ambient Light Temperature", + "Simulates an ambient light's effect on non-backlit Game Boy screens, by setting a user-controlled color temperature. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + NULL, + "video", { { "1.0", "1000K (Warmest)" }, { "0.9", "1550K" }, @@ -130,7 +192,7 @@ struct retro_core_option_definition option_defs_us[] = { { "-0.7", "10350K" }, { "-0.8", "10900K" }, { "-0.9", "11450K" }, - { "-1.0", "12000K (Coolest)" }, + { "-1.0", "12000K (Coolest)" }, { NULL, NULL }, }, "0" @@ -138,7 +200,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_border", "Video - Display Border", + "Display Border", "Defines when to display an on-screen border around the content.", + NULL, + "video", { { "always", "Always" }, { "Super Game Boy only", "Only for Super Game Boy" }, @@ -150,7 +215,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_high_pass_filter_mode", "Audio - Highpass Filter", + "Highpass Filter", "Applies a filter to the audio output, removing certain pop sounds caused by the DC Offset. If disabled, the sound will be rendered as output by the Game Boy APU, but popping effects will be heard when the emulator is paused or resumed. 'Accurate' will apply a global filter, masking popping sounds while also reducing lower frequencies. 'Preserve Waveform' applies the filter only to the DC Offset.", + NULL, + "audio", { { "accurate", "Accurate" }, { "remove dc offset", "Preserve Waveform" }, @@ -162,7 +230,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_audio_interference", "Audio - Interference Volume", + "Interference Volume", "Controls the volume of the buzzing effect caused by the electrical interference between the Game Boy SoC and the system speakers.", + NULL, + "audio", { { "0", "0%" }, { "5", "5%" }, @@ -192,7 +263,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_rumble", "Input - Rumble Mode", + "Rumble Mode", "Defines which type of content should trigger rumble effects.", + NULL, + "input", { { "all games", "Always" }, { "rumble-enabled games", "Only for rumble-enabled games" }, @@ -201,13 +275,16 @@ struct retro_core_option_definition option_defs_us[] = { }, "rumble-enabled games" }, - + /* Core options used in dual cart mode */ - + { "sameboy_link", "System - Link Cable Emulation", + "Link Cable Emulation", "Enables the emulation of the link cable, allowing communication and exchange of data between two Game Boy systems.", + NULL, + "system", { { "enabled", "Enabled" }, { "disabled", "Disabled" }, @@ -218,7 +295,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_screen_layout", "System - Screen Layout", + "Screen Layout", "When emulating two systems at once, this option defines the respective position of the two screens.", + NULL, + "system", { { "top-down", "Top-Down" }, { "left-right", "Left-Right" }, @@ -229,7 +309,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_audio_output", "System - Audio Output", + "Audio Output", "Selects which of the two emulated Game Boy systems should output audio.", + NULL, + "system", { { "Game Boy #1", NULL }, { "Game Boy #2", NULL }, @@ -240,13 +323,23 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_model_1", "System - Emulated Model for Game Boy #1 (Requires Restart)", + "Emulated Model for Game Boy #1 (Requires Restart)", "Chooses which system model the content should be started on for Game Boy #1. Certain games may activate special in-game features when ran on specific models. This option requires a content restart in order to take effect.", + NULL, + "system", { - { "Auto", "Detect automatically" }, + { "Auto", "Auto Detect DMG/CGB" }, + { "Auto (SGB)", "Auto Detect DMG/SGB/CGB" }, { "Game Boy", "Game Boy (DMG-CPU B)" }, - { "Game Boy Color C", "Game Boy Color (CPU-CGB C) (Experimental)" }, - { "Game Boy Color", "Game Boy Color (CPU-CGB E)" }, - { "Game Boy Advance", NULL }, + { "Game Boy Pocket", "Game Boy Pocket/Light" }, + { "Game Boy Color 0", "Game Boy Color (CPU CGB 0) (Experimental)" }, + { "Game Boy Color A", "Game Boy Color (CPU CGB A) (Experimental)" }, + { "Game Boy Color B", "Game Boy Color (CPU CGB B) (Experimental)" }, + { "Game Boy Color C", "Game Boy Color (CPU CGB C) (Experimental)" }, + { "Game Boy Color D", "Game Boy Color (CPU CGB D)" }, + { "Game Boy Color", "Game Boy Color (CPU CGB E)" }, + { "Game Boy Advance", "Game Boy Advance (CPU AGB A)" }, + { "Game Boy Player", "Game Boy Player (CPU AGB A)" }, { "Super Game Boy", "Super Game Boy NTSC" }, { "Super Game Boy PAL", NULL }, { "Super Game Boy 2", NULL }, @@ -254,16 +347,41 @@ struct retro_core_option_definition option_defs_us[] = { }, "Auto" }, + { + "sameboy_auto_sgb_model_1", + "System - Auto Detected SGB Model for Game Boy #1 (Requires Restart)", + "Auto Detected SGB Model for Game Boy #1 (Requires Restart)", + "Specifies which model of Super Game Boy hardware to emulate when SGB content is automatically detected for Game Boy #1. This option requires a content restart in order to take effect.", + NULL, + "system", + { + { "Super Game Boy", "Super Game Boy NTSC" }, + { "Super Game Boy PAL", NULL }, + { "Super Game Boy 2", NULL }, + { NULL, NULL }, + }, + "Super Game Boy" + }, { "sameboy_model_2", "System - Emulated Model for Game Boy #2 (Requires Restart)", + "Emulated Model for Game Boy #2 (Requires Restart)", "Chooses which system model the content should be started on for Game Boy #2. Certain games may activate special in-game features when ran on specific models. This option requires a content restart in order to take effect.", + NULL, + "system", { - { "Auto", "Detect automatically" }, + { "Auto", "Auto Detect DMG/CGB" }, + { "Auto (SGB)", "Auto Detect DMG/SGB/CGB" }, { "Game Boy", "Game Boy (DMG-CPU B)" }, - { "Game Boy Color C", "Game Boy Color (CPU-CGB C) (Experimental)" }, - { "Game Boy Color", "Game Boy Color (CPU-CGB E)" }, - { "Game Boy Advance", NULL }, + { "Game Boy Pocket", "Game Boy Pocket/Light" }, + { "Game Boy Color 0", "Game Boy Color (CPU CGB 0) (Experimental)" }, + { "Game Boy Color A", "Game Boy Color (CPU CGB A) (Experimental)" }, + { "Game Boy Color B", "Game Boy Color (CPU CGB B) (Experimental)" }, + { "Game Boy Color C", "Game Boy Color (CPU CGB C) (Experimental)" }, + { "Game Boy Color D", "Game Boy Color (CPU CGB D)" }, + { "Game Boy Color", "Game Boy Color (CPU CGB E)" }, + { "Game Boy Advance", "Game Boy Advance (CPU AGB A)" }, + { "Game Boy Player", "Game Boy Player (CPU AGB A)" }, { "Super Game Boy", "Super Game Boy NTSC" }, { "Super Game Boy PAL", NULL }, { "Super Game Boy 2", NULL }, @@ -271,10 +389,28 @@ struct retro_core_option_definition option_defs_us[] = { }, "Auto" }, + { + "sameboy_auto_sgb_model_2", + "System - Auto Detected SGB Model for Game Boy #2 (Requires Restart)", + "Auto Detected SGB Model for Game Boy #2 (Requires Restart)", + "Specifies which model of Super Game Boy hardware to emulate when SGB content is automatically detected for Game Boy #2. This option requires a content restart in order to take effect.", + NULL, + "system", + { + { "Super Game Boy", "Super Game Boy NTSC" }, + { "Super Game Boy PAL", NULL }, + { "Super Game Boy 2", NULL }, + { NULL, NULL }, + }, + "Super Game Boy" + }, { "sameboy_mono_palette_1", "Video - GB Mono Palette for Game Boy #1", + "GB Mono Palette for Game Boy #1", "Selects the color palette that should be used when playing Game Boy games on Game Boy #1.", + NULL, + "video", { { "greyscale", "Greyscale" }, { "lime", "Lime (Game Boy)" }, @@ -287,7 +423,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_mono_palette_2", "Video - GB Mono Palette for Game Boy #2", + "GB Mono Palette for Game Boy #2", "Selects the color palette that should be used when playing Game Boy games on Game Boy #2.", + NULL, + "video", { { "greyscale", "Greyscale" }, { "lime", "Lime (Game Boy)" }, @@ -300,10 +439,14 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_color_correction_mode_1", "Video - GBC Color Correction for Game Boy #1", + "GBC Color Correction for Game Boy #1", "Defines which type of color correction should be applied when playing Game Boy Color games on Game Boy #1.", + NULL, + "video", { - { "emulate hardware", "Emulate Hardware" }, - { "preserve brightness", "Preserve Brightness" }, + { "emulate hardware", "Modern – Balanced" }, + { "accurate", "Modern – Accurate" }, + { "preserve brightness", "Modern – Boost Contrast" }, { "reduce contrast", "Reduce Contrast" }, { "correct curves", "Correct Color Curves" }, { "harsh reality", "Harsh Reality (Low Contrast)" }, @@ -315,10 +458,14 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_color_correction_mode_2", "Video - GBC Color Correction for Game Boy #2", + "GBC Color Correction for Game Boy #2", "Defines which type of color correction should be applied when playing Game Boy Color games on Game Boy #2.", + NULL, + "video", { - { "emulate hardware", "Emulate Hardware" }, - { "preserve brightness", "Preserve Brightness" }, + { "emulate hardware", "Modern – Balanced" }, + { "accurate", "Modern – Accurate" }, + { "preserve brightness", "Modern – Boost Contrast" }, { "reduce contrast", "Reduce Contrast" }, { "correct curves", "Correct Color Curves" }, { "harsh reality", "Harsh Reality (Low Contrast)" }, @@ -330,7 +477,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_light_temperature_1", "Video - Ambient Light Temperature for Game Boy #1", - "Simulates an ambient light’s effect on non-backlit Game Boy screens, by setting a user-controlled color temperature applied to the screen of Game Boy #1. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + "Ambient Light Temperature for Game Boy #1", + "Simulates an ambient light's effect on non-backlit Game Boy screens, by setting a user-controlled color temperature applied to the screen of Game Boy #1. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + NULL, + "video", { { "1.0", "1000K (Warmest)" }, { "0.9", "1550K" }, @@ -352,7 +502,7 @@ struct retro_core_option_definition option_defs_us[] = { { "-0.7", "10350K" }, { "-0.8", "10900K" }, { "-0.9", "11450K" }, - { "-1.0", "12000K (Coolest)" }, + { "-1.0", "12000K (Coolest)" }, { NULL, NULL }, }, "0" @@ -360,7 +510,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_light_temperature_2", "Video - Ambient Light Temperature for Game Boy #2", - "Simulates an ambient light’s effect on non-backlit Game Boy screens, by setting a user-controlled color temperature applied to the screen of Game Boy #2. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + "Ambient Light Temperature for Game Boy #2", + "Simulates an ambient light's effect on non-backlit Game Boy screens, by setting a user-controlled color temperature applied to the screen of Game Boy #2. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + NULL, + "video", { { "1.0", "1000K (Warmest)" }, { "0.9", "1550K" }, @@ -382,7 +535,7 @@ struct retro_core_option_definition option_defs_us[] = { { "-0.7", "10350K" }, { "-0.8", "10900K" }, { "-0.9", "11450K" }, - { "-1.0", "12000K (Coolest)" }, + { "-1.0", "12000K (Coolest)" }, { NULL, NULL }, }, "0" @@ -390,7 +543,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_high_pass_filter_mode_1", "Audio - Highpass Filter for Game Boy #1", + "Highpass Filter for Game Boy #1", "Applies a filter to the audio output for Game Boy #1, removing certain pop sounds caused by the DC Offset. If disabled, the sound will be rendered as output by the Game Boy APU, but popping effects will be heard when the emulator is paused or resumed. 'Accurate' will apply a global filter, masking popping sounds while also reducing lower frequencies. 'Preserve Waveform' applies the filter only to the DC Offset.", + NULL, + "audio", { { "accurate", "Accurate" }, { "remove dc offset", "Preserve Waveform" }, @@ -402,7 +558,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_high_pass_filter_mode_2", "Audio - Highpass Filter for Game Boy #2", + "Highpass Filter for Game Boy #2", "Applies a filter to the audio output for Game Boy #2, removing certain pop sounds caused by the DC Offset. If disabled, the sound will be rendered as output by the Game Boy APU, but popping effects will be heard when the emulator is paused or resumed. 'Accurate' will apply a global filter, masking popping sounds while also reducing lower frequencies. 'Preserve Waveform' applies the filter only to the DC Offset.", + NULL, + "audio", { { "accurate", "Accurate" }, { "remove dc offset", "Preserve Waveform" }, @@ -414,7 +573,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_audio_interference_1", "Audio - Interference Volume for Game Boy #1", + "Interference Volume for Game Boy #1", "Controls the volume of the buzzing effect caused by the electrical interference between the Game Boy SoC and the system speakers on Game Boy #1.", + NULL, + "audio", { { "0", "0%" }, { "5", "5%" }, @@ -444,7 +606,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_audio_interference_2", "Audio - Interference Volume for Game Boy #2", + "Interference Volume for Game Boy #2", "Controls the volume of the buzzing effect caused by the electrical interference between the Game Boy SoC and the system speakers on Game Boy #2.", + NULL, + "audio", { { "0", "0%" }, { "5", "5%" }, @@ -474,19 +639,10 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_rumble_1", "Input - Rumble Mode for Game Boy #1", + "Rumble Mode for Game Boy #1", "Defines which type of content should trigger rumble effects when played on Game Boy #1.", - { - { "all games", "Always" }, - { "rumble-enabled games", "Only for rumble-enabled games" }, - { "never", "Never" }, - { NULL, NULL }, - }, - "rumble-enabled games" - }, - { - "sameboy_rumble_2", - "Input - Rumble Mode for Game Boy #2", - "Defines which type of content should trigger rumble effects when played on Game Boy #2.", + NULL, + "input", { { "all games", "Always" }, { "rumble-enabled games", "Only for rumble-enabled games" }, @@ -495,8 +651,28 @@ struct retro_core_option_definition option_defs_us[] = { }, "rumble-enabled games" }, - - { NULL, NULL, NULL, {{0}}, NULL }, + { + "sameboy_rumble_2", + "Input - Rumble Mode for Game Boy #2", + "Rumble Mode for Game Boy #2", + "Defines which type of content should trigger rumble effects when played on Game Boy #2.", + NULL, + "input", + { + { "all games", "Always" }, + { "rumble-enabled games", "Only for rumble-enabled games" }, + { "never", "Never" }, + { NULL, NULL }, + }, + "rumble-enabled games" + }, + + { NULL, NULL, NULL, NULL, NULL, NULL, {{0}}, NULL }, +}; + +struct retro_core_options_v2 options_us = { + option_cats_us, + option_defs_us }; /* @@ -506,8 +682,8 @@ struct retro_core_option_definition option_defs_us[] = { */ #ifndef HAVE_NO_LANGEXTRA -struct retro_core_option_definition *option_defs_intl[RETRO_LANGUAGE_LAST] = { - option_defs_us, /* RETRO_LANGUAGE_ENGLISH */ +struct retro_core_options_v2 *options_intl[RETRO_LANGUAGE_LAST] = { + &options_us, /* RETRO_LANGUAGE_ENGLISH */ NULL, /* RETRO_LANGUAGE_JAPANESE */ NULL, /* RETRO_LANGUAGE_FRENCH */ NULL, /* RETRO_LANGUAGE_SPANISH */ @@ -531,7 +707,6 @@ struct retro_core_option_definition *option_defs_intl[RETRO_LANGUAGE_LAST] = { NULL, /* RETRO_LANGUAGE_HEBREW */ NULL, /* RETRO_LANGUAGE_ASTURIAN */ NULL, /* RETRO_LANGUAGE_FINNISH */ - }; #endif @@ -549,110 +724,220 @@ struct retro_core_option_definition *option_defs_intl[RETRO_LANGUAGE_LAST] = { * be as painless as possible for core devs) */ -static INLINE void libretro_set_core_options(retro_environment_t environ_cb) +static INLINE void libretro_set_core_options(retro_environment_t environ_cb, + bool *categories_supported) { - unsigned version = 0; - - if (!environ_cb) return; - - if (environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, &version) && (version >= 1)) { + unsigned version = 0; #ifndef HAVE_NO_LANGEXTRA - struct retro_core_options_intl core_options_intl; - unsigned language = 0; - - core_options_intl.us = option_defs_us; + unsigned language = 0; +#endif + + if (!environ_cb || !categories_supported) return; + + *categories_supported = false; + + if (!environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, &version)) version = 0; + + if (version >= 2) { +#ifndef HAVE_NO_LANGEXTRA + struct retro_core_options_v2_intl core_options_intl; + + core_options_intl.us = &options_us; core_options_intl.local = NULL; - + if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) && (language < RETRO_LANGUAGE_LAST) && (language != RETRO_LANGUAGE_ENGLISH)) - core_options_intl.local = option_defs_intl[language]; - - environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL, &core_options_intl); + core_options_intl.local = options_intl[language]; + + *categories_supported = environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL, + &core_options_intl); #else - environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, &option_defs_us); + *categories_supported = environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, + &options_us); #endif } else { - size_t i; + size_t i, j; + size_t option_index = 0; size_t num_options = 0; + struct retro_core_option_definition + *option_v1_defs_us = NULL; +#ifndef HAVE_NO_LANGEXTRA + size_t num_options_intl = 0; + struct retro_core_option_v2_definition + *option_defs_intl = NULL; + struct retro_core_option_definition + *option_v1_defs_intl = NULL; + struct retro_core_options_intl + core_options_v1_intl; +#endif struct retro_variable *variables = NULL; char **values_buf = NULL; - - /* Determine number of options */ + + /* Determine total number of options */ while (true) { - if (!option_defs_us[num_options].key) break; - num_options++; + if (option_defs_us[num_options].key) num_options++; + else break; } - - /* Allocate arrays */ - variables = (struct retro_variable *)calloc(num_options + 1, sizeof(struct retro_variable)); - values_buf = (char **)calloc(num_options, sizeof(char *)); - - if (!variables || !values_buf) goto error; - - /* Copy parameters from option_defs_us array */ - for (i = 0; i < num_options; i++) { - const char *key = option_defs_us[i].key; - const char *desc = option_defs_us[i].desc; - const char *default_value = option_defs_us[i].default_value; - struct retro_core_option_value *values = option_defs_us[i].values; - size_t buf_len = 3; - size_t default_index = 0; - - values_buf[i] = NULL; - - if (desc) { - size_t num_values = 0; - - /* Determine number of values */ - while (true) { - if (!values[num_values].value) break; - - /* Check if this is the default value */ - if (default_value) { - if (strcmp(values[num_values].value, default_value) == 0) default_index = num_values; - } - - buf_len += strlen(values[num_values].value); - num_values++; + + if (version >= 1) { + /* Allocate US array */ + option_v1_defs_us = (struct retro_core_option_definition *) calloc(num_options + 1, sizeof(struct retro_core_option_definition)); + + /* Copy parameters from option_defs_us array */ + for (i = 0; i < num_options; i++) { + struct retro_core_option_v2_definition *option_def_us = &option_defs_us[i]; + struct retro_core_option_value *option_values = option_def_us->values; + struct retro_core_option_definition *option_v1_def_us = &option_v1_defs_us[i]; + struct retro_core_option_value *option_v1_values = option_v1_def_us->values; + + option_v1_def_us->key = option_def_us->key; + option_v1_def_us->desc = option_def_us->desc; + option_v1_def_us->info = option_def_us->info; + option_v1_def_us->default_value = option_def_us->default_value; + + /* Values must be copied individually... */ + while (option_values->value) { + option_v1_values->value = option_values->value; + option_v1_values->label = option_values->label; + + option_values++; + option_v1_values++; } - - /* Build values string */ - if (num_values > 0) { - size_t j; - - buf_len += num_values - 1; - buf_len += strlen(desc); - - values_buf[i] = (char *)calloc(buf_len, sizeof(char)); - if (!values_buf[i]) goto error; - - strcpy(values_buf[i], desc); - strcat(values_buf[i], "; "); - - /* Default value goes first */ - strcat(values_buf[i], values[default_index].value); - - /* Add remaining values */ - for (j = 0; j < num_values; j++) { - if (j != default_index) { - strcat(values_buf[i], "|"); - strcat(values_buf[i], values[j].value); - } + } + +#ifndef HAVE_NO_LANGEXTRA + if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) && + (language < RETRO_LANGUAGE_LAST) && (language != RETRO_LANGUAGE_ENGLISH) && + options_intl[language]) + option_defs_intl = options_intl[language]->definitions; + + if (option_defs_intl) { + /* Determine number of intl options */ + while (true) { + if (option_defs_intl[num_options_intl].key) num_options_intl++; + else break; + } + + /* Allocate intl array */ + option_v1_defs_intl = (struct retro_core_option_definition *) + calloc(num_options_intl + 1, sizeof(struct retro_core_option_definition)); + + /* Copy parameters from option_defs_intl array */ + for (i = 0; i < num_options_intl; i++) { + struct retro_core_option_v2_definition *option_def_intl = &option_defs_intl[i]; + struct retro_core_option_value *option_values = option_def_intl->values; + struct retro_core_option_definition *option_v1_def_intl = &option_v1_defs_intl[i]; + struct retro_core_option_value *option_v1_values = option_v1_def_intl->values; + + option_v1_def_intl->key = option_def_intl->key; + option_v1_def_intl->desc = option_def_intl->desc; + option_v1_def_intl->info = option_def_intl->info; + option_v1_def_intl->default_value = option_def_intl->default_value; + + /* Values must be copied individually... */ + while (option_values->value) { + option_v1_values->value = option_values->value; + option_v1_values->label = option_values->label; + + option_values++; + option_v1_values++; } } } - - variables[i].key = key; - variables[i].value = values_buf[i]; + + core_options_v1_intl.us = option_v1_defs_us; + core_options_v1_intl.local = option_v1_defs_intl; + + environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL, &core_options_v1_intl); +#else + environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, option_v1_defs_us); +#endif } - - /* Set variables */ - environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables); - - error: - + else { + /* Allocate arrays */ + variables = (struct retro_variable *)calloc(num_options + 1, + sizeof(struct retro_variable)); + values_buf = (char **)calloc(num_options, sizeof(char *)); + + if (!variables || !values_buf) goto error; + + /* Copy parameters from option_defs_us array */ + for (i = 0; i < num_options; i++) { + const char *key = option_defs_us[i].key; + const char *desc = option_defs_us[i].desc; + const char *default_value = option_defs_us[i].default_value; + struct retro_core_option_value *values = option_defs_us[i].values; + size_t buf_len = 3; + size_t default_index = 0; + + values_buf[i] = NULL; + + if (desc) { + size_t num_values = 0; + + /* Determine number of values */ + while (true) { + if (values[num_values].value) { + /* Check if this is the default value */ + if (default_value) { + if (strcmp(values[num_values].value, default_value) == 0) default_index = num_values; + + buf_len += strlen(values[num_values].value); + num_values++; + } + } + else break; + } + + /* Build values string */ + if (num_values > 0) { + buf_len += num_values - 1; + buf_len += strlen(desc); + + values_buf[i] = (char *)calloc(buf_len, sizeof(char)); + if (!values_buf[i]) goto error; + + strcpy(values_buf[i], desc); + strcat(values_buf[i], "; "); + + /* Default value goes first */ + strcat(values_buf[i], values[default_index].value); + + /* Add remaining values */ + for (j = 0; j < num_values; j++) { + if (j != default_index) { + strcat(values_buf[i], "|"); + strcat(values_buf[i], values[j].value); + } + } + } + } + + variables[option_index].key = key; + variables[option_index].value = values_buf[i]; + option_index++; + } + + /* Set variables */ + environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables); + } + +error: /* Clean up */ + + if (option_v1_defs_us) { + free(option_v1_defs_us); + option_v1_defs_us = NULL; + } + +#ifndef HAVE_NO_LANGEXTRA + if (option_v1_defs_intl) { + free(option_v1_defs_intl); + option_v1_defs_intl = NULL; + } +#endif + if (values_buf) { for (i = 0; i < num_options; i++) { if (values_buf[i]) { @@ -660,11 +945,11 @@ static INLINE void libretro_set_core_options(retro_environment_t environ_cb) values_buf[i] = NULL; } } - + free(values_buf); values_buf = NULL; } - + if (variables) { free(variables); variables = NULL; diff --git a/version.mk b/version.mk index 8964270..d313dd4 100644 --- a/version.mk +++ b/version.mk @@ -1 +1 @@ -VERSION := 0.14.7 \ No newline at end of file +VERSION := 0.16.6 \ No newline at end of file