diff --git a/bsnes/gb/Cocoa/GBAudioClient.h b/bsnes/gb/AppleCommon/GBAudioClient.h similarity index 80% rename from bsnes/gb/Cocoa/GBAudioClient.h rename to bsnes/gb/AppleCommon/GBAudioClient.h index 03ed7011..b3614118 100644 --- a/bsnes/gb/Cocoa/GBAudioClient.h +++ b/bsnes/gb/AppleCommon/GBAudioClient.h @@ -5,8 +5,8 @@ @property (nonatomic, strong) void (^renderBlock)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer); @property (nonatomic, readonly) UInt32 rate; @property (nonatomic, readonly, getter=isPlaying) bool playing; --(void) start; --(void) stop; --(id) initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block +- (void)start; +- (void)stop; +- (id)initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block andSampleRate:(UInt32) rate; @end diff --git a/bsnes/gb/Cocoa/GBAudioClient.m b/bsnes/gb/AppleCommon/GBAudioClient.m similarity index 80% rename from bsnes/gb/Cocoa/GBAudioClient.m rename to bsnes/gb/AppleCommon/GBAudioClient.m index 81a51fd5..c0c1b89b 100644 --- a/bsnes/gb/Cocoa/GBAudioClient.m +++ b/bsnes/gb/AppleCommon/GBAudioClient.m @@ -23,7 +23,7 @@ static OSStatus render( AudioComponentInstance audioUnit; } --(id) initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block +- (id)initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block andSampleRate:(UInt32) rate { if (!(self = [super init])) { @@ -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/bsnes/gb/AppleCommon/GBViewBase.h b/bsnes/gb/AppleCommon/GBViewBase.h new file mode 100644 index 00000000..8c5a5f3b --- /dev/null +++ b/bsnes/gb/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; +- (instancetype)mirroredView; +@end diff --git a/bsnes/gb/AppleCommon/GBViewBase.m b/bsnes/gb/AppleCommon/GBViewBase.m new file mode 100644 index 00000000..21de70ea --- /dev/null +++ b/bsnes/gb/AppleCommon/GBViewBase.m @@ -0,0 +1,121 @@ +#import "GBViewBase.h" + +@implementation GBViewBase +{ + uint32_t *_imageBuffers[3]; + unsigned _currentBuffer; + GB_frame_blending_mode_t _frameBlendingMode; + bool _oddFrame; + GBViewBase *_parent; + __weak GBViewBase *_child; +} + +- (void)screenSizeChanged +{ + if (_parent) return; + 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 +{ + if (_parent) return; + _currentBuffer = (_currentBuffer + 1) % self.numberOfBuffers; + _oddFrame = GB_is_odd_frame(_gb); + [_child flip]; +} + +- (unsigned) numberOfBuffers +{ + assert(!_parent); + return _frameBlendingMode? 3 : 2; +} + +- (void) createInternalView +{ + assert(false && "createInternalView must not be inherited"); +} + +- (uint32_t *)currentBuffer +{ + if (GB_unlikely(_parent)) { + return [_parent currentBuffer]; + } + return _imageBuffers[_currentBuffer]; +} + +- (uint32_t *)previousBuffer +{ + if (GB_unlikely(_parent)) { + return [_parent previousBuffer]; + } + return _imageBuffers[(_currentBuffer + 2) % self.numberOfBuffers]; +} + +- (uint32_t *) pixels +{ + assert(!_parent); + return _imageBuffers[(_currentBuffer + 1) % self.numberOfBuffers]; +} + +- (void) setFrameBlendingMode:(GB_frame_blending_mode_t)frameBlendingMode +{ + _frameBlendingMode = frameBlendingMode; + [self setNeedsDisplay]; + [_child setNeedsDisplay]; +} + +- (GB_frame_blending_mode_t)frameBlendingMode +{ + if (GB_unlikely(_parent)) { + return [_parent 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 +{ + if (_parent) return; + free(_imageBuffers[0]); + free(_imageBuffers[1]); + free(_imageBuffers[2]); +} + +#if !TARGET_OS_IPHONE +- (void)setNeedsDisplay +{ + [self setNeedsDisplay:true]; +} +#endif + +- (void)setGb:(GB_gameboy_t *)gb +{ + assert(!_parent); + _gb = gb; + if (_child) { + _child->_gb = gb; + } +} + +- (instancetype)mirroredView +{ + if (_child) return _child; + GBViewBase *ret = [[self.class alloc] initWithFrame:self.bounds]; + ret->_parent = self; + ret->_gb = _gb; + return _child = ret; +} +@end diff --git a/bsnes/gb/AppleCommon/GBViewMetal.h b/bsnes/gb/AppleCommon/GBViewMetal.h new file mode 100644 index 00000000..c865b3be --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBViewMetal.m b/bsnes/gb/AppleCommon/GBViewMetal.m similarity index 61% rename from bsnes/gb/Cocoa/GBViewMetal.m rename to bsnes/gb/AppleCommon/GBViewMetal.m index ae7443f6..8d309508 100644 --- a/bsnes/gb/Cocoa/GBViewMetal.m +++ b/bsnes/gb/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,33 @@ 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; + _Atomic unsigned _pendingFrames; } + (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 +50,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 +108,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 +128,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 +148,110 @@ 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]; + if (_pendingFrames == 2) return; + _pendingFrames++; dispatch_async(dispatch_get_main_queue(), ^{ - [(MTKView *)self.internalView setNeedsDisplay:true]; + [(MTKView *)self.internalView draw]; + _pendingFrames--; }); } +#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 +261,6 @@ static const vector_float2 rect[] = CGImageRelease(cgImage); return ret; } +#endif @end diff --git a/bsnes/gb/BESS.md b/bsnes/gb/BESS.md index e040d903..1f509fc8 100644 --- a/bsnes/gb/BESS.md +++ b/bsnes/gb/BESS.md @@ -22,9 +22,9 @@ BESS works by appending a detectable footer at the end of an existing save state BESS uses a block format where each block contains the following header: | Offset | Content | -|--------|---------------------------------------| -| 0 | A four-letter ASCII identifier | -| 4 | Length of the block, excluding header | +|--------|-----------------------------------------------------------| +| 0 | A four-letter ASCII identifier | +| 4 | Length of the block as a 32-bit integer, excluding header | Every block is followed by another block, until the END block is reached. If an implementation encounters an unsupported block, it should be completely ignored (Should not have any effect and should not trigger a failure). @@ -89,7 +89,7 @@ The values of memory-mapped registers should be written 'as-is' to memory as if * Unused register bits have Don't-Care values which should be ignored * If the model is CGB or newer, the value of KEY0 (FF4C) must be valid as it determines DMG mode * Bit 2 determines DMG mode. A value of 0x04 usually denotes DMG mode, while a value of `0x80` usually denotes CGB mode. -* Sprite priority is derived from KEY0 (FF4C) instead of OPRI (FF6C) because OPRI can be modified after booting, but only the value of OPRI during boot ROM execution takes effect +* Object priority is derived from KEY0 (FF4C) instead of OPRI (FF6C) because OPRI can be modified after booting, but only the value of OPRI during boot ROM execution takes effect * If a register doesn't exist on the emulated model (For example, KEY0 (FF4C) on a DMG), its value should be ignored. * BANK (FF50) should be 0 if the boot ROM is still mapped, and 1 otherwise, and must be valid. * Implementations should not start a serial transfer when writing the value of SB @@ -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. @@ -214,4 +256,4 @@ Other than previously specified required fail conditions, an implementation is f * An invalid length of MBC (not a multiple of 3) * A write outside the $0000-$7FFF and $A000-$BFFF ranges in the MBC block * An SGB block on a save state targeting another model -* An END block with non-zero length \ No newline at end of file +* An END block with non-zero length diff --git a/bsnes/gb/BootROMs/SameBoyLogo.png b/bsnes/gb/BootROMs/SameBoyLogo.png index c7cfc087..ad1a760c 100644 Binary files a/bsnes/gb/BootROMs/SameBoyLogo.png and b/bsnes/gb/BootROMs/SameBoyLogo.png differ diff --git a/bsnes/gb/BootROMs/agb_boot.asm b/bsnes/gb/BootROMs/agb_boot.asm index 95a2c783..a4e3ee88 100644 --- a/bsnes/gb/BootROMs/agb_boot.asm +++ b/bsnes/gb/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/bsnes/gb/BootROMs/cgb0_boot.asm b/bsnes/gb/BootROMs/cgb0_boot.asm new file mode 100644 index 00000000..af9c8d5e --- /dev/null +++ b/bsnes/gb/BootROMs/cgb0_boot.asm @@ -0,0 +1,2 @@ +DEF CGB0 = 1 +include "cgb_boot.asm" diff --git a/bsnes/gb/BootROMs/cgb_boot.asm b/bsnes/gb/BootROMs/cgb_boot.asm index 0bc2b179..ae3200bb 100644 --- a/bsnes/gb/BootROMs/cgb_boot.asm +++ b/bsnes/gb/BootROMs/cgb_boot.asm @@ -1,62 +1,61 @@ ; 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 - ld a, 2 - ld c, $70 - ld [c], a -; Clear RAM Bank 2 (Like the original boot ROM) - ld h, $D0 - call ClearMemoryPage - ld [c], a + 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 jr nz, .clearOAMLoop +IF !DEF(CGB0) ; Init waveform - ld c, $10 - ld hl, $FF30 + ld c, 16 + ld hl, _AUD3WAVERAM .waveformLoop ldi [hl], a cpl dec c jr nz, .waveformLoop +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 @@ -64,28 +63,28 @@ Start: 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 @@ -119,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 @@ -141,7 +140,7 @@ ENDC ; Expand Palettes ld de, AnimationColors ld c, 8 - ld hl, BgPalettes + ld hl, hBgPalettes xor a .expandPalettesLoop: cpl @@ -184,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 @@ -201,31 +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 -; Will be filled with NOPs +HDMAData: +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 @@ -328,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" @@ -432,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" @@ -564,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 @@ -592,7 +622,7 @@ DoubleBitsAndWriteRowTwice: WaitFrame: push hl - ld hl, $FF0F + ld hl, rIF res 0, [hl] .wait bit 0, [hl] @@ -608,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 @@ -625,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 @@ -659,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 @@ -703,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 @@ -733,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 @@ -761,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] @@ -793,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 @@ -804,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 @@ -833,8 +860,6 @@ IF !DEF(FAST) res 2, b .redNotMaxed - ; add de, bc - ; ld [hli], de ld a, e add c ld [hli], a @@ -852,29 +877,36 @@ IF !DEF(FAST) dec b jr nz, .fadeLoop ENDC - ld a, 1 + ld a, 2 + ldh [rSVBK], a + ; Clear RAM Bank 2 (Like the original boot ROM) + ld hl, _RAMBANK + call ClearMemoryPage + inc a call ClearVRAMViaHDMA call _ClearVRAMViaHDMA call ClearVRAMViaHDMA ; A = $40, so it's bank 0 - ld a, $ff - ldh [$00], a + xor a + ldh [rSVBK], a + cpl + 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 @@ -883,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 @@ -913,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 @@ -922,7 +954,7 @@ EmulateDMG: add b add b ld b, a - ldh a, [InputPalette] + ldh a, [hInputPalette] and a jr z, .nothingDown call GetKeyComboPalette @@ -935,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 @@ -956,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 @@ -990,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 @@ -999,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 @@ -1027,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 @@ -1045,30 +1077,29 @@ 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 - ld [c], a + ldh [c], a inc c .loop ld a, [hli] - ld [c], a + ldh [c], a dec d jr nz, .loop ret ClearVRAMViaHDMA: - ldh [$4F], a + ldh [rVBK], a ld hl, HDMAData _ClearVRAMViaHDMA: - ld c, $51 + call WaitFrame ; Wait for vblank + ld c, LOW(rHDMA1) ld b, 5 .loop ld a, [hli] @@ -1080,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 @@ -1095,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: @@ -1131,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 @@ -1153,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 @@ -1205,37 +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 -HDMAData: - db $88, $00, $98, $A0, $12 - db $88, $00, $80, $00, $40 - 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/bsnes/gb/BootROMs/cgb_boot_fast.asm b/bsnes/gb/BootROMs/cgb_boot_fast.asm index cddb4750..c0d6eab6 100644 --- a/bsnes/gb/BootROMs/cgb_boot_fast.asm +++ b/bsnes/gb/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/bsnes/gb/BootROMs/dmg_boot.asm b/bsnes/gb/BootROMs/dmg_boot.asm index 97a12e7c..7013033a 100644 --- a/bsnes/gb/BootROMs/dmg_boot.asm +++ b/bsnes/gb/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,88 +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 - ld hl, $01B0 +IF DEF(MGB) + lb hl, BOOTUP_A_MGB, %10110000 +ELSE + 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 @@ -148,7 +156,7 @@ DoubleBitsAndWriteRow: WaitFrame: push hl - ld hl, $FF0F + ld hl, rIF res 0, [hl] .wait bit 0, [hl] @@ -163,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/bsnes/gb/BootROMs/hardware.inc b/bsnes/gb/BootROMs/hardware.inc new file mode 100755 index 00000000..c1a8c41a --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/BootROMs/mgb_boot.asm b/bsnes/gb/BootROMs/mgb_boot.asm new file mode 100644 index 00000000..c2023038 --- /dev/null +++ b/bsnes/gb/BootROMs/mgb_boot.asm @@ -0,0 +1,2 @@ +DEF MGB = 1 +include "dmg_boot.asm" diff --git a/bsnes/gb/BootROMs/pb12.c b/bsnes/gb/BootROMs/pb12.c index cfedf6bb..7de3de20 100644 --- a/bsnes/gb/BootROMs/pb12.c +++ b/bsnes/gb/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/bsnes/gb/BootROMs/sameboot.inc b/bsnes/gb/BootROMs/sameboot.inc new file mode 100644 index 00000000..b7eecc73 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/BootROMs/sgb2_boot.asm b/bsnes/gb/BootROMs/sgb2_boot.asm index 1c3d8584..d81de18f 100644 --- a/bsnes/gb/BootROMs/sgb2_boot.asm +++ b/bsnes/gb/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/bsnes/gb/BootROMs/sgb_boot.asm b/bsnes/gb/BootROMs/sgb_boot.asm index cdb9d774..df24a5fc 100644 --- a/bsnes/gb/BootROMs/sgb_boot.asm +++ b/bsnes/gb/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,111 +44,124 @@ 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 .sendCommand xor a - ld [c], a + ldh [c], a ld a, $30 - ld [c], a - - ldh a, [$80] + ldh [c], a + + 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 + ldh [c], a ld a, $30 - ld [c], a - + ldh [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], a + ld c, LOW(rNR13) + ld a, $C1 + ldh [c], a inc c - ld a, 7 - ld [c], a - + ld a, $7 + ldh [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 @@ -168,9 +185,9 @@ SendByte: jr c, .zeroBit add a ; 10 -> 20 .zeroBit - ld [c], a + ldh [c], a ld a, $30 - ld [c], a + ldh [c], a dec d ret z jr .loop @@ -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/bsnes/gb/CONTRIBUTING.md b/bsnes/gb/CONTRIBUTING.md index 94627d1a..23f4a7be 100644 --- a/bsnes/gb/CONTRIBUTING.md +++ b/bsnes/gb/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/bsnes/gb/Cocoa/AppDelegate.m b/bsnes/gb/Cocoa/AppDelegate.m deleted file mode 100644 index 48514a05..00000000 --- a/bsnes/gb/Cocoa/AppDelegate.m +++ /dev/null @@ -1,449 +0,0 @@ -#import "AppDelegate.h" -#include "GBButtons.h" -#include "GBView.h" -#include -#import -#import -#import - -#define UPDATE_SERVER "https://sameboy.github.io" - -static uint32_t color_to_int(NSColor *color) -{ - color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; - return (((unsigned)(color.redComponent * 0xFF)) << 16) | - (((unsigned)(color.greenComponent * 0xFF)) << 8) | - ((unsigned)(color.blueComponent * 0xFF)); -} - -@implementation AppDelegate -{ - NSWindow *preferences_window; - NSArray *preferences_tabs; - NSString *_lastVersion; - NSString *_updateURL; - NSURLSessionDownloadTask *_updateTask; - enum { - UPDATE_DOWNLOADING, - UPDATE_EXTRACTING, - UPDATE_WAIT_INSTALL, - UPDATE_INSTALLING, - UPDATE_FAILED, - } _updateState; - NSString *_downloadDirectory; -} - -- (void) applicationDidFinishLaunching:(NSNotification *)notification -{ - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - for (unsigned i = 0; i < GBButtonCount; i++) { - if ([[defaults objectForKey:button_to_preference_name(i, 0)] isKindOfClass:[NSString class]]) { - [defaults removeObjectForKey:button_to_preference_name(i, 0)]; - } - } - [[NSUserDefaults standardUserDefaults] registerDefaults:@{ - @"GBRight": @(kVK_RightArrow), - @"GBLeft": @(kVK_LeftArrow), - @"GBUp": @(kVK_UpArrow), - @"GBDown": @(kVK_DownArrow), - - @"GBA": @(kVK_ANSI_X), - @"GBB": @(kVK_ANSI_Z), - @"GBSelect": @(kVK_Delete), - @"GBStart": @(kVK_Return), - - @"GBTurbo": @(kVK_Space), - @"GBRewind": @(kVK_Tab), - @"GBSlow-Motion": @(kVK_Shift), - - @"GBFilter": @"NearestNeighbor", - @"GBColorCorrection": @(GB_COLOR_CORRECTION_EMULATE_HARDWARE), - @"GBHighpassFilter": @(GB_HIGHPASS_REMOVE_DC_OFFSET), - @"GBRewindLength": @(10), - @"GBFrameBlendingMode": @([defaults boolForKey:@"DisableFrameBlending"]? GB_FRAME_BLENDING_MODE_DISABLED : GB_FRAME_BLENDING_MODE_ACCURATE), - - @"GBDMGModel": @(GB_MODEL_DMG_B), - @"GBCGBModel": @(GB_MODEL_CGB_E), - @"GBSGBModel": @(GB_MODEL_SGB2), - @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY), - - @"GBVolume": @(1.0), - }]; - - [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{ - JOYAxes2DEmulateButtonsKey: @YES, - JOYHatsEmulateButtonsKey: @YES, - }]; - - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { - [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; - } - - [self askAutoUpdates]; - - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]) { - [self checkForUpdates]; - } - - if ([[NSProcessInfo processInfo].arguments containsObject:@"--update-launch"]) { - [NSApp activateIgnoringOtherApps:true]; - } -} - -- (IBAction)toggleDeveloperMode:(id)sender -{ - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - [defaults setBool:![defaults boolForKey:@"DeveloperMode"] forKey:@"DeveloperMode"]; -} - -- (IBAction)switchPreferencesTab:(id)sender -{ - for (NSView *view in preferences_tabs) { - [view removeFromSuperview]; - } - NSView *tab = preferences_tabs[[sender tag]]; - NSRect old = [_preferencesWindow frame]; - NSRect new = [_preferencesWindow frameRectForContentRect:tab.frame]; - new.origin.x = old.origin.x; - new.origin.y = old.origin.y + (old.size.height - new.size.height); - [_preferencesWindow setFrame:new display:true animate:_preferencesWindow.visible]; - [_preferencesWindow.contentView addSubview:tab]; -} - -- (BOOL)validateMenuItem:(NSMenuItem *)anItem -{ - if ([anItem action] == @selector(toggleDeveloperMode:)) { - [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]]; - } - - if (anItem == self.linkCableMenuItem) { - return [[NSDocumentController sharedDocumentController] documents].count > 1; - } - return true; -} - -- (void)menuNeedsUpdate:(NSMenu *)menu -{ - NSMutableArray *items = [NSMutableArray array]; - NSDocument *currentDocument = [[NSDocumentController sharedDocumentController] currentDocument]; - - for (NSDocument *document in [[NSDocumentController sharedDocumentController] documents]) { - if (document == currentDocument) continue; - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:document.displayName action:@selector(connectLinkCable:) keyEquivalent:@""]; - item.representedObject = document; - item.image = [[NSWorkspace sharedWorkspace] iconForFile:document.fileURL.path]; - [item.image setSize:NSMakeSize(16, 16)]; - [items addObject:item]; - } - menu.itemArray = items; -} - -- (IBAction) showPreferences: (id) sender -{ - NSArray *objects; - if (!_preferencesWindow) { - [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects]; - NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject]; - _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier]; - preferences_tabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab, self.updatesTab]; - [self switchPreferencesTab:first_toolbar_item]; - [_preferencesWindow center]; -#ifndef UPDATE_SUPPORT - [_preferencesWindow.toolbar removeItemAtIndex:4]; -#endif - } - [_preferencesWindow makeKeyAndOrderFront:self]; -} - -- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender -{ - [self askAutoUpdates]; - /* Bring an existing panel to the foreground */ - for (NSWindow *window in [[NSApplication sharedApplication] windows]) { - if ([window isKindOfClass:[NSOpenPanel class]]) { - [(NSOpenPanel *)window makeKeyAndOrderFront:nil]; - return true; - } - } - [[NSDocumentController sharedDocumentController] openDocument:self]; - return true; -} - -- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification -{ - [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:true]; -} - -- (void)updateFound -{ - [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/raw_changes"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - - NSColor *linkColor = [NSColor colorWithRed:0.125 green:0.325 blue:1.0 alpha:1.0]; - if (@available(macOS 10.10, *)) { - linkColor = [NSColor linkColor]; - } - - NSString *changes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - NSRange cutoffRange = [changes rangeOfString:@""]; - if (cutoffRange.location != NSNotFound) { - changes = [changes substringToIndex:cutoffRange.location]; - } - - NSString *html = [NSString stringWithFormat:@"" - "" - "%@", - color_to_int([NSColor textColor]), - color_to_int(linkColor), - changes]; - - if ([(NSHTTPURLResponse *)response statusCode] == 200) { - dispatch_async(dispatch_get_main_queue(), ^{ - NSArray *objects; - [[NSBundle mainBundle] loadNibNamed:@"UpdateWindow" owner:self topLevelObjects:&objects]; - self.updateChanges.preferences.standardFontFamily = [NSFont systemFontOfSize:0].familyName; - self.updateChanges.preferences.fixedFontFamily = @"Menlo"; - self.updateChanges.drawsBackground = false; - [self.updateChanges.mainFrame loadHTMLString:html baseURL:nil]; - }); - } - }] resume]; -} - -- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems -{ - // Disable reload context menu - if ([defaultMenuItems count] <= 2) { - return nil; - } - return defaultMenuItems; -} - -- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sender.mainFrame.frameView.documentView.enclosingScrollView.drawsBackground = true; - sender.mainFrame.frameView.documentView.enclosingScrollView.backgroundColor = [NSColor textBackgroundColor]; - sender.policyDelegate = self; - [self.updateWindow center]; - [self.updateWindow makeKeyAndOrderFront:nil]; - }); -} - -- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener -{ - [listener ignore]; - [[NSWorkspace sharedWorkspace] openURL:[request URL]]; -} - -- (void)checkForUpdates -{ -#ifdef UPDATE_SUPPORT - [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/latest_version"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.updatesSpinner stopAnimation:nil]; - [self.updatesButton setEnabled:true]; - }); - if ([(NSHTTPURLResponse *)response statusCode] == 200) { - NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - NSArray *components = [string componentsSeparatedByString:@"|"]; - if (components.count != 2) return; - _lastVersion = components[0]; - _updateURL = components[1]; - if (![@GB_VERSION isEqualToString:_lastVersion] && - ![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBSkippedVersion"] isEqualToString:_lastVersion]) { - [self updateFound]; - } - } - }] resume]; -#endif -} - -- (IBAction)userCheckForUpdates:(id)sender -{ - if (self.updateWindow) { - [self.updateWindow makeKeyAndOrderFront:sender]; - } - else { - [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"GBSkippedVersion"]; - [self checkForUpdates]; - [sender setEnabled:false]; - [self.updatesSpinner startAnimation:sender]; - } -} - -- (void)askAutoUpdates -{ -#ifdef UPDATE_SUPPORT - if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAskedAutoUpdates"]) { - NSAlert *alert = [[NSAlert alloc] init]; - alert.messageText = @"Should SameBoy check for updates when launched?"; - alert.informativeText = @"SameBoy is frequently updated with new features, accuracy improvements, and bug fixes. This setting can always be changed in the preferences window."; - [alert addButtonWithTitle:@"Check on Launch"]; - [alert addButtonWithTitle:@"Don't Check on Launch"]; - - [[NSUserDefaults standardUserDefaults] setBool:[alert runModal] == NSAlertFirstButtonReturn forKey:@"GBAutoUpdatesEnabled"]; - [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBAskedAutoUpdates"]; - } -#endif -} - -- (IBAction)skipVersion:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:_lastVersion forKey:@"GBSkippedVersion"]; - [self.updateWindow performClose:sender]; -} - -- (IBAction)installUpdate:(id)sender -{ - [self.updateProgressSpinner startAnimation:nil]; - self.updateProgressButton.title = @"Cancel"; - self.updateProgressButton.enabled = true; - self.updateProgressLabel.stringValue = @"Downloading update..."; - _updateState = UPDATE_DOWNLOADING; - _updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { - _updateTask = nil; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Extracting update..."; - _updateState = UPDATE_EXTRACTING; - }); - - _downloadDirectory = [[[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory - inDomain:NSUserDomainMask - appropriateForURL:[[NSBundle mainBundle] bundleURL] - create:true - error:nil] path]; - NSTask *unzipTask; - if (!_downloadDirectory) { - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Failed to extract update."; - _updateState = UPDATE_FAILED; - self.updateProgressButton.title = @"Close"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - } - - unzipTask = [[NSTask alloc] init]; - unzipTask.launchPath = @"/usr/bin/unzip"; - unzipTask.arguments = @[location.path, @"-d", _downloadDirectory]; - [unzipTask launch]; - [unzipTask waitUntilExit]; - if (unzipTask.terminationStatus != 0 || unzipTask.terminationReason != NSTaskTerminationReasonExit) { - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Failed to extract update."; - _updateState = UPDATE_FAILED; - self.updateProgressButton.title = @"Close"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - return; - } - - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Update ready, save your game progress and click Install."; - _updateState = UPDATE_WAIT_INSTALL; - self.updateProgressButton.title = @"Install"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - }]; - [_updateTask resume]; - - self.updateProgressWindow.preventsApplicationTerminationWhenModal = false; - [self.updateWindow beginSheet:self.updateProgressWindow completionHandler:^(NSModalResponse returnCode) { - [self.updateWindow close]; - }]; -} - -- (void)performUpgrade -{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Instaling update..."; - _updateState = UPDATE_INSTALLING; - self.updateProgressButton.enabled = false; - [self.updateProgressSpinner startAnimation:nil]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSString *executablePath = [[NSBundle mainBundle] executablePath]; - NSString *contentsPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"]; - NSString *contentsTempPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"TempContents"]; - NSString *updateContentsPath = [_downloadDirectory stringByAppendingPathComponent:@"SameBoy.app/Contents"]; - NSError *error = nil; - [[NSFileManager defaultManager] moveItemAtPath:contentsPath toPath:contentsTempPath error:&error]; - if (error) { - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - _downloadDirectory = nil; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Failed to install update."; - _updateState = UPDATE_FAILED; - self.updateProgressButton.title = @"Close"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - return; - } - [[NSFileManager defaultManager] moveItemAtPath:updateContentsPath toPath:contentsPath error:&error]; - if (error) { - [[NSFileManager defaultManager] moveItemAtPath:contentsTempPath toPath:contentsPath error:nil]; - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - _downloadDirectory = nil; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Failed to install update."; - _updateState = UPDATE_FAILED; - self.updateProgressButton.title = @"Close"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - return; - } - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - [[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil]; - _downloadDirectory = nil; - atexit_b(^{ - execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL); - }); - - dispatch_async(dispatch_get_main_queue(), ^{ - [NSApp terminate:nil]; - }); - }); -} - -- (IBAction)updateAction:(id)sender -{ - switch (_updateState) { - case UPDATE_DOWNLOADING: - [_updateTask cancelByProducingResumeData:nil]; - _updateTask = nil; - [self.updateProgressWindow close]; - break; - case UPDATE_WAIT_INSTALL: - [self performUpgrade]; - break; - case UPDATE_EXTRACTING: - case UPDATE_INSTALLING: - break; - case UPDATE_FAILED: - [self.updateProgressWindow close]; - break; - } -} - -- (void)dealloc -{ - if (_downloadDirectory) { - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - } -} - -- (IBAction)nop:(id)sender -{ -} -@end diff --git a/bsnes/gb/Cocoa/AppIcon.icns b/bsnes/gb/Cocoa/AppIcon.icns index 92ad4c65..2a85022a 100644 Binary files a/bsnes/gb/Cocoa/AppIcon.icns and b/bsnes/gb/Cocoa/AppIcon.icns differ diff --git a/bsnes/gb/Cocoa/Assets.car b/bsnes/gb/Cocoa/Assets.car new file mode 100644 index 00000000..0bff5927 Binary files /dev/null and b/bsnes/gb/Cocoa/Assets.car differ diff --git a/bsnes/gb/Cocoa/AudioRecordingAccessoryView.xib b/bsnes/gb/Cocoa/AudioRecordingAccessoryView.xib new file mode 100644 index 00000000..6dda38b7 --- /dev/null +++ b/bsnes/gb/Cocoa/AudioRecordingAccessoryView.xib @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/BackstepTemplate.png b/bsnes/gb/Cocoa/BackstepTemplate.png new file mode 100644 index 00000000..b5e41152 Binary files /dev/null and b/bsnes/gb/Cocoa/BackstepTemplate.png differ diff --git a/bsnes/gb/Cocoa/BackstepTemplate@2x.png b/bsnes/gb/Cocoa/BackstepTemplate@2x.png new file mode 100644 index 00000000..be085907 Binary files /dev/null and b/bsnes/gb/Cocoa/BackstepTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/BigSurToolbar.h b/bsnes/gb/Cocoa/BigSurToolbar.h index 9057d340..a136b1bf 100644 --- a/bsnes/gb/Cocoa/BigSurToolbar.h +++ b/bsnes/gb/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/bsnes/gb/Cocoa/CPU.png b/bsnes/gb/Cocoa/CPU.png index 7f136213..f289a077 100644 Binary files a/bsnes/gb/Cocoa/CPU.png and b/bsnes/gb/Cocoa/CPU.png differ diff --git a/bsnes/gb/Cocoa/CPU@2x.png b/bsnes/gb/Cocoa/CPU@2x.png index 3c86883a..17e37121 100644 Binary files a/bsnes/gb/Cocoa/CPU@2x.png and b/bsnes/gb/Cocoa/CPU@2x.png differ diff --git a/bsnes/gb/Cocoa/CPU~solid.png b/bsnes/gb/Cocoa/CPU~solid.png new file mode 100644 index 00000000..f536b1a3 Binary files /dev/null and b/bsnes/gb/Cocoa/CPU~solid.png differ diff --git a/bsnes/gb/Cocoa/CPU~solid@2x.png b/bsnes/gb/Cocoa/CPU~solid@2x.png new file mode 100644 index 00000000..7ba3d7ab Binary files /dev/null and b/bsnes/gb/Cocoa/CPU~solid@2x.png differ diff --git a/bsnes/gb/Cocoa/CPU~solid~dark.png b/bsnes/gb/Cocoa/CPU~solid~dark.png new file mode 100644 index 00000000..99250bb1 Binary files /dev/null and b/bsnes/gb/Cocoa/CPU~solid~dark.png differ diff --git a/bsnes/gb/Cocoa/CPU~solid~dark@2x.png b/bsnes/gb/Cocoa/CPU~solid~dark@2x.png new file mode 100644 index 00000000..f9020a1b Binary files /dev/null and b/bsnes/gb/Cocoa/CPU~solid~dark@2x.png differ diff --git a/bsnes/gb/Cocoa/Cartridge.icns b/bsnes/gb/Cocoa/Cartridge.icns index 6e0c78dd..36722fb8 100644 Binary files a/bsnes/gb/Cocoa/Cartridge.icns and b/bsnes/gb/Cocoa/Cartridge.icns differ diff --git a/bsnes/gb/Cocoa/CheatSearch.xib b/bsnes/gb/Cocoa/CheatSearch.xib new file mode 100644 index 00000000..a5645b8a --- /dev/null +++ b/bsnes/gb/Cocoa/CheatSearch.xib @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/ColorCartridge.icns b/bsnes/gb/Cocoa/ColorCartridge.icns index 35fb2932..7ebafff6 100644 Binary files a/bsnes/gb/Cocoa/ColorCartridge.icns and b/bsnes/gb/Cocoa/ColorCartridge.icns differ diff --git a/bsnes/gb/Cocoa/ContinueTemplate.png b/bsnes/gb/Cocoa/ContinueTemplate.png new file mode 100644 index 00000000..eb72962c Binary files /dev/null and b/bsnes/gb/Cocoa/ContinueTemplate.png differ diff --git a/bsnes/gb/Cocoa/ContinueTemplate@2x.png b/bsnes/gb/Cocoa/ContinueTemplate@2x.png new file mode 100644 index 00000000..586ab3d2 Binary files /dev/null and b/bsnes/gb/Cocoa/ContinueTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/Display.png b/bsnes/gb/Cocoa/Display.png index 5753f558..5008a82f 100644 Binary files a/bsnes/gb/Cocoa/Display.png and b/bsnes/gb/Cocoa/Display.png differ diff --git a/bsnes/gb/Cocoa/Display@2x.png b/bsnes/gb/Cocoa/Display@2x.png index 6a71d221..8813e117 100644 Binary files a/bsnes/gb/Cocoa/Display@2x.png and b/bsnes/gb/Cocoa/Display@2x.png differ diff --git a/bsnes/gb/Cocoa/Display~solid.png b/bsnes/gb/Cocoa/Display~solid.png new file mode 100644 index 00000000..beb392f0 Binary files /dev/null and b/bsnes/gb/Cocoa/Display~solid.png differ diff --git a/bsnes/gb/Cocoa/Display~solid@2x.png b/bsnes/gb/Cocoa/Display~solid@2x.png new file mode 100644 index 00000000..d16e4921 Binary files /dev/null and b/bsnes/gb/Cocoa/Display~solid@2x.png differ diff --git a/bsnes/gb/Cocoa/Display~solid~dark.png b/bsnes/gb/Cocoa/Display~solid~dark.png new file mode 100644 index 00000000..bff7acff Binary files /dev/null and b/bsnes/gb/Cocoa/Display~solid~dark.png differ diff --git a/bsnes/gb/Cocoa/Display~solid~dark@2x.png b/bsnes/gb/Cocoa/Display~solid~dark@2x.png new file mode 100644 index 00000000..02c57524 Binary files /dev/null and b/bsnes/gb/Cocoa/Display~solid~dark@2x.png differ diff --git a/bsnes/gb/Cocoa/Document.h b/bsnes/gb/Cocoa/Document.h index d6f89de9..30966cee 100644 --- a/bsnes/gb/Cocoa/Document.h +++ b/bsnes/gb/Cocoa/Document.h @@ -1,61 +1,98 @@ #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 "GBCPUView.h" +#import "GBOSDView.h" +#import "GBDebuggerButton.h" + +enum model { + MODEL_NONE, + MODEL_DMG, + MODEL_CGB, + MODEL_AGB, + MODEL_SGB, + MODEL_MGB, + MODEL_AUTO, + + MODEL_QUICK_RESET = -1, +}; @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 *spritesTableView; -@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)); --(uint8_t) readMemory:(uint16_t) addr; --(void) writeMemory:(uint16_t) addr value:(uint8_t)value; --(void) performAtomicBlock: (void (^)())block; --(void) connectLinkCable:(NSMenuItem *)sender; --(int)loadStateFile:(const char *)path noErrorOnNotFound:(bool)noErrorOnFileNotFound; +@property IBOutlet GBDebuggerButton *debuggerContinueButton; +@property IBOutlet GBDebuggerButton *debuggerNextButton; +@property IBOutlet GBDebuggerButton *debuggerStepButton; +@property IBOutlet GBDebuggerButton *debuggerFinishButton; +@property IBOutlet GBDebuggerButton *debuggerBackstepButton; + +@property IBOutlet NSScrollView *debuggerScrollView; +@property IBOutlet NSView *debugBar; + +@property IBOutlet GBCPUView *cpuView; +@property IBOutlet NSTextField *cpuCounter; + ++ (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale; +- (void) performAtomicBlock: (void (^)())block; +- (void) connectLinkCable:(NSMenuItem *)sender; +- (int)loadStateFile:(const char *)path noErrorOnNotFound:(bool)noErrorOnFileNotFound; +- (NSString *)captureOutputForBlock: (void (^)())block; +- (NSFont *)debuggerFontOfSize:(unsigned)size; @end diff --git a/bsnes/gb/Cocoa/Document.m b/bsnes/gb/Cocoa/Document.m index 2a54a139..b556d008 100644 --- a/bsnes/gb/Cocoa/Document.m +++ b/bsnes/gb/Cocoa/Document.m @@ -1,112 +1,140 @@ -#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 "GBCheatSearchController.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 /* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */ /* Todo: Split into category files! This is so messy!!! */ -enum model { - MODEL_NONE, - MODEL_DMG, - MODEL_CGB, - MODEL_AGB, - MODEL_SGB, -}; @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 _usesAutoModel; - bool borderModeChanged; + bool _rewind; + bool _modelsChanging; + + NSCondition *_audioLock; + GB_sample_t *_audioBuffer; + size_t _audioBufferSize; + size_t _audioBufferPosition; + size_t _audioBufferNeeded; + double _volume; + + 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; + + GBCheatSearchController *_cheatSearchController; } -@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) +static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes) { Document *self = (__bridge Document *)GB_get_user_data(gb); [self log:string withAttributes: attributes]; @@ -149,6 +177,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"]; @@ -172,16 +206,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) @@ -190,50 +224,40 @@ 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_AUTO: case MODEL_CGB: return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]; @@ -245,98 +269,142 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on) return model; } + case MODEL_MGB: + return GB_MODEL_MGB; + case MODEL_AGB: - return GB_MODEL_AGB; + return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBAGBModel"]; } } -- (void) updatePalette +- (void)updatePalette { - switch ([[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorPalette"]) { - 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); - break; - } + 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_enable_skipped_frame_vblank_callbacks(&_gb, true); + 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_debugger_set_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) { + weakSelf->_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); + }]; + + [self observeStandardDefaultsKey:@"GBDebuggerFont" withBlock:^(NSString *value) { + [weakSelf updateFonts]; + }]; + + [self observeStandardDefaultsKey:@"GBDebuggerFontSize" withBlock:^(NSString *value) { + [weakSelf updateFonts]; + }]; + + [self observeStandardDefaultsKey:@"GBTurboCap" withBlock:^(NSNumber *value) { + if (!_master) { + GB_set_turbo_cap(gb, value.doubleValue); + } + }]; } -- (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 (type == GB_VBLANK_TYPE_SKIPPED_FRAME) { + double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb); + [_cpuView addSample:frameUsage]; + return; + } + 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]; - } + + double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb); + [_cpuView addSample:frameUsage]; + + if (self.consoleWindow.visible) { + double secondUsage = GB_debugger_get_second_cpu_usage(&_gb); + dispatch_async(dispatch_get_main_queue(), ^{ + [_cpuView setNeedsDisplay:true]; + _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100]; }); - borderModeChanged = false; } - GB_set_pixels_output(&gb, self.view.pixels); + + 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); + } + if (self.vramWindow.isVisible) { dispatch_async(dispatch_get_main_queue(), ^{ self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0; @@ -344,8 +412,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…"]; } } @@ -354,35 +422,34 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on) if (_gbsVisualizer) { [_gbsVisualizer addSample:sample]; } - [audioLock lock]; - if (self.audioClient.isPlaying) { - if (audioBufferPosition == audioBufferSize) { - if (audioBufferSize >= 0x4000) { - audioBufferPosition = 0; - [audioLock unlock]; + [_audioLock lock]; + if (_audioClient.isPlaying) { + 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); } - double volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; - if (volume != 1) { - sample->left *= volume; - sample->right *= volume; + 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 @@ -390,40 +457,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); - self.audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { - [audioLock lock]; + 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]; - if (audioBufferPosition < nFrames) { - audioBufferNeeded = nFrames; - [audioLock wait]; + 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 && audioBufferPosition < nFrames + 4800) { - memcpy(buffer, audioBuffer, nFrames * sizeof(*buffer)); - memmove(audioBuffer, audioBuffer + nFrames, (audioBufferPosition - nFrames) * sizeof(*buffer)); - audioBufferPosition = audioBufferPosition - nFrames; + 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; + memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer)); + _audioBufferPosition = 0; } - [audioLock unlock]; + [_audioLock unlock]; } andSampleRate:96000]; if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) { - [self.audioClient start]; + [_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"]) { @@ -453,59 +527,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]; - [self.audioClient stop]; - self.audioClient = nil; + [_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]; @@ -528,109 +608,178 @@ 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_DMG0] = @"dmg0_boot", + [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_CGB0] = @"cgb0_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]); +} + +- (enum model)bestModelForROM +{ + uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL); + if (!rom) return MODEL_CGB; + + if (rom[0x143] & 0x80) { // Has CGB features + return MODEL_CGB; + } + if (rom[0x146] == 3) { // Has SGB features + return MODEL_SGB; + } + + if (rom[0x14B] == 1) { // Nintendo-licensed (most likely has boot ROM palettes) + return MODEL_CGB; + } + + if (rom[0x14B] == 0x33 && + rom[0x144] == '0' && + rom[0x145] == '1') { // Ditto + return MODEL_CGB; + } + + return MODEL_DMG; } - (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]; - } - - GB_switch_model_and_reset(&gb, [self internalModel]); - - 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"]; + _currentModel = (enum model)[sender tag]; + _usesAutoModel = _currentModel == MODEL_AUTO; + [[NSUserDefaults standardUserDefaults] setInteger:_currentModel forKey:@"GBEmulatedModel"]; } /* Reload the ROM, SAV and SYM files */ [self loadROM]; + 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)) { + [self.view screenSizeChanged]; + } + [self updateMinSize]; + [self start]; + if (_gbsTracks) { + [self changeGBSTrack:sender]; + } - 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 { @@ -640,22 +789,103 @@ 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); } } +- (NSFont *)debuggerFontOfSize:(unsigned)size +{ + if (!size) { + size = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"]; + } + + bool retry = false; + +again:; + NSString *selectedFont = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBDebuggerFont"]; + if (@available(macOS 10.15, *)) { + if ([selectedFont isEqual:@"SF Mono"]) { + return [NSFont monospacedSystemFontOfSize:size weight:NSFontWeightRegular]; + } + } + + NSFont *ret = [NSFont fontWithName:selectedFont size:size]; + if (ret) return ret; + + if (retry) { + return [NSFont userFixedPitchFontOfSize:size]; + } + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBDebuggerFont"]; + retry = true; + goto again; +} + + +- (void)updateFonts +{ + _hexController.font = [self debuggerFontOfSize:12]; + [self.paletteView reloadData:self]; + [self.objectView reloadData:self]; + + NSFont *newFont = [self debuggerFontOfSize:0]; + NSFont *newBoldFont = [[NSFontManager sharedFontManager] convertFont:newFont toHaveTrait:NSBoldFontMask]; + self.debuggerSideViewInput.font = newFont; + + unsigned inputHeight = MAX(ceil([@" " sizeWithAttributes:@{ + NSFontAttributeName: newFont + }].height) + 6, 26); + + + NSRect frame = _consoleInput.frame; + unsigned oldHeight = frame.size.height; + frame.size.height = inputHeight; + _consoleInput.frame = frame; + + frame = _debugBar.frame; + frame.origin.y += (signed)(inputHeight - oldHeight); + _debugBar.frame = frame; + + frame = _debuggerScrollView.frame; + frame.origin.y += (signed)(inputHeight - oldHeight); + frame.size.height -= (signed)(inputHeight - oldHeight); + _debuggerScrollView.frame = frame; + + _consoleInput.font = newFont; + + for (NSTextView *view in @[_debuggerSideView, _consoleOutput]) { + NSMutableAttributedString *newString = view.attributedString.mutableCopy; + [view.attributedString enumerateAttribute:NSFontAttributeName + inRange:NSMakeRange(0, view.attributedString.length) + options:0 + usingBlock:^(NSFont *value, NSRange range, BOOL *stop) { + if ([[NSFontManager sharedFontManager] fontNamed:value.fontName hasTraits:NSBoldFontMask]) { + [newString addAttributes:@{ + NSFontAttributeName: newBoldFont + } range:range]; + } + else { + [newString addAttributes:@{ + NSFontAttributeName: newFont + } range:range]; + } + }]; + [view.textStorage setAttributedString:newString]; + } + [_consoleOutput scrollToEndOfDocument:nil]; +} + - (void)windowControllerDidLoadNib:(NSWindowController *)aController { [super windowControllerDidLoadNib:aController]; // Interface Builder bug? - [self.consoleWindow setContentSize:self.consoleWindow.minSize]; + [self.consoleWindow setContentSize:self.consoleWindow.frame.size]; /* Close Open Panels, if any */ for (NSWindow *window in [[NSApplication sharedApplication] windows]) { if ([window isKindOfClass:[NSOpenPanel class]]) { @@ -666,11 +896,11 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init]; [paragraph_style setLineSpacing:2]; - self.debuggerSideViewInput.font = [NSFont userFixedPitchFontOfSize:12]; + self.debuggerSideViewInput.font = [self debuggerFontOfSize:0]; 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,98 +923,99 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [self.vramWindow setFrame:vram_window_rect display:true animate:false]; - self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]]; - self.debuggerSplitView.dividerColor = [NSColor clearColor]; + if (@available(macOS 11.0, *)) { + self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent]; + self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent]; + self.vramWindow.subtitle = [self.fileURL.path lastPathComponent]; + } + else { + self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]]; + 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]; - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) { - current_model = MODEL_DMG; - } - else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateSGB"]) { - current_model = MODEL_SGB; - } - else { - current_model = [[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateAGB"]? MODEL_AGB : MODEL_CGB; - } + [self observeStandardDefaultsKey:@"GBVolume" withBlock:^(id newValue) { + weakSelf->_volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"]; + }]; + + + _currentModel = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBEmulatedModel"]; + _usesAutoModel = _currentModel == MODEL_AUTO; [self initCommon]; - self.view.gb = &gb; + self.view.gb = &_gb; self.view.osdView = _osdView; [self.view screenSizeChanged]; if ([self loadROM]) { @@ -798,38 +1029,41 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) } } -- (void) initMemoryView +- (void)initMemoryView { - hex_controller = [[HFController alloc] init]; - [hex_controller setBytesPerColumn:1]; - [hex_controller setEditMode:HFOverwriteMode]; + _hexController = [[HFController alloc] init]; + _hexController.font = [self debuggerFontOfSize:12]; + [_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]; @@ -840,7 +1074,18 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [layoutView setFrame:layoutViewFrame]; [layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin]; [self.memoryView addSubview:layoutView]; + self.memoryView = layoutView; + CGSize contentSize = _memoryWindow.contentView.frame.size; + while (_hexController.bytesPerLine < 16) { + contentSize.width += 4; + [_memoryWindow setContentSize:contentSize]; + } + while (_hexController.bytesPerLine > 16) { + contentSize.width -= 4; + [_memoryWindow setContentSize:contentSize]; + } + self.memoryBankItem.enabled = false; } @@ -863,11 +1108,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 @@ -895,12 +1140,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 @@ -928,37 +1178,144 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [self.gbsNextPrevButton imageForSegment:i].template = true; } - if (!self.audioClient.isPlaying) { - [self.audioClient start]; + if (!_audioClient.isPlaying) { + [_audioClient start]; } if (@available(macOS 10.10, *)) { _mainWindow.titlebarAppearsTransparent = true; } + + if (@available(macOS 26.0, *)) { + // There's a new minimum width for segmented controls in Solarium + NSRect frame = _gbsNextPrevButton.frame; + frame.origin.x -= 16; + _gbsNextPrevButton.frame = frame; + + frame = _gbsTracks.frame; + frame.size.width -= 16; + _gbsTracks.frame = frame; + } } -- (int) loadROM +- (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, true); + 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]; @@ -966,13 +1323,27 @@ 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]; + if (_usesAutoModel) { + _currentModel = [self bestModelForROM]; + } 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]; @@ -981,51 +1352,67 @@ 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]; [self.printerFeedWindow close]; [self.cheatsWindow close]; + [_cheatSearchController.window close]; [super close]; } - (IBAction) interrupt:(id)sender { [self log:"^C\n"]; - GB_debugger_break(&gb); + GB_debugger_break(&_gb); [self start]; [self.consoleWindow makeKeyAndOrderFront:nil]; + double secondUsage = GB_debugger_get_second_cpu_usage(&_gb); + _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100]; [self.consoleInput becomeFirstResponder]; } - (IBAction)mute:(id)sender { - if (self.audioClient.isPlaying) { - [self.audioClient stop]; + if (_audioClient.isPlaying) { + [_audioClient stop]; } else { - [self.audioClient start]; - if ([[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"] == 0) { + [_audioClient start]; + if (_volume == 0) { [GBWarningPopover popoverWithContents:@"Warning: Volume is set to to zero in the preferences panel" onWindow:self.mainWindow]; } } - [[NSUserDefaults standardUserDefaults] setBool:!self.audioClient.isPlaying forKey:@"Mute"]; + [[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:!self.audioClient.isPlaying]; + if (_running) { + [(NSMenuItem *)anItem setState:!_audioClient.isPlaying]; + } + else { + [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]]; + } } 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) || (anItem.tag == MODEL_AUTO && _usesAutoModel)]; } else if ([anItem action] == @selector(interrupt:)) { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { @@ -1033,102 +1420,204 @@ 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)]; + } + else if ([anItem action] == @selector(toggleDisplayObjects:)) { + [(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]; + } + else if ([anItem action] == @selector(reloadROM:)) { + return !_gbsTracks; + } + return [super validateUserInterfaceItem:anItem]; } - (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 scrollToEndOfDocument:nil]; + [textView.textStorage appendAttributedString:_pendingConsoleOutput]; + if (!_logToSideView) { + [textView scrollToEndOfDocument:nil]; + } if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { [self.consoleWindow orderFront:nil]; } - pending_console_output = nil; -} - [console_output_lock unlock]; - + _pendingConsoleOutput = nil; + } + [_consoleOutputLock unlock]; } -- (void) log: (const char *) string withAttributes: (GB_log_attributes) attributes +- (void)log:(const char *)string withAttributes:(GB_log_attributes_t)attributes { NSString *nsstring = @(string); // For ref-counting - if (capturedOutput) { - [capturedOutput appendString:nsstring]; + if (_capturedOutput) { + [_capturedOutput appendString:nsstring]; return; } - NSFont *font = [NSFont userFixedPitchFontOfSize:12]; + NSFont *font = [self debuggerFontOfSize:0]; NSUnderlineStyle underline = NSUnderlineStyleNone; if (attributes & GB_LOG_BOLD) { font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask]; @@ -1147,20 +1636,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; @@ -1168,44 +1657,58 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)showConsoleWindow:(id)sender { - [self.consoleWindow orderBack:nil]; + [self.consoleWindow orderFront:nil]; + double secondUsage = GB_debugger_get_second_cpu_usage(&_gb); + _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100]; +} + +- (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; } @@ -1216,49 +1719,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; } @@ -1267,12 +1790,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; } @@ -1283,7 +1806,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) { @@ -1300,7 +1823,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) { @@ -1321,8 +1844,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]; } } @@ -1337,80 +1860,77 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [self log:log withAttributes:0]; } -- (uint8_t) readMemory:(uint16_t)addr +- (void)performAtomicBlock: (void (^)())block { - while (!GB_is_inited(&gb)); - return GB_read_memory(&gb, addr); + while (!GB_is_inited(&_gb)); + bool isRunning = _running && !GB_debugger_is_stopped(&_gb); + if (_master) { + isRunning |= _master->_running; + } + if (!isRunning) { + block(); + return; + } + + if (_master) { + [_master performAtomicBlock:block]; + return; + } + + if ([NSThread currentThread] == _emulationThread) { + block(); + return; + } + + _pendingAtomicBlock = block; + while (_pendingAtomicBlock); } -- (void) writeMemory:(uint16_t)addr value:(uint8_t)value +- (NSString *)captureOutputForBlock: (void (^)())block { - while (!GB_is_inited(&gb)); - GB_write_memory(&gb, addr, value); -} - -- (void) performAtomicBlock: (void (^)())block -{ - while (!GB_is_inited(&gb)); - bool was_running = running && !GB_debugger_is_stopped(&gb); - if (master) { - was_running |= master->running; - } - if (was_running) { - [self stop]; - } - block(); - if (was_running) { - [self start]; - } -} - -- (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]; + } + if (_cheatSearchController.window.isVisible) { + if ([_cheatSearchController.tableView editedColumn] != 2) { + [_cheatSearchController.tableView reloadData]; + } } } - (IBAction) reloadVRAMData: (id) sender { if (self.vramWindow.isVisible) { + 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 */ @@ -1423,7 +1943,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; @@ -1445,12 +1965,12 @@ 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); - self.tilemapImageView.scrollRect = NSMakeRect(GB_read_memory(&gb, 0xFF00 | GB_IO_SCX), - GB_read_memory(&gb, 0xFF00 | GB_IO_SCY), + self.tilemapImageView.scrollRect = NSMakeRect(io_regs[GB_IO_SCX], + io_regs[GB_IO_SCY], 160, 144); self.tilemapImageView.image = [Document imageFromData:data width:256 height:256 scale:1.0]; self.tilemapImageView.layer.magnificationFilter = kCAFilterNearest; @@ -1460,13 +1980,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.spritesTableView reloadData]; - oamUpdating = false; - } + [self.objectView reloadData:self]; }); } break; @@ -1475,7 +1991,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) /* Palettes */ { dispatch_async(dispatch_get_main_queue(), ^{ - [self.paletteTableView reloadData]; + [self.paletteView reloadData:self]; }); } break; @@ -1485,7 +2001,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction) showMemory:(id)sender { - if (!hex_controller) { + if (!_hexController) { [self initMemoryView]; } [self.memoryWindow makeKeyAndOrderFront:sender]; @@ -1493,73 +2009,130 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)hexGoTo:(id)sender { + NSString *expression = [sender stringValue]; + __block uint16_t addr = 0; + __block uint16_t bank = 0; + __block bool fail = false; NSString *error = [self captureOutputForBlock:^{ - uint16_t addr; - if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, NULL)) { - return; + if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) { + fail = true; } - addr -= lineRep.valueOffset; - if (addr >= hex_controller.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]]; }]; + if (error) { NSBeep(); [GBWarningPopover popoverWithContents:error onView:sender]; } + if (fail) return; + + + 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; + } + + [_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; + } + } } - (void)hexUpdateBank:(NSControl *)sender ignoreErrors: (bool)ignore_errors { + NSString *expression = [sender stringValue]; + __block uint16_t addr, bank; + __block bool fail = false; NSString *error = [self captureOutputForBlock:^{ - uint16_t addr, bank; - if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, &bank)) { + if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) { + fail = true; return; } - - if (bank == (uint16_t) -1) { - bank = addr; - } - - uint16_t n_banks = 1; - switch ([(GBMemoryByteArray *)(hex_controller.byteArray) mode]) { - case GBMemoryROM: { - size_t rom_size; - 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; - break; - case GBMemoryExternalRAM: { - size_t ram_size; - 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; - break; - case GBMemoryEntireSpace: - break; - } - - bank %= n_banks; - - [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]]; - [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:bank]; - [hex_controller reloadData]; }]; if (error && !ignore_errors) { NSBeep(); [GBWarningPopover popoverWithContents:error onView:sender]; } + + if (fail) return; + + if (bank == (uint16_t) -1) { + bank = addr; + } + + uint16_t n_banks = 1; + switch ([(GBMemoryByteArray *)(_hexController.byteArray) mode]) { + case GBMemoryROM: { + size_t rom_size; + 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; + break; + case GBMemoryExternalRAM: { + size_t ram_size; + 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; + break; + case GBMemoryEntireSpace: + break; + } + + bank %= n_banks; + + [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:bank]; + _statusRep.bankForDescription = bank; + [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]]; + [_hexController reloadData]; } - (IBAction)hexUpdateBank:(NSControl *)sender @@ -1570,40 +2143,45 @@ 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; + [self.memoryBankInput setStringValue:(bank == (uint16_t)-1)? @"" : + [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 @@ -1615,7 +2193,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: @@ -1628,7 +2206,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) } case AVAuthorizationStatusDenied: case AVAuthorizationStatusRestricted: - GB_camera_updated(&gb); + GB_camera_updated(&_gb); return; } } @@ -1636,63 +2214,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 activeFormat] 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; @@ -1742,14 +2322,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: @@ -1777,15 +2357,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]; @@ -1797,7 +2377,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, @@ -1822,79 +2402,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.spritesTableView) { - 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.spritesTableView) { - 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 sprites in line": @""; - - } - } - return nil; -} - -- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row -{ - return tableView == self.spritesTableView; -} - -- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row -{ - return false; + return _oamInfo; } - (IBAction)showVRAMViewer:(id)sender @@ -1903,33 +2413,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) { @@ -1942,9 +2462,9 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (IBAction)savePrinterFeed:(id)sender { - bool shouldResume = running; + bool shouldResume = _running; [self stop]; - NSSavePanel * savePanel = [NSSavePanel savePanel]; + NSSavePanel *savePanel = [NSSavePanel savePanel]; [savePanel setAllowedFileTypes:@[@"png"]]; [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) { if (result == NSModalResponseOK) { @@ -1968,8 +2488,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) { [self disconnectLinkCable]; [self performAtomicBlock:^{ - accessory = GBAccessoryNone; - GB_disconnect_serial(&gb); + GB_disconnect_serial(&_gb); }]; } @@ -1977,8 +2496,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); }]; } @@ -1986,92 +2504,23 @@ 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) 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]]; - + if (@available(macOS 11.0, *)) { + self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent]; + self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent]; + self.vramWindow.subtitle = [self.fileURL.path lastPathComponent]; + } + else { + self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]]; + self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [self.fileURL.path lastPathComponent]]; + self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [self.fileURL.path lastPathComponent]]; + } } - (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview; @@ -2106,11 +2555,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 @@ -2118,29 +2562,38 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [self.cheatsWindow makeKeyAndOrderFront:nil]; } +- (IBAction)showCheatSearch:(id)sender +{ + if (!_cheatSearchController) { + _cheatSearchController = [GBCheatSearchController controllerWithDocument:self]; + } + [_cheatSearchController.window makeKeyAndOrderFront:sender]; +} + - (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); + GB_set_turbo_cap(&_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]); + GB_set_turbo_cap(&partner->_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]); } } @@ -2150,56 +2603,55 @@ 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; + GB_set_turbo_cap(&partner->_gb, 0); + _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 @@ -2210,18 +2662,12 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) } 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]; } - [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]; return ret; } @@ -2245,7 +2691,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; @@ -2283,7 +2729,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]; @@ -2312,4 +2758,215 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) [self.osdView displayText:@"Screenshot copied"]; } +- (IBAction)toggleDisplayBackground:(id)sender +{ + 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)); +} + +- (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]; +} + ++ (NSArray *)readableTypes +{ + NSMutableSet *set = [NSMutableSet setWithArray:[super readableTypes]]; + for (NSString *type in @[@"gb", @"gbc", @"isx", @"gbs"]) { + [set addObject:(__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, + (__bridge CFStringRef)type, + NULL)]; + } + return [set allObjects]; +} + @end diff --git a/bsnes/gb/Cocoa/Document.xib b/bsnes/gb/Cocoa/Document.xib index c76c148a..6c7e8de4 100644 --- a/bsnes/gb/Cocoa/Document.xib +++ b/bsnes/gb/Cocoa/Document.xib @@ -1,8 +1,9 @@ - + - + + @@ -14,21 +15,33 @@ + + + + + + + + + + + + - + - + @@ -43,11 +56,11 @@ - + - + @@ -77,60 +90,41 @@ - + - - + + - - + + - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - + - + - + - + - - - + + + - - + + - - + + @@ -138,36 +132,137 @@ - - + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - - + + + - - + + - + @@ -181,27 +276,27 @@ - + - + - + - - - + + + - - + + - + @@ -216,10 +311,38 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -233,31 +356,34 @@ + + + - + - + - + - - + + - - - + + + - + @@ -276,31 +402,29 @@ - - + + - - + + - - + - - - - - - + + + + + + - @@ -315,15 +439,18 @@ + + + - + - - + + @@ -331,7 +458,7 @@ - + @@ -341,17 +468,17 @@ - + - - + + - + @@ -360,7 +487,7 @@ - + @@ -392,7 +519,7 @@ - + @@ -431,7 +558,7 @@ - + @@ -464,7 +591,7 @@ - + @@ -482,7 +609,7 @@ - + @@ -502,252 +629,47 @@ - + - + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + - - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -755,22 +677,20 @@ - + - + - - - + - - + + @@ -787,12 +707,15 @@ + + + - + - + @@ -805,188 +728,62 @@ - + + + - + + + + + + + + + + + + + + + - - + + + + - - - - + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - - - - - - + + + - + - - + + @@ -994,19 +791,17 @@ - - + - + - @@ -1016,26 +811,24 @@ - + - - + - + - - + @@ -1051,21 +844,167 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + - + + + + @@ -1082,7 +1021,14 @@ + + + + - + + + + diff --git a/bsnes/gb/Cocoa/FinishTemplate.png b/bsnes/gb/Cocoa/FinishTemplate.png new file mode 100644 index 00000000..5aa831df Binary files /dev/null and b/bsnes/gb/Cocoa/FinishTemplate.png differ diff --git a/bsnes/gb/Cocoa/FinishTemplate@2x.png b/bsnes/gb/Cocoa/FinishTemplate@2x.png new file mode 100644 index 00000000..06bbcd49 Binary files /dev/null and b/bsnes/gb/Cocoa/FinishTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/AppDelegate.h b/bsnes/gb/Cocoa/GBApp.h similarity index 82% rename from bsnes/gb/Cocoa/AppDelegate.h rename to bsnes/gb/Cocoa/GBApp.h index a9b00487..de45e28c 100644 --- a/bsnes/gb/Cocoa/AppDelegate.h +++ b/bsnes/gb/Cocoa/GBApp.h @@ -1,7 +1,8 @@ #import #import +#import -@interface AppDelegate : NSObject +@interface GBApp : NSApplication @property (nonatomic, strong) IBOutlet NSWindow *preferencesWindow; @property (nonatomic, strong) IBOutlet NSView *graphicsTab; @@ -21,5 +22,6 @@ @property (strong) IBOutlet NSButton *updateProgressButton; @property (strong) IBOutlet NSWindow *updateProgressWindow; @property (strong) IBOutlet NSProgressIndicator *updateProgressSpinner; +- (void)updateThemesDefault:(bool)overwrite; @end diff --git a/bsnes/gb/Cocoa/GBApp.m b/bsnes/gb/Cocoa/GBApp.m new file mode 100644 index 00000000..37020144 --- /dev/null +++ b/bsnes/gb/Cocoa/GBApp.m @@ -0,0 +1,817 @@ +#import "GBApp.h" +#import "GBButtons.h" +#import "GBView.h" +#import "Document.h" +#import "GBJoyConManager.h" +#import +#import +#import +#import +#import +#include +#include + +#define UPDATE_SERVER "https://sameboy.github.io" + +@interface NSToolbarItem(private) +- (NSButton *)_view; +@end + +static uint32_t color_to_int(NSColor *color) +{ + color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + return (((unsigned)(color.redComponent * 0xFF)) << 16) | + (((unsigned)(color.greenComponent * 0xFF)) << 8) | + ((unsigned)(color.blueComponent * 0xFF)); +} + +@implementation GBApp +{ + NSArray *_preferencesTabs; + NSString *_lastVersion; + NSString *_updateURL; + NSURLSessionDownloadTask *_updateTask; + enum { + UPDATE_DOWNLOADING, + UPDATE_EXTRACTING, + UPDATE_WAIT_INSTALL, + UPDATE_INSTALLING, + UPDATE_FAILED, + } _updateState; + NSString *_downloadDirectory; + AuthorizationRef _auth; + bool _simulatingMenuEvent; +} + +- (void) applicationDidFinishLaunching:(NSNotification *)notification +{ + // Refresh icon if launched via a software update + if (@available(macOS 26.0, *)) { + // Severely broken on macOS 26 + } + else { + NSImage *icon = [[NSWorkspace sharedWorkspace] iconForFile:[[NSBundle mainBundle] bundlePath]]; + icon.size = [NSApplication sharedApplication].applicationIconImage.size; + [NSApplication sharedApplication].applicationIconImage = icon; + } + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + for (unsigned i = 0; i < GBKeyboardButtonCount; i++) { + if ([[defaults objectForKey:button_to_preference_name(i, 0)] isKindOfClass:[NSString class]]) { + [defaults removeObjectForKey:button_to_preference_name(i, 0)]; + } + } + + bool hasSFMono = false; + if (@available(macOS 10.15, *)) { + hasSFMono = [[NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular].displayName containsString:@"SF"]; + } + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"GBRight": @(kVK_RightArrow), + @"GBLeft": @(kVK_LeftArrow), + @"GBUp": @(kVK_UpArrow), + @"GBDown": @(kVK_DownArrow), + + @"GBA": @(kVK_ANSI_X), + @"GBB": @(kVK_ANSI_Z), + @"GBSelect": @(kVK_Delete), + @"GBStart": @(kVK_Return), + + @"GBTurbo": @(kVK_Space), + @"GBRewind": @(kVK_Tab), + @"GBSlow-Motion": @(kVK_Shift), + + @"GBFilter": @"NearestNeighbor", + @"GBColorCorrection": @(GB_COLOR_CORRECTION_MODERN_BALANCED), + @"GBHighpassFilter": @(GB_HIGHPASS_ACCURATE), + @"GBRewindLength": @(120), + @"GBFrameBlendingMode": @([defaults boolForKey:@"DisableFrameBlending"]? GB_FRAME_BLENDING_MODE_DISABLED : 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), + + @"GBVolume": @(1.0), + + @"GBMBC7JoystickOverride": @NO, + @"GBMBC7AllowMouse": @YES, + + @"GBJoyConAutoPair": @YES, + @"GBJoyConsDefaultsToHorizontal": @YES, + + @"GBEmulatedModel": @(MODEL_AUTO), + + @"GBDebuggerFont": hasSFMono? @"SF Mono" : @"Menlo", + @"GBDebuggerFontSize": @12, + + @"GBColorPalette": @1, + @"GBTurboCap": @0, + + // Default themes + @"GBThemes": @{ + @"Canyon": @{ + @"BrightnessBias": @0.1227009965823247, + @"Colors": @[@0xff0c1e20, @0xff122b91, @0xff466aa2, @0xfff1efae, @0xfff1efae], + @"DisabledLCDColor": @NO, + @"HueBias": @0.01782661816105247, + @"HueBiasStrength": @1, + @"Manual": @NO, + }, + @"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, + }, + @"Green Slate": @{ + @"BrightnessBias": @0.2210012227296829, + @"Colors": @[@0xff343117, @0xff6a876f, @0xff98b4a1, @0xffc3daca, @0xffc8decf], + @"DisabledLCDColor": @YES, + @"HueBias": @0.1887667975388467, + @"HueBiasStrength": @0.1272283345460892, + @"Manual": @NO, + }, + @"Green Tea": @{ + @"BrightnessBias": @-0.4946326622596153, + @"Colors": @[@0xff1a1d08, @0xff1d5231, @0xff3b9774, @0xff97e4c6, @0xffa9eed1], + @"DisabledLCDColor": @YES, + @"HueBias": @0.1912955007245464, + @"HueBiasStrength": @0.3621708039314516, + @"Manual": @NO, + }, + @"Lavender": @{ + @"BrightnessBias": @0.10072476038566, + @"Colors": @[@0xff2b2a3a, @0xff8c507c, @0xffbf82a8, @0xffe9bcce, @0xffeec3d3], + @"DisabledLCDColor": @YES, + @"HueBias": @0.7914529587142169, + @"HueBiasStrength": @0.2498168498277664, + @"Manual": @NO, + }, + @"Magic Eggplant": @{ + @"BrightnessBias": @0.0, + @"Colors": @[@0xff3c2136, @0xff942e84, @0xffc7699d, @0xfff1e4b0, @0xfff6f9b2], + @"DisabledLCDColor": @YES, + @"HueBias": @0.87717878486055778, + @"HueBiasStrength": @0.65018052788844627, + @"Manual": @NO, + }, + @"Mystic Blue": @{ + @"BrightnessBias": @-0.3291049897670746, + @"Colors": @[@0xff3b2306, @0xffa27807, @0xffd1b523, @0xfff6ebbe, @0xfffaf1e4], + @"DisabledLCDColor": @YES, + @"HueBias": @0.5282051088288426, + @"HueBiasStrength": @0.7699633836746216, + @"Manual": @NO, + }, + @"Pink Pop": @{ + @"BrightnessBias": @0.624908447265625, + @"Colors": @[@0xff28140a, @0xff7c42cb, @0xffaa83de, @0xffd1ceeb, @0xffd5d8ec], + @"DisabledLCDColor": @YES, + @"HueBias": @0.9477411056868732, + @"HueBiasStrength": @0.80024421215057373, + @"Manual": @NO, + }, + @"Radioactive Pea": @{ + @"BrightnessBias": @-0.48079556772908372, + @"Colors": @[@0xff215200, @0xff1f7306, @0xff169e34, @0xff03ceb8, @0xff00d4d1], + @"DisabledLCDColor": @YES, + @"HueBias": @0.3795131972111554, + @"HueBiasStrength": @0.34337649402390436, + @"Manual": @NO, + }, + @"Rose": @{ + @"BrightnessBias": @0.2727272808551788, + @"Colors": @[@0xff001500, @0xff4e1fae, @0xff865ac4, @0xffb7e6d3, @0xffbdffd4], + @"DisabledLCDColor": @YES, + @"HueBias": @0.9238900924101472, + @"HueBiasStrength": @0.9957716464996338, + @"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, + }, + }, + + @"NSToolbarItemForcesStandardSize": @YES, // Forces Monterey to resepect toolbar item sizes + @"NSToolbarItemWarnOnMinMaxSize": @NO, // Not going to use Constraints, Apple + }]; + + [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{ + JOYAxes2DEmulateButtonsKey: @YES, + JOYHatsEmulateButtonsKey: @YES, + }]; + + [GBJoyConManager sharedInstance]; // Starts handling Joy-Cons + + [JOYController registerListener:self]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; + } + + [self askAutoUpdates]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]) { + [self checkForUpdates]; + } + + if ([[NSProcessInfo processInfo].arguments containsObject:@"--update-launch"]) { + [NSApp activateIgnoringOtherApps:true]; + } + + if (![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBThemesVersion"] isEqualToString:@(GB_VERSION)]) { + [self updateThemesDefault:false]; + [[NSUserDefaults standardUserDefaults] setObject:@(GB_VERSION) forKey:@"GBThemesVersion"]; + } +} + +- (void)updateThemesDefault:(bool)overwrite +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *currentThemes = [defaults dictionaryForKey:@"GBThemes"].mutableCopy; + [defaults removeObjectForKey:@"GBThemes"]; + NSMutableDictionary *defaultThemes = [defaults dictionaryForKey:@"GBThemes"].mutableCopy; + if (![[NSUserDefaults standardUserDefaults] stringForKey:@"GBThemesVersion"]) { + // Force update the Pink Pop theme, it was glitchy in 1.0 + [currentThemes removeObjectForKey:@"Pink Pop"]; + } + if (overwrite) { + [currentThemes addEntriesFromDictionary:defaultThemes]; + [defaults setObject:currentThemes forKey:@"GBThemes"]; + } + else { + [defaultThemes addEntriesFromDictionary:currentThemes]; + [defaults setObject:defaultThemes forKey:@"GBThemes"]; + } +} + +- (IBAction)toggleDeveloperMode:(id)sender +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:![defaults boolForKey:@"DeveloperMode"] forKey:@"DeveloperMode"]; +} + +- (IBAction)switchPreferencesTab:(id)sender +{ + for (NSView *view in _preferencesTabs) { + [view removeFromSuperview]; + } + NSView *tab = _preferencesTabs[[sender tag]]; + NSRect old = [_preferencesWindow frame]; + NSRect new = [_preferencesWindow frameRectForContentRect:tab.frame]; + new.origin.x = old.origin.x; + new.origin.y = old.origin.y + (old.size.height - new.size.height); + [_preferencesWindow setFrame:new display:true animate:_preferencesWindow.visible]; + [_preferencesWindow.contentView addSubview:tab]; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)anItem +{ + if ([anItem action] == @selector(toggleDeveloperMode:)) { + [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]]; + } + + if (anItem == self.linkCableMenuItem) { + return [[NSDocumentController sharedDocumentController] documents].count > 1; + } + return true; +} + +- (void)menuNeedsUpdate:(NSMenu *)menu +{ + NSMutableArray *items = [NSMutableArray array]; + NSDocument *currentDocument = [[NSDocumentController sharedDocumentController] currentDocument]; + + for (NSDocument *document in [[NSDocumentController sharedDocumentController] documents]) { + if (document == currentDocument) continue; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:document.displayName action:@selector(connectLinkCable:) keyEquivalent:@""]; + item.representedObject = document; + item.image = [[NSWorkspace sharedWorkspace] iconForFile:document.fileURL.path]; + [item.image setSize:NSMakeSize(16, 16)]; + [items addObject:item]; + } + [menu removeAllItems]; + for (NSMenuItem *item in items) { + [menu addItem:item]; + } +} + +- (IBAction) showPreferences: (id) sender +{ + NSArray *objects; + if (!_preferencesWindow) { + [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects]; + NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject]; + _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier]; + _preferencesTabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab, self.updatesTab]; + [self switchPreferencesTab:first_toolbar_item]; + [_preferencesWindow center]; +#ifndef UPDATE_SUPPORT + [_preferencesWindow.toolbar removeItemAtIndex:4]; +#endif + for (unsigned i = _preferencesWindow.toolbar.items.count; i--;) { + [_preferencesWindow.toolbar.items[i] _view].imageScaling = NSImageScaleNone; + } + } + [_preferencesWindow makeKeyAndOrderFront:self]; +} + +- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender +{ + [self askAutoUpdates]; + /* Bring an existing panel to the foreground */ + for (NSWindow *window in [[NSApplication sharedApplication] windows]) { + if ([window isKindOfClass:[NSOpenPanel class]]) { + [(NSOpenPanel *)window makeKeyAndOrderFront:nil]; + return true; + } + } + [[NSDocumentController sharedDocumentController] openDocument:self]; + return true; +} + +- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification +{ + [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:true]; +} + +- (void)updateFound +{ + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/raw_changes"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + NSColor *linkColor = [NSColor colorWithRed:0.125 green:0.325 blue:1.0 alpha:1.0]; + if (@available(macOS 10.10, *)) { + linkColor = [NSColor linkColor]; + } + + NSString *changes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSRange cutoffRange = [changes rangeOfString:@""]; + if (cutoffRange.location != NSNotFound) { + changes = [changes substringToIndex:cutoffRange.location]; + } + + NSString *html = [NSString stringWithFormat:@"" + "" + "%@", + color_to_int([NSColor textColor]), + color_to_int(linkColor), + changes]; + + if ([(NSHTTPURLResponse *)response statusCode] == 200) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray *objects; + [[NSBundle mainBundle] loadNibNamed:@"UpdateWindow" owner:self topLevelObjects:&objects]; + if (@available(macOS 10.11, *)) { + self.updateChanges.preferences.standardFontFamily = @"-apple-system"; + } + else if (@available(macOS 10.10, *)) { + self.updateChanges.preferences.standardFontFamily = @"Helvetica Neue"; + } + else { + self.updateChanges.preferences.standardFontFamily = @"Lucida Grande"; + } + if (@available(macOS 10.15, *)) { + self.updateChanges.preferences.fixedFontFamily = [NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular].displayName; + } + else { + self.updateChanges.preferences.fixedFontFamily = @"Menlo"; + } + self.updateChanges.drawsBackground = false; + [self.updateChanges.mainFrame loadHTMLString:html baseURL:nil]; + }); + } + }] resume]; +} + +- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems +{ + // Disable reload context menu + if ([defaultMenuItems count] <= 2) { + return nil; + } + return defaultMenuItems; +} + +- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sender.mainFrame.frameView.documentView.enclosingScrollView.drawsBackground = true; + sender.mainFrame.frameView.documentView.enclosingScrollView.backgroundColor = [NSColor textBackgroundColor]; + sender.policyDelegate = self; + [self.updateWindow center]; + [self.updateWindow makeKeyAndOrderFront:nil]; + }); +} + +- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener +{ + [listener ignore]; + [[NSWorkspace sharedWorkspace] openURL:[request URL]]; +} + +- (void)checkForUpdates +{ +#ifdef UPDATE_SUPPORT + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/latest_version"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.updatesSpinner stopAnimation:nil]; + [self.updatesButton setEnabled:true]; + }); + if ([(NSHTTPURLResponse *)response statusCode] == 200) { + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSArray *components = [string componentsSeparatedByString:@"|"]; + if (components.count != 2) return; + _lastVersion = components[0]; + _updateURL = components[1]; + if (![@GB_VERSION isEqualToString:_lastVersion] && + ![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBSkippedVersion"] isEqualToString:_lastVersion]) { + [self updateFound]; + } + } + }] resume]; +#endif +} + +- (IBAction)userCheckForUpdates:(id)sender +{ + if (self.updateWindow) { + [self.updateWindow makeKeyAndOrderFront:sender]; + } + else { + [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"GBSkippedVersion"]; + [self checkForUpdates]; + [sender setEnabled:false]; + [self.updatesSpinner startAnimation:sender]; + } +} + +- (void)askAutoUpdates +{ +#ifdef UPDATE_SUPPORT + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAskedAutoUpdates"]) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = @"Should SameBoy check for updates when launched?"; + alert.informativeText = @"SameBoy is frequently updated with new features, accuracy improvements, and bug fixes. This setting can always be changed in the preferences window."; + [alert addButtonWithTitle:@"Check on Launch"]; + [alert addButtonWithTitle:@"Don't Check on Launch"]; + + [[NSUserDefaults standardUserDefaults] setBool:[alert runModal] == NSAlertFirstButtonReturn forKey:@"GBAutoUpdatesEnabled"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBAskedAutoUpdates"]; + } +#endif +} + +- (IBAction)skipVersion:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:_lastVersion forKey:@"GBSkippedVersion"]; + [self.updateWindow performClose:sender]; +} + +- (bool)executePath:(NSString *)path withArguments:(NSArray *)arguments +{ + if (!_auth) { + NSTask *task = [[NSTask alloc] init]; + task.launchPath = path; + task.arguments = arguments; + [task launch]; + [task waitUntilExit]; + return task.terminationStatus == 0 && task.terminationReason == NSTaskTerminationReasonExit; + } + + char *argv[arguments.count + 1]; + argv[arguments.count] = NULL; + for (unsigned i = 0; i < arguments.count; i++) { + argv[i] = (char *)arguments[i].UTF8String; + } + + return AuthorizationExecuteWithPrivileges(_auth, path.UTF8String, kAuthorizationFlagDefaults, argv, NULL) == errAuthorizationSuccess; +} + +- (void)deauthorize +{ + if (_auth) { + AuthorizationFree(_auth, kAuthorizationFlagDefaults); + _auth = nil; + } +} + +- (IBAction)installUpdate:(id)sender +{ + bool needsAuthorization = false; + if ([self executePath:@"/usr/sbin/spctl" withArguments:@[@"--status"]]) { // Succeeds when GateKeeper is on + // GateKeeper is on, we need to --add ourselves as root, else we might get a GateKeeper crash + needsAuthorization = true; + } + else if (access(_dyld_get_image_name(0), W_OK)) { + // We don't have write access, so we need authorization to update as root + needsAuthorization = true; + } + + if (needsAuthorization && !_auth) { + AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagPreAuthorize | kAuthorizationFlagInteractionAllowed, &_auth); + // Make sure we can modify the bundle + if (![self executePath:@"/usr/sbin/chown" withArguments:@[@"-R", [NSString stringWithFormat:@"%d:%d", getuid(), getgid()], [NSBundle mainBundle].bundlePath]]) { + [self deauthorize]; + return; + } + } + + [self.updateProgressSpinner startAnimation:nil]; + self.updateProgressButton.title = @"Cancel"; + self.updateProgressButton.enabled = true; + self.updateProgressLabel.stringValue = @"Downloading update…"; + _updateState = UPDATE_DOWNLOADING; + _updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + _updateTask = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Extracting update…"; + _updateState = UPDATE_EXTRACTING; + }); + + _downloadDirectory = [[[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory + inDomain:NSUserDomainMask + appropriateForURL:[[NSBundle mainBundle] bundleURL] + create:true + error:nil] path]; + if (!_downloadDirectory) { + [self deauthorize]; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to extract update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + + NSTask *unzipTask; + unzipTask = [[NSTask alloc] init]; + unzipTask.launchPath = @"/usr/bin/unzip"; + unzipTask.arguments = @[location.path, @"-d", _downloadDirectory]; + [unzipTask launch]; + [unzipTask waitUntilExit]; + if (unzipTask.terminationStatus != 0 || unzipTask.terminationReason != NSTaskTerminationReasonExit) { + [self deauthorize]; + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to extract update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Update ready, save your game progress and click Install."; + _updateState = UPDATE_WAIT_INSTALL; + self.updateProgressButton.title = @"Install"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + }]; + [_updateTask resume]; + + self.updateProgressWindow.preventsApplicationTerminationWhenModal = false; + [self.updateWindow beginSheet:self.updateProgressWindow completionHandler:^(NSModalResponse returnCode) { + [self.updateWindow close]; + }]; +} + +- (void)performUpgrade +{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Instaling update…"; + _updateState = UPDATE_INSTALLING; + self.updateProgressButton.enabled = false; + [self.updateProgressSpinner startAnimation:nil]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *executablePath = [[NSBundle mainBundle] executablePath]; + NSString *contentsPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"]; + NSString *contentsTempPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"TempContents"]; + NSString *updateContentsPath = [_downloadDirectory stringByAppendingPathComponent:@"SameBoy.app/Contents"]; + NSError *error = nil; + [[NSFileManager defaultManager] moveItemAtPath:contentsPath toPath:contentsTempPath error:&error]; + if (error) { + [self deauthorize]; + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + _downloadDirectory = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to install update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + [[NSFileManager defaultManager] moveItemAtPath:updateContentsPath toPath:contentsPath error:&error]; + if (error) { + [self deauthorize]; + [[NSFileManager defaultManager] moveItemAtPath:contentsTempPath toPath:contentsPath error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + _downloadDirectory = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to install update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil]; + + // Remove the quarantine flag so we don't have to escape translocation + NSString *bundlePath = [NSBundle mainBundle].bundlePath; + removexattr(bundlePath.UTF8String, "com.apple.quarantine", 0); + for (NSString *path in [[NSFileManager defaultManager] enumeratorAtPath:bundlePath]) { + removexattr([bundlePath stringByAppendingPathComponent:path].UTF8String, "com.apple.quarantine", 0); + }; + + _downloadDirectory = nil; + atexit_b(^{ + execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL); + }); + + dispatch_async(dispatch_get_main_queue(), ^{ + [NSApp terminate:nil]; + }); + }); +} + +- (IBAction)updateAction:(id)sender +{ + switch (_updateState) { + case UPDATE_DOWNLOADING: + [_updateTask cancelByProducingResumeData:nil]; + _updateTask = nil; + [self.updateProgressWindow close]; + break; + case UPDATE_WAIT_INSTALL: + [self performUpgrade]; + break; + case UPDATE_EXTRACTING: + case UPDATE_INSTALLING: + break; + case UPDATE_FAILED: + [self.updateProgressWindow close]; + break; + } +} + +- (void)orderFrontAboutPanel:(id)sender +{ + // NSAboutPanelOptionApplicationIcon is not available prior to 10.13, but the key is still there and working. + [[NSApplication sharedApplication] orderFrontStandardAboutPanelWithOptions:@{ + @"ApplicationIcon": [NSImage imageNamed:@"Icon"] + }]; +} + +- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button +{ + if (!button.isPressed) return; + NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; + if (!mapping) { + mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; + } + + JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: -1; + if (!mapping && usage >= JOYButtonUsageGeneric0) { + usage = GB_inline_const(JOYButtonUsage[], {JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX})[(usage - JOYButtonUsageGeneric0) & 3]; + } + + if (usage == GBJoyKitHotkey1 || usage == GBJoyKitHotkey2) { + if (_preferencesWindow && self.keyWindow == _preferencesWindow) { + return; + } + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAllowBackgroundControllers"] && !self.keyWindow) { + return; + } + + NSString *keyEquivalent = [[NSUserDefaults standardUserDefaults] stringForKey:usage == GBJoyKitHotkey1? @"GBJoypadHotkey1" : @"GBJoypadHotkey2"]; + NSEventModifierFlags flags = NSEventModifierFlagCommand; + if ([keyEquivalent hasPrefix:@"^"]) { + flags |= NSEventModifierFlagShift; + [keyEquivalent substringFromIndex:1]; + } + _simulatingMenuEvent = true; + [[NSApplication sharedApplication] sendEvent:[NSEvent keyEventWithType:NSEventTypeKeyDown + location:(NSPoint){0,} + modifierFlags:flags + timestamp:0 + windowNumber:0 + context:NULL + characters:keyEquivalent + charactersIgnoringModifiers:keyEquivalent + isARepeat:false + keyCode:0]]; + _simulatingMenuEvent = false; + } +} + +- (NSWindow *)keyWindow +{ + NSWindow *ret = [super keyWindow]; + if (!ret && _simulatingMenuEvent) { + ret = [(Document *)self.orderedDocuments.firstObject mainWindow]; + } + return ret; +} + +- (NSWindow *)mainWindow +{ + NSWindow *ret = [super mainWindow]; + if (!ret && _simulatingMenuEvent) { + ret = [(Document *)self.orderedDocuments.firstObject mainWindow]; + } + return ret; +} + +- (IBAction)openDebuggerHelp:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://sameboy.github.io/debugger/"]]; +} + +- (IBAction)openSponsor:(id)sender +{ + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://github.com/sponsors/LIJI32"]]; +} + +- (void)dealloc +{ + if (_downloadDirectory) { + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + } +} + +- (IBAction)nop:(id)sender +{ +} + +/* This runs before C constructors. If we need to escape translocation, we should + do it ASAP to minimize our launch time. */ + ++ (void)load +{ + if (@available(macOS 10.12, *)) { + /* Detect and escape translocation so we can safely update ourselves */ + if ([[[NSBundle mainBundle] bundlePath] containsString:@"/AppTranslocation/"]) { + const char *mountPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent].UTF8String; + struct statfs *mntbuf; + int mntsize = getmntinfo(&mntbuf, MNT_NOWAIT); + for (unsigned i = 0; i < mntsize; i++) { + if (strcmp(mntbuf[i].f_mntonname, mountPath) == 0) { + NSBundle *origBundle = [NSBundle bundleWithPath:@(mntbuf[i].f_mntfromname)]; + + execl(origBundle.executablePath.UTF8String, origBundle.executablePath.UTF8String, NULL); + break; + } + } + } + } +} +@end diff --git a/bsnes/gb/Cocoa/GBButtons.h b/bsnes/gb/Cocoa/GBButtons.h index 1f8b5afb..0939947b 100644 --- a/bsnes/gb/Cocoa/GBButtons.h +++ b/bsnes/gb/Cocoa/GBButtons.h @@ -1,7 +1,4 @@ -#ifndef GBButtons_h -#define GBButtons_h - -typedef enum : NSUInteger { +typedef enum { GBRight, GBLeft, GBUp, @@ -10,14 +7,24 @@ typedef enum : NSUInteger { GBB, GBSelect, GBStart, + GBRapidA, + GBRapidB, GBTurbo, GBRewind, GBUnderclock, - GBButtonCount, - GBGameBoyButtonCount = GBStart + 1, + GBHotkey1, + GBHotkey2, + GBTotalButtonCount, + GBKeyboardButtonCount = GBUnderclock + 1, + GBPerPlayerButtonCount = GBRapidB + 1, } GBButton; -extern NSString const *GBButtonNames[GBButtonCount]; +#define GBJoyKitHotkey1 JOYButtonUsageGeneric0 + 0x100 +#define GBJoyKitHotkey2 JOYButtonUsageGeneric0 + 0x101 +#define GBJoyKitRapidA JOYButtonUsageGeneric0 + 0x102 +#define GBJoyKitRapidB JOYButtonUsageGeneric0 + 0x103 + +extern NSString const *GBButtonNames[GBTotalButtonCount]; static inline NSString *n2s(uint64_t number) { @@ -31,5 +38,3 @@ static inline NSString *button_to_preference_name(GBButton button, unsigned play } return [NSString stringWithFormat:@"GB%@", GBButtonNames[button]]; } - -#endif diff --git a/bsnes/gb/Cocoa/GBButtons.m b/bsnes/gb/Cocoa/GBButtons.m index 044e9332..b430a901 100644 --- a/bsnes/gb/Cocoa/GBButtons.m +++ b/bsnes/gb/Cocoa/GBButtons.m @@ -1,4 +1,4 @@ #import #import "GBButtons.h" -NSString const *GBButtonNames[] = {@"Right", @"Left", @"Up", @"Down", @"A", @"B", @"Select", @"Start", @"Turbo", @"Rewind", @"Slow-Motion"}; +NSString const *GBButtonNames[] = {@"Right", @"Left", @"Up", @"Down", @"A", @"B", @"Select", @"Start", @"Rapid A", @"Rapid B", @"Turbo", @"Rewind", @"Slow-Motion", @"Hotkey 1", @"Hotkey 2"}; diff --git a/bsnes/gb/Cocoa/GBCPUView.h b/bsnes/gb/Cocoa/GBCPUView.h new file mode 100644 index 00000000..97e916d8 --- /dev/null +++ b/bsnes/gb/Cocoa/GBCPUView.h @@ -0,0 +1,5 @@ +#import + +@interface GBCPUView : NSView +- (void)addSample:(double)sample; +@end diff --git a/bsnes/gb/Cocoa/GBCPUView.m b/bsnes/gb/Cocoa/GBCPUView.m new file mode 100644 index 00000000..0b87fde0 --- /dev/null +++ b/bsnes/gb/Cocoa/GBCPUView.m @@ -0,0 +1,126 @@ +#import "GBCPUView.h" + +#define SAMPLE_COUNT 0x100 // ~4 seconds + +@implementation GBCPUView +{ + double _samples[SAMPLE_COUNT]; + size_t _position; +} + +- (void)drawRect:(NSRect)dirtyRect +{ + CGRect bounds = self.bounds; + NSSize size = bounds.size; + unsigned factor = [[self.window screen] backingScaleFactor]; + + NSBitmapImageRep *maskBitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL + pixelsWide:(unsigned)size.width * factor + pixelsHigh:(unsigned)size.height * factor + bitsPerSample:8 + samplesPerPixel:2 + hasAlpha:true + isPlanar:false + colorSpaceName:NSDeviceWhiteColorSpace + bytesPerRow:size.width * 2 * factor + bitsPerPixel:16]; + + + + NSGraphicsContext *mainContext = [NSGraphicsContext currentContext]; + + + NSColor *greenColor, *redColor; + if (@available(macOS 10.10, *)) { + greenColor = [NSColor systemGreenColor]; + redColor = [NSColor systemRedColor]; + } + else { + greenColor = [NSColor colorWithRed:3.0 / 16 green:0.5 blue:5.0 / 16 alpha:1.0]; + redColor = [NSColor colorWithRed:13.0 / 16 green:0.25 blue:0.25 alpha:1.0]; + } + + + NSBitmapImageRep *colorBitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL + pixelsWide:SAMPLE_COUNT + pixelsHigh:1 + bitsPerSample:8 + samplesPerPixel:3 + hasAlpha:false + isPlanar:false + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:SAMPLE_COUNT * 4 + bitsPerPixel:32]; + + unsigned lastFill = 0; + NSBezierPath *line = [NSBezierPath bezierPath]; + bool isRed = false; + { + double sample = _samples[_position % SAMPLE_COUNT]; + [line moveToPoint:NSMakePoint(0, + (sample * (size.height - 1) + 0.5) * factor)]; + isRed = sample == 1; + } + + + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:colorBitmap]]; + for (unsigned i = 1; i < SAMPLE_COUNT; i++) { + double sample = _samples[(i + _position) % SAMPLE_COUNT]; + [line lineToPoint:NSMakePoint(size.width * i * factor / (SAMPLE_COUNT - 1), + (sample * (size.height - 1) + 0.5) * factor)]; + + if (isRed != (sample == 1)) { + // Color changed + [(isRed? redColor : greenColor) setFill]; + NSRectFill(CGRectMake(lastFill, 0, i - lastFill, 1)); + lastFill = i; + + isRed ^= true; + } + } + [(isRed? redColor : greenColor) setFill]; + NSRectFill(CGRectMake(lastFill, 0, SAMPLE_COUNT - lastFill, 1)); + + NSBezierPath *fill = [line copy]; + [fill lineToPoint:NSMakePoint(size.width * factor, 0)]; + [fill lineToPoint:NSMakePoint(0, 0)]; + + NSColor *strokeColor = [NSColor whiteColor]; + NSColor *fillColor = [strokeColor colorWithAlphaComponent:1 / 3.0]; + + [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:maskBitmap]]; + [fillColor setFill]; + [fill fill]; + + [strokeColor setStroke]; + [line setLineWidth:factor]; + [line stroke]; + + CGContextRef maskContext = CGContextRetain([NSGraphicsContext currentContext].graphicsPort); + [NSGraphicsContext setCurrentContext:mainContext]; + CGContextSaveGState(mainContext.graphicsPort); + + CGImageRef maskImage = CGBitmapContextCreateImage(maskContext); + CGContextClipToMask(mainContext.graphicsPort, bounds, maskImage); + CGImageRelease(maskImage); + + NSImage *colors = [[NSImage alloc] initWithSize:NSMakeSize(SAMPLE_COUNT, 1)]; + [colors addRepresentation:colorBitmap]; + [colors drawInRect:bounds]; + + CGContextRestoreGState(mainContext.graphicsPort); + CGContextRelease(maskContext); + + + [super drawRect:dirtyRect]; +} + +- (void)addSample:(double)sample +{ + _samples[_position++] = sample; + if (_position == SAMPLE_COUNT) { + _position = 0; + } +} + +@end diff --git a/bsnes/gb/Cocoa/GBCenteredTextCell.h b/bsnes/gb/Cocoa/GBCenteredTextCell.h new file mode 100644 index 00000000..829f6baf --- /dev/null +++ b/bsnes/gb/Cocoa/GBCenteredTextCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBCenteredTextCell : NSTextFieldCell + +@end diff --git a/bsnes/gb/Cocoa/GBCenteredTextCell.m b/bsnes/gb/Cocoa/GBCenteredTextCell.m new file mode 100644 index 00000000..5039988a --- /dev/null +++ b/bsnes/gb/Cocoa/GBCenteredTextCell.m @@ -0,0 +1,29 @@ +#import "GBCenteredTextCell.h" + +@implementation GBCenteredTextCell +- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView +{ + double height = round([self.attributedStringValue size].height); + cellFrame.origin.y += (cellFrame.size.height - height) / 2; + cellFrame.size.height = height; + [super drawInteriorWithFrame:cellFrame inView:controlView]; +} + + +- (void)selectWithFrame:(NSRect)rect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)delegate start:(NSInteger)selStart length:(NSInteger)selLength +{ + double height = round([self.attributedStringValue size].height); + rect.origin.y += (rect.size.height - height) / 2; + rect.size.height = height; + [super selectWithFrame:rect inView:controlView editor:textObj delegate:delegate start:selStart length:selLength]; +} + +- (void)editWithFrame:(NSRect)rect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)delegate event:(NSEvent *)event +{ + double height = round([self.attributedStringValue size].height); + rect.origin.y += (rect.size.height - height) / 2; + rect.size.height = height; + [super editWithFrame:rect inView:controlView editor:textObj delegate:delegate event:event]; + +} +@end diff --git a/bsnes/gb/Cocoa/GBCheatSearchController.h b/bsnes/gb/Cocoa/GBCheatSearchController.h new file mode 100644 index 00000000..fb330a23 --- /dev/null +++ b/bsnes/gb/Cocoa/GBCheatSearchController.h @@ -0,0 +1,8 @@ +#import +#import "Document.h" + +@interface GBCheatSearchController : NSObject +@property IBOutlet NSWindow *window; +@property IBOutlet NSTableView *tableView; ++ (instancetype)controllerWithDocument:(Document *)document; +@end diff --git a/bsnes/gb/Cocoa/GBCheatSearchController.m b/bsnes/gb/Cocoa/GBCheatSearchController.m new file mode 100644 index 00000000..2d3729bd --- /dev/null +++ b/bsnes/gb/Cocoa/GBCheatSearchController.m @@ -0,0 +1,234 @@ +#import "GBCheatSearchController.h" +#import "GBWarningPopover.h" +#import "GBCheatWindowController.h" +#import "GBPanel.h" + +@interface GBCheatSearchController() +@property IBOutlet NSPopUpButton *dataTypeButton; +@property IBOutlet NSPopUpButton *conditionTypeButton; +@property IBOutlet NSTextField *operandField; +@property IBOutlet NSTextField *conditionField; +@property IBOutlet NSTextField *resultsLabel; +@property (strong) IBOutlet NSButton *addCheatButton; +@end + +@implementation GBCheatSearchController +{ + __weak Document *_document; + size_t _resultCount; + GB_cheat_search_result_t *_results; + GBPanel *_window; +} + ++ (instancetype)controllerWithDocument:(Document *)document +{ + GBCheatSearchController *ret = [[self alloc] init]; + ret->_document = document; + NSArray *objects; + [[NSBundle mainBundle] loadNibNamed:@"CheatSearch" owner:ret topLevelObjects:&objects]; + ret->_resultsLabel.stringValue = @""; + ret->_resultsLabel.cell.backgroundStyle = NSBackgroundStyleRaised; + ret->_window.ownerWindow = document.mainWindow; + return ret; +} + +- (IBAction)reset:(id)sender +{ + _dataTypeButton.enabled = true; + [_document performAtomicBlock:^{ + GB_cheat_search_reset(_document.gb); + }]; + _resultCount = 0; + if (_results) { + free(_results); + _results = NULL; + } + [_tableView reloadData]; + _resultsLabel.stringValue = @""; +} + +- (IBAction)search:(id)sender +{ + // Dispatch to work around firstResponder oddities + dispatch_async(dispatch_get_main_queue(), ^{ + if ([sender isKindOfClass:[NSTextField class]]) { + // Action sent by losing focus rather than pressing enter + if (![sender currentEditor]) return; + } + _dataTypeButton.enabled = false; + [_document performAtomicBlock:^{ + __block bool success = false; + NSString *error = [_document captureOutputForBlock:^{ + success = GB_cheat_search_filter(_document.gb, _conditionField.stringValue.UTF8String, _dataTypeButton.selectedTag); + }]; + if (!success) { + dispatch_async(dispatch_get_main_queue(), ^{ + [GBWarningPopover popoverWithContents:error onView:_conditionField]; + NSBeep(); + }); + return; + } + _resultCount = GB_cheat_search_result_count(_document.gb); + _results = malloc(sizeof(*_results) * _resultCount); + GB_cheat_search_get_results(_document.gb, _results); + }]; + if (_resultCount == 0) { + _dataTypeButton.enabled = true; + _resultsLabel.stringValue = @"No results."; + } + else { + _resultsLabel.stringValue = [NSString stringWithFormat:@"%@ result%s", + [NSNumberFormatter localizedStringFromNumber:@(_resultCount) + numberStyle:NSNumberFormatterDecimalStyle], + _resultCount > 1? "s" : ""]; + } + [_tableView reloadData]; + }); +} + +- (IBAction)conditionChanged:(id)sender +{ + unsigned index = [_conditionTypeButton indexOfSelectedItem]; + _conditionField.enabled = index == 11; + _operandField.enabled = index >= 1 && index <= 6; + switch ([_conditionTypeButton indexOfSelectedItem]) { + case 0: _conditionField.stringValue = @"1"; break; + case 1: _conditionField.stringValue = [NSString stringWithFormat:@"new == (%@)", _operandField.stringValue]; break; + case 2: _conditionField.stringValue = [NSString stringWithFormat:@"new != (%@)", _operandField.stringValue]; break; + case 3: _conditionField.stringValue = [NSString stringWithFormat:@"new > (%@)", _operandField.stringValue]; break; + case 4: _conditionField.stringValue = [NSString stringWithFormat:@"new >= (%@)", _operandField.stringValue]; break; + case 5: _conditionField.stringValue = [NSString stringWithFormat:@"new < (%@)", _operandField.stringValue]; break; + case 6: _conditionField.stringValue = [NSString stringWithFormat:@"new <= (%@)", _operandField.stringValue]; break; + case 7: _conditionField.stringValue = @"new != old"; break; + case 8: _conditionField.stringValue = @"new == old"; break; + case 9: _conditionField.stringValue = @"new > old"; break; + case 10: _conditionField.stringValue = @"new < old"; break; + } +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + return _resultCount; +} + +- (uint8_t *)addressForRow:(unsigned)row +{ + uint8_t *base; + uint32_t offset; + if (_results[row].addr < 0xc000) { + base = GB_get_direct_access(_document.gb, GB_DIRECT_ACCESS_CART_RAM, NULL, NULL); + offset = (_results[row].addr & 0x1FFF) + _results[row].bank * 0x2000; + } + else if (_results[row].addr < 0xe000) { + base = GB_get_direct_access(_document.gb, GB_DIRECT_ACCESS_RAM, NULL, NULL); + offset = (_results[row].addr & 0xFFF) + _results[row].bank * 0x1000; + } + else { + base = GB_get_direct_access(_document.gb, GB_DIRECT_ACCESS_HRAM, NULL, NULL); + offset = (_results[row].addr & 0x7F); + } + return base + offset; +} + +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + switch ([[tableView tableColumns] indexOfObject:tableColumn]) { + case 0: + return [NSString stringWithFormat:@"$%02x:$%04x", _results[row].bank, _results[row].addr]; + case 1: + if (_dataTypeButton.selectedTag & GB_CHEAT_SEARCH_DATA_TYPE_16BIT) { + return [NSString stringWithFormat:@"$%04x", _results[row].value]; + } + return [NSString stringWithFormat:@"$%02x", _results[row].value]; + default: { + const uint8_t *data = [self addressForRow:row]; + GB_cheat_search_data_type_t dataType = _dataTypeButton.selectedTag; + uint16_t value = data[0]; + if (!(dataType & GB_CHEAT_SEARCH_DATA_TYPE_16BIT)) { + return [NSString stringWithFormat:@"$%02x", value]; + } + value |= data[1] << 8; + if ((dataType & GB_CHEAT_SEARCH_DATA_TYPE_BE_BIT)) { + value = __builtin_bswap16(value); + } + return [NSString stringWithFormat:@"$%04x", value]; + } + } +} + +- (void)tableView:(NSTableView *)tableView setObjectValue:(NSString *)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + [_document performAtomicBlock:^{ + __block bool success = false; + __block uint16_t value; + NSString *error = [_document captureOutputForBlock:^{ + success = !GB_debugger_evaluate(_document.gb, object.UTF8String, &value, NULL); + }]; + if (!success) { + dispatch_async(dispatch_get_main_queue(), ^{ + [GBWarningPopover popoverWithContents:error onView:tableView]; + NSBeep(); + }); + return; + } + uint8_t *dest = [self addressForRow:row]; + GB_cheat_search_data_type_t dataType = _dataTypeButton.selectedTag; + if (dataType & GB_CHEAT_SEARCH_DATA_TYPE_BE_BIT) { + value = __builtin_bswap16(value); + } + dest[0] = value; + if (dataType & GB_CHEAT_SEARCH_DATA_TYPE_16BIT) { + dest[1] = value >> 8; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [tableView reloadData]; + }); + }]; +} + +- (void)controlTextDidChange:(NSNotification *)obj +{ + [self conditionChanged:nil]; +} + +- (IBAction)addCheat:(id)sender +{ + GB_cheat_search_result_t *result = _results + _tableView.selectedRow; + uint8_t *data = [self addressForRow:_tableView.selectedRow]; + GB_cheat_search_data_type_t dataType = _dataTypeButton.selectedTag; + size_t rowToSelect = 0; + GB_get_cheats(_document.gb, &rowToSelect); + [_document performAtomicBlock:^{ + GB_add_cheat(_document.gb, + (dataType & GB_CHEAT_SEARCH_DATA_TYPE_16BIT)? "New Cheat (Part 1)" : "New Cheat", + result->addr, result->bank, + *data, + 0, false, + true); + if (dataType & GB_CHEAT_SEARCH_DATA_TYPE_16BIT) { + GB_add_cheat(_document.gb, + (dataType & GB_CHEAT_SEARCH_DATA_TYPE_16BIT)? "New Cheat (Part 2)" : "New Cheat", + result->addr + 1, result->bank, + data[1], + 0, false, + true); + } + GB_set_cheats_enabled(_document.gb, true); + }]; + [_document.cheatsWindow makeKeyAndOrderFront:nil]; + [_document.cheatWindowController.cheatsTable reloadData]; + [_document.cheatWindowController.cheatsTable selectRow:rowToSelect byExtendingSelection:false]; + [_document.cheatWindowController.cheatsTable.delegate tableViewSelectionDidChange:nil]; +} + +- (void)tableViewSelectionDidChange:(NSNotification *)notification +{ + _addCheatButton.enabled = _tableView.numberOfSelectedRows != 0; +} + +- (void)dealloc +{ + if (_results) free(_results); +} + +@end diff --git a/bsnes/gb/Cocoa/GBCheatTextFieldCell.m b/bsnes/gb/Cocoa/GBCheatTextFieldCell.m index 1fdafea0..bc09e815 100644 --- a/bsnes/gb/Cocoa/GBCheatTextFieldCell.m +++ b/bsnes/gb/Cocoa/GBCheatTextFieldCell.m @@ -6,7 +6,7 @@ @implementation GBCheatTextView -- (bool)_insertText:(NSString *)string replacementRange:(NSRange)range +- (bool)_internalInsertText:(NSString *)string replacementRange:(NSRange)range { if (range.location == NSNotFound) { range = self.selectedRange; @@ -60,19 +60,19 @@ return true; } if (([string isEqualToString:@"$"] || [string isEqualToString:@":"]) && range.length == 0 && range.location == 0) { - if ([self _insertText:@"$00:" replacementRange:range]) { + if ([self _internalInsertText:@"$00:" replacementRange:range]) { self.selectedRange = NSMakeRange(1, 2); return true; } } if ([string isEqualToString:@":"] && range.length + range.location == self.string.length) { - if ([self _insertText:@":$0" replacementRange:range]) { + if ([self _internalInsertText:@":$0" replacementRange:range]) { self.selectedRange = NSMakeRange(self.string.length - 2, 2); return true; } } if ([string isEqualToString:@"$"]) { - if ([self _insertText:@"$0" replacementRange:range]) { + if ([self _internalInsertText:@"$0" replacementRange:range]) { self.selectedRange = NSMakeRange(range.location + 1, 1); return true; } @@ -88,8 +88,10 @@ - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - if (![self _insertText:string replacementRange:replacementRange]) { - NSBeep(); + if (![self _internalInsertText:string replacementRange:replacementRange]) { + if (![self _internalInsertText:[@"$" stringByAppendingString:string] replacementRange:replacementRange]) { + NSBeep(); + } } } diff --git a/bsnes/gb/Cocoa/GBCheatWindowController.m b/bsnes/gb/Cocoa/GBCheatWindowController.m index 5cc8f595..dd1cc5a2 100644 --- a/bsnes/gb/Cocoa/GBCheatWindowController.m +++ b/bsnes/gb/Cocoa/GBCheatWindowController.m @@ -42,7 +42,7 @@ return nil; } -- (nullable id)tableView:(NSTableView *)tableView objectValueForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { size_t cheatCount; GB_gameboy_t *gb = self.document.gameboy; @@ -58,7 +58,7 @@ return @NO; case 2: - return @"Add Cheat..."; + return @"Add Cheat…"; case 3: return @""; @@ -92,14 +92,18 @@ self.importCodeField.stringValue.UTF8String, self.importDescriptionField.stringValue.UTF8String, true)) { - self.importCodeField.stringValue = @""; - self.importDescriptionField.stringValue = @""; - [self.cheatsTable reloadData]; - [self tableViewSelectionDidChange:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.importCodeField.stringValue = @""; + self.importDescriptionField.stringValue = @""; + [self.cheatsTable reloadData]; + [self tableViewSelectionDidChange:nil]; + }); } else { - NSBeep(); - [GBWarningPopover popoverWithContents:@"This code is not a valid GameShark or GameGenie code" onView:self.importCodeField]; + dispatch_async(dispatch_get_main_queue(), ^{ + NSBeep(); + [GBWarningPopover popoverWithContents:@"This code is not a valid GameShark or Game Genie code" onView:self.importCodeField]; + }); } }]; } diff --git a/bsnes/gb/Cocoa/GBColorCell.h b/bsnes/gb/Cocoa/GBColorCell.h deleted file mode 100644 index a622c788..00000000 --- a/bsnes/gb/Cocoa/GBColorCell.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -@interface GBColorCell : NSTextFieldCell - -@end diff --git a/bsnes/gb/Cocoa/GBColorCell.m b/bsnes/gb/Cocoa/GBColorCell.m deleted file mode 100644 index 761ad43b..00000000 --- a/bsnes/gb/Cocoa/GBColorCell.m +++ /dev/null @@ -1,49 +0,0 @@ -#import "GBColorCell.h" - -static inline double scale_channel(uint8_t x) -{ - x &= 0x1f; - return x / 31.0; -} - -@implementation GBColorCell -{ - NSInteger _integerValue; -} - -- (void)setObjectValue:(id)objectValue -{ - _integerValue = [objectValue integerValue]; - uint8_t r = _integerValue & 0x1F, - g = (_integerValue >> 5) & 0x1F, - b = (_integerValue >> 10) & 0x1F; - super.objectValue = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"$%04x", (uint16_t)(_integerValue & 0x7FFF)] attributes:@{ - NSForegroundColorAttributeName: r * 3 + g * 4 + b * 2 > 120? [NSColor blackColor] : [NSColor whiteColor], - NSFontAttributeName: [NSFont userFixedPitchFontOfSize:12] - }]; -} - -- (NSInteger)integerValue -{ - return _integerValue; -} - -- (int)intValue -{ - return (int)_integerValue; -} - - -- (NSColor *) backgroundColor -{ - /* Todo: color correction */ - uint16_t color = self.integerValue; - return [NSColor colorWithRed:scale_channel(color) green:scale_channel(color >> 5) blue:scale_channel(color >> 10) alpha:1.0]; -} - -- (BOOL)drawsBackground -{ - return true; -} - -@end diff --git a/bsnes/gb/Cocoa/GBColorTextCell.h b/bsnes/gb/Cocoa/GBColorTextCell.h new file mode 100644 index 00000000..e01251eb --- /dev/null +++ b/bsnes/gb/Cocoa/GBColorTextCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBColorTextCell : NSTextFieldCell + +@end diff --git a/bsnes/gb/Cocoa/GBColorTextCell.m b/bsnes/gb/Cocoa/GBColorTextCell.m new file mode 100644 index 00000000..dfa9ceee --- /dev/null +++ b/bsnes/gb/Cocoa/GBColorTextCell.m @@ -0,0 +1,19 @@ +#import "GBColorTextCell.h" + +@implementation GBColorTextCell + +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView +{ + [self.backgroundColor set]; + NSRectFill(cellFrame); + + NSBezierPath *path = [NSBezierPath bezierPathWithRect:cellFrame]; + path.lineWidth = 2; + +[[NSColor colorWithWhite:0 alpha:0.25] setStroke]; + [path addClip]; + [path stroke]; + + [self drawInteriorWithFrame:cellFrame inView:controlView]; +} +@end diff --git a/bsnes/gb/Cocoa/GBDebuggerButton.h b/bsnes/gb/Cocoa/GBDebuggerButton.h new file mode 100644 index 00000000..5c3a12f6 --- /dev/null +++ b/bsnes/gb/Cocoa/GBDebuggerButton.h @@ -0,0 +1,7 @@ +#import + +@class GBDocument; +@interface GBDebuggerButton : NSButton +@property (weak) IBOutlet NSTextField *textField; +@end + diff --git a/bsnes/gb/Cocoa/GBDebuggerButton.m b/bsnes/gb/Cocoa/GBDebuggerButton.m new file mode 100644 index 00000000..32f46c39 --- /dev/null +++ b/bsnes/gb/Cocoa/GBDebuggerButton.m @@ -0,0 +1,49 @@ +#import "GBDebuggerButton.h" + +@implementation GBDebuggerButton +{ + NSTrackingArea *_trackingArea; +} +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + self.toolTip = self.title; + self.imagePosition = NSImageOnly; // Newer versions of AppKit refuse to respect the value from the nib file + return self; +} + +- (void)mouseEntered:(NSEvent *)event +{ + if (@available(macOS 10.10, *)) { + NSDictionary *attributes = @{ + NSForegroundColorAttributeName: [NSColor colorWithWhite:1.0 alpha:0.5], + NSFontAttributeName: self.textField.font + }; + self.textField.placeholderAttributedString = + [[NSAttributedString alloc] initWithString:self.alternateTitle attributes:attributes]; + } +} + +- (void)mouseExited:(NSEvent *)event +{ + if (@available(macOS 10.10, *)) { + if ([self.textField.placeholderAttributedString.string isEqualToString:self.alternateTitle]) { + self.textField.placeholderAttributedString = nil; + } + } +} + +- (void)updateTrackingAreas +{ + [super updateTrackingAreas]; + if (_trackingArea) { + [self removeTrackingArea:_trackingArea]; + } + + _trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds] + options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways + owner:self + userInfo:nil]; + [self addTrackingArea:_trackingArea]; +} +@end diff --git a/bsnes/gb/Cocoa/GBDeleteButtonCell.h b/bsnes/gb/Cocoa/GBDeleteButtonCell.h new file mode 100644 index 00000000..08c7e6e3 --- /dev/null +++ b/bsnes/gb/Cocoa/GBDeleteButtonCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBDeleteButtonCell : NSButtonCell + +@end diff --git a/bsnes/gb/Cocoa/GBDeleteButtonCell.m b/bsnes/gb/Cocoa/GBDeleteButtonCell.m new file mode 100644 index 00000000..97e34700 --- /dev/null +++ b/bsnes/gb/Cocoa/GBDeleteButtonCell.m @@ -0,0 +1,30 @@ +#import "GBDeleteButtonCell.h" + +@implementation GBDeleteButtonCell + +// Image scaling is broken on some older macOS versions +- (void)drawImage:(NSImage *)image withFrame:(NSRect)frame inView:(NSView *)controlView +{ + double size = 13; + unsigned offset = 1; + if (@available(macOS 10.10, *)) { + size = 15; + offset = 0; + } + frame.origin.x += round((frame.size.width - size) / 2) + offset; + frame.origin.y += round((frame.size.height - size) / 2) - offset; + frame.size.width = frame.size.height = size; + [super drawImage:image withFrame:frame inView:controlView]; +} + +- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView +{ + [self drawImage:self.image withFrame:cellFrame inView:controlView]; +} + +-(void)drawBezelWithFrame:(NSRect)frame inView:(NSView *)controlView +{ + +} + +@end diff --git a/bsnes/gb/Cocoa/GBDisabledButton.h b/bsnes/gb/Cocoa/GBDisabledButton.h new file mode 100644 index 00000000..053fb88f --- /dev/null +++ b/bsnes/gb/Cocoa/GBDisabledButton.h @@ -0,0 +1,5 @@ +#import + +@interface GBDisabledButton : NSButton + +@end diff --git a/bsnes/gb/Cocoa/GBDisabledButton.m b/bsnes/gb/Cocoa/GBDisabledButton.m new file mode 100644 index 00000000..c1c5d39b --- /dev/null +++ b/bsnes/gb/Cocoa/GBDisabledButton.m @@ -0,0 +1,8 @@ +#import "GBDisabledButton.h" + +@implementation GBDisabledButton +- (void)mouseDown:(NSEvent *)event +{ + +} +@end diff --git a/bsnes/gb/Cocoa/GBGLShader.m b/bsnes/gb/Cocoa/GBGLShader.m index 920226b6..1d390908 100644 --- a/bsnes/gb/Cocoa/GBGLShader.m +++ b/bsnes/gb/Cocoa/GBGLShader.m @@ -163,7 +163,7 @@ void main(void) {\n\ /* OpenGL is black magic. Closing one view causes others to be completely black unless we reload their shaders */ /* We're probably not freeing thing in the right place. */ - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged$DefaultsObserver" object:nil]; } + (GLuint)shaderWithContents:(NSString*)contents type:(GLenum)type diff --git a/bsnes/gb/Cocoa/GBHexStatusBarRepresenter.h b/bsnes/gb/Cocoa/GBHexStatusBarRepresenter.h new file mode 100644 index 00000000..5551dedb --- /dev/null +++ b/bsnes/gb/Cocoa/GBHexStatusBarRepresenter.h @@ -0,0 +1,10 @@ +#import +#import + + +@interface GBHexStatusBarRepresenter : HFRepresenter +@property GB_gameboy_t *gb; +@property (nonatomic) bool useDecimalLength; +@property (nonatomic) uint16_t bankForDescription; +@property (nonatomic) uint16_t baseAddress; +@end diff --git a/bsnes/gb/Cocoa/GBHexStatusBarRepresenter.m b/bsnes/gb/Cocoa/GBHexStatusBarRepresenter.m new file mode 100644 index 00000000..1739811c --- /dev/null +++ b/bsnes/gb/Cocoa/GBHexStatusBarRepresenter.m @@ -0,0 +1,220 @@ +#import "GBHexStatusBarRepresenter.h" +#import + +@interface GBHexStatusBarView : NSView +{ + NSCell *_cell; + NSSize _cellSize; + GBHexStatusBarRepresenter *_representer; + NSDictionary *_cellAttributes; + bool _registeredForAppNotifications; +} + +- (void)setRepresenter:(GBHexStatusBarRepresenter *)rep; +- (void)setString:(NSString *)string; + +@end + + +@implementation GBHexStatusBarView + +- (void)_sharedInitStatusBarView +{ + NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [style setAlignment:NSCenterTextAlignment]; + style.lineBreakMode = NSLineBreakByTruncatingTail; + _cellAttributes = @{ + NSForegroundColorAttributeName: [NSColor windowFrameTextColor], + NSFontAttributeName: [NSFont labelFontOfSize:[NSFont smallSystemFontSize]], + NSParagraphStyleAttributeName: style, + }; + _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 +{ + self = [super initWithCoder:coder]; + [self _sharedInitStatusBarView]; + return self; +} + +- (BOOL)isFlipped +{ + return true; +} + +- (void)setRepresenter:(GBHexStatusBarRepresenter *)rep +{ + _representer = rep; +} + +- (void)setString:(NSString *)string +{ + [_cell setAttributedStringValue:[[NSAttributedString alloc] initWithString:string attributes:_cellAttributes]]; + _cellSize = [_cell cellSize]; + [self setNeedsDisplay:true]; +} + +- (void)drawRect:(NSRect)clip +{ + NSRect bounds = [self bounds]; + 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 +{ + _representer.useDecimalLength ^= true; +} + +- (void)windowDidChangeKeyStatus:(NSNotification *)note +{ + [self setNeedsDisplay:true]; +} + +- (void)viewDidMoveToWindow +{ + HFRegisterViewForWindowAppearanceChanges(self, @selector(windowDidChangeKeyStatus:), !_registeredForAppNotifications); + _registeredForAppNotifications = true; + [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); +} + +@end + +@implementation GBHexStatusBarRepresenter + +- (instancetype)init +{ + self = [super init]; + return self; +} + +- (NSView *)createView { + GBHexStatusBarView *view = [[GBHexStatusBarView alloc] initWithFrame:NSMakeRect(0, 0, 100, 18)]; + [view setRepresenter:self]; + [view setAutoresizingMask:NSViewWidthSizable]; + return view; +} + +- (NSString *)describeLength:(unsigned long long)length +{ + if (self.useDecimalLength) { + return [NSString stringWithFormat:@"%llu byte%s", length, length == 1 ? "" : "s"]; + } + return [NSString stringWithFormat:@"$%llX byte%s", length, length == 1 ? "" : "s"]; +} + +- (NSString *)describeOffset:(unsigned long long)offset isRangeEnd:(bool)isRangeEnd +{ + if (!_gb) { + return [NSString stringWithFormat:@"$%llX", offset]; + } + return @(GB_debugger_describe_address(_gb, offset + _baseAddress, offset < 0x4000? -1 :_bankForDescription, false, isRangeEnd)); +} + + +- (NSString *)stringForEmptySelectionAtOffset:(unsigned long long)offset length:(unsigned long long)length +{ + return [self describeOffset:offset isRangeEnd:false]; +} + +- (NSString *)stringForSingleByteSelectionAtOffset:(unsigned long long)offset length:(unsigned long long)length +{ + return [NSString stringWithFormat:@"Byte %@ selected", [self describeOffset:offset isRangeEnd:false]]; +} + +- (NSString *)stringForSingleRangeSelection:(HFRange)range length:(unsigned long long)length +{ + return [NSString stringWithFormat:@"Range %@ to %@ selected (%@)", + [self describeOffset:range.location isRangeEnd:false], + [self describeOffset:range.location + range.length isRangeEnd:true], + [self describeLength:range.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 { + string = @"Multiple ranges selected"; + } + } + if (! string) string = @""; + [[self view] setString:string]; +} + +- (void)setUseDecimalLength:(bool)useDecimalLength +{ + _useDecimalLength = useDecimalLength; + [self updateString]; +} + +- (void)setBaseAddress:(uint16_t)baseAddress +{ + _baseAddress = baseAddress; + [self updateString]; +} + +- (void) setBankForDescription:(uint16_t)bankForDescription +{ + _bankForDescription = bankForDescription; + [self updateString]; +} + +- (void)controllerDidChange:(HFControllerPropertyBits)bits +{ + if (bits & (HFControllerContentLength | HFControllerSelectedRanges)) { + [self updateString]; + } +} + ++ (NSPoint)defaultLayoutPosition +{ + return NSMakePoint(0, -1); +} + +@end diff --git a/bsnes/gb/Cocoa/GBHueSliderCell.h b/bsnes/gb/Cocoa/GBHueSliderCell.h new file mode 100644 index 00000000..f293124b --- /dev/null +++ b/bsnes/gb/Cocoa/GBHueSliderCell.h @@ -0,0 +1,9 @@ +#import + +@interface NSSlider (GBHueSlider) +-(NSColor *)colorValue; +@end + +@interface GBHueSliderCell : NSSliderCell +-(NSColor *)colorValue; +@end diff --git a/bsnes/gb/Cocoa/GBHueSliderCell.m b/bsnes/gb/Cocoa/GBHueSliderCell.m new file mode 100644 index 00000000..1b65ca83 --- /dev/null +++ b/bsnes/gb/Cocoa/GBHueSliderCell.m @@ -0,0 +1,125 @@ +#import "GBHueSliderCell.h" + +@interface NSSliderCell(privateAPI) +- (double)_normalizedDoubleValue; +@end + +@implementation GBHueSliderCell +{ + bool _drawingTrack; +} + +-(NSColor *)colorValue +{ + double hue = self.doubleValue / 360.0; + double r = 0, g = 0, b =0 ; + double t = fmod(hue * 6, 1); + switch ((int)(hue * 6) % 6) { + case 0: + r = 1; + g = t; + break; + case 1: + r = 1 - t; + g = 1; + break; + case 2: + g = 1; + b = t; + break; + case 3: + g = 1 - t; + b = 1; + break; + case 4: + b = 1; + r = t; + break; + case 5: + b = 1 - t; + r = 1; + break; + } + return [NSColor colorWithRed:r green:g blue:b alpha:1.0]; +} + +-(void)drawKnob:(NSRect)knobRect +{ + [super drawKnob:knobRect]; + NSBezierPath *path = nil; + if (@available(macos 26.0, *)) { + NSRect peekRect = knobRect; + peekRect.size.height /= 2; + peekRect.size.width -= peekRect.size.height; + peekRect.origin.x += peekRect.size.height / 2; + peekRect.origin.y += peekRect.size.height / 2; + path = [NSBezierPath bezierPathWithRoundedRect:peekRect xRadius:peekRect.size.height / 2 yRadius:peekRect.size.height / 2]; + } + else { + NSRect peekRect = knobRect; + peekRect.size.width /= 2; + peekRect.size.height = peekRect.size.width; + peekRect.origin.x += peekRect.size.width / 2; + peekRect.origin.y += peekRect.size.height / 2; + path = [NSBezierPath bezierPathWithOvalInRect:peekRect]; + + } + NSColor *color = self.colorValue; + if (!self.enabled) { + color = [color colorWithAlphaComponent:0.5]; + } + [color setFill]; + [path fill]; + [[NSColor colorWithWhite:0 alpha:0.25] setStroke]; + [path setLineWidth:0.5]; + [path stroke]; +} + +-(double)_normalizedDoubleValue +{ + if (_drawingTrack) return 0; + return [super _normalizedDoubleValue]; +} + +-(void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped +{ + if (!self.enabled) { + [super drawBarInside:rect flipped:flipped]; + return; + } + + _drawingTrack = true; + [super drawBarInside:rect flipped:flipped]; + _drawingTrack = false; + + NSGradient *gradient = [[NSGradient alloc] initWithColors:@[ + [NSColor redColor], + [NSColor yellowColor], + [NSColor greenColor], + [NSColor cyanColor], + [NSColor blueColor], + [NSColor magentaColor], + [NSColor redColor], + ]]; + + rect.origin.y += rect.size.height / 2 - 0.5; + rect.size.height = 1; + rect.size.width -= 2; + rect.origin.x += 1; + [[NSColor redColor] set]; + NSRectFill(rect); + + rect.size.width -= self.knobThickness + 2; + rect.origin.x += self.knobThickness / 2 - 1; + + [gradient drawInRect:rect angle:0]; +} + +@end + +@implementation NSSlider (GBHueSlider) +- (NSColor *)colorValue +{ + return ((GBHueSliderCell *)self.cell).colorValue; +} +@end diff --git a/bsnes/gb/Cocoa/GBImageCell.h b/bsnes/gb/Cocoa/GBImageCell.h deleted file mode 100644 index 0323b41d..00000000 --- a/bsnes/gb/Cocoa/GBImageCell.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -@interface GBImageCell : NSImageCell - -@end diff --git a/bsnes/gb/Cocoa/GBImageCell.m b/bsnes/gb/Cocoa/GBImageCell.m deleted file mode 100644 index de75e0e9..00000000 --- a/bsnes/gb/Cocoa/GBImageCell.m +++ /dev/null @@ -1,10 +0,0 @@ -#import "GBImageCell.h" - -@implementation GBImageCell -- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView -{ - CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; - CGContextSetInterpolationQuality(context, kCGInterpolationNone); - [super drawWithFrame:cellFrame inView:controlView]; -} -@end diff --git a/bsnes/gb/Cocoa/GBImageView.m b/bsnes/gb/Cocoa/GBImageView.m index 3525e72e..835f05dc 100644 --- a/bsnes/gb/Cocoa/GBImageView.m +++ b/bsnes/gb/Cocoa/GBImageView.m @@ -10,18 +10,18 @@ } @end -@implementation GBImageView -{ - NSTrackingArea *trackingArea; -} +@interface GBGridView : NSView +@end + +@implementation GBGridView + - (void)drawRect:(NSRect)dirtyRect { - CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; - CGContextSetInterpolationQuality(context, kCGInterpolationNone); - [super drawRect:dirtyRect]; - CGFloat y_ratio = self.frame.size.height / self.image.size.height; - CGFloat x_ratio = self.frame.size.width / self.image.size.width; - for (GBImageViewGridConfiguration *conf in self.verticalGrids) { + GBImageView *parent = (GBImageView *)self.superview; + + CGFloat y_ratio = parent.frame.size.height / parent.image.size.height; + CGFloat x_ratio = parent.frame.size.width / parent.image.size.width; + for (GBImageViewGridConfiguration *conf in parent.verticalGrids) { [conf.color set]; for (CGFloat y = conf.size * y_ratio; y < self.frame.size.height; y += conf.size * y_ratio) { NSBezierPath *line = [NSBezierPath bezierPath]; @@ -32,7 +32,7 @@ } } - for (GBImageViewGridConfiguration *conf in self.horizontalGrids) { + for (GBImageViewGridConfiguration *conf in parent.horizontalGrids) { [conf.color set]; for (CGFloat x = conf.size * x_ratio; x < self.frame.size.width; x += conf.size * x_ratio) { NSBezierPath *line = [NSBezierPath bezierPath]; @@ -43,11 +43,12 @@ } } - if (self.displayScrollRect) { - NSBezierPath *path = [NSBezierPath bezierPathWithRect:CGRectInfinite]; + if (parent.displayScrollRect) { + // CGRectInfinite in NSBezierPath is broken in newer macOS versions + NSBezierPath *path = [NSBezierPath bezierPathWithRect:CGRectMake(-0x4000, -0x4000, 0x8000, 0x8000)]; for (unsigned x = 0; x < 2; x++) { for (unsigned y = 0; y < 2; y++) { - NSRect rect = self.scrollRect; + NSRect rect = parent.scrollRect; rect.origin.x *= x_ratio; rect.origin.y *= y_ratio; rect.size.width *= x_ratio; @@ -56,7 +57,7 @@ rect.origin.x -= self.frame.size.width * x; rect.origin.y += self.frame.size.height * y; - + NSBezierPath *subpath = [NSBezierPath bezierPathWithRect:rect]; [path appendBezierPath:subpath]; @@ -72,36 +73,62 @@ [path stroke]; } } +@end + +@implementation GBImageView +{ + NSTrackingArea *_trackingArea; + GBGridView *_gridView; + NSRect _scrollRect; +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + self.wantsLayer = true; + _gridView = [[GBGridView alloc] initWithFrame:self.bounds]; + _gridView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + [self addSubview:_gridView]; + return self; +} + +-(void)viewWillDraw +{ + [super viewWillDraw]; + for (CALayer *layer in self.layer.sublayers) { + layer.magnificationFilter = kCAFilterNearest; + } +} - (void)setHorizontalGrids:(NSArray *)horizontalGrids { self->_horizontalGrids = horizontalGrids; - [self setNeedsDisplay]; + [_gridView setNeedsDisplay:true]; } - (void)setVerticalGrids:(NSArray *)verticalGrids { self->_verticalGrids = verticalGrids; - [self setNeedsDisplay]; + [_gridView setNeedsDisplay:true]; } - (void)setDisplayScrollRect:(bool)displayScrollRect { self->_displayScrollRect = displayScrollRect; - [self setNeedsDisplay]; + [_gridView setNeedsDisplay:true]; } - (void)updateTrackingAreas { - if (trackingArea != nil) { - [self removeTrackingArea:trackingArea]; + if (_trackingArea != nil) { + [self removeTrackingArea:_trackingArea]; } - trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds] + _trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds] options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingMouseMoved owner:self userInfo:nil]; - [self addTrackingArea:trackingArea]; + [self addTrackingArea:_trackingArea]; } - (void)mouseExited:(NSEvent *)theEvent @@ -124,4 +151,17 @@ } } +- (void)setScrollRect:(NSRect)scrollRect +{ + if (memcmp(&scrollRect, &_scrollRect, sizeof(scrollRect)) != 0) { + _scrollRect = scrollRect; + [_gridView setNeedsDisplay:true]; + } +} + +- (NSRect)scrollRect +{ + return _scrollRect; +} + @end diff --git a/bsnes/gb/Cocoa/GBJoyConManager.h b/bsnes/gb/Cocoa/GBJoyConManager.h new file mode 100644 index 00000000..128beb51 --- /dev/null +++ b/bsnes/gb/Cocoa/GBJoyConManager.h @@ -0,0 +1,12 @@ +#import +#import +#import + +@interface GBJoyConManager : NSObject ++ (instancetype)sharedInstance; +- (IBAction)autopair:(id)sender; + +@property (nonatomic) bool arrangementMode; +@property (weak) IBOutlet NSTableView *tableView; +@end + diff --git a/bsnes/gb/Cocoa/GBJoyConManager.m b/bsnes/gb/Cocoa/GBJoyConManager.m new file mode 100644 index 00000000..c830cfbc --- /dev/null +++ b/bsnes/gb/Cocoa/GBJoyConManager.m @@ -0,0 +1,313 @@ +#import "GBJoyConManager.h" +#import "GBTintedImageCell.h" +#import + +@implementation GBJoyConManager +{ + GBTintedImageCell *_tintedImageCell; + NSImageCell *_imageCell; + NSMutableDictionary *_pairings; + NSMutableDictionary *_gripSettings; + bool _unpairing; +} + ++ (instancetype)sharedInstance +{ + static GBJoyConManager *manager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manager = [[super allocWithZone:nil] _init]; + }); + return manager; +} + +- (NSArray *)joycons +{ + NSMutableArray *ret = [[JOYController allControllers] mutableCopy]; + for (JOYController *controller in [JOYController allControllers]) { + if (controller.joyconType == JOYJoyConTypeNone) { + [ret removeObject:controller]; + } + } + return ret; +} + ++ (instancetype)allocWithZone:(struct _NSZone *)zone +{ + return [self sharedInstance]; +} + ++ (instancetype)alloc +{ + return [self sharedInstance]; +} + +- (instancetype)init +{ + return [self.class sharedInstance]; +} + +- (instancetype) _init +{ + self = [super init]; + _imageCell = [[NSImageCell alloc] init]; + _tintedImageCell = [[GBTintedImageCell alloc] init]; + if (@available(macOS 10.14, *)) { + _tintedImageCell.tint = [NSColor controlAccentColor]; + } + else { + _tintedImageCell.tint = [NSColor selectedMenuItemColor]; + } + _pairings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoyConPairings"] ?: @{} mutableCopy]; + _gripSettings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoyConGrips"] ?: @{} mutableCopy]; + + // Sanity check the pairings + for (NSString *key in _pairings) { + if (![_pairings[_pairings[key]] isEqualToString:key]) { + [_pairings removeAllObjects]; + break; + } + } + + [JOYController registerListener:self]; + for (JOYController *controller in [JOYController allControllers]) { + [self controllerConnected:controller]; + } + + return self; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + return self.joycons.count; +} + +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + if (row >= [self numberOfRowsInTableView:tableView]) return nil; + + unsigned columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + JOYController *controller = self.joycons[row]; + switch (columnIndex) { + case 0: { + switch (controller.joyconType) { + case JOYJoyConTypeNone: + return nil; + case JOYJoyConTypeLeft: + return [NSImage imageNamed:[NSString stringWithFormat:@"%sJoyConLeftTemplate", controller.usesHorizontalJoyConGrip? "Horizontal" :""]]; + case JOYJoyConTypeRight: + return [NSImage imageNamed:[NSString stringWithFormat:@"%sJoyConRightTemplate", controller.usesHorizontalJoyConGrip? "Horizontal" :""]]; + case JOYJoyConTypeDual: + return [NSImage imageNamed:@"JoyConDualTemplate"]; + } + } + case 1: { + NSMutableAttributedString *ret = [[NSMutableAttributedString alloc] initWithString:controller.deviceName + attributes:@{NSFontAttributeName: + [NSFont systemFontOfSize:[NSFont systemFontSize]]}]; + + [ret appendAttributedString:[[NSAttributedString alloc] initWithString:[@"\n" stringByAppendingString:controller.uniqueID] + attributes:@{NSFontAttributeName: + [NSFont systemFontOfSize:[NSFont smallSystemFontSize]], + NSForegroundColorAttributeName:[NSColor disabledControlTextColor]}]]; + return ret; + } + case 2: + return @([(_gripSettings[controller.uniqueID] ?: @(-1)) unsignedIntValue] + 1); + } + return nil; +} + +- (void)updateGripForController:(JOYController *)controller +{ + NSNumber *grip = _gripSettings[controller.uniqueID]; + if (!grip) { + controller.usesHorizontalJoyConGrip = [[NSUserDefaults standardUserDefaults] boolForKey:@"GBJoyConsDefaultsToHorizontal"]; + return; + } + controller.usesHorizontalJoyConGrip = [grip unsignedIntValue] == 1; +} + +- (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + unsigned columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + if (columnIndex != 2) return; + if (row >= [self numberOfRowsInTableView:tableView]) return; + JOYController *controller = self.joycons[row]; + if (controller.joyconType == JOYJoyConTypeDual) { + return; + } + switch ([object unsignedIntValue]) { + case 0: + [_gripSettings removeObjectForKey:controller.uniqueID]; + break; + case 1: + _gripSettings[controller.uniqueID] = @(0); + break; + case 2: + _gripSettings[controller.uniqueID] = @(1); + break; + } + [[NSUserDefaults standardUserDefaults] setObject:_gripSettings forKey:@"GBJoyConGrips"]; + [self updateGripForController:controller]; +} + +- (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + if (row >= [self numberOfRowsInTableView:tableView]) return [[NSCell alloc] init]; + + unsigned columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + if (columnIndex == 2) { + JOYCombinedController *controller = (JOYCombinedController *)self.joycons[row]; + if (controller.joyconType == JOYJoyConTypeDual) { + NSButtonCell *cell = [[NSButtonCell alloc] initTextCell:@"Separate Joy-Cons"]; + cell.bezelStyle = NSBezelStyleRounded; + cell.action = @selector(invoke); + id block = ^(void) { + dispatch_async(dispatch_get_main_queue(), ^{ + for (JOYController *child in controller.children) { + [_pairings removeObjectForKey:child.uniqueID]; + } + [[NSUserDefaults standardUserDefaults] setObject:_pairings forKey:@"GBJoyConPairings"]; + _unpairing = true; + [controller breakApart]; + _unpairing = false; + }); + }; + // To retain the block + objc_setAssociatedObject(cell, @selector(breakApart), block, OBJC_ASSOCIATION_RETAIN); + cell.target = block; + return cell; + } + } + if (columnIndex == 0) { + JOYController *controller = self.joycons[row]; + for (JOYButton *button in controller.buttons) { + if (button.isPressed) { + return _tintedImageCell; + } + } + return _imageCell; + } + return nil; +} + +- (void)controllerConnected:(JOYController *)controller +{ + [self updateGripForController:controller]; + for (JOYController *partner in [JOYController allControllers]) { + if ([partner.uniqueID isEqualToString:_pairings[controller.uniqueID]]) { + [self pairJoyCon:controller withJoyCon:partner]; + break; + } + } + if (controller.joyconType == JOYJoyConTypeLeft || controller.joyconType == JOYJoyConTypeRight) { + [self autopair:nil]; + } + if (_arrangementMode) { + [self.tableView reloadData]; + } +} + +- (IBAction)autopair:(id)sender +{ + if (_unpairing) return; + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBJoyConAutoPair"]) return; + NSArray *controllers = [[JOYController allControllers] copy]; + for (JOYController *first in controllers) { + if (_pairings[first.uniqueID]) continue; // Has an established partner + if (first.joyconType != JOYJoyConTypeLeft) continue; + for (JOYController *second in controllers) { + if (_pairings[second.uniqueID]) continue; // Has an established partner + if (second.joyconType != JOYJoyConTypeRight) continue; + [self pairJoyCon:first withJoyCon:second]; + break; + } + } + if (_arrangementMode) { + [self.tableView reloadData]; + } +} + +- (void)controllerDisconnected:(JOYController *)controller +{ + if (_arrangementMode) { + [self.tableView reloadData]; + } +} + +- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + unsigned columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + return columnIndex == 2; +} + +- (JOYCombinedController *)pairJoyCon:(JOYController *)first withJoyCon:(JOYController *)second +{ + if (first.joyconType != JOYJoyConTypeLeft && first.joyconType != JOYJoyConTypeRight) return nil; // Not a Joy-Con + if (second.joyconType != JOYJoyConTypeLeft && second.joyconType != JOYJoyConTypeRight) return nil; // Not a Joy-Con + if (first.joyconType == second.joyconType) return nil; // Not a sensible pair + + _pairings[first.uniqueID] = second.uniqueID; + _pairings[second.uniqueID] = first.uniqueID; + first.usesHorizontalJoyConGrip = false; + second.usesHorizontalJoyConGrip = false; + [[NSUserDefaults standardUserDefaults] setObject:_pairings forKey:@"GBJoyConPairings"]; + return [[JOYCombinedController alloc] initWithChildren:@[first, second]]; +} + +- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button +{ + if (!_arrangementMode) return; + if (controller.joyconType == JOYJoyConTypeNone) return; + [self.tableView setNeedsDisplay:true]; + if (controller.joyconType != JOYJoyConTypeLeft && controller.joyconType != JOYJoyConTypeRight) return; + if (button.usage != JOYButtonUsageL1 && button.usage != JOYButtonUsageR1) return; + + + // L or R were pressed on a single Joy-Con, try and pair available Joy-Cons + JOYController *left = nil; + JOYController *right = nil; + for (JOYController *controller in [JOYController allControllers]) { + if (!left && controller.joyconType == JOYJoyConTypeLeft) { + for (JOYButton *button in controller.buttons) { + if (button.usage == JOYButtonUsageL1 && button.isPressed) { + left = controller; + break; + } + } + } + if (!right && controller.joyconType == JOYJoyConTypeRight) { + for (JOYButton *button in controller.buttons) { + if (button.usage == JOYButtonUsageR1 && button.isPressed) { + right = controller; + break; + } + } + } + if (left && right) { + [self pairJoyCon:left withJoyCon:right]; + return; + } + } +} + +- (IBAction)toggleHorizontalDefault:(NSButton *)sender +{ + for (JOYController *controller in self.joycons) { + [self updateGripForController:controller]; + } + if (_arrangementMode) { + [self.tableView reloadData]; + } +} + +- (void)setArrangementMode:(bool)arrangementMode +{ + _arrangementMode = arrangementMode; + if (arrangementMode) { + [self.tableView reloadData]; + } +} + +@end diff --git a/bsnes/gb/Cocoa/GBMemoryByteArray.m b/bsnes/gb/Cocoa/GBMemoryByteArray.m index 32526ade..55e1bd15 100644 --- a/bsnes/gb/Cocoa/GBMemoryByteArray.m +++ b/bsnes/gb/Cocoa/GBMemoryByteArray.m @@ -1,4 +1,3 @@ -#define GB_INTERNAL // Todo: Some memory accesses are being done using the struct directly #import "GBMemoryByteArray.h" #import "GBCompleteByteSlice.h" @@ -32,69 +31,110 @@ } } +- (uint16_t)base +{ + switch (_mode) { + case GBMemoryEntireSpace: return 0; + case GBMemoryROM: return 0; + case GBMemoryVRAM: return 0x8000; + case GBMemoryExternalRAM: return 0xA000; + case GBMemoryRAM: return 0xC000; + } +} + - (void)copyBytes:(unsigned char *)dst range:(HFRange)range { - __block uint16_t addr = (uint16_t) range.location; - __block unsigned long long length = range.length; - if (_mode == GBMemoryEntireSpace) { - while (length) { - *(dst++) = [_document readMemory:addr++]; - length--; - } + // Do everything in 0x1000 chunks, never cross a 0x1000 boundary + if ((range.location & 0xF000) != ((range.location + range.length) & 0xF000)) { + size_t partial = 0x1000 - (range.location & 0xFFF); + [self copyBytes:dst + partial range:HFRangeMake(range.location + partial, range.length - partial)]; + range.length = partial; } - else { - [_document performAtomicBlock:^{ - unsigned char *_dst = dst; - uint16_t bank_backup = 0; - GB_gameboy_t *gb = _document.gameboy; - switch (_mode) { - case GBMemoryROM: - bank_backup = gb->mbc_rom_bank; - gb->mbc_rom_bank = self.selectedBank; - break; - case GBMemoryVRAM: - bank_backup = gb->cgb_vram_bank; - if (GB_is_cgb(gb)) { - gb->cgb_vram_bank = self.selectedBank; + range.location += self.base; + + GB_gameboy_t *gb = _document.gameboy; + + switch (range.location >> 12) { + case 0x0: + case 0x1: + case 0x2: + case 0x3: { + uint16_t bank; + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_ROM0, NULL, &bank); + memcpy(dst, data + bank * 0x4000 + range.location, range.length); + break; + } + case 0x4: + case 0x5: + case 0x6: + case 0x7: { + uint16_t bank; + size_t size; + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_ROM, &size, &bank); + if (_mode != GBMemoryEntireSpace) { + bank = self.selectedBank & (size / 0x4000 - 1); + } + memcpy(dst, data + bank * 0x4000 + range.location - 0x4000, range.length); + break; + } + case 0x8: + case 0x9: { + uint16_t bank; + size_t size; + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_VRAM, &size, &bank); + if (_mode != GBMemoryEntireSpace) { + bank = self.selectedBank & (size / 0x2000 - 1); + } + memcpy(dst, data + bank * 0x2000 + range.location - 0x8000, range.length); + break; + } + case 0xA: + case 0xB: { + // Some carts are special, use memory read directly in full mem mode + if (_mode == GBMemoryEntireSpace) { + case 0xF: + slow_path: + [_document performAtomicBlock:^{ + for (unsigned i = 0; i < range.length; i++) { + dst[i] = GB_safe_read_memory(gb, range.location + i); } - addr += 0x8000; - break; - case GBMemoryExternalRAM: - bank_backup = gb->mbc_ram_bank; - gb->mbc_ram_bank = self.selectedBank; - addr += 0xA000; - break; - case GBMemoryRAM: - bank_backup = gb->cgb_ram_bank; - if (GB_is_cgb(gb)) { - gb->cgb_ram_bank = self.selectedBank; - } - addr += 0xC000; - break; - default: - assert(false); + }]; + break; } - while (length) { - *(_dst++) = [_document readMemory:addr++]; - length--; + else { + uint16_t bank; + size_t size; + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &size, &bank); + bank = self.selectedBank & (size / 0x2000 - 1); + if (size == 0) { + memset(dst, 0xFF, range.length); + } + else if (range.location + range.length - 0xA000 > size) { + goto slow_path; + } + else { + memcpy(dst, data + bank * 0x2000 + range.location - 0xA000, range.length); + } + break; } - switch (_mode) { - case GBMemoryROM: - gb->mbc_rom_bank = bank_backup; - break; - case GBMemoryVRAM: - gb->cgb_vram_bank = bank_backup; - break; - case GBMemoryExternalRAM: - gb->mbc_ram_bank = bank_backup; - break; - case GBMemoryRAM: - gb->cgb_ram_bank = bank_backup; - break; - default: - assert(false); + } + case 0xC: + case 0xE: { + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_RAM, NULL, NULL); + memcpy(dst, data + (range.location & 0xFFF), range.length); + break; + } + + case 0xD: { + uint16_t bank; + size_t size; + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_RAM, &size, &bank); + if (_mode != GBMemoryEntireSpace) { + bank = self.selectedBank & (size / 0x1000 - 1); } - }]; + memcpy(dst, data + bank * 0x1000 + range.location - 0xD000, range.length); + break; + } } } @@ -113,65 +153,104 @@ return ret; } -- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)lrange +- (void)insertByteSlice:(HFByteSlice *)slice inRange:(HFRange)range { - if (slice.length != lrange.length) return; /* Insertion is not allowed, only overwriting. */ - [_document performAtomicBlock:^{ - uint16_t addr = (uint16_t) lrange.location; - uint16_t bank_backup = 0; - GB_gameboy_t *gb = _document.gameboy; - switch (_mode) { - case GBMemoryROM: - bank_backup = gb->mbc_rom_bank; - gb->mbc_rom_bank = self.selectedBank; + if (slice.length != range.length) return; /* Insertion is not allowed, only overwriting. */ + // Do everything in 0x1000 chunks, never cross a 0x1000 boundary + if ((range.location & 0xF000) != ((range.location + range.length) & 0xF000)) { + size_t partial = 0x1000 - (range.location & 0xFFF); + if (slice.length - partial) { + [self insertByteSlice:[slice subsliceWithRange:HFRangeMake(partial, slice.length - partial)] + inRange:HFRangeMake(range.location + partial, range.length - partial)]; + } + range.length = partial; + } + range.location += self.base; + + GB_gameboy_t *gb = _document.gameboy; + + + switch (range.location >> 12) { + case 0x0: + case 0x1: + case 0x2: + case 0x3: + case 0x4: + case 0x5: + case 0x6: + case 0x7: { + return; // ROM not writeable + } + case 0x8: + case 0x9: { + uint16_t bank; + size_t size; + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_VRAM, &size, &bank); + if (_mode != GBMemoryEntireSpace) { + bank = self.selectedBank & (size / 0x2000 - 1); + } + uint8_t sliceData[range.length]; + [slice copyBytes:sliceData range:HFRangeMake(0, range.length)]; + memcpy(data + bank * 0x2000 + range.location - 0x8000, sliceData, range.length); + break; + } + case 0xA: + case 0xB: { + // Some carts are special, use memory write directly in full mem mode + if (_mode == GBMemoryEntireSpace) { + case 0xF: + slow_path: + [_document performAtomicBlock:^{ + uint8_t sliceData[range.length]; + [slice copyBytes:sliceData range:HFRangeMake(0, range.length)]; + for (unsigned i = 0; i < range.length; i++) { + GB_write_memory(gb, range.location + i, sliceData[i]); + } + }]; break; - case GBMemoryVRAM: - bank_backup = gb->cgb_vram_bank; - if (GB_is_cgb(gb)) { - gb->cgb_vram_bank = self.selectedBank; + } + else { + uint16_t bank; + size_t size; + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &size, &bank); + bank = self.selectedBank & (size / 0x2000 - 1); + if (size == 0) { + // Nothing to write to } - addr += 0x8000; - break; - case GBMemoryExternalRAM: - bank_backup = gb->mbc_ram_bank; - gb->mbc_ram_bank = self.selectedBank; - addr += 0xA000; - break; - case GBMemoryRAM: - bank_backup = gb->cgb_ram_bank; - if (GB_is_cgb(gb)) { - gb->cgb_ram_bank = self.selectedBank; + else if (range.location + range.length - 0xA000 > size) { + goto slow_path; + } + else { + uint8_t sliceData[range.length]; + [slice copyBytes:sliceData range:HFRangeMake(0, range.length)]; + memcpy(data + bank * 0x2000 + range.location - 0xA000, sliceData, range.length); } - addr += 0xC000; - break; - default: break; + } } - uint8_t values[lrange.length]; - [slice copyBytes:values range:HFRangeMake(0, lrange.length)]; - uint8_t *src = values; - unsigned long long length = lrange.length; - while (length) { - [_document writeMemory:addr++ value:*(src++)]; - length--; + case 0xC: + case 0xE: { + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_RAM, NULL, NULL); + uint8_t sliceData[range.length]; + [slice copyBytes:sliceData range:HFRangeMake(0, range.length)]; + memcpy(data + (range.location & 0xFFF), sliceData, range.length); + break; } - switch (_mode) { - case GBMemoryROM: - gb->mbc_rom_bank = bank_backup; - break; - case GBMemoryVRAM: - gb->cgb_vram_bank = bank_backup; - break; - case GBMemoryExternalRAM: - gb->mbc_ram_bank = bank_backup; - break; - case GBMemoryRAM: - gb->cgb_ram_bank = bank_backup; - break; - default: - break; + + case 0xD: { + uint16_t bank; + size_t size; + uint8_t *data = GB_get_direct_access(gb, GB_DIRECT_ACCESS_RAM, &size, &bank); + if (_mode != GBMemoryEntireSpace) { + bank = self.selectedBank & (size / 0x1000 - 1); + } + uint8_t sliceData[range.length]; + [slice copyBytes:sliceData range:HFRangeMake(0, range.length)]; + + memcpy(data + bank * 0x1000 + range.location - 0xD000, sliceData, range.length); + break; } - }]; + } } @end diff --git a/bsnes/gb/Cocoa/GBObjectView.h b/bsnes/gb/Cocoa/GBObjectView.h new file mode 100644 index 00000000..2d1c955d --- /dev/null +++ b/bsnes/gb/Cocoa/GBObjectView.h @@ -0,0 +1,6 @@ +#import +#import "Document.h" + +@interface GBObjectView : NSView +- (void)reloadData:(Document *)document; +@end diff --git a/bsnes/gb/Cocoa/GBObjectView.m b/bsnes/gb/Cocoa/GBObjectView.m new file mode 100644 index 00000000..719d66d0 --- /dev/null +++ b/bsnes/gb/Cocoa/GBObjectView.m @@ -0,0 +1,134 @@ +#import "GBObjectView.h" + +@interface GBObjectViewItem : NSObject +@property IBOutlet NSView *view; +@property IBOutlet NSImageView *image; +@property IBOutlet NSTextField *oamAddress; +@property IBOutlet NSTextField *position; +@property IBOutlet NSTextField *attributes; +@property IBOutlet NSTextField *tile; +@property IBOutlet NSTextField *tileAddress; +@property IBOutlet NSImageView *warningIcon; +@property IBOutlet NSBox *verticalLine; +@end + +@implementation GBObjectViewItem +{ + @public + uint32_t _lastImageData[128]; + uint8_t _lastHeight; +} +@end + +@implementation GBObjectView +{ + NSMutableArray *_items; +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + _items = [NSMutableArray array]; + CGFloat height = self.frame.size.height; + for (unsigned i = 0; i < 40; i++) { + GBObjectViewItem *item = [[GBObjectViewItem alloc] init]; + [_items addObject:item]; + [[NSBundle mainBundle] loadNibNamed:@"GBObjectViewItem" owner:item topLevelObjects:nil]; + item.view.hidden = true; + [self addSubview:item.view]; + [item.view setFrameOrigin:NSMakePoint((i % 4) * 120, height - (i / 4 * 68) - 68)]; + item.oamAddress.toolTip = @"OAM address"; + item.position.toolTip = @"Position"; + item.attributes.toolTip = @"Attributes"; + item.tile.toolTip = @"Tile index"; + item.tileAddress.toolTip = @"Tile address"; + item.warningIcon.toolTip = @"Dropped: too many objects in line"; + if ((i % 4) == 3) { + [item.verticalLine removeFromSuperview]; + } + item.view.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin; + } + return self; +} + +- (void)reloadData:(Document *)document +{ + GB_oam_info_t *info = document.oamInfo; + uint8_t length = document.oamCount; + bool cgb = GB_is_cgb(document.gb); + uint8_t height = document.oamHeight; + NSFont *font = [document debuggerFontOfSize:11]; + NSFont *boldFont = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask]; + + for (unsigned i = 0; i < 40; i++) { + GBObjectViewItem *item = _items[i]; + if (i >= length) { + item.view.hidden = true; + } + else { + item.view.hidden = false; + + item.oamAddress.font = boldFont; + item.position.font = font; + item.attributes.font = font; + item.tile.font = font; + item.tileAddress.font = font; + + item.oamAddress.stringValue = [NSString stringWithFormat:@"$%04X", info[i].oam_addr]; + item.position.stringValue = [NSString stringWithFormat:@"(%d, %d)", + ((signed)(unsigned)info[i].x) - 8, + ((signed)(unsigned)info[i].y) - 16]; + item.tile.stringValue = [NSString stringWithFormat:@"$%02X", info[i].tile]; + item.tileAddress.stringValue = [NSString stringWithFormat:@"$%04X", 0x8000 + info[i].tile * 0x10]; + item.warningIcon.hidden = !info[i].obscured_by_line_limit; + if (cgb) { + item.attributes.stringValue = [NSString stringWithFormat:@"%c%c%c%d%d", + info[i].flags & 0x80? 'P' : '-', + info[i].flags & 0x40? 'Y' : '-', + info[i].flags & 0x20? 'X' : '-', + info[i].flags & 0x08? 1 : 0, + info[i].flags & 0x07]; + } + else { + item.attributes.stringValue = [NSString stringWithFormat:@"%c%c%c%d", + info[i].flags & 0x80? 'P' : '-', + info[i].flags & 0x40? 'Y' : '-', + info[i].flags & 0x20? 'X' : '-', + info[i].flags & 0x10? 1 : 0]; + } + size_t imageSize = 8 * 4 * height; + if (height == item->_lastHeight && memcmp(item->_lastImageData, info[i].image, imageSize) == 0) { + continue; + } + memcpy(item->_lastImageData, info[i].image, imageSize); + item->_lastHeight = height; + item.image.image = [Document imageFromData:[NSData dataWithBytesNoCopy:info[i].image + length:64 * 4 * 2 + freeWhenDone:false] + width:8 + height:height + scale:32.0 / height]; + } + } + + NSRect frame = self.frame; + CGFloat newHeight = MAX(68 * ((length + 3) / 4), self.superview.frame.size.height); + frame.origin.y -= newHeight - frame.size.height; + frame.size.height = newHeight; + self.frame = frame; +} + +- (void)drawRect:(NSRect)dirtyRect +{ + if (@available(macOS 10.14, *)) { + [[NSColor alternatingContentBackgroundColors].lastObject setFill]; + } + else { + [[NSColor colorWithDeviceWhite:0.96 alpha:1] setFill]; + } + NSRect frame = self.frame; + for (unsigned i = 1; i <= 5; i++) { + NSRectFill(NSMakeRect(0, frame.size.height - i * 68 * 2, frame.size.width, 68)); + } +} +@end diff --git a/bsnes/gb/Cocoa/GBObjectViewItem.xib b/bsnes/gb/Cocoa/GBObjectViewItem.xib new file mode 100644 index 00000000..0ba1d5d8 --- /dev/null +++ b/bsnes/gb/Cocoa/GBObjectViewItem.xib @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/GBOpenGLView.m b/bsnes/gb/Cocoa/GBOpenGLView.m index 2e4eb70f..11c487ff 100644 --- a/bsnes/gb/Cocoa/GBOpenGLView.m +++ b/bsnes/gb/Cocoa/GBOpenGLView.m @@ -1,6 +1,7 @@ #import "GBOpenGLView.h" #import "GBView.h" -#include +#import "NSObject+DefaultsObserver.h" +#import @implementation GBOpenGLView @@ -27,13 +28,13 @@ - (instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(NSOpenGLPixelFormat *)format { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(filterChanged) name:@"GBFilterChanged" object:nil]; - return [super initWithFrame:frameRect pixelFormat:format]; -} + __unsafe_unretained GBOpenGLView *weakSelf = self; + self = [super initWithFrame:frameRect pixelFormat:format]; + [self observeStandardDefaultsKey:@"GBFilter" withBlock:^(id newValue) { + weakSelf.shader = nil; + [weakSelf setNeedsDisplay:true]; -- (void) filterChanged -{ - self.shader = nil; - [self setNeedsDisplay:true]; + }]; + return self; } @end diff --git a/bsnes/gb/Cocoa/GBPaletteEditorController.h b/bsnes/gb/Cocoa/GBPaletteEditorController.h new file mode 100644 index 00000000..e14e53bc --- /dev/null +++ b/bsnes/gb/Cocoa/GBPaletteEditorController.h @@ -0,0 +1,20 @@ +#import +#import + +@interface GBPaletteEditorController : NSObject +@property (weak) IBOutlet NSColorWell *colorWell0; +@property (weak) IBOutlet NSColorWell *colorWell1; +@property (weak) IBOutlet NSColorWell *colorWell2; +@property (weak) IBOutlet NSColorWell *colorWell3; +@property (weak) IBOutlet NSColorWell *colorWell4; +@property (weak) IBOutlet NSButton *disableLCDColorCheckbox; +@property (weak) IBOutlet NSButton *manualModeCheckbox; +@property (weak) IBOutlet NSSlider *brightnessSlider; +@property (weak) IBOutlet NSSlider *hueSlider; +@property (weak) IBOutlet NSSlider *hueStrengthSlider; +@property (weak) IBOutlet NSTableView *themesList; +@property (weak) IBOutlet NSMenu *menu; +@property (weak) IBOutlet NSSegmentedControl *segmentControl; +@property IBOutlet NSMenu *segmentMenu; ++ (const GB_palette_t *)userPalette; +@end diff --git a/bsnes/gb/Cocoa/GBPaletteEditorController.m b/bsnes/gb/Cocoa/GBPaletteEditorController.m new file mode 100644 index 00000000..30c873f6 --- /dev/null +++ b/bsnes/gb/Cocoa/GBPaletteEditorController.m @@ -0,0 +1,414 @@ +#import "GBPaletteEditorController.h" +#import "GBHueSliderCell.h" +#import "GBApp.h" +#import + +#define MAGIC 'SBPL' + +typedef struct __attribute__ ((packed)) { + uint32_t magic; + bool manual:1; + bool disabled_lcd_color:1; + unsigned padding:6; + struct GB_color_s colors[5]; + int32_t brightness_bias; + uint32_t hue_bias; + uint32_t hue_bias_strength; +} theme_t; + +static double blend(double from, double to, double position) +{ + return from * (1 - position) + to * position; +} + +@implementation NSColor (GBColor) + +- (struct GB_color_s)gbColor +{ + NSColor *sRGB = [self colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + return (struct GB_color_s){round(sRGB.redComponent * 255), round(sRGB.greenComponent * 255), round(sRGB.blueComponent * 255)}; +} + +- (uint32_t)intValue +{ + struct GB_color_s color = self.gbColor; + return (color.r << 0) | (color.g << 8) | (color.b << 16) | 0xFF000000; +} + +@end + +@implementation GBPaletteEditorController + +- (NSArray *)colorWells +{ + return @[_colorWell0, _colorWell1, _colorWell2, _colorWell3, _colorWell4]; +} + +- (void)updateEnabledControls +{ + if (self.manualModeCheckbox.state) { + _brightnessSlider.enabled = false; + _hueSlider.enabled = false; + _hueStrengthSlider.enabled = false; + _colorWell1.enabled = true; + _colorWell2.enabled = true; + _colorWell3.enabled = true; + if (!(_colorWell4.enabled = self.disableLCDColorCheckbox.state)) { + _colorWell4.color = _colorWell3.color; + } + } + else { + _colorWell1.enabled = false; + _colorWell2.enabled = false; + _colorWell3.enabled = false; + _colorWell4.enabled = true; + _brightnessSlider.enabled = true; + _hueSlider.enabled = true; + _hueStrengthSlider.enabled = true; + [self updateAutoColors]; + } +} + +- (NSColor *)autoColorAtPositon:(double)position +{ + NSColor *first = [_colorWell0.color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + NSColor *second = [_colorWell4.color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + double brightness = 1 / pow(4, (_brightnessSlider.doubleValue - 128) / 128.0); + position = pow(position, brightness); + NSColor *hue = _hueSlider.colorValue; + double bias = _hueStrengthSlider.doubleValue / 256.0; + double red = 1 / pow(4, (hue.redComponent * 2 - 1) * bias); + double green = 1 / pow(4, (hue.greenComponent * 2 - 1) * bias); + double blue = 1 / pow(4, (hue.blueComponent * 2 - 1) * bias); + NSColor *ret = [NSColor colorWithRed:blend(first.redComponent, second.redComponent, pow(position, red)) + green:blend(first.greenComponent, second.greenComponent, pow(position, green)) + blue:blend(first.blueComponent, second.blueComponent, pow(position, blue)) + alpha:1.0]; + return ret; +} + +- (IBAction)updateAutoColors:(id)sender +{ + if (!self.manualModeCheckbox.state) { + [self updateAutoColors]; + } + else { + [self savePalette:sender]; + } +} + +- (void)updateAutoColors +{ + if (_disableLCDColorCheckbox.state) { + _colorWell1.color = [self autoColorAtPositon:8 / 25.0]; + _colorWell2.color = [self autoColorAtPositon:16 / 25.0]; + _colorWell3.color = [self autoColorAtPositon:24 / 25.0]; + } + else { + _colorWell1.color = [self autoColorAtPositon:1 / 3.0]; + _colorWell2.color = [self autoColorAtPositon:2 / 3.0]; + _colorWell3.color = _colorWell4.color; + } + [self savePalette:nil]; +} + +- (IBAction)disabledLCDColorCheckboxChanged:(id)sender +{ + [self updateEnabledControls]; +} + +- (IBAction)manualModeChanged:(id)sender +{ + [self updateEnabledControls]; +} + +- (IBAction)updateColor4:(id)sender +{ + if (!self.disableLCDColorCheckbox.state) { + self.colorWell4.color = self.colorWell3.color; + } + [self savePalette:self]; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + if (themes.count == 0) { + [defaults setObject:@"Untitled Palette" forKey:@"GBCurrentTheme"]; + [self savePalette:nil]; + return 1; + } + return themes.count; +} + +-(void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + NSString *oldName = [self tableView:tableView objectValueForTableColumn:tableColumn row:row]; + if ([oldName isEqualToString:object]) { + return; + } + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *themes = [[defaults dictionaryForKey:@"GBThemes"] ?: @{} mutableCopy]; + NSString *newName = object; + unsigned i = 2; + if (!newName.length) { + newName = @"Untitled Palette"; + } + while (themes[newName]) { + newName = [NSString stringWithFormat:@"%@ %d", object, i]; + } + themes[newName] = themes[oldName]; + [themes removeObjectForKey:oldName]; + if ([oldName isEqualToString:[defaults stringForKey:@"GBCurrentTheme"]]) { + [defaults setObject:newName forKey:@"GBCurrentTheme"]; + } + [defaults setObject:themes forKey:@"GBThemes"]; + [tableView reloadData]; + [self awakeFromNib]; +} + +- (IBAction)deleteTheme:(id)sender +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *name = [defaults stringForKey:@"GBCurrentTheme"]; + NSMutableDictionary *themes = [[defaults dictionaryForKey:@"GBThemes"] ?: @{} mutableCopy]; + [themes removeObjectForKey:name]; + [defaults setObject:themes forKey:@"GBThemes"]; + [_themesList reloadData]; + [self awakeFromNib]; +} + +- (void)tableViewSelectionDidChange:(NSNotification *)notification +{ + NSString *name = [self tableView:nil objectValueForTableColumn:nil row:_themesList.selectedRow]; + [[NSUserDefaults standardUserDefaults] setObject:name forKey:@"GBCurrentTheme"]; + [self loadPalette]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorPaletteChanged" object:nil]; +} + +- (void)tableViewSelectionIsChanging:(NSNotification *)notification +{ + [self tableViewSelectionDidChange:notification]; +} + +- (void)awakeFromNib +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + NSString *theme = [defaults stringForKey:@"GBCurrentTheme"]; + if (theme && themes[theme]) { + unsigned index = [[themes.allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] indexOfObject:theme]; + [_themesList selectRowIndexes:[NSIndexSet indexSetWithIndex:index] byExtendingSelection:false]; + [_themesList scrollRowToVisible:index]; + } + else { + [_themesList selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:false]; + [_themesList scrollRowToVisible:0]; + } + [self tableViewSelectionDidChange:nil]; + if (@available(macOS 10.10, *)) { + _themesList.enclosingScrollView.contentView.automaticallyAdjustsContentInsets = false; + _themesList.enclosingScrollView.contentView.contentInsets = NSEdgeInsetsMake(0, 0, 10, 0); + } +} + +- (IBAction)addTheme:(id)sender +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + NSString *newName = @"Untitled Palette"; + unsigned i = 2; + while (themes[newName]) { + newName = [NSString stringWithFormat:@"Untitled Palette %d", i++]; + } + [defaults setObject:newName forKey:@"GBCurrentTheme"]; + [self savePalette:sender]; + [_themesList reloadData]; + [self awakeFromNib]; +} + +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + return [themes.allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)][row]; +} + +- (void)loadPalette +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *theme = [defaults dictionaryForKey:@"GBThemes"][[defaults stringForKey:@"GBCurrentTheme"]]; + NSArray *colors = theme[@"Colors"]; + if (colors.count == 5) { + unsigned i = 0; + for (NSNumber *color in colors) { + uint32_t c = [color unsignedIntValue]; + self.colorWells[i++].color = [NSColor colorWithRed:(c & 0xFF) / 255.0 + green:((c >> 8) & 0xFF) / 255.0 + blue:((c >> 16) & 0xFF) / 255.0 + alpha:1.0]; + } + } + _disableLCDColorCheckbox.state = [theme[@"DisabledLCDColor"] boolValue]; + _manualModeCheckbox.state = [theme[@"Manual"] boolValue]; + _brightnessSlider.doubleValue = [theme[@"BrightnessBias"] doubleValue] * 128 + 128; + _hueSlider.doubleValue = [theme[@"HueBias"] doubleValue] * 360; + _hueStrengthSlider.doubleValue = [theme[@"HueBiasStrength"] doubleValue] * 256; + [self updateEnabledControls]; +} + +- (IBAction)savePalette:(id)sender +{ + NSDictionary *theme = @{ + @"Colors": + @[@(_colorWell0.color.intValue), + @(_colorWell1.color.intValue), + @(_colorWell2.color.intValue), + @(_colorWell3.color.intValue), + @(_colorWell4.color.intValue)], + @"DisabledLCDColor": _disableLCDColorCheckbox.state? @YES : @NO, + @"Manual": _manualModeCheckbox.state? @YES : @NO, + @"BrightnessBias": @((_brightnessSlider.doubleValue - 128) / 128.0), + @"HueBias": @(_hueSlider.doubleValue / 360.0), + @"HueBiasStrength": @(_hueStrengthSlider.doubleValue / 256.0) + }; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *themes = [[defaults dictionaryForKey:@"GBThemes"] ?: @{} mutableCopy]; + themes[[defaults stringForKey:@"GBCurrentTheme"]] = theme; + [defaults setObject:themes forKey:@"GBThemes"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorPaletteChanged" object:nil]; +} + ++ (const GB_palette_t *)userPalette +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + switch ([defaults integerForKey:@"GBColorPalette"]) { + case 1: return &GB_PALETTE_DMG; + case 2: return &GB_PALETTE_MGB; + case 3: return &GB_PALETTE_GBL; + default: return &GB_PALETTE_GREY; + case -1: { + static GB_palette_t customPalette; + NSArray *colors = [defaults dictionaryForKey:@"GBThemes"][[defaults stringForKey:@"GBCurrentTheme"]][@"Colors"]; + if (colors.count == 5) { + 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; + } + } +} + +- (IBAction)export:(id)sender +{ + NSSavePanel *savePanel = [NSSavePanel savePanel]; + [savePanel setAllowedFileTypes:@[@"sbp"]]; + savePanel.nameFieldStringValue = [NSString stringWithFormat:@"%@.sbp", [[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]]; + if ([savePanel runModal] == NSModalResponseOK) { + theme_t theme = {0,}; + theme.magic = MAGIC; + theme.manual = _manualModeCheckbox.state; + theme.disabled_lcd_color = _disableLCDColorCheckbox.state; + unsigned i = 0; + for (NSColorWell *well in self.colorWells) { + theme.colors[i++] = well.color.gbColor; + } + theme.brightness_bias = (_brightnessSlider.doubleValue - 128) * (0x40000000 / 128); + theme.hue_bias = round(_hueSlider.doubleValue * (0x80000000 / 360.0)); + theme.hue_bias_strength = (_hueStrengthSlider.doubleValue) * (0x80000000 / 256); + size_t size = sizeof(theme); + if (theme.manual) { + size = theme.disabled_lcd_color? 5 + 5 * sizeof(theme.colors[0]) : 5 + 4 * sizeof(theme.colors[0]); + } + [[NSData dataWithBytes:&theme length:size] writeToURL:savePanel.URL atomically:false]; + } +} + +- (IBAction)import:(id)sender +{ + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; + [openPanel setAllowedFileTypes:@[@"sbp"]]; + if ([openPanel runModal] == NSModalResponseOK) { + NSData *data = [NSData dataWithContentsOfURL:openPanel.URL]; + theme_t theme = {0,}; + memcpy(&theme, data.bytes, MIN(sizeof(theme), data.length)); + if (theme.magic != MAGIC) { + NSBeep(); + return; + } + _manualModeCheckbox.state = theme.manual; + _disableLCDColorCheckbox.state = theme.disabled_lcd_color; + unsigned i = 0; + for (NSColorWell *well in self.colorWells) { + well.color = [NSColor colorWithRed:theme.colors[i].r / 255.0 + green:theme.colors[i].g / 255.0 + blue:theme.colors[i].b / 255.0 + alpha:1.0]; + i++; + } + if (!theme.disabled_lcd_color) { + _colorWell4.color = _colorWell3.color; + } + _brightnessSlider.doubleValue = theme.brightness_bias / (0x40000000 / 128.0) + 128; + _hueSlider.doubleValue = theme.hue_bias / (0x80000000 / 360.0); + _hueStrengthSlider.doubleValue = theme.hue_bias_strength / (0x80000000 / 256.0); + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + NSString *baseName = openPanel.URL.lastPathComponent.stringByDeletingPathExtension; + NSString *newName = baseName; + i = 2; + while (themes[newName]) { + newName = [NSString stringWithFormat:@"%@ %d", baseName, i++]; + } + [defaults setObject:newName forKey:@"GBCurrentTheme"]; + [self savePalette:sender]; + [self.themesList reloadData]; + [self awakeFromNib]; + } +} + +- (IBAction)segmentAction:(NSSegmentedControl *)sender +{ + switch (sender.selectedSegment) { + case 0: [self addTheme:sender]; return; + case 1: [self deleteTheme:sender]; return; + case 3: { + NSSize buttonSize = _segmentControl.bounds.size; + [_segmentMenu popUpMenuPositioningItem:_segmentMenu.itemArray.lastObject + atLocation:NSMakePoint(buttonSize.width, + 0) + inView:sender]; + return; + } + } +} + +- (IBAction)restoreDefaultPalettes:(id)sender +{ + [(GBApp *)NSApp updateThemesDefault:true]; + [self awakeFromNib]; +} + +- (IBAction)done:(NSButton *)sender +{ + [sender.window.sheetParent endSheet:sender.window]; +} + ++ (instancetype)alloc +{ + static id singleton = nil; + if (singleton) return singleton; + return (singleton = [super allocWithZone:nil]); +} + ++ (instancetype)allocWithZone:(struct _NSZone *)zone +{ + return [self alloc]; +} + +@end diff --git a/bsnes/gb/Cocoa/GBPaletteView.h b/bsnes/gb/Cocoa/GBPaletteView.h new file mode 100644 index 00000000..d92cb5f1 --- /dev/null +++ b/bsnes/gb/Cocoa/GBPaletteView.h @@ -0,0 +1,6 @@ +#import +#import "Document.h" + +@interface GBPaletteView : NSView +- (void)reloadData:(Document *)document; +@end diff --git a/bsnes/gb/Cocoa/GBPaletteView.m b/bsnes/gb/Cocoa/GBPaletteView.m new file mode 100644 index 00000000..d123a2b4 --- /dev/null +++ b/bsnes/gb/Cocoa/GBPaletteView.m @@ -0,0 +1,93 @@ +#import "GBPaletteView.h" + +@interface GBPaletteViewItem : NSObject +@property IBOutlet NSView *view; +@property (strong) IBOutlet NSTextField *label; +@property (strong) IBOutlet NSTextField *color0; +@property (strong) IBOutlet NSTextField *color1; +@property (strong) IBOutlet NSTextField *color2; +@property (strong) IBOutlet NSTextField *color3; +@end + +@implementation GBPaletteViewItem +@end + +@implementation GBPaletteView +{ + NSMutableArray *_colors; +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + _colors = [NSMutableArray array]; + CGFloat height = self.frame.size.height; + for (unsigned i = 0; i < 16; i++) { + GBPaletteViewItem *item = [[GBPaletteViewItem alloc] init]; + [[NSBundle mainBundle] loadNibNamed:@"GBPaletteViewRow" owner:item topLevelObjects:nil]; + [self addSubview:item.view]; + [item.view setFrameOrigin:NSMakePoint(0, height - (i * 24) - 24)]; + item.label.stringValue = [NSString stringWithFormat:@"%@ %d", i < 8? @"Background" : @"Object", i % 8]; + item.view.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin; + [_colors addObject:item.color0]; + [_colors addObject:item.color1]; + [_colors addObject:item.color2]; + [_colors addObject:item.color3]; + + } + return self; +} + +- (void)reloadData:(Document *)document +{ + GB_gameboy_t *gb = document.gb; + uint8_t *bg = GB_get_direct_access(gb, GB_DIRECT_ACCESS_BGP, NULL, NULL); + uint8_t *obj = GB_get_direct_access(gb, GB_DIRECT_ACCESS_OBP, NULL, NULL); + NSFont *font = [document debuggerFontOfSize:13]; + + for (unsigned i = 0; i < 4 * 8 * 2; i++) { + uint8_t index = i % (4 * 8); + uint8_t *palette = i >= 4 * 8 ? obj : bg; + uint16_t color = (palette[(index << 1) + 1] << 8) | palette[(index << 1)]; + uint32_t nativeColor = GB_convert_rgb15(gb, color, false); + + uint8_t r = color & 0x1F, + g = (color >> 5) & 0x1F, + b = (color >> 10) & 0x1F; + + NSTextField *field = _colors[i]; + field.stringValue = [NSString stringWithFormat:@"$%04X", color]; + field.textColor = r * 3 + g * 4 + b * 2 > 120? [NSColor blackColor] : [NSColor whiteColor]; + field.toolTip = [NSString stringWithFormat:@"Red: %d, Green: %d, Blue: %d", r, g, b]; + field.font = font; + field.backgroundColor = [NSColor colorWithRed:(nativeColor & 0xFF) / 255.0 + green:((nativeColor >> 8) & 0xFF) / 255.0 + blue:((nativeColor >> 16) & 0xFF) / 255.0 + alpha:1.0]; + } +} + +- (void)drawRect:(NSRect)dirtyRect +{ + NSRect frame = self.frame; + if (@available(macOS 10.14, *)) { + [[NSColor alternatingContentBackgroundColors].lastObject setFill]; + } + else { + [[NSColor colorWithDeviceWhite:0.96 alpha:1] setFill]; + } + for (unsigned i = 1; i <= 8; i++) { + NSRectFill(NSMakeRect(0, frame.size.height - i * 24 * 2, frame.size.width, 24)); + } + + if (@available(macOS 10.14, *)) { + [[NSColor alternatingContentBackgroundColors].firstObject setFill]; + } + else { + [[NSColor controlBackgroundColor] setFill]; + } + for (unsigned i = 0; i < 8; i++) { + NSRectFill(NSMakeRect(0, frame.size.height - i * 24 * 2 - 24, frame.size.width, 24)); + } +} +@end diff --git a/bsnes/gb/Cocoa/GBPaletteViewRow.xib b/bsnes/gb/Cocoa/GBPaletteViewRow.xib new file mode 100644 index 00000000..38d2749a --- /dev/null +++ b/bsnes/gb/Cocoa/GBPaletteViewRow.xib @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/GBPanel.h b/bsnes/gb/Cocoa/GBPanel.h new file mode 100644 index 00000000..162ed69a --- /dev/null +++ b/bsnes/gb/Cocoa/GBPanel.h @@ -0,0 +1,5 @@ +#import + +@interface GBPanel : NSPanel +@property (weak) IBOutlet NSWindow *ownerWindow; +@end diff --git a/bsnes/gb/Cocoa/GBPanel.m b/bsnes/gb/Cocoa/GBPanel.m new file mode 100644 index 00000000..0683a085 --- /dev/null +++ b/bsnes/gb/Cocoa/GBPanel.m @@ -0,0 +1,11 @@ +#import "GBPanel.h" + +@implementation GBPanel +- (void)becomeKeyWindow +{ + if ([_ownerWindow canBecomeMainWindow]) { + [_ownerWindow makeMainWindow]; + } + [super becomeKeyWindow]; +} +@end diff --git a/bsnes/gb/Cocoa/GBPreferenceButton.h b/bsnes/gb/Cocoa/GBPreferenceButton.h new file mode 100644 index 00000000..0207f9b4 --- /dev/null +++ b/bsnes/gb/Cocoa/GBPreferenceButton.h @@ -0,0 +1,6 @@ +#import + +@interface GBPreferenceButton : NSButton +@property (nonatomic) IBInspectable NSString *preferenceName; +@property IBInspectable BOOL invertValue; +@end diff --git a/bsnes/gb/Cocoa/GBPreferenceButton.m b/bsnes/gb/Cocoa/GBPreferenceButton.m new file mode 100644 index 00000000..ef2182d4 --- /dev/null +++ b/bsnes/gb/Cocoa/GBPreferenceButton.m @@ -0,0 +1,30 @@ +#import "GBPreferenceButton.h" +#import "NSObject+DefaultsObserver.h" + +@implementation GBPreferenceButton + +- (BOOL)sendAction:(SEL)action to:(id)target +{ + [[NSUserDefaults standardUserDefaults] setBool:self.state ^ self.invertValue forKey:_preferenceName]; + return [super sendAction:action to:target]; +} + +- (void)updateValue +{ + if (!_preferenceName) return; + self.state = [[NSUserDefaults standardUserDefaults] boolForKey:_preferenceName] ^ self.invertValue; +} + +- (void)setPreferenceName:(NSString *)preferenceName +{ + _preferenceName = preferenceName; + [self observeStandardDefaultsKey:_preferenceName selector:@selector(updateValue)]; +} + +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + [self updateValue]; +} + +@end diff --git a/bsnes/gb/Cocoa/GBPreferencePopUpButton.h b/bsnes/gb/Cocoa/GBPreferencePopUpButton.h new file mode 100644 index 00000000..c9fcca8f --- /dev/null +++ b/bsnes/gb/Cocoa/GBPreferencePopUpButton.h @@ -0,0 +1,9 @@ +#import + +@interface GBPreferenceMenuItem : NSMenuItem +@property (nonatomic) IBInspectable NSString *preferenceValue; +@end + +@interface GBPreferencePopUpButton : NSPopUpButton +@property (nonatomic) IBInspectable NSString *preferenceName; +@end diff --git a/bsnes/gb/Cocoa/GBPreferencePopUpButton.m b/bsnes/gb/Cocoa/GBPreferencePopUpButton.m new file mode 100644 index 00000000..a76fdc31 --- /dev/null +++ b/bsnes/gb/Cocoa/GBPreferencePopUpButton.m @@ -0,0 +1,51 @@ +#import "GBPreferencePopUpButton.h" +#import "NSObject+DefaultsObserver.h" + +@implementation GBPreferenceMenuItem +@end + +@implementation GBPreferencePopUpButton + +- (BOOL)sendAction:(SEL)action to:(id)target +{ + GBPreferenceMenuItem *item = (GBPreferenceMenuItem *)self.selectedItem; + if ([item isKindOfClass:[GBPreferenceMenuItem class]]) { + [[NSUserDefaults standardUserDefaults] setObject:item.preferenceValue forKey:_preferenceName]; + } + else { + [[NSUserDefaults standardUserDefaults] setInteger:item.tag forKey:_preferenceName]; + } + return [super sendAction:action to:target]; +} + +- (void)updateValue +{ + if (!_preferenceName) return; + NSString *stringValue = [[NSUserDefaults standardUserDefaults] objectForKey:_preferenceName]; + if ([stringValue isKindOfClass:[NSString class]]) { + for (GBPreferenceMenuItem *item in self.menu.itemArray) { + if ([item isKindOfClass:[GBPreferenceMenuItem class]] && + [item.preferenceValue isEqualToString:stringValue]) { + [self selectItem:item]; + return; + } + } + } + else { + [self selectItemWithTag:[[NSUserDefaults standardUserDefaults] integerForKey:_preferenceName]]; + } +} + +- (void)setPreferenceName:(NSString *)preferenceName +{ + _preferenceName = preferenceName; + [self observeStandardDefaultsKey:_preferenceName selector:@selector(updateValue)]; +} + +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + [self updateValue]; +} + +@end diff --git a/bsnes/gb/Cocoa/GBPreferencesSlider.h b/bsnes/gb/Cocoa/GBPreferencesSlider.h new file mode 100644 index 00000000..731493af --- /dev/null +++ b/bsnes/gb/Cocoa/GBPreferencesSlider.h @@ -0,0 +1,6 @@ +#import + +@interface GBPreferencesSlider : NSSlider +@property (nonatomic) IBInspectable NSString *preferenceName; +@property IBInspectable unsigned denominator; +@end diff --git a/bsnes/gb/Cocoa/GBPreferencesSlider.m b/bsnes/gb/Cocoa/GBPreferencesSlider.m new file mode 100644 index 00000000..eaa8354c --- /dev/null +++ b/bsnes/gb/Cocoa/GBPreferencesSlider.m @@ -0,0 +1,29 @@ +#import "GBPreferencesSlider.h" +#import "NSObject+DefaultsObserver.h" + +@implementation GBPreferencesSlider + +- (BOOL)sendAction:(SEL)action to:(id)target +{ + [[NSUserDefaults standardUserDefaults] setDouble:self.doubleValue / (self.denominator ?: 1) forKey:_preferenceName]; + return [super sendAction:action to:target]; +} + +- (void)updateValue +{ + if (!_preferenceName) return; + self.doubleValue = [[NSUserDefaults standardUserDefaults] doubleForKey:_preferenceName] * (self.denominator ?: 1); +} + +- (void)setPreferenceName:(NSString *)preferenceName +{ + _preferenceName = preferenceName; + [self observeStandardDefaultsKey:_preferenceName selector:@selector(updateValue)]; +} + +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + [self updateValue]; +} +@end diff --git a/bsnes/gb/Cocoa/GBPreferencesWindow.h b/bsnes/gb/Cocoa/GBPreferencesWindow.h index e11c5d3c..f5a51fe4 100644 --- a/bsnes/gb/Cocoa/GBPreferencesWindow.h +++ b/bsnes/gb/Cocoa/GBPreferencesWindow.h @@ -1,32 +1,28 @@ #import #import +#import "GBPaletteEditorController.h" +#import "GBTitledPopUpButton.h" @interface GBPreferencesWindow : NSWindow -@property (nonatomic, strong) IBOutlet NSTableView *controlsTableView; -@property (nonatomic, strong) IBOutlet NSPopUpButton *graphicsFilterPopupButton; -@property (nonatomic, strong) IBOutlet NSButton *analogControlsCheckbox; -@property (nonatomic, strong) IBOutlet NSButton *aspectRatioCheckbox; -@property (nonatomic, strong) IBOutlet NSPopUpButton *highpassFilterPopupButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *frameBlendingModePopupButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *colorPalettePopupButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *displayBorderPopupButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *rewindPopupButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *rtcPopupButton; -@property (nonatomic, strong) IBOutlet NSButton *configureJoypadButton; -@property (nonatomic, strong) IBOutlet NSButton *skipButton; -@property (nonatomic, strong) IBOutlet NSMenuItem *bootROMsFolderItem; -@property (nonatomic, strong) IBOutlet NSPopUpButtonCell *bootROMsButton; -@property (nonatomic, strong) IBOutlet NSPopUpButton *rumbleModePopupButton; -@property (nonatomic, weak) IBOutlet NSSlider *temperatureSlider; -@property (nonatomic, weak) IBOutlet NSSlider *interferenceSlider; -@property (nonatomic, weak) IBOutlet NSPopUpButton *dmgPopupButton; -@property (nonatomic, weak) IBOutlet NSPopUpButton *sgbPopupButton; -@property (nonatomic, weak) IBOutlet NSPopUpButton *cgbPopupButton; -@property (nonatomic, weak) IBOutlet NSPopUpButton *preferredJoypadButton; -@property (nonatomic, weak) IBOutlet NSPopUpButton *playerListButton; -@property (nonatomic, weak) IBOutlet NSButton *autoUpdatesCheckbox; -@property (weak) IBOutlet NSSlider *volumeSlider; -@property (weak) IBOutlet NSButton *OSDCheckbox; -@property (weak) IBOutlet NSButton *screenshotFilterCheckbox; +@property IBOutlet NSTableView *controlsTableView; +@property IBOutlet NSButton *configureJoypadButton; +@property IBOutlet NSButton *skipButton; +@property IBOutlet NSMenuItem *bootROMsFolderItem; +@property IBOutlet NSPopUpButtonCell *bootROMsButton; +@property IBOutlet NSPopUpButton *preferredJoypadButton; +@property IBOutlet NSPopUpButton *playerListButton; +@property IBOutlet GBPaletteEditorController *paletteEditorController; +@property IBOutlet NSWindow *paletteEditor; +@property IBOutlet NSWindow *joyconsSheet; +@property IBOutlet NSPopUpButton *colorCorrectionPopupButton; +@property IBOutlet NSPopUpButton *highpassFilterPopupButton; +@property IBOutlet NSPopUpButton *colorPalettePopupButton; +@property IBOutlet NSPopUpButton *hotkey1PopupButton; +@property IBOutlet NSPopUpButton *hotkey2PopupButton; +@property IBOutlet NSButton *turboCapButton; +@property IBOutlet NSSlider *turboCapSlider; +@property IBOutlet NSTextField *turboCapLabel; + +@property IBOutlet GBTitledPopUpButton *fontPopupButton; +@property IBOutlet NSStepper *fontSizeStepper; @end diff --git a/bsnes/gb/Cocoa/GBPreferencesWindow.m b/bsnes/gb/Cocoa/GBPreferencesWindow.m index 60df2dde..ed0f3498 100644 --- a/bsnes/gb/Cocoa/GBPreferencesWindow.m +++ b/bsnes/gb/Cocoa/GBPreferencesWindow.m @@ -1,8 +1,10 @@ #import "GBPreferencesWindow.h" +#import "GBJoyConManager.h" #import "NSString+StringForKey.h" #import "GBButtons.h" #import "BigSurToolbar.h" #import "GBViewMetal.h" +#import "GBWarningPopover.h" #import @implementation GBPreferencesWindow @@ -13,52 +15,7 @@ NSString *joystick_being_configured; bool joypad_wait; - NSPopUpButton *_graphicsFilterPopupButton; - NSPopUpButton *_highpassFilterPopupButton; - NSPopUpButton *_colorCorrectionPopupButton; - NSPopUpButton *_frameBlendingModePopupButton; - NSPopUpButton *_colorPalettePopupButton; - NSPopUpButton *_displayBorderPopupButton; - NSPopUpButton *_rewindPopupButton; - NSPopUpButton *_rtcPopupButton; - NSButton *_aspectRatioCheckbox; - NSButton *_analogControlsCheckbox; NSEventModifierFlags previousModifiers; - - NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton; - NSPopUpButton *_preferredJoypadButton; - NSPopUpButton *_rumbleModePopupButton; - NSSlider *_temperatureSlider; - NSSlider *_interferenceSlider; - NSSlider *_volumeSlider; - NSButton *_autoUpdatesCheckbox; - NSButton *_OSDCheckbox; - NSButton *_screenshotFilterCheckbox; -} - -+ (NSArray *)filterList -{ - /* The filter list as ordered in the popup button */ - static NSArray * filters = nil; - if (!filters) { - filters = @[ - @"NearestNeighbor", - @"Bilinear", - @"SmoothBilinear", - @"MonoLCD", - @"LCD", - @"CRT", - @"Scale2x", - @"Scale4x", - @"AAScale2x", - @"AAScale4x", - @"HQ2x", - @"OmniScale", - @"OmniScaleLegacy", - @"AAOmniScaleLegacy", - ]; - } - return filters; } - (NSWindowToolbarStyle)toolbarStyle @@ -75,160 +32,24 @@ [super close]; } -- (NSPopUpButton *)graphicsFilterPopupButton +static inline NSString *keyEquivalentString(NSMenuItem *item) { - return _graphicsFilterPopupButton; -} - -- (void)setGraphicsFilterPopupButton:(NSPopUpButton *)graphicsFilterPopupButton -{ - _graphicsFilterPopupButton = graphicsFilterPopupButton; - NSString *filter = [[NSUserDefaults standardUserDefaults] objectForKey:@"GBFilter"]; - [_graphicsFilterPopupButton selectItemAtIndex:[[[self class] filterList] indexOfObject:filter]]; -} - -- (NSPopUpButton *)highpassFilterPopupButton -{ - return _highpassFilterPopupButton; -} - -- (void)setColorCorrectionPopupButton:(NSPopUpButton *)colorCorrectionPopupButton -{ - _colorCorrectionPopupButton = colorCorrectionPopupButton; - NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]; - [_colorCorrectionPopupButton selectItemAtIndex:mode]; -} - - -- (NSPopUpButton *)colorCorrectionPopupButton -{ - return _colorCorrectionPopupButton; -} - -- (void)setTemperatureSlider:(NSSlider *)temperatureSlider -{ - _temperatureSlider = temperatureSlider; - [temperatureSlider setDoubleValue:[[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"] * 256]; -} - -- (NSSlider *)temperatureSlider -{ - return _temperatureSlider; -} - -- (void)setInterferenceSlider:(NSSlider *)interferenceSlider -{ - _interferenceSlider = interferenceSlider; - [interferenceSlider setDoubleValue:[[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"] * 256]; -} - -- (NSSlider *)interferenceSlider -{ - return _interferenceSlider; -} - -- (void)setVolumeSlider:(NSSlider *)volumeSlider -{ - _volumeSlider = volumeSlider; - [volumeSlider setDoubleValue:[[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"] * 256]; -} - -- (NSSlider *)volumeSlider -{ - return _volumeSlider; -} - -- (void)setFrameBlendingModePopupButton:(NSPopUpButton *)frameBlendingModePopupButton -{ - _frameBlendingModePopupButton = frameBlendingModePopupButton; - NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; - [_frameBlendingModePopupButton selectItemAtIndex:mode]; -} - -- (NSPopUpButton *)frameBlendingModePopupButton -{ - return _frameBlendingModePopupButton; -} - -- (void)setColorPalettePopupButton:(NSPopUpButton *)colorPalettePopupButton -{ - _colorPalettePopupButton = colorPalettePopupButton; - NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorPalette"]; - [_colorPalettePopupButton selectItemAtIndex:mode]; -} - -- (NSPopUpButton *)colorPalettePopupButton -{ - return _colorPalettePopupButton; -} - -- (void)setDisplayBorderPopupButton:(NSPopUpButton *)displayBorderPopupButton -{ - _displayBorderPopupButton = displayBorderPopupButton; - NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]; - [_displayBorderPopupButton selectItemWithTag:mode]; -} - -- (NSPopUpButton *)displayBorderPopupButton -{ - return _displayBorderPopupButton; -} - -- (void)setRumbleModePopupButton:(NSPopUpButton *)rumbleModePopupButton -{ - _rumbleModePopupButton = rumbleModePopupButton; - NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRumbleMode"]; - [_rumbleModePopupButton selectItemWithTag:mode]; -} - -- (NSPopUpButton *)rumbleModePopupButton -{ - return _rumbleModePopupButton; -} - -- (void)setRewindPopupButton:(NSPopUpButton *)rewindPopupButton -{ - _rewindPopupButton = rewindPopupButton; - NSInteger length = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]; - [_rewindPopupButton selectItemWithTag:length]; -} - -- (NSPopUpButton *)rewindPopupButton -{ - return _rewindPopupButton; -} - -- (NSPopUpButton *)rtcPopupButton -{ - return _rtcPopupButton; -} - -- (void)setRtcPopupButton:(NSPopUpButton *)rtcPopupButton -{ - _rtcPopupButton = rtcPopupButton; - NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]; - [_rtcPopupButton selectItemAtIndex:mode]; -} - -- (void)setHighpassFilterPopupButton:(NSPopUpButton *)highpassFilterPopupButton -{ - _highpassFilterPopupButton = highpassFilterPopupButton; - [_highpassFilterPopupButton selectItemAtIndex:[[[NSUserDefaults standardUserDefaults] objectForKey:@"GBHighpassFilter"] unsignedIntegerValue]]; + return [NSString stringWithFormat:@"%s%@", (item.keyEquivalentModifierMask & NSEventModifierFlagShift)? "^":"", item.keyEquivalent]; } - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { if (self.playerListButton.selectedTag == 0) { - return GBButtonCount; + return GBKeyboardButtonCount; } - return GBGameBoyButtonCount; + return GBPerPlayerButtonCount; } - (unsigned) usesForKey:(unsigned) key { unsigned ret = 0; for (unsigned player = 4; player--;) { - for (unsigned button = player == 0? GBButtonCount:GBGameBoyButtonCount; button--;) { + for (unsigned button = player == 0? GBKeyboardButtonCount:GBPerPlayerButtonCount; button--;) { NSNumber *other = [[NSUserDefaults standardUserDefaults] valueForKey:button_to_preference_name(button, player)]; if (other && [other unsignedIntValue] == key) { ret++; @@ -245,7 +66,7 @@ } if (is_button_being_modified && button_being_modified == row) { - return @"Select a new key..."; + return @"Select a new key…"; } NSNumber *key = [[NSUserDefaults standardUserDefaults] valueForKey:button_to_preference_name(row, self.playerListButton.selectedTag)]; @@ -304,109 +125,66 @@ previousModifiers = event.modifierFlags; } -- (IBAction)graphicFilterChanged:(NSPopUpButton *)sender +- (void)updatePalettesMenu { - [[NSUserDefaults standardUserDefaults] setObject:[[self class] filterList][[sender indexOfSelectedItem]] - forKey:@"GBFilter"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil]; -} - -- (IBAction)highpassFilterChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) - forKey:@"GBHighpassFilter"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBHighpassFilterChanged" object:nil]; -} - -- (IBAction)changeAnalogControls:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState - forKey:@"GBAnalogControls"]; -} - -- (IBAction)changeAspectRatio:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] != NSOnState - forKey:@"GBAspectRatioUnkept"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBAspectChanged" object:nil]; -} - -- (IBAction)colorCorrectionChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) - forKey:@"GBColorCorrection"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorCorrectionChanged" object:nil]; -} - -- (IBAction)lightTemperatureChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender doubleValue] / 256.0) - forKey:@"GBLightTemperature"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBLightTemperatureChanged" object:nil]; -} - -- (IBAction)interferenceVolumeChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender doubleValue] / 256.0) - forKey:@"GBInterferenceVolume"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBInterferenceVolumeChanged" object:nil]; -} - -- (IBAction)volumeChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender doubleValue] / 256.0) - forKey:@"GBVolume"]; -} - -- (IBAction)franeBlendingModeChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) - forKey:@"GBFrameBlendingMode"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFrameBlendingModeChanged" object:nil]; - + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themes = [defaults dictionaryForKey:@"GBThemes"]; + NSMenu *menu = _colorPalettePopupButton.menu; + while (menu.itemArray.count != 4) { + [menu removeItemAtIndex:4]; + } + [menu addItem:[NSMenuItem separatorItem]]; + for (NSString *name in [themes.allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]) { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:name action:nil keyEquivalent:@""]; + item.tag = -2; + [menu addItem:item]; + } + if (themes) { + [menu addItem:[NSMenuItem separatorItem]]; + } + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Custom…" action:nil keyEquivalent:@""]; + item.tag = -1; + [menu addItem:item]; } - (IBAction)colorPaletteChanged:(id)sender { - [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) - forKey:@"GBColorPalette"]; + signed tag = [sender selectedItem].tag; + if (tag == -2) { + [[NSUserDefaults standardUserDefaults] setObject:@(-1) + forKey:@"GBColorPalette"]; + [[NSUserDefaults standardUserDefaults] setObject:[sender selectedItem].title + forKey:@"GBCurrentTheme"]; + + } + else if (tag == -1) { + [[NSUserDefaults standardUserDefaults] setObject:@(-1) + forKey:@"GBColorPalette"]; + [_paletteEditorController awakeFromNib]; + [self beginSheet:_paletteEditor completionHandler:^(NSModalResponse returnCode) { + [self updatePalettesMenu]; + [_colorPalettePopupButton selectItemWithTitle:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] ?: @""]; + }]; + } + else { + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedItem].tag) + forKey:@"GBColorPalette"]; + } [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorPaletteChanged" object:nil]; } -- (IBAction)displayBorderChanged:(id)sender +- (IBAction)hotkey1Changed:(id)sender { - [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedItem].tag) - forKey:@"GBBorderMode"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBBorderModeChanged" object:nil]; + [[NSUserDefaults standardUserDefaults] setObject:keyEquivalentString([sender selectedItem]) + forKey:@"GBJoypadHotkey1"]; } -- (IBAction)rumbleModeChanged:(id)sender +- (IBAction)hotkey2Changed:(id)sender { - [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedItem].tag) - forKey:@"GBRumbleMode"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBRumbleModeChanged" object:nil]; + [[NSUserDefaults standardUserDefaults] setObject:keyEquivalentString([sender selectedItem]) + forKey:@"GBJoypadHotkey2"]; } -- (IBAction)rewindLengthChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) - forKey:@"GBRewindLength"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBRewindLengthChanged" object:nil]; -} - -- (IBAction)rtcModeChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) - forKey:@"GBRTCMode"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBRTCModeChanged" object:nil]; - -} - -- (IBAction)changeAutoUpdates:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState - forKey:@"GBAutoUpdatesEnabled"]; -} - (IBAction) configureJoypad:(id)sender { @@ -427,7 +205,7 @@ if (joystick_configuration_state == GBUnderclock) { [self.configureJoypadButton setTitle:@"Press Button for Slo-Mo"]; // Full name is too long :< } - else if (joystick_configuration_state < GBButtonCount) { + else if (joystick_configuration_state < GBTotalButtonCount) { [self.configureJoypadButton setTitle:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]]; } else { @@ -449,7 +227,7 @@ if (!button.isPressed) return; if (joystick_configuration_state == -1) return; - if (joystick_configuration_state == GBButtonCount) return; + if (joystick_configuration_state == GBTotalButtonCount) return; if (!joystick_being_configured) { joystick_being_configured = controller.uniqueID; } @@ -488,9 +266,13 @@ [GBB] = JOYButtonUsageB, [GBSelect] = JOYButtonUsageSelect, [GBStart] = JOYButtonUsageStart, + [GBRapidA] = GBJoyKitRapidA, + [GBRapidB] = GBJoyKitRapidB, [GBTurbo] = JOYButtonUsageL1, [GBRewind] = JOYButtonUsageL2, [GBUnderclock] = JOYButtonUsageR1, + [GBHotkey1] = GBJoyKitHotkey1, + [GBHotkey2] = GBJoyKitHotkey2, }; if (joystick_configuration_state == GBUnderclock) { @@ -498,7 +280,6 @@ double max = 0; for (JOYAxis *axis in controller.axes) { if ((axis.value > 0.5 || (axis.equivalentButtonUsage == button.usage)) && axis.value >= max) { - max = axis.value; mapping[@"AnalogUnderclock"] = @(axis.uniqueID); break; } @@ -525,28 +306,6 @@ [self advanceConfigurationStateMachine]; } -- (NSButton *)analogControlsCheckbox -{ - return _analogControlsCheckbox; -} - -- (void)setAnalogControlsCheckbox:(NSButton *)analogControlsCheckbox -{ - _analogControlsCheckbox = analogControlsCheckbox; - [_analogControlsCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]]; -} - -- (NSButton *)aspectRatioCheckbox -{ - return _aspectRatioCheckbox; -} - -- (void)setAspectRatioCheckbox:(NSButton *)aspectRatioCheckbox -{ - _aspectRatioCheckbox = aspectRatioCheckbox; - [_aspectRatioCheckbox setState: ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAspectRatioUnkept"]]; -} - - (void)awakeFromNib { [super awakeFromNib]; @@ -554,6 +313,96 @@ [[NSDistributedNotificationCenter defaultCenter] addObserver:self.controlsTableView selector:@selector(reloadData) name:(NSString*)kTISNotifySelectedKeyboardInputSourceChanged object:nil]; [JOYController registerListener:self]; joystick_configuration_state = -1; + [self refreshJoypadMenu:nil]; + + NSString *keyEquivalent = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBJoypadHotkey1"]; + for (NSMenuItem *item in _hotkey1PopupButton.menu.itemArray) { + if ([keyEquivalent isEqualToString:keyEquivalentString(item)]) { + [_hotkey1PopupButton selectItem:item]; + break; + } + } + + keyEquivalent = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBJoypadHotkey2"]; + for (NSMenuItem *item in _hotkey2PopupButton.menu.itemArray) { + if ([keyEquivalent isEqualToString:keyEquivalentString(item)]) { + [_hotkey2PopupButton selectItem:item]; + break; + } + } + + [self updatePalettesMenu]; + NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorPalette"]; + if (mode >= 0) { + [_colorPalettePopupButton selectItemWithTag:mode]; + } + else { + [_colorPalettePopupButton selectItemWithTitle:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] ?: @""]; + } + + _fontSizeStepper.intValue = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"]; + [self updateFonts]; + + double cap = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]; + if (cap) { + _turboCapSlider.intValue = round(cap * 100); + _turboCapButton.state = NSOnState; + } + [self turboCapToggled:_turboCapButton]; +} + +- (IBAction)fontSizeChanged:(id)sender +{ + NSString *selectedFont = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBDebuggerFont"]; + [[NSUserDefaults standardUserDefaults] setInteger:[sender intValue] forKey:@"GBDebuggerFontSize"]; + [_fontPopupButton setDisplayTitle:[NSString stringWithFormat:@"%@ %upt", selectedFont, (unsigned)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"]]]; +} + +- (IBAction)fontChanged:(id)sender +{ + NSString *selectedFont = _fontPopupButton.selectedItem.title; + [[NSUserDefaults standardUserDefaults] setObject:selectedFont forKey:@"GBDebuggerFont"]; + [_fontPopupButton setDisplayTitle:[NSString stringWithFormat:@"%@ %upt", selectedFont, (unsigned)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"]]]; + +} + +- (void)updateFonts +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSFontManager *fontManager = [NSFontManager sharedFontManager]; + NSArray *allFamilies = [fontManager availableFontFamilies]; + NSMutableSet *families = [NSMutableSet set]; + for (NSString *family in allFamilies) { + if ([fontManager fontNamed:family hasTraits:NSFixedPitchFontMask]) { + [families addObject:family]; + } + } + + bool hasSFMono = false; + if (@available(macOS 10.15, *)) { + hasSFMono = [[NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular].displayName containsString:@"SF"]; + } + + if (hasSFMono) { + [families addObject:@"SF Mono"]; + } + + NSArray *sortedFamilies = [[families allObjects] sortedArrayUsingSelector:@selector(compare:)]; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (![families containsObject:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBDebuggerFont"]]) { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBDebuggerFont"]; + } + + [_fontPopupButton.menu removeAllItems]; + for (NSString *family in sortedFamilies) { + [_fontPopupButton addItemWithTitle:family]; + } + NSString *selectedFont = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBDebuggerFont"]; + [_fontPopupButton selectItemWithTitle:selectedFont]; + [_fontPopupButton setDisplayTitle:[NSString stringWithFormat:@"%@ %upt", selectedFont, (unsigned)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"]]]; + }); + }); } - (void)dealloc @@ -606,77 +455,6 @@ [self updateBootROMFolderButton]; } -- (void)setDmgPopupButton:(NSPopUpButton *)dmgPopupButton -{ - _dmgPopupButton = dmgPopupButton; - [_dmgPopupButton selectItemWithTag:[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"]]; -} - -- (NSPopUpButton *)dmgPopupButton -{ - return _dmgPopupButton; -} - -- (void)setSgbPopupButton:(NSPopUpButton *)sgbPopupButton -{ - _sgbPopupButton = sgbPopupButton; - [_sgbPopupButton selectItemWithTag:[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]]; -} - -- (NSPopUpButton *)sgbPopupButton -{ - return _sgbPopupButton; -} - -- (void)setCgbPopupButton:(NSPopUpButton *)cgbPopupButton -{ - _cgbPopupButton = cgbPopupButton; - [_cgbPopupButton selectItemWithTag:[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]]; -} - -- (NSPopUpButton *)cgbPopupButton -{ - return _cgbPopupButton; -} - -- (IBAction)dmgModelChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) - forKey:@"GBDMGModel"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBDMGModelChanged" object:nil]; - -} - -- (IBAction)sgbModelChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) - forKey:@"GBSGBModel"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBSGBModelChanged" object:nil]; -} - -- (IBAction)cgbModelChanged:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) - forKey:@"GBCGBModel"]; - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBCGBModelChanged" object:nil]; -} - -- (IBAction)reloadButtonsData:(id)sender -{ - [self.controlsTableView reloadData]; -} - -- (void)setPreferredJoypadButton:(NSPopUpButton *)preferredJoypadButton -{ - _preferredJoypadButton = preferredJoypadButton; - [self refreshJoypadMenu:nil]; -} - -- (NSPopUpButton *)preferredJoypadButton -{ - return _preferredJoypadButton; -} - - (void)controllerConnected:(JOYController *)controller { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ @@ -740,55 +518,79 @@ [[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"JoyKitDefaultControllers"]; } -- (NSButton *)autoUpdatesCheckbox +- (IBAction)displayColorCorrectionHelp:(id)sender { - return _autoUpdatesCheckbox; + [GBWarningPopover popoverWithContents: + 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 contrast of the original display.", + [GB_COLOR_CORRECTION_MODERN_ACCURATE] = @"Emulates a modern display. Colors have their hues and brightness corrected.", + }) [self.colorCorrectionPopupButton.selectedItem.tag] + title:self.colorCorrectionPopupButton.selectedItem.title + onView:sender + timeout:6 + preferredEdge:NSRectEdgeMaxX]; } -- (void)setAutoUpdatesCheckbox:(NSButton *)autoUpdatesCheckbox +- (IBAction)displayHighPassHelp:(id)sender { - _autoUpdatesCheckbox = autoUpdatesCheckbox; - [_autoUpdatesCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]]; + [GBWarningPopover popoverWithContents: + 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.", + }) [self.highpassFilterPopupButton.selectedItem.tag] + title:self.highpassFilterPopupButton.selectedItem.title + onView:sender + timeout:6 + preferredEdge:NSRectEdgeMaxX]; } -- (NSButton *)OSDCheckbox +- (IBAction)arrangeJoyCons:(id)sender { - return _OSDCheckbox; + [GBJoyConManager sharedInstance].arrangementMode = true; + [self beginSheet:self.joyconsSheet completionHandler:nil]; } -- (void)setOSDCheckbox:(NSButton *)OSDCheckbox +- (IBAction)closeJoyConsSheet:(id)sender { - _OSDCheckbox = OSDCheckbox; - [_OSDCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBOSDEnabled"]]; + [self endSheet:self.joyconsSheet]; + [GBJoyConManager sharedInstance].arrangementMode = false; } -- (IBAction)changeOSDEnabled:(id)sender +- (IBAction)turboCapToggled:(NSButton *)sender { - [[NSUserDefaults standardUserDefaults] setBool:[(NSButton *)sender state] == NSOnState - forKey:@"GBOSDEnabled"]; - -} - -- (IBAction)changeFilterScreenshots:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setBool:[(NSButton *)sender state] == NSOnState - forKey:@"GBFilterScreenshots"]; -} - -- (NSButton *)screenshotFilterCheckbox -{ - return _screenshotFilterCheckbox; -} - -- (void)setScreenshotFilterCheckbox:(NSButton *)screenshotFilterCheckbox -{ - _screenshotFilterCheckbox = screenshotFilterCheckbox; - if (![GBViewMetal isSupported]) { - [_screenshotFilterCheckbox setEnabled:false]; + if (sender.state) { + _turboCapSlider.enabled = true; + [self turboCapChanged:_turboCapSlider]; + if (@available(macOS 10.10, *)) { + _turboCapLabel.textColor = [NSColor labelColor]; + } + else { + _turboCapLabel.textColor = [NSColor blackColor]; + } } else { - [_screenshotFilterCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]]; + _turboCapSlider.enabled = false; + _turboCapLabel.enabled = false; + [[NSUserDefaults standardUserDefaults] setDouble:0 forKey:@"GBTurboCap"]; + if (@available(macOS 10.10, *)) { + _turboCapLabel.textColor = [NSColor disabledControlTextColor]; + } + else { + _turboCapLabel.textColor = [NSColor colorWithWhite:0 alpha:0.25]; + } } } +- (IBAction)turboCapChanged:(NSSlider *)sender +{ + _turboCapLabel.stringValue = [NSString stringWithFormat:@"%d%%", sender.intValue]; + [[NSUserDefaults standardUserDefaults] setDouble:sender.doubleValue / 100.0 forKey:@"GBTurboCap"]; +} + @end diff --git a/bsnes/gb/Cocoa/GBS.xib b/bsnes/gb/Cocoa/GBS.xib old mode 100644 new mode 100755 index 534ff559..65bd44f8 --- a/bsnes/gb/Cocoa/GBS.xib +++ b/bsnes/gb/Cocoa/GBS.xib @@ -44,7 +44,7 @@ - - - - - - - - - - - - - - + - - + + @@ -114,6 +91,29 @@ + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/GBS11.xib b/bsnes/gb/Cocoa/GBS11.xib new file mode 100755 index 00000000..5c3c86f5 --- /dev/null +++ b/bsnes/gb/Cocoa/GBS11.xib @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/GBSplitView.m b/bsnes/gb/Cocoa/GBSplitView.m index d24d5806..ca51068d 100644 --- a/bsnes/gb/Cocoa/GBSplitView.m +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h index b7603360..b1a3171b 100644 --- a/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h +++ b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h @@ -1,5 +1,5 @@ #import -#include +#import @interface GBTerminalTextFieldCell : NSTextFieldCell @property (nonatomic) GB_gameboy_t *gb; diff --git a/bsnes/gb/Cocoa/GBTerminalTextFieldCell.m b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.m index e1ba9577..5000e460 100644 --- a/bsnes/gb/Cocoa/GBTerminalTextFieldCell.m +++ b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.m @@ -1,25 +1,33 @@ #import #import "GBTerminalTextFieldCell.h" +#import "NSTextFieldCell+Inset.h" @interface GBTerminalTextView : NSTextView +{ + @public __weak NSTextField *_field; +} @property GB_gameboy_t *gb; @end @implementation GBTerminalTextFieldCell { - GBTerminalTextView *field_editor; + GBTerminalTextView *_fieldEditor; } -- (NSTextView *)fieldEditorForView:(NSView *)controlView +- (NSTextView *)fieldEditorForView:(NSTextField *)controlView { - if (field_editor) { - field_editor.gb = self.gb; - return field_editor; + if (_fieldEditor) { + _fieldEditor.gb = self.gb; + return _fieldEditor; } - field_editor = [[GBTerminalTextView alloc] init]; - [field_editor setFieldEditor:true]; - field_editor.gb = self.gb; - return field_editor; + _fieldEditor = [[GBTerminalTextView alloc] init]; + [_fieldEditor setFieldEditor:true]; + _fieldEditor.gb = self.gb; + _fieldEditor->_field = (NSTextField *)controlView; + ((NSTextFieldCell *)controlView.cell).textInset = + _fieldEditor.textContainerInset = + NSMakeSize(7, 2); + return _fieldEditor; } @end @@ -37,7 +45,7 @@ { self = [super init]; if (!self) { - return NULL; + return nil; } lines = [[NSMutableOrderedSet alloc] init]; return self; @@ -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/bsnes/gb/Cocoa/GBTintedImageCell.h b/bsnes/gb/Cocoa/GBTintedImageCell.h new file mode 100644 index 00000000..eb6c8b33 --- /dev/null +++ b/bsnes/gb/Cocoa/GBTintedImageCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBTintedImageCell : NSImageCell +@property NSColor *tint; +@end diff --git a/bsnes/gb/Cocoa/GBTintedImageCell.m b/bsnes/gb/Cocoa/GBTintedImageCell.m new file mode 100644 index 00000000..af4faa66 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBTitledPopUpButton.h b/bsnes/gb/Cocoa/GBTitledPopUpButton.h new file mode 100644 index 00000000..2e8cc469 --- /dev/null +++ b/bsnes/gb/Cocoa/GBTitledPopUpButton.h @@ -0,0 +1,5 @@ +#import + +@interface GBTitledPopUpButton : NSPopUpButton +@property NSString *displayTitle; +@end diff --git a/bsnes/gb/Cocoa/GBTitledPopUpButton.m b/bsnes/gb/Cocoa/GBTitledPopUpButton.m new file mode 100644 index 00000000..29b7b591 --- /dev/null +++ b/bsnes/gb/Cocoa/GBTitledPopUpButton.m @@ -0,0 +1,22 @@ +#import "GBTitledPopUpButton.h" + +@implementation GBTitledPopUpButton + +- (void)setDisplayTitle:(NSString *)displayTitle +{ + if (!displayTitle) { + ((NSPopUpButtonCell *)self.cell).usesItemFromMenu = true; + ((NSPopUpButtonCell *)self.cell).menuItem = nil; + return; + } + ((NSPopUpButtonCell *)self.cell).usesItemFromMenu = false; + ((NSPopUpButtonCell *)self.cell).menuItem = [[NSMenuItem alloc] initWithTitle:displayTitle action:nil keyEquivalent:@""]; +} + + +- (NSString *)displayTitle +{ + return ((NSPopUpButtonCell *)self.cell).menuItem.title; +} + +@end diff --git a/bsnes/gb/Cocoa/GBToolbarFieldCell.h b/bsnes/gb/Cocoa/GBToolbarFieldCell.h new file mode 100644 index 00000000..e33398a1 --- /dev/null +++ b/bsnes/gb/Cocoa/GBToolbarFieldCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBToolbarFieldCell : NSSearchFieldCell + +@end diff --git a/bsnes/gb/Cocoa/GBToolbarFieldCell.m b/bsnes/gb/Cocoa/GBToolbarFieldCell.m new file mode 100644 index 00000000..2877728f --- /dev/null +++ b/bsnes/gb/Cocoa/GBToolbarFieldCell.m @@ -0,0 +1,40 @@ +#import "GBToolbarFieldCell.h" +#import + +@interface NSTextFieldCell() +- (void)textDidChange:(id)sender; +@end + +@implementation GBToolbarFieldCell + +- (void)textDidChange:(id)sender +{ + IMP imp = [NSTextFieldCell instanceMethodForSelector:_cmd]; + method_setImplementation(class_getInstanceMethod([GBToolbarFieldCell class], _cmd), imp); + ((void(*)(id, SEL, id))imp)(self, _cmd, sender); +} + +- (void)endEditing:(NSText *)textObj +{ + IMP imp = [NSTextFieldCell instanceMethodForSelector:_cmd]; + method_setImplementation(class_getInstanceMethod([GBToolbarFieldCell class], _cmd), imp); + ((void(*)(id, SEL, id))imp)(self, _cmd, textObj); +} + +- (void)resetSearchButtonCell +{ +} + +- (void)resetCancelButtonCell +{ +} + +// We only need this hack on Solarium, avoid regressions ++ (instancetype)allocWithZone:(struct _NSZone *)zone +{ + if (@available(macOS 26.0, *)) { + return [super allocWithZone:zone]; + } + return (id)[NSTextFieldCell allocWithZone:zone]; +} +@end diff --git a/bsnes/gb/Cocoa/GBToolbarPopUpButtonCell.h b/bsnes/gb/Cocoa/GBToolbarPopUpButtonCell.h new file mode 100644 index 00000000..3eb70175 --- /dev/null +++ b/bsnes/gb/Cocoa/GBToolbarPopUpButtonCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBToolbarPopUpButtonCell : NSPopUpButtonCell + +@end diff --git a/bsnes/gb/Cocoa/GBToolbarPopUpButtonCell.m b/bsnes/gb/Cocoa/GBToolbarPopUpButtonCell.m new file mode 100644 index 00000000..6de2748b --- /dev/null +++ b/bsnes/gb/Cocoa/GBToolbarPopUpButtonCell.m @@ -0,0 +1,17 @@ +#import "GBToolbarPopUpButtonCell.h" + +@implementation GBToolbarPopUpButtonCell + +-(void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView +{ + [super drawInteriorWithFrame:CGRectInset(cellFrame, 5, 0) inView:controlView]; +} + ++ (instancetype)allocWithZone:(struct _NSZone *)zone +{ + if (@available(macOS 26.0, *)) { + return [super allocWithZone:zone]; + } + return (id)[NSPopUpButtonCell allocWithZone:zone]; +} +@end diff --git a/bsnes/gb/Cocoa/GBView.h b/bsnes/gb/Cocoa/GBView.h index 01481a7d..eff3268c 100644 --- a/bsnes/gb/Cocoa/GBView.h +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBView.m b/bsnes/gb/Cocoa/GBView.m index 6c92c3f0..adefc2b9 100644 --- a/bsnes/gb/Cocoa/GBView.m +++ b/bsnes/gb/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,8 +115,13 @@ 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; + + bool _rapidA[4], _rapidB[4]; + uint8_t _rapidACount[4], _rapidBCount[4]; } + (instancetype)alloc @@ -136,18 +140,22 @@ 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:@"GBForceIntegerScale" 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 + options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseMoved owner:self userInfo:nil]; [self addTrackingArea:tracking_area]; @@ -156,57 +164,98 @@ static const uint8_t workboy_vk_to_key[] = { [self addSubview:self.internalView]; 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]; @@ -215,6 +264,7 @@ static const uint8_t workboy_vk_to_key[] = { [self setRumble:0]; [JOYController unregisterListener:self]; } + - (instancetype)initWithCoder:(NSCoder *)coder { if (!(self = [super initWithCoder:coder])) { @@ -235,7 +285,9 @@ static const uint8_t workboy_vk_to_key[] = { - (void)setFrame:(NSRect)frame { - frame = self.superview.frame; + NSView *superview = self.superview; + if (GB_unlikely(!superview)) return; + frame = superview.frame; if (_gb && ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAspectRatioUnkept"]) { double ratio = frame.size.width / frame.size.height; double width = GB_get_screen_width(_gb); @@ -253,6 +305,19 @@ static const uint8_t workboy_vk_to_key[] = { frame.origin.x = 0; } } + + if (_gb && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBForceIntegerScale"]) { + double factor = self.window.backingScaleFactor; + double width = GB_get_screen_width(_gb) / factor; + double height = GB_get_screen_height(_gb) / factor; + + double new_width = floor(frame.size.width / width) * width; + double new_height = floor(frame.size.height / height) * height; + frame.origin.x += floor((frame.size.width - new_width) / 2); + frame.origin.y += floor((frame.size.height - new_height) / 2); + frame.size.width = new_width; + frame.size.height = new_height; + } [super setFrame:frame]; } @@ -293,18 +358,24 @@ 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]; + + for (unsigned i = GB_get_player_count(_gb); i--;) { + if (_rapidA[i]) { + _rapidACount[i]++; + GB_set_key_state_for_player(_gb, GB_KEY_A, i, !(_rapidACount[i] & 2)); + } + if (_rapidB[i]) { + _rapidBCount[i]++; + GB_set_key_state_for_player(_gb, GB_KEY_B, i, !(_rapidBCount[i] & 2)); + } + } + [super flip]; } -(void)keyDown:(NSEvent *)theEvent @@ -331,7 +402,7 @@ static const uint8_t workboy_vk_to_key[] = { player_count = 2; } for (unsigned player = 0; player < player_count; player++) { - for (GBButton button = 0; button < GBButtonCount; button++) { + for (GBButton button = 0; button < GBKeyboardButtonCount; button++) { NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)]; if (!key) continue; @@ -362,17 +433,38 @@ static const uint8_t workboy_vk_to_key[] = { analogClockMultiplierValid = false; break; + case GBRapidA: + _rapidA[player] = true; + _rapidACount[player] = 0; + GB_set_key_state_for_player(_gb, GB_KEY_A, player, true); + break; + + case GBRapidB: + _rapidB[player] = true; + _rapidBCount[player] = 0; + GB_set_key_state_for_player(_gb, GB_KEY_B, player, true); + break; + default: if (self.document.partner) { if (player == 0) { GB_set_key_state_for_player(_gb, (GB_key_t)button, 0, true); + if ((GB_key_t)button <= GB_KEY_DOWN) { + GB_set_use_faux_analog_inputs(_gb, 0, false); + } } else { GB_set_key_state_for_player(self.document.partner.gb, (GB_key_t)button, 0, true); + if ((GB_key_t)button <= GB_KEY_DOWN) { + GB_set_use_faux_analog_inputs(self.document.partner.gb, 0, false); + } } } else { GB_set_key_state_for_player(_gb, (GB_key_t)button, player, true); + if ((GB_key_t)button <= GB_KEY_DOWN) { + GB_set_use_faux_analog_inputs(_gb, player, false); + } } break; } @@ -405,7 +497,7 @@ static const uint8_t workboy_vk_to_key[] = { player_count = 2; } for (unsigned player = 0; player < player_count; player++) { - for (GBButton button = 0; button < GBButtonCount; button++) { + for (GBButton button = 0; button < GBKeyboardButtonCount; button++) { NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)]; if (!key) continue; @@ -431,6 +523,16 @@ static const uint8_t workboy_vk_to_key[] = { underclockKeyDown = false; analogClockMultiplierValid = false; break; + + case GBRapidA: + _rapidA[player] = false; + GB_set_key_state_for_player(_gb, GB_KEY_A, player, false); + break; + + case GBRapidB: + _rapidB[player] = false; + GB_set_key_state_for_player(_gb, GB_KEY_B, player, false); + break; default: if (self.document.partner) { @@ -459,9 +561,34 @@ static const uint8_t workboy_vk_to_key[] = { [lastController setRumbleAmplitude:amp]; } +- (bool)shouldControllerUseJoystickForMotion:(JOYController *)controller +{ + if (!_gb) return false; + if (!GB_has_accelerometer(_gb)) return false; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7JoystickOverride"]) return true; + for (JOYAxes3D *axes in controller.axes3D) { + if (axes.usage == JOYAxes3DUsageOrientation || axes.usage == JOYAxes3DUsageAcceleration) { + return false; + } + } + 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 (![self.window isMainWindow]) return; + if (!_gb) return; + if (![self allowController]) return; NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; if (!mapping) { @@ -481,58 +608,156 @@ static const uint8_t workboy_vk_to_key[] = { } } +- (bool)controller:(JOYController *)controller applicableForPlayer:(unsigned)player effectivePlayer:(unsigned *)effectivePlayer effectiveGB:(GB_gameboy_t **)effectiveGB +{ + NSDictionary *controllerMapping = [self controllerMapping]; + + JOYController *preferredJoypad = controllerMapping[@(player)]; + if (preferredJoypad && preferredJoypad != controller) return false; // The player has a different assigned controller + if (!preferredJoypad && self.playerCount != 1) return false; // The player has no assigned controller in multiplayer mode, prevent controller inputs + + dispatch_async(dispatch_get_main_queue(), ^{ + [controller setPlayerLEDs:[controller LEDMaskForPlayer:player]]; + }); + + *effectiveGB = _gb; + *effectivePlayer = player; + + if (player && self.document.partner) { + *effectiveGB = self.document.partner.gb; + *effectivePlayer = 0; + if (controller != self.document.partner.view->lastController) { + [self setRumble:0]; + self.document.partner.view->lastController = controller; + } + } + else { + if (controller != lastController) { + [self setRumble:0]; + lastController = controller; + } + } + return true; +} + +- (void)controller:(JOYController *)controller movedAxes2D:(JOYAxes2D *)axes +{ + if (!_gb) return; + /* Always handle only the most dominant 2D input. */ + for (JOYAxes2D *otherAxes in controller.axes2D) { + if (otherAxes == axes) continue; + if (otherAxes.distance > axes.distance) { + return; + } + } + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + if ([self shouldControllerUseJoystickForMotion:controller] && !self.mouseControlsActive) { + GB_set_accelerometer_values(_gb, -axes.value.x, -axes.value.y); + } + else if ([defaults boolForKey:@"GBFauxAnalogInputs"]) { + unsigned playerCount = self.playerCount; + for (unsigned player = 0; player < playerCount; player++) { + unsigned effectivePlayer; + GB_gameboy_t *effectiveGB; + if (![self controller:controller applicableForPlayer:player effectivePlayer:&effectivePlayer effectiveGB:&effectiveGB]) continue; + + GB_set_use_faux_analog_inputs(effectiveGB, effectivePlayer, true); + NSPoint position = axes.value; + GB_set_faux_analog_inputs(effectiveGB, effectivePlayer, position.x, position.y); + } + } +} + +- (void)controller:(JOYController *)controller movedAxes3D:(JOYAxes3D *)axes +{ + 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) { + // Only use orientation if there's no acceleration axes + if (axes.usage == JOYAxes3DUsageAcceleration) { + return; + } + } + JOYPoint3D point = axes.normalizedValue; + GB_set_accelerometer_values(effectiveGB, point.x, point.z); + } + else if (axes.usage == JOYAxes3DUsageAcceleration) { + JOYPoint3D point = axes.gUnitsValue; + GB_set_accelerometer_values(effectiveGB, point.x, point.z); + } +} + - (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button { - if (![self.window isMainWindow]) return; + if (!_gb) 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; - } - dispatch_async(dispatch_get_main_queue(), ^{ - [controller setPlayerLEDs:[controller LEDMaskForPlayer:player]]; - }); - NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + bool fauxAnalog = [defaults boolForKey:@"GBFauxAnalogInputs"]; + + for (unsigned player = 0; player < playerCount; player++) { + unsigned effectivePlayer; + GB_gameboy_t *effectiveGB; + if (![self controller:controller applicableForPlayer:player effectivePlayer:&effectivePlayer effectiveGB:&effectiveGB]) continue; + + NSDictionary *mapping = [defaults dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; if (!mapping) { - mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; + mapping = [defaults dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; } 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; - unsigned effectivePlayer = player; - - if (player && self.document.partner) { - effectiveGB = self.document.partner.gb; - effectivePlayer = 0; - if (controller != self.document.partner.view->lastController) { - [self setRumble:0]; - self.document.partner.view->lastController = controller; + if (usage >= JOYButtonUsageDPadLeft && usage <= JOYButtonUsageDPadDown) { + if (fauxAnalog && button.type == JOYButtonTypeAxes2DEmulated) { + // This isn't a real button, it's an emulated Axes2D. We want to handle it as an Axes2D instead + continue; } - } - else { - if (controller != lastController) { - [self setRumble:0]; - lastController = controller; + else { + // User used a digital direction input, revert to non-analog inputs + GB_set_use_faux_analog_inputs(effectiveGB, effectivePlayer, false); } } - switch (usage) { + switch ((unsigned)usage) { case JOYButtonUsageNone: break; case JOYButtonUsageA: GB_set_key_state_for_player(effectiveGB, GB_KEY_A, effectivePlayer, button.isPressed); break; @@ -577,7 +802,16 @@ static const uint8_t workboy_vk_to_key[] = { case JOYButtonUsageDPadUp: GB_set_key_state_for_player(effectiveGB, GB_KEY_UP, effectivePlayer, button.isPressed); break; case JOYButtonUsageDPadDown: GB_set_key_state_for_player(effectiveGB, GB_KEY_DOWN, effectivePlayer, button.isPressed); break; - default: + case GBJoyKitRapidA: + _rapidA[effectivePlayer] = button.isPressed; + _rapidACount[effectivePlayer] = 0; + GB_set_key_state_for_player(_gb, GB_KEY_A, effectivePlayer, button.isPressed); + break; + + case GBJoyKitRapidB: + _rapidB[effectivePlayer] = button.isPressed; + _rapidBCount[effectivePlayer] = 0; + GB_set_key_state_for_player(_gb, GB_KEY_B, effectivePlayer, button.isPressed); break; } } @@ -588,11 +822,18 @@ static const uint8_t workboy_vk_to_key[] = { return true; } +- (bool)mouseControlsActive +{ + return _gb && GB_is_inited(_gb) && GB_has_accelerometer(_gb) && + _mouseControlEnabled && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBMBC7AllowMouse"]; +} + - (void)mouseEntered:(NSEvent *)theEvent { if (!mouse_hidden) { mouse_hidden = true; - if (_mouseHidingEnabled) { + if (_mouseHidingEnabled && + !self.mouseControlsActive) { [NSCursor hide]; } } @@ -610,6 +851,46 @@ static const uint8_t workboy_vk_to_key[] = { [super mouseExited:theEvent]; } +- (void)mouseDown:(NSEvent *)event +{ + _mouseControlEnabled = true; + if (self.mouseControlsActive) { + if (event.type == NSEventTypeLeftMouseDown) { + GB_set_key_state(_gb, GB_KEY_A, true); + } + } +} + +- (void)mouseUp:(NSEvent *)event +{ + if (self.mouseControlsActive) { + if (event.type == NSEventTypeLeftMouseUp) { + GB_set_key_state(_gb, GB_KEY_A, false); + } + } +} + +- (void)mouseMoved:(NSEvent *)event +{ + if (self.mouseControlsActive) { + NSPoint point = [self convertPoint:[event locationInWindow] toView:nil]; + + point.x /= self.frame.size.width; + point.x *= 2; + point.x -= 1; + + point.y /= self.frame.size.height; + point.y *= 2; + point.y -= 1; + + if (GB_get_screen_width(_gb) != 160) { // has border + point.x *= 256 / 160.0; + point.y *= 224 / 114.0; + } + GB_set_accelerometer_values(_gb, -point.x, point.y); + } +} + - (void)setMouseHidingEnabled:(bool)mouseHidingEnabled { if (mouseHidingEnabled == _mouseHidingEnabled) return; @@ -642,16 +923,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/bsnes/gb/Cocoa/GBViewMetal.h b/bsnes/gb/Cocoa/GBViewMetal.h deleted file mode 100644 index 521c3c72..00000000 --- a/bsnes/gb/Cocoa/GBViewMetal.h +++ /dev/null @@ -1,7 +0,0 @@ -#import -#import -#import "GBView.h" - -@interface GBViewMetal : GBView -+ (bool) isSupported; -@end diff --git a/bsnes/gb/Cocoa/GBVisualizerView.h b/bsnes/gb/Cocoa/GBVisualizerView.h index 5ee4638e..f3ccc291 100644 --- a/bsnes/gb/Cocoa/GBVisualizerView.h +++ b/bsnes/gb/Cocoa/GBVisualizerView.h @@ -1,5 +1,5 @@ #import -#include +#import @interface GBVisualizerView : NSView - (void)addSample:(GB_sample_t *)sample; diff --git a/bsnes/gb/Cocoa/GBVisualizerView.m b/bsnes/gb/Cocoa/GBVisualizerView.m index 61688e62..185e1c77 100644 --- a/bsnes/gb/Cocoa/GBVisualizerView.m +++ b/bsnes/gb/Cocoa/GBVisualizerView.m @@ -1,5 +1,6 @@ #import "GBVisualizerView.h" -#include +#import "GBPaletteEditorController.h" +#import #define SAMPLE_COUNT 1024 @@ -28,24 +29,7 @@ static NSColor *color_to_effect_color(typeof(GB_PALETTE_DMG.colors[0]) color) - (void)drawRect:(NSRect)dirtyRect { - const GB_palette_t *palette; - switch ([[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorPalette"]) { - case 1: - palette = &GB_PALETTE_DMG; - break; - - case 2: - palette = &GB_PALETTE_MGB; - break; - - case 3: - palette = &GB_PALETTE_GBL; - break; - - default: - palette = &GB_PALETTE_GREY; - break; - } + const GB_palette_t *palette = [GBPaletteEditorController userPalette]; NSSize size = self.bounds.size; [color_to_effect_color(palette->colors[0]) setFill]; diff --git a/bsnes/gb/Cocoa/GBWarningPopover.h b/bsnes/gb/Cocoa/GBWarningPopover.h index 1d695b13..953aa3c9 100644 --- a/bsnes/gb/Cocoa/GBWarningPopover.h +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBWarningPopover.m b/bsnes/gb/Cocoa/GBWarningPopover.m index 74f6444d..b66186ba 100644 --- a/bsnes/gb/Cocoa/GBWarningPopover.m +++ b/bsnes/gb/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/bsnes/gb/Cocoa/GBWindow.h b/bsnes/gb/Cocoa/GBWindow.h new file mode 100644 index 00000000..f391e076 --- /dev/null +++ b/bsnes/gb/Cocoa/GBWindow.h @@ -0,0 +1,5 @@ +#import + +@interface GBWindow : NSWindow + +@end diff --git a/bsnes/gb/Cocoa/GBWindow.m b/bsnes/gb/Cocoa/GBWindow.m new file mode 100644 index 00000000..ef17df12 --- /dev/null +++ b/bsnes/gb/Cocoa/GBWindow.m @@ -0,0 +1,22 @@ +#import "GBWindow.h" + +@interface NSWindow(private) +- (void)_zoomFill:(id)sender; +@end + +/* + For some reason, Apple replaced the alt + zoom button behavior to be "fill" rather than zoom. + I don't like that. It prevents SameBoy's integer scaling from working. Let's restore it. +*/ + +@implementation GBWindow +- (void)_zoomFill:(id)sender +{ + if (sender == [self standardWindowButton:NSWindowZoomButton] && + ((self.currentEvent.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask) == NSEventModifierFlagOption)) { + [self zoom:sender]; + return; + } + [super _zoomFill:sender]; +} +@end diff --git a/bsnes/gb/Cocoa/HelpTemplate.png b/bsnes/gb/Cocoa/HelpTemplate.png new file mode 100644 index 00000000..6b12375e Binary files /dev/null and b/bsnes/gb/Cocoa/HelpTemplate.png differ diff --git a/bsnes/gb/Cocoa/HelpTemplate@2x.png b/bsnes/gb/Cocoa/HelpTemplate@2x.png new file mode 100644 index 00000000..d7f8237a Binary files /dev/null and b/bsnes/gb/Cocoa/HelpTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/HorizontalJoyConLeftTemplate.png b/bsnes/gb/Cocoa/HorizontalJoyConLeftTemplate.png new file mode 100644 index 00000000..7c4b5974 Binary files /dev/null and b/bsnes/gb/Cocoa/HorizontalJoyConLeftTemplate.png differ diff --git a/bsnes/gb/Cocoa/HorizontalJoyConLeftTemplate@2x.png b/bsnes/gb/Cocoa/HorizontalJoyConLeftTemplate@2x.png new file mode 100644 index 00000000..816706d0 Binary files /dev/null and b/bsnes/gb/Cocoa/HorizontalJoyConLeftTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/HorizontalJoyConRightTemplate.png b/bsnes/gb/Cocoa/HorizontalJoyConRightTemplate.png new file mode 100644 index 00000000..866992be Binary files /dev/null and b/bsnes/gb/Cocoa/HorizontalJoyConRightTemplate.png differ diff --git a/bsnes/gb/Cocoa/HorizontalJoyConRightTemplate@2x.png b/bsnes/gb/Cocoa/HorizontalJoyConRightTemplate@2x.png new file mode 100644 index 00000000..908ba48f Binary files /dev/null and b/bsnes/gb/Cocoa/HorizontalJoyConRightTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/Icon.png b/bsnes/gb/Cocoa/Icon.png new file mode 100644 index 00000000..ce9bd5b8 Binary files /dev/null and b/bsnes/gb/Cocoa/Icon.png differ diff --git a/bsnes/gb/Cocoa/Icon@2x.png b/bsnes/gb/Cocoa/Icon@2x.png new file mode 100644 index 00000000..db569f34 Binary files /dev/null and b/bsnes/gb/Cocoa/Icon@2x.png differ diff --git a/bsnes/gb/Cocoa/Info.plist b/bsnes/gb/Cocoa/Info.plist index 5e409c98..6e91b3bb 100644 --- a/bsnes/gb/Cocoa/Info.plist +++ b/bsnes/gb/Cocoa/Info.plist @@ -22,6 +22,8 @@ LSItemContentTypes com.github.liji32.sameboy.gb + public.gbrom + com.retroarch.gb LSTypeIsPackage 0 @@ -42,6 +44,7 @@ LSItemContentTypes com.github.liji32.sameboy.gbc + com.retroarch.gbc LSTypeIsPackage 0 @@ -88,13 +91,35 @@ NSDocumentClass Document + + CFBundleTypeExtensions + + gbcart + + CFBundleTypeIconFile + ColorCartridge + CFBundleTypeName + Game Boy Cartridge + CFBundleTypeRole + Viewer + LSItemContentTypes + + LSTypeIsPackage + 1 + NSDocumentClass + Document + CFBundleExecutable SameBoy CFBundleIconFile - AppIcon.icns + AppIcon + CFBundleIconName + AppIcon CFBundleIdentifier com.github.liji32.sameboy + LSApplicationCategoryType + public.app-category.games CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -102,7 +127,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - Version @VERSION + @VERSION CFBundleSignature ???? CFBundleSupportedPlatforms @@ -112,11 +137,11 @@ LSMinimumSystemVersion 10.9 NSHumanReadableCopyright - Copyright © 2015-2021 Lior Halphon + Copyright © 2015-@COPYRIGHT_YEAR Lior Halphon NSMainNibFile MainMenu NSPrincipalClass - NSApplication + GBApp UTExportedTypeDeclarations @@ -197,7 +222,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/bsnes/gb/Cocoa/InterruptTemplate.png b/bsnes/gb/Cocoa/InterruptTemplate.png new file mode 100644 index 00000000..35307279 Binary files /dev/null and b/bsnes/gb/Cocoa/InterruptTemplate.png differ diff --git a/bsnes/gb/Cocoa/InterruptTemplate@2x.png b/bsnes/gb/Cocoa/InterruptTemplate@2x.png new file mode 100644 index 00000000..eb262436 Binary files /dev/null and b/bsnes/gb/Cocoa/InterruptTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/JoyConDualTemplate.png b/bsnes/gb/Cocoa/JoyConDualTemplate.png new file mode 100644 index 00000000..42e7a271 Binary files /dev/null and b/bsnes/gb/Cocoa/JoyConDualTemplate.png differ diff --git a/bsnes/gb/Cocoa/JoyConDualTemplate@2x.png b/bsnes/gb/Cocoa/JoyConDualTemplate@2x.png new file mode 100644 index 00000000..938fd7f3 Binary files /dev/null and b/bsnes/gb/Cocoa/JoyConDualTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/JoyConLeftTemplate.png b/bsnes/gb/Cocoa/JoyConLeftTemplate.png new file mode 100644 index 00000000..924c427b Binary files /dev/null and b/bsnes/gb/Cocoa/JoyConLeftTemplate.png differ diff --git a/bsnes/gb/Cocoa/JoyConLeftTemplate@2x.png b/bsnes/gb/Cocoa/JoyConLeftTemplate@2x.png new file mode 100644 index 00000000..6b2f9969 Binary files /dev/null and b/bsnes/gb/Cocoa/JoyConLeftTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/JoyConRightTemplate.png b/bsnes/gb/Cocoa/JoyConRightTemplate.png new file mode 100644 index 00000000..1fccf5f6 Binary files /dev/null and b/bsnes/gb/Cocoa/JoyConRightTemplate.png differ diff --git a/bsnes/gb/Cocoa/JoyConRightTemplate@2x.png b/bsnes/gb/Cocoa/JoyConRightTemplate@2x.png new file mode 100644 index 00000000..d9c385ca Binary files /dev/null and b/bsnes/gb/Cocoa/JoyConRightTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/Joypad.png b/bsnes/gb/Cocoa/Joypad.png index f30d8f99..7692cbf8 100644 Binary files a/bsnes/gb/Cocoa/Joypad.png and b/bsnes/gb/Cocoa/Joypad.png differ diff --git a/bsnes/gb/Cocoa/Joypad@2x.png b/bsnes/gb/Cocoa/Joypad@2x.png index d91ee30b..fb7bd1f2 100644 Binary files a/bsnes/gb/Cocoa/Joypad@2x.png and b/bsnes/gb/Cocoa/Joypad@2x.png differ diff --git a/bsnes/gb/Cocoa/Joypad~dark.png b/bsnes/gb/Cocoa/Joypad~dark.png index 8a7687b5..9ab114da 100644 Binary files a/bsnes/gb/Cocoa/Joypad~dark.png and b/bsnes/gb/Cocoa/Joypad~dark.png differ diff --git a/bsnes/gb/Cocoa/Joypad~dark@2x.png b/bsnes/gb/Cocoa/Joypad~dark@2x.png index ce2a07cc..c8fcb249 100644 Binary files a/bsnes/gb/Cocoa/Joypad~dark@2x.png and b/bsnes/gb/Cocoa/Joypad~dark@2x.png differ diff --git a/bsnes/gb/Cocoa/Joypad~solid.png b/bsnes/gb/Cocoa/Joypad~solid.png new file mode 100644 index 00000000..5ac95dda Binary files /dev/null and b/bsnes/gb/Cocoa/Joypad~solid.png differ diff --git a/bsnes/gb/Cocoa/Joypad~solid@2x.png b/bsnes/gb/Cocoa/Joypad~solid@2x.png new file mode 100644 index 00000000..b3ae37af Binary files /dev/null and b/bsnes/gb/Cocoa/Joypad~solid@2x.png differ diff --git a/bsnes/gb/Cocoa/KeyboardShortcutPrivateAPIs.h b/bsnes/gb/Cocoa/KeyboardShortcutPrivateAPIs.h index a80dfde9..1a37caf4 100644 --- a/bsnes/gb/Cocoa/KeyboardShortcutPrivateAPIs.h +++ b/bsnes/gb/Cocoa/KeyboardShortcutPrivateAPIs.h @@ -1,16 +1,13 @@ -#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. */ @interface NSKeyboardShortcut : NSObject + (id)shortcutWithPreferencesEncoding:(NSString *)encoding; -+ (id)shortcutWithKeyEquivalent:(NSString *)key_equivalent modifierMask:(unsigned long long)mask; -- (id)initWithKeyEquivalent:(NSString *)key_equivalent modifierMask:(unsigned long long)mask; ++ (id)shortcutWithKeyEquivalent:(NSString *)key_equivalent modifierMask:(NSUInteger)mask; +- (id)initWithKeyEquivalent:(NSString *)key_equivalent modifierMask:(NSUInteger)mask; -@property(readonly) unsigned long long modifierMask; +@property(readonly) NSUInteger modifierMask; @property(readonly) NSString *keyEquivalent; @property(readonly) NSString *preferencesEncoding; @property(readonly) NSString *localizedModifierMaskDisplayName; @@ -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/bsnes/gb/Cocoa/License.html b/bsnes/gb/Cocoa/License.html index 98465141..b7616e7d 100644 --- a/bsnes/gb/Cocoa/License.html +++ b/bsnes/gb/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/bsnes/gb/Cocoa/MainMenu.xib b/bsnes/gb/Cocoa/MainMenu.xib index 348e9605..6be9574e 100644 --- a/bsnes/gb/Cocoa/MainMenu.xib +++ b/bsnes/gb/Cocoa/MainMenu.xib @@ -1,18 +1,17 @@ - + - + - + - - + @@ -27,13 +26,19 @@ - + + + + + + + - + @@ -77,6 +82,12 @@ + + + + + +

@@ -91,6 +102,12 @@ + + + + + + @@ -141,7 +158,7 @@ - + @@ -180,18 +197,73 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -248,7 +320,7 @@ - + @@ -316,7 +388,7 @@ - + @@ -332,35 +404,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + @@ -375,18 +427,24 @@ - + - + + + + + + + - + - + @@ -394,24 +452,24 @@ - + - + - + - + - + @@ -424,14 +482,14 @@ - + - + - + @@ -443,13 +501,58 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -474,6 +577,16 @@ + + + + + + + + + + @@ -494,9 +607,10 @@ - + + - + @@ -506,4 +620,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/NSImageNamedDarkSupport.m b/bsnes/gb/Cocoa/NSImageNamedDarkSupport.m index 821ba3b9..73e7b64b 100644 --- a/bsnes/gb/Cocoa/NSImageNamedDarkSupport.m +++ b/bsnes/gb/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/bsnes/gb/Cocoa/NSObject+DefaultsObserver.h b/bsnes/gb/Cocoa/NSObject+DefaultsObserver.h new file mode 100644 index 00000000..18469f81 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/NSObject+DefaultsObserver.m b/bsnes/gb/Cocoa/NSObject+DefaultsObserver.m new file mode 100644 index 00000000..8dac9125 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Cocoa/NSTextFieldCell+Inset.h b/bsnes/gb/Cocoa/NSTextFieldCell+Inset.h new file mode 100644 index 00000000..3b3cac8c --- /dev/null +++ b/bsnes/gb/Cocoa/NSTextFieldCell+Inset.h @@ -0,0 +1,6 @@ +#import +#import + +@interface NSTextFieldCell (Inset) +@property NSSize textInset; +@end diff --git a/bsnes/gb/Cocoa/NSTextFieldCell+Inset.m b/bsnes/gb/Cocoa/NSTextFieldCell+Inset.m new file mode 100644 index 00000000..79c00082 --- /dev/null +++ b/bsnes/gb/Cocoa/NSTextFieldCell+Inset.m @@ -0,0 +1,77 @@ +#import "NSTextFieldCell+Inset.h" +#import +#import + +@interface NSTextFieldCell () +@property NSSize textInset; +- (bool)_isEditingInView:(NSView *)view; +@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]; +} + +- (void)drawWithFrameHook:(NSRect)cellFrame inView:(NSView *)controlView +{ + NSSize inset = self.textInset; + if (self.drawsBackground) { + [self.backgroundColor setFill]; + if ([self _isEditingInView:controlView]) { + NSRectFill(cellFrame); + } + else { + NSRectFill(NSMakeRect(cellFrame.origin.x, cellFrame.origin.y, + cellFrame.size.width, inset.height)); + NSRectFill(NSMakeRect(cellFrame.origin.x, cellFrame.origin.y + cellFrame.size.height - inset.height, + cellFrame.size.width, inset.height)); + + NSRectFill(NSMakeRect(cellFrame.origin.x, cellFrame.origin.y + inset.height, + inset.width, cellFrame.size.height - inset.height * 2)); + NSRectFill(NSMakeRect(cellFrame.origin.x + cellFrame.size.width - inset.width, cellFrame.origin.y + inset.height, + inset.width, cellFrame.size.height - inset.height * 2)); + } + } + cellFrame.origin.x += inset.width; + cellFrame.origin.y += inset.height; + cellFrame.size.width -= inset.width * 2; + cellFrame.size.height -= inset.height * 2; + [self drawWithFrameHook:cellFrame inView:controlView]; +} + ++ (void)load +{ + method_exchangeImplementations(class_getInstanceMethod(self, @selector(drawWithFrame:inView:)), + class_getInstanceMethod(self, @selector(drawWithFrameHook:inView:))); +} + +@end + + +@implementation NSTextField (Inset) + +- (bool)wantsUpdateLayerHook +{ + CGSize inset = ((NSTextFieldCell *)self.cell).textInset; + if (inset.width || inset.height) return false; + return [self wantsUpdateLayerHook]; +} + ++ (void)load +{ + Method method = class_getInstanceMethod(self, @selector(wantsUpdateLayer)); + if (class_addMethod(self, @selector(wantsUpdateLayer), method_getImplementation(method), method_getTypeEncoding(method))) { + method = class_getInstanceMethod(self, @selector(wantsUpdateLayer)); + } + method_exchangeImplementations(method, + class_getInstanceMethod(self, @selector(wantsUpdateLayerHook))); +} + +@end diff --git a/bsnes/gb/Cocoa/NSToolbarItem+NoOverflow.m b/bsnes/gb/Cocoa/NSToolbarItem+NoOverflow.m new file mode 100644 index 00000000..5b9c53c0 --- /dev/null +++ b/bsnes/gb/Cocoa/NSToolbarItem+NoOverflow.m @@ -0,0 +1,24 @@ +#import +#import + +static id nop(id self, SEL _cmd) +{ + return nil; +} + +static double minSize(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)minSize); +} + +@end diff --git a/bsnes/gb/Cocoa/Next.png b/bsnes/gb/Cocoa/Next.png index cd9a4c31..6776010a 100644 Binary files a/bsnes/gb/Cocoa/Next.png and b/bsnes/gb/Cocoa/Next.png differ diff --git a/bsnes/gb/Cocoa/Next@2x.png b/bsnes/gb/Cocoa/Next@2x.png index 1debb1d5..c6b9d3ac 100644 Binary files a/bsnes/gb/Cocoa/Next@2x.png and b/bsnes/gb/Cocoa/Next@2x.png differ diff --git a/bsnes/gb/Cocoa/NextTemplate.png b/bsnes/gb/Cocoa/NextTemplate.png new file mode 100644 index 00000000..071e750e Binary files /dev/null and b/bsnes/gb/Cocoa/NextTemplate.png differ diff --git a/bsnes/gb/Cocoa/NextTemplate@2x.png b/bsnes/gb/Cocoa/NextTemplate@2x.png new file mode 100644 index 00000000..616fb2e3 Binary files /dev/null and b/bsnes/gb/Cocoa/NextTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/Pause.png b/bsnes/gb/Cocoa/Pause.png index 2bb380b7..afb22e68 100644 Binary files a/bsnes/gb/Cocoa/Pause.png and b/bsnes/gb/Cocoa/Pause.png differ diff --git a/bsnes/gb/Cocoa/Pause@2x.png b/bsnes/gb/Cocoa/Pause@2x.png index 36b6da01..1c167f65 100644 Binary files a/bsnes/gb/Cocoa/Pause@2x.png and b/bsnes/gb/Cocoa/Pause@2x.png differ diff --git a/bsnes/gb/Cocoa/Play.png b/bsnes/gb/Cocoa/Play.png index 3f870921..65aae90d 100644 Binary files a/bsnes/gb/Cocoa/Play.png and b/bsnes/gb/Cocoa/Play.png differ diff --git a/bsnes/gb/Cocoa/Play@2x.png b/bsnes/gb/Cocoa/Play@2x.png index 0de05530..a0d59ae5 100644 Binary files a/bsnes/gb/Cocoa/Play@2x.png and b/bsnes/gb/Cocoa/Play@2x.png differ diff --git a/bsnes/gb/Cocoa/PopoverView.xib b/bsnes/gb/Cocoa/PopoverView.xib index 7ccdf496..dbaa2f55 100644 --- a/bsnes/gb/Cocoa/PopoverView.xib +++ b/bsnes/gb/Cocoa/PopoverView.xib @@ -1,8 +1,8 @@ - + - + @@ -13,7 +13,7 @@ - + diff --git a/bsnes/gb/Cocoa/Preferences.xib b/bsnes/gb/Cocoa/Preferences.xib index 6d726521..f6ed505e 100644 --- a/bsnes/gb/Cocoa/Preferences.xib +++ b/bsnes/gb/Cocoa/Preferences.xib @@ -1,12 +1,12 @@ - + - + - + @@ -20,39 +20,48 @@ - + - - + - + + + + + + + + + - + + + @@ -67,100 +76,150 @@ - - - - - - + - - - - - - + + + + + + + + - - - - - - - + + + - + - - + + - + - - + + - + - + + + + - - - - - - - - - - + + + + - - - - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - + + @@ -168,39 +227,45 @@ - - + + - + - - - - - + + + + + + + + - - - + + + - - + + - - - + + + + + + - - + + @@ -208,37 +273,37 @@ - - + + - + - - + + - + - - + + - - - + + + - - + + @@ -247,19 +312,19 @@ - + - + - - - + + + @@ -267,8 +332,8 @@ - - + + @@ -276,12 +341,21 @@ - - + + + + + + + + + + + - + @@ -292,41 +366,104 @@ + + + + + + + + + + + + + + + + + + - + - + + - + + + + + + + + + + + + + + + + + + + + + - + - + - - + + @@ -334,8 +471,8 @@ - - + + @@ -343,8 +480,8 @@ - - + + @@ -352,8 +489,8 @@ - - + + @@ -361,12 +498,12 @@ - - + + - + @@ -376,12 +513,12 @@ - - - + + + - - + + @@ -389,29 +526,29 @@ - - + + - + - + - - - - - - + + + + + + - - - + + + - - + + @@ -420,52 +557,34 @@ - + - - - - - - - - - - - - - - - - - - - - + + - + - + - - - + + + - + - + @@ -475,7 +594,7 @@ - + @@ -484,12 +603,12 @@ - - + + - + @@ -504,18 +623,96 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -524,36 +721,39 @@ - - + + - - - + + + + + + - - + + - + - - + + - - - + + + - - + + @@ -561,7 +761,7 @@ - + @@ -570,56 +770,347 @@ - - + + - - - + + + + + + + - + - + - - + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - + + - + - @@ -630,9 +1121,8 @@ - + - @@ -661,9 +1151,9 @@ - - - + + + @@ -671,11 +1161,11 @@ - - + + - + @@ -686,106 +1176,34 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - @@ -803,14 +1221,418 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Text Cell +Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + diff --git a/bsnes/gb/Cocoa/Previous.png b/bsnes/gb/Cocoa/Previous.png index cc91221d..5b1fff4d 100644 Binary files a/bsnes/gb/Cocoa/Previous.png and b/bsnes/gb/Cocoa/Previous.png differ diff --git a/bsnes/gb/Cocoa/Previous@2x.png b/bsnes/gb/Cocoa/Previous@2x.png index 77b01575..f0f7f65c 100644 Binary files a/bsnes/gb/Cocoa/Previous@2x.png and b/bsnes/gb/Cocoa/Previous@2x.png differ diff --git a/bsnes/gb/Cocoa/Rewind.png b/bsnes/gb/Cocoa/Rewind.png index 999f358f..3c72958d 100644 Binary files a/bsnes/gb/Cocoa/Rewind.png and b/bsnes/gb/Cocoa/Rewind.png differ diff --git a/bsnes/gb/Cocoa/Rewind@2x.png b/bsnes/gb/Cocoa/Rewind@2x.png index d845b549..6cb3417a 100644 Binary files a/bsnes/gb/Cocoa/Rewind@2x.png and b/bsnes/gb/Cocoa/Rewind@2x.png differ diff --git a/bsnes/gb/Cocoa/SameBoy.entitlements b/bsnes/gb/Cocoa/SameBoy.entitlements new file mode 100644 index 00000000..64720978 --- /dev/null +++ b/bsnes/gb/Cocoa/SameBoy.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.device.camera + + + \ No newline at end of file diff --git a/bsnes/gb/Cocoa/SolariumFixer.m b/bsnes/gb/Cocoa/SolariumFixer.m new file mode 100644 index 00000000..59bf63f9 --- /dev/null +++ b/bsnes/gb/Cocoa/SolariumFixer.m @@ -0,0 +1,168 @@ +#import +#import + +// Comment out to debug +#define NSLog(...) + +// Solarium has weird proportions, we need to fix them. +@implementation NSControl (SolariumFixer) + +- (void)awakeFromNib +{ + if (@available(macOS 26.0, *)) { + if ([self.superview isKindOfClass:objc_getClass("NSToolbarItemViewer")]) return; + + if ([self isKindOfClass:[NSStepper class]]) { + NSLog(@"Stepper needs adjustment: %s in window %@", sel_getName(self.action), self.window.title); + CGRect frame = self.frame; + frame.origin.y += 1; + self.frame = frame; + return; + } + if (self.controlSize != NSControlSizeRegular) return; + + if ([self isKindOfClass:[NSPopUpButton class]] ) { + NSLog(@"Popup button needs adjustment: %@ in window %@", ((NSButton *)self).title, self.window.title); + CGRect frame = self.frame; + if (frame.size.height != 25) { + NSLog(@"%@ in window %@ has the wrong height!", ((NSButton *)self).title, self.window.title); + return; + } + frame.size.width -= 2 + 5; // Remove 5 from the right and 2 from the left + frame.origin.x += 2; + frame.origin.y += 2; + self.frame = frame; + } + else if ([self isKindOfClass:[NSSegmentedControl class]] ) { + NSLog(@"Segmented button needs adjustment: %s in window %@", sel_getName(self.action), self.window.title); + CGRect frame = self.frame; + if (frame.size.height != 25) { + NSLog(@"%s in window %@ has the wrong height!", sel_getName(self.action), self.window.title); + return; + } + frame.origin.x += 8; + frame.origin.y += 1; + self.frame = frame; + } + else if ([self isKindOfClass:[NSTextField class]]) { + NSTextField *field = (id)self; + if (![field isBezeled]) return; + NSLog(@"Text field needs adjustment: %@ in window %@", ((NSTextField *)self).placeholderString, self.window.title); + CGRect frame = self.frame; + if (frame.size.height == 21) { + frame.size.height = 24; + } + else { + NSLog(@"%@ in window %@ has the wrong height!", ((NSTextField *)self).placeholderString, self.window.title); + return; + } + frame.size.width -= 1 + 1; // Remove 1 from the right and 1 from the left + frame.origin.x += 1; + frame.origin.y -= 1; + + self.frame = frame; + } + else if ([self isKindOfClass:[NSButton class]]) { + NSLog(@"Button: %@ in window %@", @(sel_getName(self.action)), self.window.title); + NSButton *button = (id)self; + if (!button.isBordered) return; + if (button.bezelStyle == NSBezelStylePush) { + NSLog(@"Button needs adjustment: %@ in window %@", @(sel_getName(self.action)), self.window.title); + CGRect frame = self.frame; + frame.size.width -= 7 + 7; // Remove 7 from the right and 7 from the left + frame.origin.x += 7; + frame.origin.y += 5; + if (frame.size.height == 32) { + frame.size.height = 25; + } + else { + NSLog(@"%@ in window %@ has the wrong height!", @(sel_getName(self.action)), self.window.title); + } self.frame = frame; + } + else if (button.bezelStyle == NSBezelStyleRegularSquare) { + CGRect frame = self.frame; + if (frame.size.height != 18) return; + NSLog(@"Check/Radio needs adjustment: %@ in window %@", ((NSButton *)self).title, self.window.title); + frame.size.width -= 2; + frame.origin.x += 2; + frame.origin.y += 1; + frame.size.height = 16; + self.frame = frame; + } + else if (button.bezelStyle == NSBezelStyleHelpButton) { + CGRect frame = self.frame; + NSLog(@"Help button needs adjustment: %@ in window %@", ((NSButton *)self).title, self.window.title); + frame.origin.y += 2; + self.frame = frame; + } + } + else if ([self isKindOfClass:[NSSlider class]]) { + NSLog(@"Slider needs adjustment: %s in window %@", sel_getName(self.action), self.window.title); + CGRect frame = self.frame; + frame.origin.y += 3; + self.frame = frame; + } + } +} + +@end + +@implementation NSToolbarItem (SolariumFixer) + +static CGSize minSizeHook(id self, SEL _cmd) +{ + return CGSizeMake(8, 0); +} + +- (void)awakeFromNib +{ + if (@available(macOS 26.0, *)) { + NSLog(@"Toolbar item %@ has view %@", self.label, self.view); + if ([self.view isKindOfClass:[NSTextField class]]) { + NSLog(@"Handling (Text field)"); + self.bordered = true; + + NSSize maxSize = self.maxSize; + maxSize.height = 36; + self.maxSize = maxSize; + + NSSize minSize = self.minSize; + minSize.height = 36; + self.minSize = minSize; + + ((NSTextField *)self.view).backgroundColor = [NSColor clearColor]; + ((NSTextField *)self.view).bezeled = false; + ((NSTextField *)self.view).bordered = true; + + // Work around even more AppKit bugs + self.toolbar.displayMode++; + self.toolbar.displayMode--; + } + else if ([self.view isKindOfClass:[NSPopUpButton class]]) { + NSLog(@"Handling (Pop up button)"); + self.bordered = true; + + NSSize maxSize = self.maxSize; + maxSize.height = 28; + self.maxSize = maxSize; + + NSSize minSize = self.minSize; + minSize.height = 28; + self.minSize = minSize; + } + } + else if (@available(macOS 11.0, *)) { // While at it, make macOS 11-15 a bit more consistent + if ([self.view isKindOfClass:[NSTextField class]]) { + ((NSTextField *)self.view).bezelStyle = NSTextFieldRoundedBezel; + } + } +} + ++ (void)load +{ + if (@available(macOS 26.0, *)) { + method_setImplementation(class_getInstanceMethod(objc_getClass("NSToolbarFlexibleSpaceItem"), @selector(minSize)), + (void *)minSizeHook); + } +} +@end diff --git a/bsnes/gb/Cocoa/Speaker.png b/bsnes/gb/Cocoa/Speaker.png index 1f6b556a..b969dbcf 100644 Binary files a/bsnes/gb/Cocoa/Speaker.png and b/bsnes/gb/Cocoa/Speaker.png differ diff --git a/bsnes/gb/Cocoa/Speaker@2x.png b/bsnes/gb/Cocoa/Speaker@2x.png index 41c46ffd..eef4c263 100644 Binary files a/bsnes/gb/Cocoa/Speaker@2x.png and b/bsnes/gb/Cocoa/Speaker@2x.png differ diff --git a/bsnes/gb/Cocoa/Speaker~dark.png b/bsnes/gb/Cocoa/Speaker~dark.png index f3f820a3..b71c9ef0 100644 Binary files a/bsnes/gb/Cocoa/Speaker~dark.png and b/bsnes/gb/Cocoa/Speaker~dark.png differ diff --git a/bsnes/gb/Cocoa/Speaker~dark@2x.png b/bsnes/gb/Cocoa/Speaker~dark@2x.png index bdc3eb70..8c004a5a 100644 Binary files a/bsnes/gb/Cocoa/Speaker~dark@2x.png and b/bsnes/gb/Cocoa/Speaker~dark@2x.png differ diff --git a/bsnes/gb/Cocoa/Speaker~solid.png b/bsnes/gb/Cocoa/Speaker~solid.png new file mode 100644 index 00000000..0d7c2635 Binary files /dev/null and b/bsnes/gb/Cocoa/Speaker~solid.png differ diff --git a/bsnes/gb/Cocoa/Speaker~solid@2x.png b/bsnes/gb/Cocoa/Speaker~solid@2x.png new file mode 100644 index 00000000..af4f1c16 Binary files /dev/null and b/bsnes/gb/Cocoa/Speaker~solid@2x.png differ diff --git a/bsnes/gb/Cocoa/StepTemplate.png b/bsnes/gb/Cocoa/StepTemplate.png new file mode 100644 index 00000000..ca24d1cd Binary files /dev/null and b/bsnes/gb/Cocoa/StepTemplate.png differ diff --git a/bsnes/gb/Cocoa/StepTemplate@2x.png b/bsnes/gb/Cocoa/StepTemplate@2x.png new file mode 100644 index 00000000..8d4b1af7 Binary files /dev/null and b/bsnes/gb/Cocoa/StepTemplate@2x.png differ diff --git a/bsnes/gb/Cocoa/UpdateWindow.xib b/bsnes/gb/Cocoa/UpdateWindow.xib index e34f8f21..3fbfacea 100644 --- a/bsnes/gb/Cocoa/UpdateWindow.xib +++ b/bsnes/gb/Cocoa/UpdateWindow.xib @@ -7,7 +7,7 @@ - + @@ -21,7 +21,6 @@ - @@ -97,7 +96,6 @@ DQ - @@ -125,7 +123,7 @@ Gw - + diff --git a/bsnes/gb/Cocoa/Updates.png b/bsnes/gb/Cocoa/Updates.png new file mode 100644 index 00000000..824875c9 Binary files /dev/null and b/bsnes/gb/Cocoa/Updates.png differ diff --git a/bsnes/gb/Cocoa/Updates@2x.png b/bsnes/gb/Cocoa/Updates@2x.png new file mode 100644 index 00000000..ce9bd5b8 Binary files /dev/null and b/bsnes/gb/Cocoa/Updates@2x.png differ diff --git a/bsnes/gb/Cocoa/Updates~solid.png b/bsnes/gb/Cocoa/Updates~solid.png new file mode 100644 index 00000000..9f7bc3ef Binary files /dev/null and b/bsnes/gb/Cocoa/Updates~solid.png differ diff --git a/bsnes/gb/Cocoa/Updates~solid@2x.png b/bsnes/gb/Cocoa/Updates~solid@2x.png new file mode 100644 index 00000000..ca143294 Binary files /dev/null and b/bsnes/gb/Cocoa/Updates~solid@2x.png differ diff --git a/bsnes/gb/Cocoa/Updates~solid~dark.png b/bsnes/gb/Cocoa/Updates~solid~dark.png new file mode 100644 index 00000000..5f4aefa2 Binary files /dev/null and b/bsnes/gb/Cocoa/Updates~solid~dark.png differ diff --git a/bsnes/gb/Cocoa/Updates~solid~dark@2x.png b/bsnes/gb/Cocoa/Updates~solid~dark@2x.png new file mode 100644 index 00000000..08afe011 Binary files /dev/null and b/bsnes/gb/Cocoa/Updates~solid~dark@2x.png differ diff --git a/bsnes/gb/Core/apu.c b/bsnes/gb/Core/apu.c index 537ae016..acb73376 100644 --- a/bsnes/gb/Core/apu.c +++ b/bsnes/gb/Core/apu.c @@ -2,10 +2,117 @@ #include #include #include +#include +#include #include "gb.h" -#define likely(x) __builtin_expect((x), 1) -#define unlikely(x) __builtin_expect((x), 0) +/* Band limited synthesis loosely based on: http://www.slack.net/~ant/bl-synth/ */ +static int32_t band_limited_steps[GB_BAND_LIMITED_PHASES][GB_BAND_LIMITED_WIDTH]; + +static void __attribute__((constructor)) band_limited_init(void) +{ + const unsigned master_size = GB_BAND_LIMITED_WIDTH * GB_BAND_LIMITED_PHASES; + double *master = malloc(master_size * sizeof(*master)); + memset(master, 0, master_size * sizeof(*master)); + + const double lowpass = 15.0 / 16.0; // 1.0 means using Nyquist as the exact cutoff + const double to_angle = M_PI / GB_BAND_LIMITED_PHASES * lowpass; + double sum = 0; + nounroll for (signed i = 0; i < master_size; i++) { + // Exact Blackman window + const double a0 = 7938 / 18608.0; + const double a1 = 9240 / 18608.0; + const double a2 = 1430 / 18608.0; + double window_angle = (2.0 * M_PI * i) / (master_size); + double window = a0 - a1 * cos(window_angle) + a2 * cos(2 * window_angle); + + double angle = (i - (signed)master_size / 2) * to_angle; + sum += master[i] = (angle == 0? 1 : sin(angle) / angle) * window; + } + + nounroll for (signed i = 0; i < master_size; i++) { + master[i] /= sum; + } + + nounroll for (signed phase = 0; phase < GB_BAND_LIMITED_PHASES; phase++) { + int32_t error = GB_BAND_LIMITED_ONE; + nounroll for (signed i = 0; i < GB_BAND_LIMITED_WIDTH; i++) { + double sum = 0; + nounroll for (signed j = 0; j < GB_BAND_LIMITED_PHASES; j++) { + signed index = i * GB_BAND_LIMITED_PHASES - phase + j; + if (index >= 0) { + sum += master[index]; + } + } + int32_t cur = sum * GB_BAND_LIMITED_ONE; + error -= cur; + band_limited_steps[phase][i] = cur; + } + + // Make sure the deltas sum to 1.0 + band_limited_steps[phase][GB_BAND_LIMITED_WIDTH / 2] += error; + } + free(master); +} + +static void band_limited_update(GB_band_limited_t *band_limited, const GB_sample_t *input, unsigned phase) +{ + if (input->packed == band_limited->input.packed) return; + unsigned delay = phase / GB_BAND_LIMITED_PHASES; + phase = phase & (GB_BAND_LIMITED_PHASES - 1); + + struct { + signed left, right; + } delta = { + .left = input->left - band_limited->input.left, + .right = input->right - band_limited->input.right, + }; + band_limited->input.packed = input->packed; + + for (unsigned i = 0; i < GB_BAND_LIMITED_WIDTH; i++) { + unsigned offset = (i + band_limited->pos + delay) & (sizeof(band_limited->buffer) / sizeof(band_limited->buffer[0]) - 1); + band_limited->buffer[offset].left += delta.left * band_limited_steps[phase][i]; + band_limited->buffer[offset].right += delta.right * band_limited_steps[phase][i]; + } +} + +static void band_limited_update_unfiltered(GB_band_limited_t *band_limited, const GB_sample_t *input, unsigned delay) +{ + if (input->packed == band_limited->input.packed) return; + + struct { + signed left, right; + } delta = { + .left = input->left - band_limited->input.left, + .right = input->right - band_limited->input.right, + }; + band_limited->input.packed = input->packed; + + unsigned offset = (band_limited->pos + delay) & (sizeof(band_limited->buffer) / sizeof(band_limited->buffer[0]) - 1); + band_limited->buffer[offset].left += delta.left * GB_BAND_LIMITED_ONE; + band_limited->buffer[offset].right += delta.right * GB_BAND_LIMITED_ONE; +} + +static void band_limited_read(GB_band_limited_t *band_limited, GB_sample_t *output, uint32_t multiplier) +{ + band_limited->output.left += band_limited->buffer[band_limited->pos].left; + band_limited->output.right += band_limited->buffer[band_limited->pos].right; + + band_limited->buffer[band_limited->pos].left = band_limited->buffer[band_limited->pos].right = 0; + band_limited->pos = (band_limited->pos + 1) & (sizeof(band_limited->buffer) / sizeof(band_limited->buffer[0]) - 1); + + output->left = band_limited->output.left * multiplier / GB_BAND_LIMITED_ONE; + output->right = band_limited->output.right * multiplier / GB_BAND_LIMITED_ONE; +} + +static inline uint32_t sample_fraction_multiply(GB_gameboy_t *gb, unsigned multiplier) +{ + if (unlikely(multiplier == 0)) return 0; + if (likely(multiplier < GB_QUICK_MULTIPLY_COUNT + 1)) { + return gb->apu_output.quick_fraction_multiply_cache[multiplier - 1]; + } + return gb->apu_output.quick_fraction_multiply_cache[0] * multiplier; +} static const uint8_t duties[] = { 0, 0, 0, 0, 0, 0, 0, 1, @@ -14,17 +121,9 @@ 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) +bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, GB_channel_t index) { - 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; - gb->apu_output.summed_samples[index].right += gb->apu_output.current_sample[index].right * multiplier; - 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) -{ - if (gb->model >= GB_MODEL_AGB) { + if (gb->model > GB_MODEL_CGB_E) { /* On the AGB, mixing is done digitally, so there are no per-channel DACs. Instead, all channels are summed digital regardless of whatever the DAC state would be on a CGB or earlier model. */ @@ -43,12 +142,14 @@ bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index) case GB_NOISE: return gb->io_registers[GB_IO_NR42] & 0xF8; + + nodefault; } 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; @@ -61,13 +162,14 @@ static uint8_t agb_bias_for_channel(GB_gameboy_t *gb, unsigned index) return 0; case GB_NOISE: return gb->apu.noise_channel.current_volume; + + 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_AGB) { + if (gb->model > GB_MODEL_CGB_E) { /* On the AGB, because no analog mixing is done, the behavior of NR51 is a bit different. A channel that is not connected to a terminal is idenitcal to a connected channel playing PCM sample 0. */ @@ -76,38 +178,42 @@ 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 */ + /* For some reason, channel 3 is inverted on the AGB, and has a different "silence" value */ value ^= 0xF; + silence = 7 * 2; } - 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 * 2 + bias : silence)) * left_volume, + .right = (0xF - (right? value * 2 + bias : silence)) * 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; + if (unlikely(gb->apu_output.max_cycles_per_sample == 1)) { + band_limited_update_unfiltered(&gb->apu_output.band_limited[index], &output, cycles_offset); } else { - output.left = 0xf * left_volume; - } - - if (*(uint32_t *)&(gb->apu_output.current_sample[index]) != *(uint32_t *)&output) { - refresh_channel(gb, index, cycles_offset); - gb->apu_output.current_sample[index] = output; + band_limited_update(&gb->apu_output.band_limited[index], + &output, + (((gb->apu_output.sample_fraction + sample_fraction_multiply(gb, cycles_offset)) >> 8) * GB_BAND_LIMITED_PHASES) >> 20); } } return; } + if (value == 0 && gb->apu.samples[index] == 0) return; + if (!GB_apu_is_DAC_enabled(gb, index)) { value = gb->apu.samples[index]; } @@ -124,10 +230,17 @@ 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) { - refresh_channel(gb, index, cycles_offset); - gb->apu_output.current_sample[index] = 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 (unlikely(gb->apu_output.max_cycles_per_sample == 1)) { + band_limited_update_unfiltered(&gb->apu_output.band_limited[index], &output, cycles_offset); + } + else { + band_limited_update(&gb->apu_output.band_limited[index], + &output, + (((gb->apu_output.sample_fraction + sample_fraction_multiply(gb, cycles_offset)) >> 8) * GB_BAND_LIMITED_PHASES) >> 20); } } } @@ -142,16 +255,16 @@ static signed interference(GB_gameboy_t *gb) /* These aren't scientifically measured, but based on ear based on several recordings */ signed ret = 0; if (gb->halted) { - if (gb->model != GB_MODEL_AGB) { + if (gb->model <= GB_MODEL_CGB_E) { ret -= MAX_CH_AMP / 5; } else { 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_AGB) { + if ((gb->io_registers[GB_IO_STAT] & 3) == 3 && gb->model <= GB_MODEL_CGB_E) { ret += MAX_CH_AMP / 14; } else if ((gb->io_registers[GB_IO_STAT] & 3) == 1) { @@ -163,7 +276,7 @@ static signed interference(GB_gameboy_t *gb) ret += MAX_CH_AMP / 10; } - if (GB_is_cgb(gb) && gb->model < GB_MODEL_AGB && (gb->io_registers[GB_IO_RP] & 1)) { + if (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_E && (gb->io_registers[GB_IO_RP] & 1)) { ret += MAX_CH_AMP / 10; } @@ -183,7 +296,7 @@ static void render(GB_gameboy_t *gb) unrolled for (unsigned i = 0; i < GB_N_CHANNELS; i++) { double multiplier = CH_STEP; - if (gb->model < GB_MODEL_AGB) { + if (gb->model <= GB_MODEL_CGB_E) { if (!GB_apu_is_DAC_enabled(gb, i)) { gb->apu_output.dac_discharge[i] -= ((double) DAC_DECAY_SPEED) / gb->apu_output.sample_rate; if (gb->apu_output.dac_discharge[i] < 0) { @@ -204,26 +317,26 @@ static void render(GB_gameboy_t *gb) } } } + + GB_sample_t channel_output; + band_limited_read(&gb->apu_output.band_limited[i], &channel_output, multiplier); - if (likely(gb->apu_output.last_update[i] == 0)) { - output.left += gb->apu_output.current_sample[i].left * multiplier; - output.right += gb->apu_output.current_sample[i].right * multiplier; - } - else { - refresh_channel(gb, i, 0); - output.left += (signed long) gb->apu_output.summed_samples[i].left * multiplier - / gb->apu_output.cycles_since_render; - output.right += (signed long) gb->apu_output.summed_samples[i].right * multiplier - / gb->apu_output.cycles_since_render; - gb->apu_output.summed_samples[i] = (GB_sample_t){0, 0}; - } - gb->apu_output.last_update[i] = 0; + output.left += channel_output.left; + output.right += channel_output.right; } gb->apu_output.cycles_since_render = 0; + if (unlikely(gb->apu_output.sample_fraction < (1 << 28))) { + gb->apu_output.sample_fraction = 0; + } + else { + gb->apu_output.sample_fraction -= 1 << 28; + } + + 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, - output.right - gb->apu_output.highpass_diff.right} : + (GB_sample_t) {output.left - (int16_t)gb->apu_output.highpass_diff.left, + output.right - (int16_t)gb->apu_output.highpass_diff.right} : output; switch (gb->apu_output.highpass_mode) { @@ -231,32 +344,30 @@ static void render(GB_gameboy_t *gb) gb->apu_output.highpass_diff = (GB_double_sample_t) {0, 0}; break; case GB_HIGHPASS_ACCURATE: - gb->apu_output.highpass_diff = (GB_double_sample_t) - {output.left - filtered_output.left * gb->apu_output.highpass_rate, - output.right - filtered_output.right * gb->apu_output.highpass_rate}; + gb->apu_output.highpass_diff = (GB_double_sample_t) { + output.left - (output.left - gb->apu_output.highpass_diff.left) * gb->apu_output.highpass_rate, + output.right - (output.right - gb->apu_output.highpass_diff.right) * gb->apu_output.highpass_rate + }; break; case GB_HIGHPASS_REMOVE_DC_OFFSET: { unsigned mask = gb->io_registers[GB_IO_NR51]; 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) - {left_volume * (1 - gb->apu_output.highpass_rate) + gb->apu_output.highpass_diff.left * gb->apu_output.highpass_rate, - right_volume * (1 - gb->apu_output.highpass_rate) + gb->apu_output.highpass_diff.right * gb->apu_output.highpass_rate}; + gb->apu_output.highpass_diff = (GB_double_sample_t) { + left_volume * (1 - gb->apu_output.highpass_rate) + gb->apu_output.highpass_diff.left * gb->apu_output.highpass_rate, + right_volume * (1 - gb->apu_output.highpass_rate) + gb->apu_output.highpass_diff.right * gb->apu_output.highpass_rate + }; case GB_HIGHPASS_MAX:; } @@ -276,17 +387,35 @@ 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, unsigned cycles) { - if (gb->apu.square_channels[index].current_sample_index & 0x80) return; + if (gb->apu.square_channels[index].sample_surpressed) { + if (gb->model > GB_MODEL_CGB_E) { + update_sample(gb, index, gb->apu.samples[index], 0); + } + return; + } uint8_t duty = gb->io_registers[index == GB_SQUARE_1? GB_IO_NR11 :GB_IO_NR21] >> 6; update_sample(gb, index, duties[gb->apu.square_channels[index].current_sample_index + duty * 8]? gb->apu.square_channels[index].current_volume : 0, - 0); + cycles); } static inline void update_wave_sample(GB_gameboy_t *gb, unsigned cycles) @@ -303,11 +432,19 @@ static inline void update_wave_sample(GB_gameboy_t *gb, unsigned cycles) } } -/* the effects of NRX2 writes on current volume are not well documented and differ - between models and variants. The exact behavior can only be verified on CGB as it - requires the PCM12 register. The behavior implemented here was verified on *my* - CGB, which might behave differently from other CGB revisions, as well as from the - DMG, MGB or SGB/2 */ +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) { @@ -321,7 +458,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) { @@ -348,27 +485,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); @@ -378,11 +505,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_envelope_clock[index].locked) return; if (!(nrx2 & 7)) return; if (gb->cgb_double_speed) { if (index == GB_SQUARE_1) { @@ -393,33 +521,26 @@ 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_envelope_clock[index].locked = true; - } - } - else { - if (gb->apu.square_channels[index].current_volume > 0) { gb->apu.square_channels[index].current_volume--; } - else { - gb->apu.square_envelope_clock[index].locked = true; - } - } if (gb->apu.is_active[index]) { - update_square_sample(gb, index); + update_square_sample(gb, index, 0); } } static void tick_noise_envelope(GB_gameboy_t *gb) { + 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 (gb->apu.noise_envelope_clock.locked) return; if (!(nr42 & 7)) return; if (gb->cgb_double_speed) { @@ -427,21 +548,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_envelope_clock.locked = true; - } - } - else { - if (gb->apu.noise_channel.current_volume > 0) { gb->apu.noise_channel.current_volume--; } - else { - gb->apu.noise_envelope_clock.locked = true; - } - } if (gb->apu.is_active[GB_NOISE]) { update_sample(gb, GB_NOISE, @@ -451,6 +562,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) { @@ -465,18 +592,23 @@ 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; + } } } void GB_apu_div_event(GB_gameboy_t *gb) { + GB_apu_run(gb, true); if (!gb->apu.global_enable) return; if (gb->apu.skip_div_event == GB_SKIP_DIV_EVENT_SKIP) { gb->apu.skip_div_event = GB_SKIP_DIV_EVENT_SKIPPED; @@ -491,27 +623,25 @@ void GB_apu_div_event(GB_gameboy_t *gb) if ((gb->apu.div_divider & 7) == 7) { unrolled for (unsigned i = GB_SQUARE_2 + 1; i--;) { - if (!gb->apu.square_envelope_clock[i].clock) { + if (!gb->apu.square_channels[i].envelope_clock.clock) { gb->apu.square_channels[i].volume_countdown--; gb->apu.square_channels[i].volume_countdown &= 7; } } - if (!gb->apu.noise_envelope_clock.clock) { + if (!gb->apu.noise_channel.envelope_clock.clock) { gb->apu.noise_channel.volume_countdown--; gb->apu.noise_channel.volume_countdown &= 7; } } unrolled for (unsigned i = GB_SQUARE_2 + 1; i--;) { - if (gb->apu.square_envelope_clock[i].clock) { + if (gb->apu.square_channels[i].envelope_clock.clock) { tick_square_envelope(gb, i); - gb->apu.square_envelope_clock[i].clock = false; } } - if (gb->apu.noise_envelope_clock.clock) { + if (gb->apu.noise_channel.envelope_clock.clock) { tick_noise_envelope(gb); - gb->apu.noise_envelope_clock.clock = false; } if ((gb->apu.div_divider & 1) == 1) { @@ -529,7 +659,7 @@ void GB_apu_div_event(GB_gameboy_t *gb) if (gb->apu.wave_channel.length_enabled) { if (gb->apu.wave_channel.pulse_length) { if (!--gb->apu.wave_channel.pulse_length) { - if (gb->apu.is_active[GB_WAVE] && gb->model == GB_MODEL_AGB) { + if (gb->apu.is_active[GB_WAVE] && gb->model > GB_MODEL_CGB_E) { if (gb->apu.wave_channel.sample_countdown == 0) { gb->apu.wave_channel.current_sample_byte = gb->io_registers[GB_IO_WAV_START + (((gb->apu.wave_channel.current_sample_index + 1) & 0xF) >> 1)]; @@ -564,15 +694,24 @@ void GB_apu_div_event(GB_gameboy_t *gb) void GB_apu_div_secondary_event(GB_gameboy_t *gb) { + GB_apu_run(gb, true); + if (!gb->apu.global_enable) return; 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_envelope_clock[i].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_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); } } @@ -590,70 +729,136 @@ static void step_lfsr(GB_gameboy_t *gb, unsigned cycles_offset) gb->apu.noise_channel.lfsr &= ~high_bit_mask; } - gb->apu.current_lfsr_sample = gb->apu.noise_channel.lfsr & 1; + gb->apu.noise_channel.current_lfsr_sample = gb->apu.noise_channel.lfsr & 1; if (gb->apu.is_active[GB_NOISE]) { update_sample(gb, GB_NOISE, - gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_lfsr_sample ? gb->apu.noise_channel.current_volume : 0, cycles_offset); } } -void GB_apu_run(GB_gameboy_t *gb) +void GB_apu_run(GB_gameboy_t *gb, bool force) { - /* Convert 4MHZ to 2MHz. apu_cycles is always divisable by 4. */ - uint8_t cycles = gb->apu.apu_cycles >> 2; - gb->apu.apu_cycles = 0; - if (!cycles) return; + uint32_t clock_rate = GB_get_clock_rate(gb); + bool orig_force = force; - if (unlikely(gb->apu.channel_3_delayed_bugged_read)) { - gb->apu.channel_3_delayed_bugged_read = false; - gb->apu.wave_channel.current_sample_byte = - gb->io_registers[GB_IO_WAV_START + (gb->address_bus & 0xF)]; +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_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; + } + + /* 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) { + /* 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; + while (cycles_left) { + cycles_left--; + if (--gb->apu.wave_channel.bugged_read_countdown == 0) { + gb->apu.wave_channel.current_sample_byte = + gb->io_registers[GB_IO_WAV_START + (gb->address_bus & 0xF)]; + if (gb->apu.is_active[GB_WAVE]) { + update_wave_sample(gb, 0); + } + break; + } + } } bool start_ch4 = false; if (likely(!gb->stopped || GB_is_cgb(gb))) { - if (gb->apu.channel_4_dmg_delayed_start) { - if (gb->apu.channel_4_dmg_delayed_start == cycles) { - gb->apu.channel_4_dmg_delayed_start = 0; + if (gb->apu.noise_channel.dmg_delayed_start) { + if (gb->apu.noise_channel.dmg_delayed_start == cycles) { + gb->apu.noise_channel.dmg_delayed_start = 0; start_ch4 = true; } - else if (gb->apu.channel_4_dmg_delayed_start > cycles) { - gb->apu.channel_4_dmg_delayed_start -= cycles; + else if (gb->apu.noise_channel.dmg_delayed_start > cycles) { + gb->apu.noise_channel.dmg_delayed_start -= cycles; } else { /* Split it into two */ - cycles -= gb->apu.channel_4_dmg_delayed_start; - gb->apu.apu_cycles = gb->apu.channel_4_dmg_delayed_start * 4; - GB_apu_run(gb); + cycles -= gb->apu.noise_channel.dmg_delayed_start; + 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); } } @@ -668,18 +873,34 @@ void GB_apu_run(GB_gameboy_t *gb) unrolled for (unsigned i = GB_SQUARE_1; i <= GB_SQUARE_2; i++) { if (gb->apu.is_active[i]) { - uint8_t cycles_left = cycles; + 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; gb->apu.square_channels[i].current_sample_index++; gb->apu.square_channels[i].current_sample_index &= 0x7; + gb->apu.square_channels[i].sample_surpressed = false; 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, cycles - cycles_left); - 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; } @@ -688,7 +909,7 @@ void GB_apu_run(GB_gameboy_t *gb) gb->apu.wave_channel.wave_form_just_read = false; if (gb->apu.is_active[GB_WAVE]) { - uint8_t cycles_left = cycles; + uint16_t cycles_left = cycles; while (unlikely(cycles_left > gb->apu.wave_channel.sample_countdown)) { cycles_left -= gb->apu.wave_channel.sample_countdown + 1; gb->apu.wave_channel.sample_countdown = gb->apu.wave_channel.sample_length ^ 0x7FF; @@ -698,14 +919,17 @@ void GB_apu_run(GB_gameboy_t *gb) 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; gb->apu.wave_channel.wave_form_just_read = false; } } - else if (gb->apu.wave_channel.enable && gb->apu.channel_3_pulsed && gb->model < GB_MODEL_AGB) { - uint8_t cycles_left = cycles; + else if (gb->apu.wave_channel.enable && gb->apu.wave_channel.pulsed && gb->model <= GB_MODEL_CGB_E) { + uint16_t cycles_left = cycles; while (unlikely(cycles_left > gb->apu.wave_channel.sample_countdown)) { cycles_left -= gb->apu.wave_channel.sample_countdown + 1; gb->apu.wave_channel.sample_countdown = gb->apu.wave_channel.sample_length ^ 0x7FF; @@ -714,26 +938,30 @@ void GB_apu_run(GB_gameboy_t *gb) gb->io_registers[GB_IO_WAV_START + (gb->address_bus & 0xF)]; } else { - gb->apu.channel_3_delayed_bugged_read = true; + gb->apu.wave_channel.bugged_read_countdown = 1; } } if (cycles_left) { gb->apu.wave_channel.sample_countdown -= cycles_left; } + if (gb->apu.wave_channel.sample_countdown == 0) { + gb->apu.wave_channel.bugged_read_countdown = 2; + } } // The noise channel can step even if inactive on the DMG if (gb->apu.is_active[GB_NOISE] || !GB_is_cgb(gb)) { - uint8_t cycles_left = cycles; + uint16_t cycles_left = cycles; unsigned divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 2; if (!divisor) divisor = 2; if (gb->apu.noise_channel.counter_countdown == 0) { gb->apu.noise_channel.counter_countdown = divisor; } - while (unlikely(cycles_left >= gb->apu.noise_channel.counter_countdown)) { + // This while doesn't get an unlikely because the noise channel steps frequently enough + while (cycles_left >= gb->apu.noise_channel.counter_countdown) { cycles_left -= gb->apu.noise_channel.counter_countdown; - gb->apu.noise_channel.counter_countdown = divisor + gb->apu.channel_4_delta; - gb->apu.channel_4_delta = 0; + gb->apu.noise_channel.counter_countdown = divisor + gb->apu.noise_channel.delta; + gb->apu.noise_channel.delta = 0; bool old_bit = (gb->apu.noise_channel.counter >> (gb->io_registers[GB_IO_NR43] >> 4)) & 1; gb->apu.noise_channel.counter++; gb->apu.noise_channel.counter &= 0x3FFF; @@ -749,19 +977,22 @@ void GB_apu_run(GB_gameboy_t *gb) } if (cycles_left) { gb->apu.noise_channel.counter_countdown -= cycles_left; - gb->apu.channel_4_countdown_reloaded = false; + gb->apu.noise_channel.countdown_reloaded = false; } else { - gb->apu.channel_4_countdown_reloaded = true; + gb->apu.noise_channel.countdown_reloaded = true; + gb->apu_output.edge_triggered[GB_NOISE] = true; } } } if (gb->apu_output.sample_rate) { gb->apu_output.cycles_since_render += cycles; + gb->apu_output.sample_fraction += sample_fraction_multiply(gb, cycles); + assert(gb->apu_output.sample_fraction < (4 << 28)); - if (gb->apu_output.sample_cycles >= gb->apu_output.cycles_per_sample) { - gb->apu_output.sample_cycles -= gb->apu_output.cycles_per_sample; + if (gb->apu_output.sample_cycles >= clock_rate) { + gb->apu_output.sample_cycles -= clock_rate; render(gb); } } @@ -773,6 +1004,7 @@ void GB_apu_run(GB_gameboy_t *gb) 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, @@ -781,10 +1013,13 @@ 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) { + GB_apu_run(gb, true); if (reg == GB_IO_NR52) { uint8_t value = 0; for (unsigned i = 0; i < GB_N_CHANNELS; i++) { @@ -817,7 +1052,7 @@ uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg) if (!GB_is_cgb(gb) && !gb->apu.wave_channel.wave_form_just_read) { return 0xFF; } - if (gb->model == GB_MODEL_AGB) { + if (gb->model > GB_MODEL_CGB_E) { return 0xFF; } reg = GB_IO_WAV_START + gb->apu.wave_channel.current_sample_index / 2; @@ -839,7 +1074,6 @@ static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb) switch (gb->model) { /* Pre CGB revisions are assumed to be like CGB-C, A and 0 for the lack of a better guess. TODO: It could be verified with audio based test ROMs. */ -#if 0 case GB_MODEL_CGB_B: if (effective_counter & 8) { effective_counter |= 0xE; // Seems to me F under some circumstances? @@ -866,16 +1100,16 @@ static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb) effective_counter |= 0x20; } break; -#endif 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: - // case GB_MODEL_CGB_0: - // case GB_MODEL_CGB_A: + case GB_MODEL_CGB_0: + case GB_MODEL_CGB_A: case GB_MODEL_CGB_C: if (effective_counter & 8) { effective_counter |= 0xE; // Sometimes F on some instances @@ -905,7 +1139,6 @@ static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb) effective_counter |= 0x20; } break; -#if 0 case GB_MODEL_CGB_D: if (effective_counter & ((gb->io_registers[GB_IO_NR43] & 8)? 0x40 : 0x80)) { // This is so weird effective_counter |= 0xFF; @@ -926,7 +1159,6 @@ static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb) effective_counter |= 0x10; } break; -#endif case GB_MODEL_CGB_E: if (effective_counter & ((gb->io_registers[GB_IO_NR43] & 8)? 0x40 : 0x80)) { // This is so weird effective_counter |= 0xFF; @@ -935,7 +1167,8 @@ static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb) effective_counter |= 0x10; } break; - case GB_MODEL_AGB: + 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: @@ -948,8 +1181,79 @@ 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); if (!gb->apu.global_enable && reg != GB_IO_NR52 && reg < GB_IO_WAV_START && (GB_is_cgb(gb) || ( reg != GB_IO_NR11 && @@ -962,7 +1266,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END && gb->apu.is_active[GB_WAVE]) { - if ((!GB_is_cgb(gb) && !gb->apu.wave_channel.wave_form_just_read) || gb->model == GB_MODEL_AGB) { + if ((!GB_is_cgb(gb) && !gb->apu.wave_channel.wave_form_just_read) || gb->model > GB_MODEL_CGB_E) { return; } reg = GB_IO_WAV_START + gb->apu.wave_channel.current_sample_index / 2; @@ -977,7 +1281,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) /* These registers affect the output of all 4 channels (but not the output of the PCM registers).*/ /* We call update_samples with the current value so the APU output is updated with the new outputs */ for (unsigned i = GB_N_CHANNELS; i--;) { - update_sample(gb, i, gb->apu.samples[i], 0); + int8_t sample = gb->apu.samples[i]; + gb->apu.samples[i] = 0x10; // Invalidate to force update + update_sample(gb, i, sample, 0); } break; case GB_IO_NR52: { @@ -999,6 +1305,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)) { @@ -1011,9 +1318,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; @@ -1025,17 +1338,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; @@ -1045,8 +1358,8 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) else if (gb->apu.is_active[index]) { nrx2_glitch(gb, &gb->apu.square_channels[index].current_volume, value, gb->io_registers[reg], &gb->apu.square_channels[index].volume_countdown, - &gb->apu.square_envelope_clock[index]); - update_square_sample(gb, index); + &gb->apu.square_channels[index].envelope_clock); + update_square_sample(gb, index, 0); } break; @@ -1054,27 +1367,31 @@ 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: { - /* TODO: GB_MODEL_CGB_D fails channel_1_sweep_restart_2, don't forget when adding support for this revision! */ - 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->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].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; } } } @@ -1082,43 +1399,52 @@ 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_envelope_clock[index].locked = false; - gb->apu.square_envelope_clock[index].clock = false; + 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->model == GB_MODEL_CGB_E || gb->model == GB_MODEL_CGB_D) { + 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; } /* Todo: verify with the schematics what's going on in here */ else if (gb->apu.square_channels[index].sample_length == 0x7FF && old_sample_length != 0x7FF && - (gb->apu.square_channels[index].current_sample_index & 0x80)) { + (gb->apu.square_channels[index].sample_surpressed)) { extra_delay += 2; } } /* 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 started sound). The playback itself is not instant which is why we don't update the sample for other cases. */ if (gb->apu.is_active[index]) { - update_square_sample(gb, index); + update_square_sample(gb, index, 0); } gb->apu.square_channels[index].volume_countdown = gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] & 7; @@ -1126,8 +1452,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); - /* We use the highest bit in current_sample_index to mark this sample is not actually playing yet, */ - gb->apu.square_channels[index].current_sample_index |= 0x80; + 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; @@ -1135,21 +1460,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); @@ -1157,16 +1482,13 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) else { gb->apu.sweep_length_addend = 0; } - gb->apu.channel_1_restart_hold = 2 - gb->apu.lf_div + GB_is_cgb(gb) * 2; - if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { - gb->apu.channel_1_restart_hold += 2; - } + gb->apu.channel_1_restart_hold = 2 - gb->apu.lf_div + (GB_is_cgb(gb) && gb->model != GB_MODEL_CGB_D) * 2; gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7) ^ 7; } } /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ - if ((value & 0x40) && + if (((value & 0x40) || (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_B)) && // Current value is irrelevant on CGB-B and older !gb->apu.square_channels[index].length_enabled && (gb->apu.div_divider & 1) && gb->apu.square_channels[index].pulse_length) { @@ -1189,10 +1511,10 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR30: gb->apu.wave_channel.enable = value & 0x80; if (!gb->apu.wave_channel.enable) { - gb->apu.channel_3_pulsed = false; + gb->apu.wave_channel.pulsed = false; if (gb->apu.is_active[GB_WAVE]) { // Todo: I assume this happens on pre-CGB models; test this with an audible test - if (gb->apu.wave_channel.sample_countdown == 0 && gb->model < GB_MODEL_AGB) { + if (gb->apu.wave_channel.sample_countdown == 0 && gb->model <= GB_MODEL_CGB_E) { gb->apu.wave_channel.current_sample_byte = gb->io_registers[GB_IO_WAV_START + (gb->pc & 0xF)]; } else if (gb->apu.wave_channel.wave_form_just_read && gb->model <= GB_MODEL_CGB_C) { @@ -1207,7 +1529,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); } @@ -1215,12 +1537,16 @@ 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; gb->apu.wave_channel.sample_length |= (value & 7) << 8; if (value & 0x80) { - gb->apu.channel_3_pulsed = true; + gb->apu.wave_channel.pulsed = true; /* DMG bug: wave RAM gets corrupted if the channel is retriggerred 1 cycle before the APU reads from it. */ if (!GB_is_cgb(gb) && @@ -1232,10 +1558,13 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) DMG-B: Most of them behave as emulated. A few behave differently. SGB: As far as I know, all tested instances behave as emulated. MGB, SGB2: Most instances behave non-deterministically, a few behave as emulated. + + For DMG-B emulation I emulate the most common behavior, which blargg's tests expect (not my own DMG-B, which fails it) + For MGB emulation, I emulate my Game Boy Light, which happens to be deterministic. Additionally, I believe DMGs, including those we behave differently than emulated, are all deterministic. */ - if (offset < 4) { + if (offset < 4 && gb->model != GB_MODEL_MGB) { gb->io_registers[GB_IO_WAV_START] = gb->io_registers[GB_IO_WAV_START + offset]; } else { @@ -1263,7 +1592,7 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ - if ((value & 0x40) && + if (((value & 0x40) || (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_B)) && // Current value is irrelevant on CGB-B and older !gb->apu.wave_channel.length_enabled && (gb->apu.div_divider & 1) && gb->apu.wave_channel.pulse_length) { @@ -1285,7 +1614,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; } @@ -1299,9 +1628,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) else if (gb->apu.is_active[GB_NOISE]) { nrx2_glitch(gb, &gb->apu.noise_channel.current_volume, value, gb->io_registers[reg], &gb->apu.noise_channel.volume_countdown, - &gb->apu.noise_envelope_clock); + &gb->apu.noise_channel.envelope_clock); update_sample(gb, GB_NOISE, - gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_lfsr_sample ? gb->apu.noise_channel.current_volume : 0, 0); } @@ -1314,18 +1643,18 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) bool old_bit = (effective_counter >> (gb->io_registers[GB_IO_NR43] >> 4)) & 1; gb->io_registers[GB_IO_NR43] = value; bool new_bit = (effective_counter >> (gb->io_registers[GB_IO_NR43] >> 4)) & 1; - if (gb->apu.channel_4_countdown_reloaded) { + if (gb->apu.noise_channel.countdown_reloaded) { unsigned divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 2; 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.channel_4_delta = 0; + gb->apu.noise_channel.delta = 0; } /* Step LFSR */ if (new_bit && (!old_bit || gb->model <= GB_MODEL_CGB_C)) { @@ -1344,15 +1673,15 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR44: { if (value & 0x80) { - gb->apu.noise_envelope_clock.locked = false; - gb->apu.noise_envelope_clock.clock = false; + gb->apu.noise_channel.envelope_clock.locked = false; + gb->apu.noise_channel.envelope_clock.clock = false; if (!GB_is_cgb(gb) && (gb->apu.noise_channel.alignment & 3) != 0) { - gb->apu.channel_4_dmg_delayed_start = 6; + gb->apu.noise_channel.dmg_delayed_start = 6; } else { unsigned divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 2; if (!divisor) divisor = 2; - gb->apu.channel_4_delta = 0; + gb->apu.noise_channel.delta = 0; gb->apu.noise_channel.counter_countdown = divisor + 4; if (divisor == 2) { if (gb->model <= GB_MODEL_CGB_C) { @@ -1367,15 +1696,15 @@ 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) { gb->apu.noise_channel.counter_countdown -= 2; - gb->apu.channel_4_delta = 2; + gb->apu.noise_channel.delta = 2; } else { gb->apu.noise_channel.counter_countdown -= 4; @@ -1405,12 +1734,12 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) cases. */ if (gb->apu.is_active[GB_NOISE]) { update_sample(gb, GB_NOISE, - gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_lfsr_sample ? gb->apu.noise_channel.current_volume : 0, 0); } gb->apu.noise_channel.lfsr = 0; - gb->apu.current_lfsr_sample = false; + gb->apu.noise_channel.current_lfsr_sample = false; gb->apu.noise_channel.volume_countdown = gb->io_registers[GB_IO_NR42] & 7; if (!gb->apu.is_active[GB_NOISE] && (gb->io_registers[GB_IO_NR42] & 0xF8) != 0) { @@ -1450,26 +1779,43 @@ 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) { - + if (gb->apu_output.sample_rate != sample_rate) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } 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.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); + gb->apu_output.quick_fraction_multiply_cache[0] = round(sample_rate * 2.0 / GB_get_clock_rate(gb) * (1 << 28)); + for (unsigned i = 1; i < GB_QUICK_MULTIPLY_COUNT; i++) { + gb->apu_output.quick_fraction_multiply_cache[i] = gb->apu_output.quick_fraction_multiply_cache[0] * (i + 1); + } + } + else { + gb->apu_output.max_cycles_per_sample = 0x400; } - gb->apu_output.rate_set_in_clocks = false; - GB_apu_update_cycles_per_sample(gb); } void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample) { - + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) if (cycles_per_sample == 0) { GB_set_sample_rate(gb, 0); return; } - gb->apu_output.cycles_per_sample = 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.rate_set_in_clocks = true; + gb->apu_output.max_cycles_per_sample = ceil(cycles_per_sample / 4); + + gb->apu_output.quick_fraction_multiply_cache[0] = round(gb->apu_output.sample_rate * 2.0 / GB_get_clock_rate(gb) * (1 << 28)); + for (unsigned i = 1; i < GB_QUICK_MULTIPLY_COUNT; i++) { + gb->apu_output.quick_fraction_multiply_cache[i] = gb->apu_output.quick_fraction_multiply_cache[0] * (i + 1); + } +} + +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) @@ -1482,15 +1828,255 @@ void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode) gb->apu_output.highpass_mode = mode; } -void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb) -{ - if (gb->apu_output.rate_set_in_clocks) return; - if (gb->apu_output.sample_rate) { - gb->apu_output.cycles_per_sample = 2 * GB_get_clock_rate(gb) / (double)gb->apu_output.sample_rate; /* 2 * because we use 8MHz units */ - } -} - 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) { + int ret = errno ?: EIO; + fclose(gb->apu_output.output_file); + gb->apu_output.output_file = NULL; + return ret; + } + return 0; + } + case GB_AUDIO_FORMAT_WAV: { + wav_header_t header = {0,}; + if (fwrite(&header, sizeof(header), 1, gb->apu_output.output_file) != 1) { + int ret = errno ?: EIO; + fclose(gb->apu_output.output_file); + gb->apu_output.output_file = NULL; + return ret; + } + 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/bsnes/gb/Core/apu.h b/bsnes/gb/Core/apu.h index ead4088a..e2ba7d7e 100644 --- a/bsnes/gb/Core/apu.h +++ b/bsnes/gb/Core/apu.h @@ -1,24 +1,25 @@ -#ifndef apu_h -#define apu_h +#pragma once #include #include #include -#include "gb_struct_def.h" +#include +#include "defs.h" +#define GB_BAND_LIMITED_WIDTH 64 +#define GB_BAND_LIMITED_PHASES 256 + +#define GB_QUICK_MULTIPLY_COUNT 64 #ifdef GB_INTERNAL +#define GB_BAND_LIMITED_ONE 0x10000 // fixed point value equal to 1 + /* Speed = 1 / Length (in seconds) */ #define DAC_DECAY_SPEED 20000 #define DAC_ATTACK_SPEED 20000 /* Divides nicely and never overflows with 4 channels and 8 (1-8) volume levels */ -#ifdef WIIU -/* Todo: Remove this hack once https://github.com/libretro/RetroArch/issues/6252 is fixed*/ -#define MAX_CH_AMP (0xFF0 / 2) -#else #define MAX_CH_AMP 0xFF0 -#endif #define CH_STEP (MAX_CH_AMP/0xF/8) #endif @@ -26,11 +27,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 { @@ -38,19 +50,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); @@ -58,7 +71,7 @@ typedef void (*GB_sample_callback_t)(GB_gameboy_t *gb, GB_sample_t *sample); typedef struct { bool global_enable; - uint8_t apu_cycles; + uint16_t apu_cycles; uint8_t samples[GB_N_CHANNELS]; bool is_active[GB_N_CHANNELS]; @@ -70,24 +83,30 @@ 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 { uint16_t pulse_length; // Reloaded from NRX1 (xorred), in 256Hz DIV ticks uint8_t current_volume; // Reloaded from NRX2 uint8_t volume_countdown; // Reloaded from NRX2 - uint8_t current_sample_index; /* For save state compatibility, - highest bit is reused (See NR14/NR24's - write code)*/ + uint8_t current_sample_index; + bool sample_surpressed; uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length, xorred $7FF) 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 { @@ -100,9 +119,9 @@ typedef struct uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length, xorred $7FF) uint8_t current_sample_index; uint8_t current_sample_byte; // Current sample byte. - - GB_PADDING(int8_t, wave_form)[32]; bool wave_form_just_read; + bool pulsed; + uint8_t bugged_read_countdown; } wave_channel; struct { @@ -113,32 +132,26 @@ typedef struct bool narrow; uint8_t counter_countdown; // Counts from 0-7 to 0 to tick counter (Scaled from 512KHz to 2MHz) - uint8_t __padding; uint16_t counter; // A bit from this 14-bit register ticks LFSR bool length_enabled; // NR44 uint8_t alignment; // If (NR43 & 7) != 0, samples are aligned to 512KHz clock instead of // 1MHz. This variable keeps track of the alignment. - + bool current_lfsr_sample; + int8_t delta; + bool countdown_reloaded; + uint8_t dmg_delayed_start; + GB_envelope_clock_t envelope_clock; } noise_channel; - /* Todo: merge these into their structs when breaking save state compatibility */ -#define GB_SKIP_DIV_EVENT_INACTIVE 0 -#define GB_SKIP_DIV_EVENT_SKIPPED 1 -#define GB_SKIP_DIV_EVENT_SKIP 2 - uint8_t skip_div_event; - bool current_lfsr_sample; + GB_ENUM(uint8_t, { + GB_SKIP_DIV_EVENT_INACTIVE, + GB_SKIP_DIV_EVENT_SKIPPED, + GB_SKIP_DIV_EVENT_SKIP, + }) skip_div_event; uint8_t pcm_mask[2]; // For CGB-0 to CGB-C PCM read glitch - uint8_t channel_1_restart_hold; - int8_t channel_4_delta; - bool channel_4_countdown_reloaded; - uint8_t channel_4_dmg_delayed_start; - uint16_t channel1_completed_addend; - GB_envelope_clock_t square_envelope_clock[2]; - GB_envelope_clock_t noise_envelope_clock; - bool channel_3_pulsed; - bool channel_3_delayed_bugged_read; + bool apu_cycles_in_2mhz; // For compatibility with 0.16.x save states } GB_apu_t; typedef enum { @@ -148,18 +161,34 @@ 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 { + struct { + int32_t left, right; + } buffer[GB_BAND_LIMITED_WIDTH * 2], output; + uint8_t pos; + GB_sample_t input; +} GB_band_limited_t; + typedef struct { unsigned sample_rate; - double sample_cycles; // In 8 MHz units - double cycles_per_sample; + 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; - unsigned last_update[GB_N_CHANNELS]; - GB_sample_t current_sample[GB_N_CHANNELS]; - GB_sample_t summed_samples[GB_N_CHANNELS]; + uint32_t cycles_since_render; + uint32_t sample_fraction; // Counter in 1 / sample_rate, in 4.28 fixed format + uint32_t quick_fraction_multiply_cache[GB_QUICK_MULTIPLY_COUNT]; + + GB_band_limited_t band_limited[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; @@ -167,26 +196,38 @@ typedef struct { GB_sample_callback_t sample_callback; - bool rate_set_in_clocks; 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 -bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index); -void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value); -uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg); -void GB_apu_div_event(GB_gameboy_t *gb); -void GB_apu_div_secondary_event(GB_gameboy_t *gb); -void GB_apu_init(GB_gameboy_t *gb); -void GB_apu_run(GB_gameboy_t *gb); -void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb); +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); +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/bsnes/gb/Core/camera.c b/bsnes/gb/Core/camera.c index 7751f18f..d6e61447 100644 --- a/bsnes/gb/Core/camera.c +++ b/bsnes/gb/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; @@ -107,11 +110,23 @@ 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) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->camera_get_pixel_callback = callback; } void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } + 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 +140,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/bsnes/gb/Core/camera.h b/bsnes/gb/Core/camera.h index 21c69b68..3811140a 100644 --- a/bsnes/gb/Core/camera.h +++ b/bsnes/gb/Core/camera.h @@ -1,11 +1,19 @@ -#ifndef camera_h -#define camera_h +#pragma once #include -#include "gb_struct_def.h" +#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/bsnes/gb/Core/cheat_search.c b/bsnes/gb/Core/cheat_search.c new file mode 100644 index 00000000..a38d60e4 --- /dev/null +++ b/bsnes/gb/Core/cheat_search.c @@ -0,0 +1,142 @@ +#include "gb.h" + +void GB_cheat_search_reset(GB_gameboy_t *gb) +{ + if (gb->cheat_search_data) { + free(gb->cheat_search_data); + gb->cheat_search_data = NULL; + } + if (gb->cheat_search_bitmap) { + free(gb->cheat_search_bitmap); + gb->cheat_search_bitmap = NULL; + } + gb->cheat_search_count = 0; +} + +bool GB_cheat_search_filter(GB_gameboy_t *gb, const char *expression, GB_cheat_search_data_type_t data_type) +{ + GB_ASSERT_NOT_RUNNING(gb) + + // Make sure the expression is valid first + if (GB_debugger_evaluate_cheat_filter(gb, expression, NULL, 0, 0)) { + return false; + } + gb->cheat_search_data_type = data_type; + + if (gb->cheat_search_count == 0) { + GB_cheat_search_reset(gb); + gb->cheat_search_count = gb->ram_size + gb->mbc_ram_size + sizeof(gb->hram); + gb->cheat_search_data = malloc(gb->cheat_search_count); + gb->cheat_search_bitmap = malloc((gb->cheat_search_count + 7) / 8); + memset(gb->cheat_search_data, 0, gb->cheat_search_count); + memset(gb->cheat_search_bitmap, 0, (gb->cheat_search_count + 7) / 8); + } + + uint8_t mask = 1; + uint8_t *old_data = gb->cheat_search_data; + uint8_t *bitmap = gb->cheat_search_bitmap; + uint8_t *new_data = gb->ram; + + for (unsigned i = gb->ram_size + gb->mbc_ram_size + sizeof(gb->hram); i--;) { + if (*bitmap & mask) { + goto skip; + } + bool result = false; + if (data_type & GB_CHEAT_SEARCH_DATA_TYPE_16BIT) { + // The last byte of each section always fails on 16-bit searches + if ((new_data != gb->ram + gb->ram_size - 1 && + new_data != gb->mbc_ram + gb->mbc_ram_size - 1 && + new_data != gb->hram + sizeof(gb->hram) - 1)) { + uint16_t old = old_data[0] | (old_data[1] << 8); + uint16_t new = new_data[0] | (new_data[1] << 8); + if (data_type & GB_CHEAT_SEARCH_DATA_TYPE_BE_BIT) { + old = __builtin_bswap16(old); + new = __builtin_bswap16(new); + } + GB_debugger_evaluate_cheat_filter(gb, expression, &result, old, new); + } + } + else { + GB_debugger_evaluate_cheat_filter(gb, expression, &result, *old_data, *new_data); + } + if (result) { + // Filter passed, update old value + *old_data = *new_data; + if (data_type & GB_CHEAT_SEARCH_DATA_TYPE_16BIT) { + old_data[1] = new_data[1]; + } + } + else { + // Did not pass filter, remove address + *bitmap |= mask; + gb->cheat_search_count--; + } + skip:; + old_data++; + if (new_data == gb->ram + gb->ram_size - 1 && gb->mbc_ram_size) { + new_data = gb->mbc_ram; + } + else if (new_data == gb->mbc_ram + gb->mbc_ram_size - 1) { + new_data = gb->hram; + } + else { + new_data++; + } + mask <<= 1; + if (mask == 0) { + mask = 1; + bitmap++; + } + } + + return true; +} + +size_t GB_cheat_search_result_count(GB_gameboy_t *gb) +{ + return gb->cheat_search_count; +} + +void GB_cheat_search_get_results(GB_gameboy_t *gb, GB_cheat_search_result_t *results) +{ + uint8_t mask = 1; + uint8_t *old_data = gb->cheat_search_data; + uint8_t *bitmap = gb->cheat_search_bitmap; + size_t count = gb->cheat_search_count; + while (count) { + if (!(*bitmap & mask)) { + count--; + if (gb->cheat_search_data_type & GB_CHEAT_SEARCH_DATA_TYPE_16BIT) { + // Do not check for end of section, data_type is required to be the same as the last filter call + uint16_t old = old_data[0] | (old_data[1] << 8); + if (gb->cheat_search_data_type & GB_CHEAT_SEARCH_DATA_TYPE_BE_BIT) { + old = __builtin_bswap16(old); + } + results->value = old; + } + else { + results->value = *old_data; + } + size_t offset = old_data - gb->cheat_search_data; + if (offset < gb->ram_size) { + results->bank = offset / 0x1000; + results->addr = (offset & 0xfff) + (results->bank? 0xd000 : 0xc000); + } + else if (offset < gb->ram_size + gb->mbc_ram_size) { + results->addr = (offset & 0x1fff) + 0xa000; + results->bank = (offset - gb->ram_size) / 0x2000; + } + else { + results->addr = (offset & 0x7f) + 0xff80; + results->bank = 0; + } + results++; + } + old_data++; + mask <<= 1; + if (mask == 0) { + mask = 1; + bitmap++; + } + } +} diff --git a/bsnes/gb/Core/cheat_search.h b/bsnes/gb/Core/cheat_search.h new file mode 100644 index 00000000..ea7ccd89 --- /dev/null +++ b/bsnes/gb/Core/cheat_search.h @@ -0,0 +1,25 @@ +#pragma once +#ifndef GB_DISABLE_CHEAT_SEARCH +#include "defs.h" +#include +#include +#include + +typedef struct { + uint16_t addr; + uint16_t bank; + uint16_t value; +} GB_cheat_search_result_t; + +typedef enum { + GB_CHEAT_SEARCH_DATA_TYPE_8BIT = 0, + GB_CHEAT_SEARCH_DATA_TYPE_16BIT = 1, + GB_CHEAT_SEARCH_DATA_TYPE_BE_BIT = 2, // Not used alone + GB_CHEAT_SEARCH_DATA_TYPE_16BIT_BE = GB_CHEAT_SEARCH_DATA_TYPE_16BIT | GB_CHEAT_SEARCH_DATA_TYPE_BE_BIT, +} GB_cheat_search_data_type_t; + +void GB_cheat_search_reset(GB_gameboy_t *gb); +bool GB_cheat_search_filter(GB_gameboy_t *gb, const char *expression, GB_cheat_search_data_type_t data_type); +size_t GB_cheat_search_result_count(GB_gameboy_t *gb); +void GB_cheat_search_get_results(GB_gameboy_t *gb, GB_cheat_search_result_t *results); +#endif diff --git a/bsnes/gb/Core/cheats.c b/bsnes/gb/Core/cheats.c index c7c43fe3..fa959bec 100644 --- a/bsnes/gb/Core/cheats.c +++ b/bsnes/gb/Core/cheats.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include static inline uint8_t hash_addr(uint16_t addr) { @@ -30,24 +32,30 @@ static uint16_t bank_for_addr(GB_gameboy_t *gb, uint16_t addr) return 0; } -void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value) +static noinline void apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value) { - if (!gb->cheat_enabled) return; - if (!gb->boot_rom_finished) return; + if (unlikely(!gb->boot_rom_finished)) return; const GB_cheat_hash_t *hash = gb->cheat_hash[hash_addr(address)]; - if (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; - } + 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 + apply_cheat(gb, address, value); +} + bool GB_cheats_enabled(GB_gameboy_t *gb) { return gb->cheat_enabled; @@ -58,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; @@ -83,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]; @@ -115,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; } @@ -124,24 +141,30 @@ 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) +void GB_remove_all_cheats(GB_gameboy_t *gb) { + while (gb->cheats) { + GB_remove_cheat(gb, gb->cheats[0]); + } +} + +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; if (sscanf(cheat, "%02hhx%02hhx%04hx%c", &bank, &value, &address, &dummy) == 3) { - 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 == 1? GB_CHEAT_ANY_BANK : (bank & 0xF), value, 0, false, enabled); } } - /* GameGenie */ + /* Game Genie */ { char stripped_cheat[10] = {0,}; for (unsigned i = 0; i < 9 && *cheat; i++) { @@ -158,6 +181,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; @@ -166,8 +192,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) { @@ -176,15 +201,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) { @@ -194,6 +220,7 @@ void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *des } assert(cheat); + if (!cheat) return; if (cheat->address != address) { /* Remove from old bucket */ @@ -206,7 +233,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; } @@ -239,35 +266,37 @@ void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *des #define CHEAT_MAGIC 'SBCh' -void GB_load_cheats(GB_gameboy_t *gb, const char *path) +int GB_load_cheats(GB_gameboy_t *gb, const char *path, bool replace_existing) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + FILE *f = fopen(path, "rb"); if (!f) { - return; + return errno; } uint32_t magic = 0; uint32_t struct_size = 0; fread(&magic, sizeof(magic), 1, f); fread(&struct_size, sizeof(struct_size), 1, f); - if (magic != CHEAT_MAGIC && magic != __builtin_bswap32(CHEAT_MAGIC)) { + if (magic != LE32(CHEAT_MAGIC) && magic != BE32(CHEAT_MAGIC)) { GB_log(gb, "The file is not a SameBoy cheat database"); - return; + return -1; } if (struct_size != sizeof(GB_cheat_t)) { GB_log(gb, "This cheat database is not compatible with this version of SameBoy"); - return; + return -1; } // Remove all cheats first - while (gb->cheats) { - GB_remove_cheat(gb, gb->cheats[0]); + if (replace_existing) { + GB_remove_all_cheats(gb); } GB_cheat_t cheat; while (fread(&cheat, sizeof(cheat), 1, f)) { - if (magic == __builtin_bswap32(CHEAT_MAGIC)) { + if (magic != CHEAT_MAGIC) { cheat.address = __builtin_bswap16(cheat.address); cheat.bank = __builtin_bswap16(cheat.bank); } @@ -275,7 +304,7 @@ void GB_load_cheats(GB_gameboy_t *gb, const char *path) GB_add_cheat(gb, cheat.description, cheat.address, cheat.bank, cheat.value, cheat.old_value, cheat.use_old_value, cheat.enabled); } - return; + return 0; } int GB_save_cheats(GB_gameboy_t *gb, const char *path) @@ -307,7 +336,6 @@ int GB_save_cheats(GB_gameboy_t *gb, const char *path) } } - errno = 0; fclose(f); - return errno; + return 0; } diff --git a/bsnes/gb/Core/cheats.h b/bsnes/gb/Core/cheats.h index cf8aa20d..976f4c8a 100644 --- a/bsnes/gb/Core/cheats.h +++ b/bsnes/gb/Core/cheats.h @@ -1,27 +1,24 @@ -#ifndef cheats_h -#define cheats_h -#include "gb_struct_def.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); +void GB_remove_all_cheats(GB_gameboy_t *gb); bool GB_cheats_enabled(GB_gameboy_t *gb); void GB_set_cheats_enabled(GB_gameboy_t *gb, bool enabled); -void GB_load_cheats(GB_gameboy_t *gb, const char *path); +int GB_load_cheats(GB_gameboy_t *gb, const char *path, bool replace_existing); int GB_save_cheats(GB_gameboy_t *gb, const char *path); #ifdef GB_INTERNAL -#ifdef GB_DISABLE_CHEATS -#define GB_apply_cheat(...) -#else -void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value); -#endif +internal void GB_apply_cheat(GB_gameboy_t *gb, uint16_t address, uint8_t *value); #endif typedef struct { @@ -38,5 +35,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/bsnes/gb/Core/debugger.c b/bsnes/gb/Core/debugger.c index db4b02fd..2a003e72 100644 --- a/bsnes/gb/Core/debugger.c +++ b/bsnes/gb/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,19 +128,23 @@ 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, bool prefer_no_padding) { 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; } if (!symbol) { - snprintf(output, sizeof(output), "$%04x", value); + if (prefer_no_padding) { + snprintf(output, sizeof(output), "$%x", value); + } + else { + snprintf(output, sizeof(output), "$%04x", value); + } } - else if (symbol->addr == value) { if (prefer_name) { snprintf(output, sizeof(output), "%s ($%04x)", symbol->name, value); @@ -155,12 +165,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, false); 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; @@ -344,24 +362,33 @@ static struct { {"&", 1, and}, {"^", 1, xor}, {"<<", 2, shleft}, - {"<=", 3, lower_equals}, - {"<", 3, lower}, + {"<=", -2, lower_equals}, + {"<", -2, lower}, {">>", 2, shright}, - {">=", 3, greater_equals}, - {">", 3, greater}, - {"==", 3, equals}, + {">=", -2, greater_equals}, + {">", -2, greater}, + {"==", -2, equals}, {"=", -1, NULL, assign}, - {"!=", 3, different}, - {":", 4, bank}, + {"!=", -2, different}, + {":", 3, bank}, }; -value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, - size_t length, bool *error, - uint16_t *watchpoint_address, uint8_t *watchpoint_new_value); +typedef struct { + union { + uint16_t old_address; + uint16_t old_value; + }; + uint16_t new_value; + bool old_as_value; +} evaluate_conf_t; + +static value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, + size_t length, bool *error, + const evaluate_conf_t *conf); static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, size_t length, bool *error, - uint16_t *watchpoint_address, uint8_t *watchpoint_new_value) + const evaluate_conf_t *conf) { *error = false; // Strip whitespace @@ -389,7 +416,7 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, } if (string[i] == ')') depth--; } - if (depth == 0) return debugger_evaluate_lvalue(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); + if (depth == 0) return debugger_evaluate_lvalue(gb, string + 1, length - 2, error, conf); } else if (string[0] == '[' && string[length - 1] == ']') { // Attempt to strip square parentheses (memory dereference) @@ -404,7 +431,7 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, if (string[i] == ']') depth--; } if (depth == 0) { - return (lvalue_t){LVALUE_MEMORY, .memory_address = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value)}; + return (lvalue_t){LVALUE_MEMORY, .memory_address = debugger_evaluate(gb, string + 1, length - 2, error, conf)}; } } else if (string[0] == '{' && string[length - 1] == '}') { @@ -420,7 +447,7 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, if (string[i] == '}') depth--; } if (depth == 0) { - return (lvalue_t){LVALUE_MEMORY16, .memory_address = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value)}; + return (lvalue_t){LVALUE_MEMORY16, .memory_address = debugger_evaluate(gb, string + 1, length - 2, error, conf)}; } } @@ -459,9 +486,9 @@ static lvalue_t debugger_evaluate_lvalue(GB_gameboy_t *gb, const char *string, } #define ERROR ((value_t){0,}) -value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, - size_t length, bool *error, - uint16_t *watchpoint_address, uint8_t *watchpoint_new_value) +static value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, + size_t length, bool *error, + const evaluate_conf_t *conf) { /* Disable watchpoints while evaluating expressions */ uint16_t n_watchpoints = gb->n_watchpoints; @@ -496,7 +523,7 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, if (string[i] == ')') depth--; } if (depth == 0) { - ret = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); + ret = debugger_evaluate(gb, string + 1, length - 2, error, conf); goto exit; } } @@ -514,7 +541,7 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } if (depth == 0) { - value_t addr = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); + value_t addr = debugger_evaluate(gb, string + 1, length - 2, error, conf); banking_state_t state; if (addr.bank) { save_banking_state(gb, &state); @@ -541,7 +568,7 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } if (depth == 0) { - value_t addr = debugger_evaluate(gb, string + 1, length - 2, error, watchpoint_address, watchpoint_new_value); + value_t addr = debugger_evaluate(gb, string + 1, length - 2, error, conf); banking_state_t state; if (addr.bank) { save_banking_state(gb, &state); @@ -586,15 +613,15 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, } if (operator_index != -1) { unsigned right_start = (unsigned)(operator_pos + strlen(operators[operator_index].string)); - value_t right = debugger_evaluate(gb, string + right_start, length - right_start, error, watchpoint_address, watchpoint_new_value); + value_t right = debugger_evaluate(gb, string + right_start, length - right_start, error, conf); if (*error) goto exit; if (operators[operator_index].lvalue_operator) { - lvalue_t left = debugger_evaluate_lvalue(gb, string, operator_pos, error, watchpoint_address, watchpoint_new_value); + lvalue_t left = debugger_evaluate_lvalue(gb, string, operator_pos, error, conf); if (*error) goto exit; ret = operators[operator_index].lvalue_operator(gb, left, right.value); goto exit; } - value_t left = debugger_evaluate(gb, string, operator_pos, error, watchpoint_address, watchpoint_new_value); + value_t left = debugger_evaluate(gb, string, operator_pos, error, conf); if (*error) goto exit; ret = operators[operator_index].operator(left, right); goto exit; @@ -626,22 +653,22 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string, case 'p': if (string[1] == 'c') {ret = (value_t){true, bank_for_addr(gb, gb->pc), gb->pc}; goto exit;} } } - else if (length == 3) { - if (watchpoint_address && memcmp(string, "old", 3) == 0) { - ret = VALUE_16(GB_read_memory(gb, *watchpoint_address)); + else if (length == 3 && conf) { + if (memcmp(string, "old", 3) == 0) { + if (conf->old_as_value) { + ret = VALUE_16(conf->old_value); + } + else { + ret = VALUE_16(GB_read_memory(gb, conf->old_address)); + } goto exit; } - if (watchpoint_new_value && memcmp(string, "new", 3) == 0) { - ret = VALUE_16(*watchpoint_new_value); + if (memcmp(string, "new", 3) == 0) { + ret = VALUE_16(conf->new_value); goto exit; } - /* $new is identical to $old in read conditions */ - if (watchpoint_address && memcmp(string, "new", 3) == 0) { - ret = VALUE_16(GB_read_memory(gb, *watchpoint_address)); - goto exit; - } } char symbol_name[length + 1]; @@ -677,6 +704,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 +734,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 +773,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 +891,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 +905,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, false)); + GB_log(gb, "DE = %s\n", value_to_string(gb, gb->de, false, false, false)); + GB_log(gb, "HL = %s\n", value_to_string(gb, gb->hl, false, false, false)); + GB_log(gb, "SP = %s\n", value_to_string(gb, gb->sp, false, false, false)); + GB_log(gb, "PC = %s\n", value_to_string(gb, gb->pc, false, false, false)); GB_log(gb, "IME = %s\n", gb->ime? "Enabled" : "Disabled"); return true; } @@ -830,7 +919,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 +944,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 +976,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 +996,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 +1004,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; @@ -965,56 +1049,67 @@ static bool breakpoint(GB_gameboy_t *gb, char *arguments, char *modifiers, const condition += strlen(" if "); /* Verify condition is sane (Todo: This might have side effects!) */ bool error; - debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, NULL, NULL); + debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, NULL); 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); - + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL); 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); + 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 +1125,54 @@ 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; + 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)); + + 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])); + + 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 +1183,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 +1200,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 +1223,76 @@ print_usage: condition += strlen(" if "); /* Verify condition is sane (Todo: This might have side effects!) */ bool error; - /* To make $new and $old legal */ - uint16_t dummy = 0; - uint8_t dummy2 = 0; - debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, &dummy, &dummy2); + /* To make new and old legal */ + static const evaluate_conf_t conf = { + .old_as_value = true, + .old_value = 0, + .new_value = 0, + }; + debugger_evaluate(gb, condition, (unsigned)strlen(condition), &error, &conf); 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); + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, 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); + 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, + }; + + const char *flags_string = inline_const(const char *[], { + [WATCHPOINT_READ] = "read-only", + [WATCHPOINT_WRITE] = "write-only", + [WATCHPOINT_READ | WATCHPOINT_WRITE] = "read-write", + })[flags]; + + GB_log(gb, "Watchpoint %u set at %s", id, debugger_value_to_string(gb, result, true, false)); + if (length) { + GB_log(gb, " - %s%s, %s\n", debugger_value_to_string(gb, end, true, true), inclusive? " (inclusive)" : "", flags_string); } else { - gb->watchpoints[index].condition = NULL; + GB_log(gb, ", %s\n", flags_string); } - gb->n_watchpoints++; - - GB_log(gb, "Watchpoint set at %s\n", debugger_value_to_string(gb, result, true)); return true; } @@ -1228,43 +1310,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; + + 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)); + + 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])); + + 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 +1354,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 +1390,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 +1417,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).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 +1451,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)++; @@ -1375,20 +1466,26 @@ static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu return true; } - if (!modifiers || !modifiers[0]) { - modifiers = "a"; + if (!modifiers) { + modifiers = ""; } - else if (modifiers[1]) { + else if (modifiers[0] && modifiers[1]) { print_usage(gb, command); return true; } bool error; - value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + value_t result = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL); if (!error) { switch (modifiers[0]) { + case '\0': + if (!result.has_bank) { + GB_log(gb, "=%s\n", value_to_string(gb, result.value, false, false, true)); + break; + } + // fallthrough 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); @@ -1416,6 +1513,7 @@ static bool print(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu break; } default: + print_usage(gb, command); break; } } @@ -1430,7 +1528,7 @@ static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const de } bool error; - value_t addr = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + value_t addr = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL); uint16_t count = 32; if (modifiers) { @@ -1451,7 +1549,7 @@ static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const de while (count) { GB_log(gb, "%02x:%04x: ", addr.bank, addr.value); for (unsigned i = 0; i < 16 && count; i++) { - GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i)); + GB_log(gb, "%02x ", GB_safe_read_memory(gb, addr.value + i)); count--; } addr.value += 16; @@ -1464,7 +1562,7 @@ static bool examine(GB_gameboy_t *gb, char *arguments, char *modifiers, const de while (count) { GB_log(gb, "%04x: ", addr.value); for (unsigned i = 0; i < 16 && count; i++) { - GB_log(gb, "%02x ", GB_read_memory(gb, addr.value + i)); + GB_log(gb, "%02x ", GB_safe_read_memory(gb, addr.value + i)); count--; } addr.value += 16; @@ -1482,7 +1580,7 @@ static bool disassemble(GB_gameboy_t *gb, char *arguments, char *modifiers, cons } bool error; - value_t addr = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL, NULL); + value_t addr = debugger_evaluate(gb, arguments, (unsigned)strlen(arguments), &error, NULL); uint16_t count = 5; if (modifiers) { @@ -1537,15 +1635,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_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); @@ -1588,20 +1693,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; } @@ -1609,13 +1730,58 @@ 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; } +double GB_debugger_get_frame_cpu_usage(GB_gameboy_t *gb) +{ + if (gb->last_frame_busy_cycles || gb->last_frame_idle_cycles) { + return (double)gb->last_frame_busy_cycles / (gb->last_frame_busy_cycles + gb->last_frame_idle_cycles); + } + return 0; +} + +double GB_debugger_get_second_cpu_usage(GB_gameboy_t *gb) +{ + if (gb->last_second_busy_cycles || gb->last_second_idle_cycles) { + return (double)gb->last_second_busy_cycles / (gb->last_second_busy_cycles + gb->last_second_idle_cycles); + } + return 0; +} + +double GB_debugger_get_second_cpu_usage(GB_gameboy_t *gb); + +static bool usage(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->last_frame_busy_cycles || gb->last_frame_idle_cycles) { + GB_log(gb, "CPU usage (last frame): %.2f%%\n", (double)gb->last_frame_busy_cycles / (gb->last_frame_busy_cycles + gb->last_frame_idle_cycles) * 100); + } + else { + GB_log(gb, "CPU usage (last frame): N/A\n"); + } + + if (gb->last_second_busy_cycles || gb->last_second_idle_cycles) { + GB_log(gb, "CPU usage (last 60 frames): %.2f%%\n", (double)gb->last_second_busy_cycles / (gb->last_second_busy_cycles + gb->last_second_idle_cycles) * 100); + } + else { + GB_log(gb, "CPU usage (last 60 frames): N/A\n"); + } + + return true; +} static bool palettes(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { @@ -1638,9 +1804,9 @@ static bool palettes(GB_gameboy_t *gb, char *arguments, char *modifiers, const d } } - GB_log(gb, "Sprites palettes: \n"); + GB_log(gb, "Object palettes: \n"); for (unsigned i = 0; i < 32; i++) { - GB_log(gb, "%04x ", ((uint16_t *)&gb->sprite_palettes_data)[i]); + GB_log(gb, "%04x ", ((uint16_t *)&gb->object_palettes_data)[i]); if (i % 4 == 3) { GB_log(gb, "\n"); } @@ -1649,6 +1815,29 @@ static bool palettes(GB_gameboy_t *gb, char *arguments, char *modifiers, const d return true; } +static bool dma(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_is_dma_active(gb)) { + GB_log(gb, "DMA is inactive\n"); + return true; + } + + if (gb->dma_current_dest == 0xFF) { + GB_log(gb, "DMA warming up\n"); // Shouldn't actually happen, as it only lasts 2 T-cycles + return true; + } + + GB_log(gb, "Next DMA write: [$FE%02X] = [$%04X]\n", gb->dma_current_dest, gb->dma_current_src); + + return true; +} + static bool lcd(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command) { NO_MODIFIERS @@ -1657,15 +1846,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, " %s: %s\n", (gb->cgb_mode? "Sprite 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_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] & 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"}; @@ -1680,7 +1869,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) { @@ -1690,8 +1879,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); @@ -1700,6 +1894,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; } @@ -1707,54 +1912,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, @@ -1769,18 +1980,18 @@ 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], - gb->apu.square_channels[channel].current_sample_index & 0x7f, - gb->apu.square_channels[channel].current_sample_index >> 7 ? " (suppressed)" : ""); + 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)" : ""); if (channel == GB_SQUARE_1) { GB_log(gb, " Frequency sweep %s and %s\n", ((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); @@ -1794,50 +2005,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); + } } @@ -1851,7 +2065,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)++; @@ -1877,9 +2091,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) : @@ -1900,6 +2114,8 @@ static bool wave(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug static bool undo(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; @@ -1920,56 +2136,119 @@ 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 backward, 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"}, - {"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) 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}, + {"usage", 2, usage, "Display CPU usage"}, + {"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 */ @@ -1980,7 +2259,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--; @@ -2002,7 +2281,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) @@ -2025,7 +2321,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); } @@ -2037,22 +2333,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; @@ -2063,35 +2348,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; @@ -2099,102 +2367,82 @@ 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, false)); + } + else { + GB_log(gb, "Watchpoint %u: [%s] = $%02x\n", watchpoint->id, value_to_string(gb, addr, true, false, 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; + evaluate_conf_t conf = { + .old_as_value = flags == WATCHPOINT_READ, + .new_value = value, + }; + if (flags == WATCHPOINT_READ) { + conf.old_value = value; + } + else { + conf.old_address = addr; + } + bool condition = debugger_evaluate(gb, watchpoint->condition, + (unsigned)strlen(watchpoint->condition), + &error, &conf).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; } + + GB_display_sync(gb); + GB_apu_run(gb, true); char *command_string = input; char *arguments = strchr(input, ' '); @@ -2214,6 +2462,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)); @@ -2242,7 +2491,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; } } @@ -2278,7 +2527,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); } } @@ -2309,12 +2558,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); @@ -2322,47 +2569,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, 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, 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, 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)); @@ -2386,12 +2639,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; } @@ -2401,6 +2654,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) @@ -2409,13 +2678,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(); @@ -2457,7 +2732,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; @@ -2470,43 +2745,95 @@ 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 ((addr >> 12) == 0xC) { + bank = 0; + } + 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); + value_t value = debugger_evaluate(gb, string, strlen(string), &error, 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; } +#ifndef GB_DISABLE_CHEAT_SEARCH +internal bool GB_debugger_evaluate_cheat_filter(GB_gameboy_t *gb, const char *string, bool *result, uint16_t old, uint16_t new) +{ + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + + bool error = false; + evaluate_conf_t conf = { + .old_as_value = true, + .old_value = old, + .new_value = new, + }; + value_t value = debugger_evaluate(gb, string, strlen(string), &error, &conf); + if (result) { + *result = value.value; + } + + return error; +} +#endif + 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) @@ -2517,6 +2844,12 @@ 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); +} + +void GB_debugger_set_reload_callback(GB_gameboy_t *gb, GB_debugger_reload_callback_t callback) +{ + gb->debugger_reload_callback = callback; } /* Jump-to breakpoints */ @@ -2541,19 +2874,19 @@ static bool is_in_trivial_memory(uint16_t addr) return false; } -typedef uint16_t GB_opcode_address_getter_t(GB_gameboy_t *gb, uint8_t opcode); +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; } @@ -2631,7 +2964,7 @@ static uint16_t jp_hl(GB_gameboy_t *gb, uint8_t opcode) return gb->hl; } -static GB_opcode_address_getter_t *opcodes[256] = { +static opcode_address_getter_t *opcodes[256] = { /* X0 X1 X2 X3 X4 X5 X6 X7 */ /* X8 X9 Xa Xb Xc Xd Xe Xf */ trivial_1, trivial_3, trivial_1, trivial_1, trivial_1, trivial_1, trivial_2, trivial_1, /* 0X */ @@ -2668,12 +3001,12 @@ static GB_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; } @@ -2681,7 +3014,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; } @@ -2709,7 +3042,7 @@ static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *add return JUMP_TO_NONE; } - GB_opcode_address_getter_t *getter = opcodes[opcode]; + opcode_address_getter_t *getter = opcodes[opcode]; if (!getter) { gb->n_watchpoints = n_watchpoints; return JUMP_TO_NONE; @@ -2723,5 +3056,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/bsnes/gb/Core/debugger.h b/bsnes/gb/Core/debugger.h index 0678b30c..16ab4239 100644 --- a/bsnes/gb/Core/debugger.h +++ b/bsnes/gb/Core/debugger.h @@ -1,33 +1,13 @@ -#ifndef debugger_h -#define debugger_h +#pragma once +#ifndef GB_DISABLE_DEBUGGER #include #include -#include "gb_struct_def.h" +#include "defs.h" #include "symbol_hash.h" +typedef void (*GB_debugger_reload_callback_t)(GB_gameboy_t *gb); -#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 -void GB_debugger_run(GB_gameboy_t *gb); -void GB_debugger_handle_async_commands(GB_gameboy_t *gb); -void GB_debugger_call_hook(GB_gameboy_t *gb, uint16_t call_addr); -void GB_debugger_ret_hook(GB_gameboy_t *gb); -void GB_debugger_test_write_watchpoint(GB_gameboy_t *gb, uint16_t addr, uint8_t value); -void GB_debugger_test_read_watchpoint(GB_gameboy_t *gb, uint16_t addr); -const GB_bank_symbol_t *GB_debugger_find_symbol(GB_gameboy_t *gb, uint16_t addr); -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 +15,43 @@ 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 */ +void GB_debugger_set_reload_callback(GB_gameboy_t *gb, GB_debugger_reload_callback_t callback); + +double GB_debugger_get_frame_cpu_usage(GB_gameboy_t *gb); +double GB_debugger_get_second_cpu_usage(GB_gameboy_t *gb); + +#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); +#ifndef GB_DISABLE_CHEAT_SEARCH +internal bool GB_debugger_evaluate_cheat_filter(GB_gameboy_t *gb, const char *string, bool *result, uint16_t old, uint16_t new); +#endif +#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/bsnes/gb/Core/defs.h b/bsnes/gb/Core/defs.h new file mode 100644 index 00000000..5f279e23 --- /dev/null +++ b/bsnes/gb/Core/defs.h @@ -0,0 +1,67 @@ +#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__; &_;})) + +#if !defined(typeof) +#if defined(__cplusplus) || __STDC_VERSION__ < 202311 +#define typeof __typeof__ +#endif +#endif + +#ifdef GB_INTERNAL + +// "Keyword" definitions +#define likely(x) GB_likely(x) +#define unlikely(x) GB_unlikely(x) +#define inline_const GB_inline_const + +#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(); +#define nodefault default: unreachable() + +#ifdef GB_BIG_ENDIAN +#define LE16(x) __builtin_bswap16(x) +#define LE32(x) __builtin_bswap32(x) +#define LE64(x) __builtin_bswap64(x) +#define BE16(x) (x) +#define BE32(x) (x) +#define BE64(x) (x) +#else +#define LE16(x) (x) +#define LE32(x) (x) +#define LE64(x) (x) +#define BE16(x) __builtin_bswap16(x) +#define BE32(x) __builtin_bswap32(x) +#define BE64(x) __builtin_bswap64(x) +#endif +#endif + +struct GB_gameboy_s; +typedef struct GB_gameboy_s GB_gameboy_t; diff --git a/bsnes/gb/Core/display.c b/bsnes/gb/Core/display.c index aa958de4..a9c530ed 100644 --- a/bsnes/gb/Core/display.c +++ b/bsnes/gb/Core/display.c @@ -5,31 +5,100 @@ #include #include "gb.h" +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_update_dmg_palette(GB_gameboy_t *gb) +{ + const GB_palette_t *palette = gb->dmg_palette ?: &GB_PALETTE_GREY; + if (gb->rgb_encode_callback && !GB_is_cgb(gb)) { + gb->object_palettes_rgb[4] = gb->object_palettes_rgb[0] = gb->background_palettes_rgb[0] = + gb->rgb_encode_callback(gb, palette->colors[3].r, palette->colors[3].g, palette->colors[3].b); + gb->object_palettes_rgb[5] = gb->object_palettes_rgb[1] = gb->background_palettes_rgb[1] = + gb->rgb_encode_callback(gb, palette->colors[2].r, palette->colors[2].g, palette->colors[2].b); + gb->object_palettes_rgb[6] = gb->object_palettes_rgb[2] = gb->background_palettes_rgb[2] = + gb->rgb_encode_callback(gb, palette->colors[1].r, palette->colors[1].g, palette->colors[1].b); + gb->object_palettes_rgb[7] = gb->object_palettes_rgb[3] = gb->background_palettes_rgb[3] = + gb->rgb_encode_callback(gb, palette->colors[0].r, palette->colors[0].g, palette->colors[0].b); + + // LCD off color + gb->background_palettes_rgb[4] = + gb->rgb_encode_callback(gb, palette->colors[4].r, palette->colors[4].g, palette->colors[4].b); + } +} + +void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette) +{ + gb->dmg_palette = palette; + GB_update_dmg_palette(gb); +} + +const GB_palette_t *GB_get_palette(GB_gameboy_t *gb) +{ + return gb->dmg_palette; +} + +void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback) +{ + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } + gb->vblank_callback = callback; +} + +void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback) +{ + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } + + gb->rgb_encode_callback = callback; + GB_update_dmg_palette(gb); + + for (unsigned i = 0; i < 32; i++) { + GB_palette_changed(gb, true, i * 2); + GB_palette_changed(gb, false, i * 2); + } +} + +void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output) +{ + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + gb->screen = output; +} + /* FIFO functions */ 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 +106,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 +118,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; @@ -85,7 +147,7 @@ static void fifo_overlay_object_row(GB_fifo_t *fifo, uint8_t lower, uint8_t uppe /* - Each line is 456 cycles. Without scrolling, sprites or a window: + Each line is 456 cycles. Without scrolling, objects or a window: Mode 2 - 80 cycles / OAM Transfer Mode 3 - 172 cycles / Rendering Mode 0 - 204 cycles / HBlank @@ -99,19 +161,35 @@ 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; uint8_t x; uint8_t tile; uint8_t flags; -} GB_object_t; +} object_t; -static void 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; + +#ifndef GB_DISABLE_DEBUGGER + gb->last_frame_idle_cycles = gb->current_frame_idle_cycles; + gb->last_frame_busy_cycles = gb->current_frame_busy_cycles; + gb->current_frame_idle_cycles = 0; + gb->current_frame_busy_cycles = 0; + + if (gb->usage_frame_count++ == 60) { + gb->last_second_idle_cycles = gb->current_second_idle_cycles; + gb->last_second_busy_cycles = gb->current_second_busy_cycles; + gb->current_second_idle_cycles = 0; + gb->current_second_busy_cycles = 0; + gb->usage_frame_count = 0; + } +#endif /* TODO: Slow in turbo mode! */ if (GB_is_hle_sgb(gb)) { @@ -119,14 +197,30 @@ static void display_vblank(GB_gameboy_t *gb) } if (gb->turbo) { +#ifndef GB_DISABLE_DEBUGGER + if (unlikely(gb->backstep_instructions)) return; +#endif if (GB_timing_sync_turbo(gb)) { + if (gb->vblank_callback && gb->enable_skipped_frame_vblank_callbacks) { + gb->vblank_callback(gb, GB_VBLANK_TYPE_SKIPPED_FRAME); + } return; } } - 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)) { + 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; @@ -157,13 +251,19 @@ static void display_vblank(GB_gameboy_t *gb) GB_borrow_sgb_border(gb); uint32_t border_colors[16 * 4]; - if (!gb->has_sgb_border && GB_is_cgb(gb) && gb->model != GB_MODEL_AGB) { + if (!gb->has_sgb_border && GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_E) { uint16_t colors[] = { 0x2095, 0x5129, 0x1EAF, 0x1EBA, 0x4648, 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! + } + else if (gb->model == GB_MODEL_CGB_A) { + index = 0; // CGB A was only available in red! + } gb->borrowed_border.palette[0] = LE16(colors[index]); gb->borrowed_border.palette[10] = LE16(colors[5 + index]); gb->borrowed_border.palette[14] = LE16(colors[10 + index]); @@ -206,7 +306,7 @@ static void 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); } @@ -220,7 +320,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 { @@ -238,17 +338,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]; } @@ -269,48 +369,76 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border) b = scale_channel_with_curve_sgb(b); } else { - bool agb = gb->model == GB_MODEL_AGB; + bool agb = gb->model > GB_MODEL_CGB_E; r = agb? scale_channel_with_curve_agb(r) : scale_channel_with_curve(r); g = agb? scale_channel_with_curve_agb(g) : scale_channel_with_curve(g); b = agb? scale_channel_with_curve_agb(b) : scale_channel_with_curve(b); 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)); @@ -323,10 +451,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; @@ -349,17 +477,17 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border) void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index) { if (!gb->rgb_encode_callback || !GB_is_cgb(gb)) return; - uint8_t *palette_data = background_palette? gb->background_palettes_data : gb->sprite_palettes_data; + uint8_t *palette_data = background_palette? gb->background_palettes_data : gb->object_palettes_data; uint16_t color = palette_data[index & ~1] | (palette_data[index | 1] << 8); - (background_palette? gb->background_palettes_rgb : gb->sprite_palettes_rgb)[index / 2] = GB_convert_rgb15(gb, color, false); + (background_palette? gb->background_palettes_rgb : gb->object_palettes_rgb)[index / 2] = GB_convert_rgb15(gb, color, false); } void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t mode) { 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); } @@ -370,30 +498,38 @@ 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 - - */ +static void wy_check(GB_gameboy_t *gb) +{ + if (!(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) return; + + uint8_t comparison = gb->current_line; + if ((!GB_is_cgb(gb) || gb->cgb_double_speed) && gb->ly_for_comparison != (uint8_t)-1) { + comparison = gb->ly_for_comparison; + } + + if ((gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE) && + gb->io_registers[GB_IO_WY] == comparison) { + gb->wy_triggered = true; + } +} 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 */ - /* TODO: This behavior might not be correct for CGB revisions other than C and E */ - if (gb->ly_for_comparison != (uint16_t)-1 || gb->model <= GB_MODEL_CGB_C) { + if (gb->ly_for_comparison != (uint16_t)-1 || (gb->model <= GB_MODEL_CGB_C && !gb->cgb_double_speed)) { if (gb->ly_for_comparison == gb->io_registers[GB_IO_LYC]) { gb->lyc_interrupt_line = true; gb->io_registers[GB_IO_STAT] |= 4; @@ -425,20 +561,19 @@ 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 */ + if (gb->hdma_on_hblank && (gb->io_registers[GB_IO_STAT] & 3)) { + gb->hdma_on = true; + } + /* When the LCD is off, LY is 0 and STAT mode is 0. */ gb->io_registers[GB_IO_LY] = 0; gb->io_registers[GB_IO_STAT] &= ~3; - if (gb->hdma_on_hblank) { - gb->hdma_on_hblank = false; - gb->hdma_on = false; - - /* Todo: is this correct? */ - gb->hdma_steps_left = 0xff; - } + gb->oam_read_blocked = false; gb->vram_read_blocked = false; @@ -451,78 +586,125 @@ void GB_lcd_off(GB_gameboy_t *gb) gb->accessed_oam_row = -1; gb->wy_triggered = false; + + if (unlikely(gb->lcd_line_callback)) { + gb->lcd_line_callback(gb, 0); + } +} + +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? + 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 (gb->n_visible_objs == 10) return; - - /* TODO: It appears that DMA blocks PPU access to OAM, but it needs verification. */ - if (gb->dma_steps_left && (gb->dma_cycles >= 0 || gb->is_dma_restarting)) { - return; - } - - if (gb->oam_ppu_blocked) { - return; + 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 (unlikely(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) && (gb->halted || gb->stopped))) { + if (gb->model < GB_MODEL_CGB_E) { + return; + } + /* 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. */ + } + + 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 */ - GB_object_t *objects = (GB_object_t *) &gb->oam; - bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; - signed y = objects[index].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->obj_comparators[j] <= objects[index].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->obj_comparators + j + 1, gb->obj_comparators + 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->obj_comparators[j] = objects[index].x; + gb->objects_x[j] = gb->mode2_x_bus; + gb->objects_y[j] = gb->mode2_y_bus; gb->n_visible_objs++; } } -static uint8_t data_for_tile_sel_glitch(GB_gameboy_t *gb, bool *should_use) -{ - /* - Based on Matt Currie's research here: - https://github.com/mattcurrie/mealybug-tearoom-tests/blob/master/the-comprehensive-game-boy-ppu-documentation.md#tile_sel-bit-4 - */ - - *should_use = true; - if (gb->io_registers[GB_IO_LCDC] & 0x10) { - *should_use = !(gb->current_tile & 0x80); - /* if (gb->model != GB_MODEL_CGB_D) */ return gb->current_tile; - // TODO: CGB D behaves differently - } - return gb->data_for_sel_glitch; -} - - static void render_pixel_if_possible(GB_gameboy_t *gb) { - GB_fifo_item_t *fifo_item = NULL; - GB_fifo_item_t *oam_fifo_item = NULL; + const GB_fifo_item_t *fifo_item = NULL; + 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)) { - 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; } } + // (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 == (uint8_t)-17) { + gb->position_in_line = -16; + } + else if ((gb->position_in_line & 7) == (gb->io_registers[GB_IO_SCX] & 7)) { + gb->position_in_line = -8; + } + else if (gb->window_is_being_fetched && (gb->position_in_line & 7) == 6 && (gb->io_registers[GB_IO_SCX] & 7) == 7) { // TODO: Why does this happen? + gb->position_in_line = -8; + } + else if (gb->position_in_line == (uint8_t) -9) { + gb->position_in_line = -16; + return; + } + else { + gb->line_has_fractional_scrolling = true; + } + } - if (!fifo_item) return; - + gb->window_is_being_fetched = false; + /* Drop pixels for scrollings */ if (gb->position_in_line >= 160 || (gb->disable_rendering && !gb->sgb)) { gb->position_in_line++; @@ -531,7 +713,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; } @@ -539,6 +721,12 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) bg_enabled = false; } } + + if (unlikely(gb->background_disabled)) { + bg_enabled = false; + static const GB_fifo_item_t empty_item = {0,}; + fifo_item = &empty_item; + } uint8_t icd_pixel = 0; uint32_t *dest = NULL; @@ -597,7 +785,7 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) *dest = gb->rgb_encode_callback(gb, 0, 0, 0); } else { - *dest = gb->sprite_palettes_rgb[oam_fifo_item->palette * 4 + pixel]; + *dest = gb->object_palettes_rgb[oam_fifo_item->palette * 4 + pixel]; } } @@ -609,100 +797,184 @@ static void render_pixel_if_possible(GB_gameboy_t *gb) gb->position_in_line++; gb->lcd_x++; - gb->window_is_being_fetched = false; } -/* All verified CGB timings are based on CGB CPU E. CGB CPUs >= D are known to have - slightly different timings than CPUs <= C. - - Todo: Add support to CPU C and older */ +static inline void dma_sync(GB_gameboy_t *gb, unsigned *cycles) +{ + if (unlikely(GB_is_dma_active(gb))) { + unsigned offset = *cycles - gb->display_cycles; // Time passed in 8MHz ticks + if (offset) { + *cycles = gb->display_cycles; + if (!gb->cgb_double_speed) { + offset >>= 1; // Convert to T-cycles + } + unsigned old = gb->dma_cycles; + gb->dma_cycles = offset; + GB_dma_run(gb); + gb->dma_cycles = old - offset; + } + } +} static inline uint8_t fetcher_y(GB_gameboy_t *gb) { return gb->wx_triggered? gb->window_y : gb->current_line + gb->io_registers[GB_IO_SCY]; } -static void advance_fetcher_state_machine(GB_gameboy_t *gb) +static inline uint8_t vram_read(GB_gameboy_t *gb, uint16_t addr) { - typedef enum { - GB_FETCHER_GET_TILE, - GB_FETCHER_GET_TILE_DATA_LOWER, - GB_FETCHER_GET_TILE_DATA_HIGH, - GB_FETCHER_PUSH, - GB_FETCHER_SLEEP, - } fetcher_step_t; + if (unlikely(gb->vram_ppu_blocked)) { + return 0xFF; + } + if (unlikely(gb->hdma_in_progress)) { + gb->addr_for_hdma_conflict = 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? + // 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 */ + /* TODO: probably not 100% on the first few reads during halt/stop modes*/ + unsigned offset = 1 - (gb->halted || gb->stopped); + if (GB_is_cgb(gb)) { + if (gb->dma_ppu_vram_conflict) { + addr = (gb->dma_ppu_vram_conflict_addr & 0x1FFF) | (addr & 0x2000); + } + else if (gb->dma_cycles_modulo && !gb->halted && !gb->stopped) { + addr &= 0x2000; + addr |= ((gb->dma_current_src - offset) & 0x1FFF); + } + else { + addr &= 0x2000 | ((gb->dma_current_src - offset) & 0x1FFF); + gb->dma_ppu_vram_conflict_addr = addr; + gb->dma_ppu_vram_conflict = !gb->halted && !gb->stopped; + } + } + else { + addr |= ((gb->dma_current_src - offset) & 0x1FFF); + } + gb->oam[gb->dma_current_dest - offset] = gb->vram[(addr & 0x1FFF) | (gb->cgb_vram_bank? 0x2000 : 0)]; + } + return gb->vram[addr]; +} + +typedef enum { + /* VRAM reads take 2 T-cycles. In read address is determined in the first + cycle, and the read actually completes in the second cycle.*/ + GB_FETCHER_GET_TILE_T1, + GB_FETCHER_GET_TILE_T2, + GB_FETCHER_GET_TILE_DATA_LOWER_T1, + GB_FETCHER_GET_TILE_DATA_LOWER_T2, + GB_FETCHER_GET_TILE_DATA_HIGH_T1, + GB_FETCHER_GET_TILE_DATA_HIGH_T2, + GB_FETCHER_PUSH, +} fetcher_step_t; + +static uint8_t data_for_tile_sel_glitch(GB_gameboy_t *gb, bool *should_use, bool *cgb_d_glitch) +{ + /* + Based on Matt Currie's research here: + https://github.com/mattcurrie/mealybug-tearoom-tests/blob/master/the-comprehensive-game-boy-ppu-documentation.md#tile_sel-bit-4 + */ + *should_use = true; + *cgb_d_glitch = false; - static const fetcher_step_t fetcher_state_machine [8] = { - GB_FETCHER_SLEEP, - GB_FETCHER_GET_TILE, - GB_FETCHER_SLEEP, - GB_FETCHER_GET_TILE_DATA_LOWER, - GB_FETCHER_SLEEP, - GB_FETCHER_GET_TILE_DATA_HIGH, - GB_FETCHER_PUSH, - GB_FETCHER_PUSH, - }; - switch (fetcher_state_machine[gb->fetcher_state & 7]) { - case GB_FETCHER_GET_TILE: { + if (gb->last_tileset) { + 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->last_tile_data_address &= ~0x1000; + if (gb->fetcher_state == GB_FETCHER_GET_TILE_DATA_LOWER_T2) { + *cgb_d_glitch = true; + return 0; + } + return 0; + } + return gb->data_for_sel_glitch; +} + +internal void GB_update_wx_glitch(GB_gameboy_t *gb) +{ + if (!GB_is_cgb(gb)) return; + if (!(gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE) || !gb->wy_triggered) { + gb->cgb_wx_glitch = false; + return; + } + if (unlikely(gb->io_registers[GB_IO_WX] == 0)) { + // (gb->position_in_line + 16 <= 8) is (gb->position_in_line <= -8) in unsigned + gb->cgb_wx_glitch = ((uint8_t)(gb->position_in_line + 16) <= 8 || + (gb->position_in_line == (uint8_t)-7 && gb->line_has_fractional_scrolling)); + return; + } + gb->cgb_wx_glitch = (uint8_t)(gb->position_in_line + 7 + gb->window_is_being_fetched) == gb->io_registers[GB_IO_WX]; +} + +static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) +{ + switch ((fetcher_step_t)gb->fetcher_state) { + case GB_FETCHER_GET_TILE_T1: { + GB_update_wx_glitch(gb); 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; } - /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ uint8_t y = fetcher_y(gb); uint8_t x = 0; 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, - but SameBoy is affected by a change of both 7 and 6 (but not less). */ - x = ((gb->io_registers[GB_IO_SCX] + gb->position_in_line + 8) / 8) & 0x1F; + x = ((gb->io_registers[GB_IO_SCX] + gb->position_in_line + 8 - (GB_is_cgb(gb) && !gb->during_object_fetch)) / 8) & 0x1F; } if (gb->model > GB_MODEL_CGB_C) { /* This value is cached on the CGB-D and newer, so it cannot be used to mix tiles together */ gb->fetcher_y = y; } gb->last_tile_index_address = map + x + y / 8 * 32; - gb->current_tile = gb->vram[gb->last_tile_index_address]; - if (gb->vram_ppu_blocked) { - gb->current_tile = 0xFF; + } + gb->fetcher_state++; + break; + case GB_FETCHER_GET_TILE_T2: { + if (gb->cgb_wx_glitch) { + gb->fetcher_state++; + break; } + dma_sync(gb, cycles); + gb->current_tile = vram_read(gb, gb->last_tile_index_address); if (GB_is_cgb(gb)) { /* The CGB actually accesses both the tile index AND the attributes in the same T-cycle. This probably means the CGB has a 16-bit data bus for the VRAM. */ - gb->current_tile_attributes = gb->vram[gb->last_tile_index_address + 0x2000]; - if (gb->vram_ppu_blocked) { - gb->current_tile_attributes = 0xFF; - } + gb->current_tile_attributes = vram_read(gb, gb->last_tile_index_address + 0x2000); } } gb->fetcher_state++; break; - case GB_FETCHER_GET_TILE_DATA_LOWER: { - bool use_glitched = false; - if (gb->tile_sel_glitch) { - gb->current_tile_data[0] = data_for_tile_sel_glitch(gb, &use_glitched); - } + case GB_FETCHER_GET_TILE_DATA_LOWER_T1: { + GB_update_wx_glitch(gb); + uint8_t y_flip = 0; uint16_t tile_address = 0; 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) { + gb->last_tileset = gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL; + if (gb->last_tileset) { tile_address = gb->current_tile * 0x10; } else { @@ -714,36 +986,44 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) if (gb->current_tile_attributes & 0x40) { y_flip = 0x7; } - if (!use_glitched) { - gb->current_tile_data[0] = - gb->vram[tile_address + ((y & 7) ^ y_flip) * 2]; - if (gb->vram_ppu_blocked) { - gb->current_tile_data[0] = 0xFF; - } + gb->last_tile_data_address = tile_address + ((y & 7) ^ y_flip) * 2; + } + gb->fetcher_state++; + break; + + case GB_FETCHER_GET_TILE_DATA_LOWER_T2: { + if (gb->cgb_wx_glitch) { + gb->current_tile_data[0] = gb->current_tile_data[1]; + gb->fetcher_state++; + break; } - else { - gb->data_for_sel_glitch = - gb->vram[tile_address + ((y & 7) ^ y_flip) * 2]; - if (gb->vram_ppu_blocked) { - gb->data_for_sel_glitch = 0xFF; - } + dma_sync(gb, cycles); + bool use_glitched = false; + bool cgb_d_glitch = false; + if (gb->tile_sel_glitch) { + gb->current_tile_data[0] = data_for_tile_sel_glitch(gb, &use_glitched, &cgb_d_glitch); + } + if (!use_glitched) { + gb->current_tile_data[0] = vram_read(gb, gb->last_tile_data_address); + } + if (gb->last_tileset && gb->tile_sel_glitch) { + gb->data_for_sel_glitch = vram_read(gb, gb->last_tile_data_address); + } + else if (cgb_d_glitch) { + gb->data_for_sel_glitch = vram_read(gb, gb->last_tile_data_address & ~0x1000); } } gb->fetcher_state++; break; - case GB_FETCHER_GET_TILE_DATA_HIGH: { - /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */ - - bool use_glitched = false; - if (gb->tile_sel_glitch) { - gb->current_tile_data[1] = data_for_tile_sel_glitch(gb, &use_glitched); - } + case GB_FETCHER_GET_TILE_DATA_HIGH_T1: { + GB_update_wx_glitch(gb); 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) { + gb->last_tileset = gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL; + if (gb->last_tileset) { tile_address = gb->current_tile * 0x10; } else { @@ -757,99 +1037,577 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb) y_flip = 0x7; } gb->last_tile_data_address = tile_address + ((y & 7) ^ y_flip) * 2 + 1; - if (!use_glitched) { - gb->current_tile_data[1] = - gb->vram[gb->last_tile_data_address]; - if (gb->vram_ppu_blocked) { - gb->current_tile_data[1] = 0xFF; + } + gb->fetcher_state++; + break; + + case GB_FETCHER_GET_TILE_DATA_HIGH_T2: { + if (gb->cgb_wx_glitch) { + gb->current_tile_data[1] = gb->current_tile_data[0]; + gb->fetcher_state++; + if (gb->wx_triggered) { + gb->window_tile_x++; + gb->window_tile_x &= 0x1F; + } + break; + } + dma_sync(gb, cycles); + bool use_glitched = false; + bool cgb_d_glitch = false; + if (gb->tile_sel_glitch) { + gb->current_tile_data[1] = data_for_tile_sel_glitch(gb, &use_glitched, &cgb_d_glitch); + if (cgb_d_glitch) { + gb->last_tile_data_address--; } } - else { - if ((gb->io_registers[GB_IO_LCDC] & 0x10) && gb->tile_sel_glitch) { - gb->data_for_sel_glitch = gb->vram[gb->last_tile_data_address]; - if (gb->vram_ppu_blocked) { - gb->data_for_sel_glitch = 0xFF; - } - } + if (!use_glitched) { + gb->data_for_sel_glitch = gb->current_tile_data[1] = + vram_read(gb, gb->last_tile_data_address); + } + if (gb->last_tileset && gb->tile_sel_glitch) { + gb->data_for_sel_glitch = vram_read(gb, gb->last_tile_data_address); + + } + else if (cgb_d_glitch) { + gb->data_for_sel_glitch = vram_read(gb, (gb->tile_sel_glitch & ~0x1000) + 1); } } if (gb->wx_triggered) { gb->window_tile_x++; - gb->window_tile_x &= 0x1f; + gb->window_tile_x &= 0x1F; } // fallthrough + default: case GB_FETCHER_PUSH: { - if (gb->fetcher_state < 7) { - gb->fetcher_state++; - } + gb->fetcher_state = GB_FETCHER_PUSH; 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; - } - break; - - case GB_FETCHER_SLEEP: - { - gb->fetcher_state++; + gb->fetcher_state = GB_FETCHER_GET_TILE_T1; } break; } } -static uint16_t get_object_line_address(GB_gameboy_t *gb, const GB_object_t *object) +static uint16_t get_object_line_address(GB_gameboy_t *gb, uint8_t y, uint8_t tile, uint8_t flags) { - /* TODO: what does the PPU read if DMA is active? */ - if (gb->oam_ppu_blocked) { - static const GB_object_t blocked = {0xFF, 0xFF, 0xFF, 0xFF}; - object = &blocked; - } + 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); - bool height_16 = (gb->io_registers[GB_IO_LCDC] & 4) != 0; /* Todo: Which T-cycle actually reads this? */ - uint8_t tile_y = (gb->current_line - object->y) & (height_16? 0xF : 7); - - if (object->flags & 0x40) { /* Flip Y */ + if (flags & 0x40) { /* Flip Y */ tile_y ^= height_16? 0xF : 7; } /* Todo: I'm not 100% sure an access to OAM can't trigger the OAM bug while we're accessing this */ - uint16_t line_address = (height_16? object->tile & 0xFE : object->tile) * 0x10 + tile_y * 2; + uint16_t line_address = (height_16? tile & 0xFE : tile) * 0x10 + tile_y * 2; - if (gb->cgb_mode && (object->flags & 0x8)) { /* Use VRAM bank 2 */ + if (gb->cgb_mode && (flags & 0x8)) { /* Use VRAM bank 2 */ line_address += 0x2000; } return line_address; } +static inline uint8_t flip(uint8_t x) +{ + x = (x & 0xF0) >> 4 | (x & 0x0F) << 4; + x = (x & 0xCC) >> 2 | (x & 0x33) << 2; + x = (x & 0xAA) >> 1 | (x & 0x55) << 1; + return x; +} + +static inline void get_tile_data(const GB_gameboy_t *gb, uint8_t tile_x, uint8_t y, uint16_t map, uint8_t *attributes, uint8_t *data0, uint8_t *data1) +{ + uint8_t current_tile = gb->vram[map + (tile_x & 0x1F) + y / 8 * 32]; + *attributes = GB_is_cgb(gb)? gb->vram[0x2000 + map + (tile_x & 0x1F) + y / 8 * 32] : 0; + + uint16_t tile_address = 0; + + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_TILE_SEL) { + tile_address = current_tile * 0x10; + } + else { + tile_address = (int8_t)current_tile * 0x10 + 0x1000; + } + if (*attributes & 8) { + tile_address += 0x2000; + } + uint8_t y_flip = 0; + if (*attributes & 0x40) { + y_flip = 0x7; + } + + *data0 = gb->vram[tile_address + ((y & 7) ^ y_flip) * 2]; + *data1 = gb->vram[tile_address + ((y & 7) ^ y_flip) * 2 + 1]; + + if (*attributes & 0x20) { + *data0 = flip(*data0); + *data1 = flip(*data1); + } + +} + +static void render_line(GB_gameboy_t *gb) +{ + if (gb->disable_rendering) return; + if (!gb->screen) return; + if (gb->current_line > 144) return; // Corrupt save state + + struct { + unsigned pixel:2; // Color, 0-3 + unsigned priority:6; // Object priority – 0 in DMG, OAM index in CGB + unsigned palette:3; // Palette, 0 - 7 (CGB); 0-1 in DMG (or just 0 for BG) + bool bg_priority:1; // BG priority bit + } _object_buffer[160 + 16]; // allocate extra to avoid per pixel checks + 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] & 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; + const object_t *object = &objects[object_index]; + gb->n_visible_objs--; + + uint16_t line_address = get_object_line_address(gb, object->y, object->tile, object->flags); + uint8_t data0 = gb->vram[line_address]; + uint8_t data1 = gb->vram[line_address + 1]; + if (gb->n_visible_objs == 0) { + gb->data_for_sel_glitch = data1; + } + if (object->flags & 0x20) { + data0 = flip(data0); + data1 = flip(data1); + } + + typeof(_object_buffer[0]) *p = _object_buffer + object->x; + if (object->x >= 168) { + continue; + } + unrolled for (unsigned x = 0; x < 8; x++) { + unsigned pixel = (data0 >> 7) | ((data1 >> 7) << 1); + data0 <<= 1; + data1 <<= 1; + if (pixel && (!p->pixel || priority < p->priority)) { + p->pixel = pixel; + p->priority = priority; + + if (gb->cgb_mode) { + p->palette = object->flags & 0x7; + } + else { + p->palette = (object->flags & 0x10) >> 4; + } + p->bg_priority = object->flags & 0x80; + } + p++; + } + } + } + else { + object_buffer = (const void *)empty_object_buffer; + } + + + uint32_t *restrict p = gb->screen; + typeof(object_buffer[0]) *object_buffer_pointer = object_buffer + 8; + if (gb->border_mode == GB_BORDER_ALWAYS) { + p += (BORDERED_WIDTH - (WIDTH)) / 2 + BORDERED_WIDTH * (BORDERED_HEIGHT - LINES) / 2; + p += BORDERED_WIDTH * gb->current_line; + } + else { + p += WIDTH * gb->current_line; + } + + 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)) { + uint8_t pixel = object_buffer_pointer->pixel; + if (!gb->cgb_mode) { + pixel = ((gb->io_registers[GB_IO_OBP0 + object_buffer_pointer->palette] >> (pixel << 1)) & 3); + } + *(p++) = gb->object_palettes_rgb[pixel + (object_buffer_pointer->palette & 7) * 4]; + } + else { + *(p++) = bg; + } + object_buffer_pointer++; + } + return; + } + + unsigned pixels = 0; + 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] & GB_LCDC_BG_MAP) { + map = 0x1C00; + } + uint8_t y = gb->current_line + gb->io_registers[GB_IO_SCY]; + uint8_t attributes; + uint8_t data0, data1; + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + +#define DO_PIXEL() \ +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] & 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);\ + }\ + *(p++) = gb->object_palettes_rgb[pixel + (object_buffer_pointer->palette & 7) * 4];\ +}\ +else {\ + if (!gb->cgb_mode) {\ + pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3);\ + }\ + *(p++) = gb->background_palettes_rgb[pixel + (attributes & 7) * 4];\ +}\ +pixels++;\ +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] & 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] & GB_LCDC_WIN_MAP? 0x1C00 : 0x1800; + tile_x = -1; + y = ++gb->window_y; + break; + } + DO_PIXEL(); + } + tile_x++; + + while (pixels < 160 - 8) { + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + nounroll for (unsigned i = 0; i < 8; i++) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + goto activate_window; + } + DO_PIXEL(); + } + tile_x++; + } + + gb->fetcher_state = (160 - pixels) & 7; + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + while (pixels < 160) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + goto activate_window; + } + DO_PIXEL(); + } + tile_x++; + + get_tile_data(gb, tile_x, y, map, &attributes, gb->current_tile_data, gb->current_tile_data + 1); +#undef DO_PIXEL +} + +static void render_line_sgb(GB_gameboy_t *gb) +{ + if (gb->current_line > 144) return; // Corrupt save state + + struct { + unsigned pixel:2; // Color, 0-3 + unsigned palette:1; // Palette, 0 - 7 (CGB); 0-1 in DMG (or just 0 for BG) + bool bg_priority:1; // BG priority bit + } _object_buffer[160 + 16]; // allocate extra to avoid per pixel checks + 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] & 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) { + const object_t *object = &objects[gb->visible_objs[gb->n_visible_objs - 1]]; + gb->n_visible_objs--; + + uint16_t line_address = get_object_line_address(gb, object->y, object->tile, object->flags); + uint8_t data0 = gb->vram[line_address]; + uint8_t data1 = gb->vram[line_address + 1]; + if (object->flags & 0x20) { + data0 = flip(data0); + data1 = flip(data1); + } + + typeof(_object_buffer[0]) *p = _object_buffer + object->x; + if (object->x >= 168) { + continue; + } + unrolled for (unsigned x = 0; x < 8; x++) { + unsigned pixel = (data0 >> 7) | ((data1 >> 7) << 1); + data0 <<= 1; + data1 <<= 1; + if (!p->pixel) { + p->pixel = pixel; + p->palette = (object->flags & 0x10) >> 4; + p->bg_priority = object->flags & 0x80; + } + p++; + } + } + } + else { + object_buffer = (const void *)empty_object_buffer; + } + + + uint8_t *restrict p = gb->sgb->screen_buffer; + 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] & GB_LCDC_BG_EN))) { + for (unsigned i = 160; i--;) { + if (unlikely(object_buffer_pointer->pixel)) { + uint8_t pixel = object_buffer_pointer->pixel; + pixel = ((gb->io_registers[GB_IO_OBP0 + object_buffer_pointer->palette] >> (pixel << 1)) & 3); + *(p++) = pixel; + } + else { + *(p++) = gb->io_registers[GB_IO_BGP] & 3; + } + object_buffer_pointer++; + } + return; + } + + unsigned pixels = 0; + 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] & GB_LCDC_BG_MAP) { + map = 0x1C00; + } + uint8_t y = gb->current_line + gb->io_registers[GB_IO_SCY]; + uint8_t attributes; + uint8_t data0, data1; + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + +#define DO_PIXEL() \ +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] & 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;\ +}\ +else {\ + pixel = ((gb->io_registers[GB_IO_BGP] >> (pixel << 1)) & 3);\ + *(p++) = pixel;\ +}\ +pixels++;\ +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] & 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] & GB_LCDC_WIN_MAP? 0x1C00 : 0x1800; + tile_x = -1; + y = ++gb->window_y; + break; + } + DO_PIXEL(); + } + tile_x++; + + while (pixels < 160 - 8) { + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + nounroll for (unsigned i = 0; i < 8; i++) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + goto activate_window; + } + DO_PIXEL(); + } + tile_x++; + } + + get_tile_data(gb, tile_x, y, map, &attributes, &data0, &data1); + while (pixels < 160) { + if (check_window && gb->io_registers[GB_IO_WX] == pixels + 7) { + goto activate_window; + } + DO_PIXEL(); + } +} + +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->wx_triggered) return 0; + if (gb->wy_triggered) { + if (gb->io_registers[GB_IO_LCDC] & GB_LCDC_WIN_ENABLE) { + if ((gb->io_registers[GB_IO_WX] < 7 || gb->io_registers[GB_IO_WX] == 166 || gb->io_registers[GB_IO_WX] == 167)) { + 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] & GB_LCDC_WIN_ENABLE))) return 167 + (gb->io_registers[GB_IO_SCX] & 7); + + if (gb->hdma_on_hblank) return 0; + + // 300 is a bit more than the maximum Mode 3 length + + // No HBlank interrupt + if (!(gb->io_registers[GB_IO_STAT] & 0x8)) return 300; + // No STAT interrupt requested + if (!(gb->interrupt_enable & 2)) return 300; + + + 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, uint8_t cycles) +void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) { + if (gb->wy_triggered) { + gb->wy_check_scheduled = false; + } + else if (gb->wy_check_scheduled) { + force = true; + unsigned cycles_to_check; + // TODO: When speed-switching while the LCD is on, the modulo might be affected. Odd-modes are going to be fun. + if (gb->cgb_double_speed) { + cycles_to_check = (8 - ((gb->wy_check_modulo + 6) & 7)); + } + else if (GB_is_cgb(gb)) { + cycles_to_check = (8 - ((gb->wy_check_modulo + 0) & 7)); + } + else { + cycles_to_check = (8 - ((gb->wy_check_modulo + 2) & 7)); + } + + if (cycles >= cycles_to_check) { + gb->wy_check_scheduled = false; + GB_display_run(gb, cycles_to_check, true); + wy_check(gb); + if (gb->display_state == 21 && GB_is_cgb(gb) && !gb->cgb_double_speed) { + gb->wy_just_checked = true; + } + cycles -= cycles_to_check; + } + } + + 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; + + gb->frame_parity_ticks += cycles; + gb->wy_check_modulo += cycles; + + 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)) { - gb->cycles_in_stop_mode += cycles; - if (gb->cycles_in_stop_mode >= LCDC_PERIOD) { - gb->cycles_in_stop_mode -= LCDC_PERIOD; - display_vblank(gb); + if (gb->cycles_since_vblank_callback >= LCDC_PERIOD) { + GB_display_vblank(gb, GB_VBLANK_TYPE_ARTIFICIAL); } return; } - GB_object_t *objects = (GB_object_t *) &gb->oam; - - GB_STATE_MACHINE(gb, display, cycles, 2) { + + GB_BATCHABLE_STATE_MACHINE(gb, display, cycles, 2, !force) { GB_STATE(gb, display, 1); GB_STATE(gb, display, 2); - // GB_STATE(gb, display, 3); - // GB_STATE(gb, display, 4); - // GB_STATE(gb, display, 5); + GB_STATE(gb, display, 3); + GB_STATE(gb, display, 4); + GB_STATE(gb, display, 5); 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); @@ -858,18 +1616,18 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_STATE(gb, display, 15); GB_STATE(gb, display, 16); GB_STATE(gb, display, 17); - // GB_STATE(gb, display, 19); + + GB_STATE(gb, display, 19); GB_STATE(gb, display, 20); GB_STATE(gb, display, 21); GB_STATE(gb, display, 22); GB_STATE(gb, display, 23); GB_STATE(gb, display, 24); - GB_STATE(gb, display, 25); GB_STATE(gb, display, 26); GB_STATE(gb, display, 27); GB_STATE(gb, display, 28); GB_STATE(gb, display, 29); - GB_STATE(gb, display, 30); + GB_STATE(gb, display, 31); GB_STATE(gb, display, 32); GB_STATE(gb, display, 33); @@ -882,19 +1640,23 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) 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)) { + gb->wy_check_modulo = cycles; + gb->wy_just_checked = false; + + if (!(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) { while (true) { - GB_SLEEP(gb, display, 1, LCDC_PERIOD); - display_vblank(gb); - gb->cgb_repeated_a_frame = true; + if (gb->cycles_since_vblank_callback < LCDC_PERIOD) { + GB_SLEEP(gb, display, 1, LCDC_PERIOD - gb->cycles_since_vblank_callback); + } + 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); } @@ -903,6 +1665,8 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) gb->current_line = 0; gb->window_y = -1; gb->wy_triggered = false; + gb->position_in_line = -16; + gb->line_has_fractional_scrolling = false; gb->ly_for_comparison = 0; gb->io_registers[GB_IO_STAT] &= ~3; @@ -922,6 +1686,7 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) 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; @@ -940,19 +1705,64 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_SLEEP(gb, display, 37, 2); gb->cgb_palettes_blocked = true; - gb->cycles_for_line += (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C)? 2 : 3; - GB_SLEEP(gb, display, 38, (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C)? 2 : 3); + gb->cycles_for_line += 3; + GB_SLEEP(gb, display, 38, 3); gb->vram_read_blocked = true; gb->vram_write_blocked = true; 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++; + wy_check(gb); + 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; + gb->line_has_fractional_scrolling = false; + } + } + while (true) { /* Lines 0 - 143 */ - gb->window_y = -1; for (; gb->current_line < LINES; gb->current_line++) { + wy_check(gb); + + if (unlikely(gb->lcd_line_callback)) { + gb->lcd_line_callback(gb, gb->current_line); + } gb->oam_write_blocked = GB_is_cgb(gb) && !gb->cgb_double_speed; gb->accessed_oam_row = 0; @@ -962,7 +1772,7 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_SLEEP(gb, display, 6, 1); gb->io_registers[GB_IO_LY] = gb->current_line; - gb->oam_read_blocked = true; + gb->oam_read_blocked = !gb->cgb_double_speed || gb->model >= GB_MODEL_CGB_D; gb->ly_for_comparison = gb->current_line? -1 : 0; /* The OAM STAT interrupt occurs 1 T-cycle before STAT actually changes, except on line 0. @@ -977,17 +1787,23 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_STAT_update(gb); GB_SLEEP(gb, display, 7, 1); - + gb->oam_read_blocked = true; gb->io_registers[GB_IO_STAT] &= ~3; gb->io_registers[GB_IO_STAT] |= 2; gb->mode_for_interrupt = 2; gb->oam_write_blocked = true; gb->ly_for_comparison = gb->current_line; + wy_check(gb); + GB_STAT_update(gb); 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); + } for (gb->oam_search_index = 0; gb->oam_search_index < 40; gb->oam_search_index++) { if (GB_is_cgb(gb)) { add_object_from_index(gb, gb->oam_search_index); @@ -1003,11 +1819,10 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) gb->vram_write_blocked = false; gb->cgb_palettes_blocked = false; gb->oam_write_blocked = GB_is_cgb(gb); - GB_STAT_update(gb); } } 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; @@ -1021,36 +1836,39 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) GB_STAT_update(gb); - uint8_t idle_cycles = 3; - if (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C) { - idle_cycles = 2; - } - gb->cycles_for_line += idle_cycles; - GB_SLEEP(gb, display, 10, idle_cycles); + gb->cycles_for_line += 3; + GB_SLEEP(gb, display, 10, 3); gb->cgb_palettes_blocked = true; gb->cycles_for_line += 2; GB_SLEEP(gb, display, 32, 2); mode_3_start: - /* TODO: Timing seems incorrect, might need an access conflict handling. */ - if ((gb->io_registers[GB_IO_LCDC] & 0x20) && - gb->io_registers[GB_IO_WY] == gb->current_line) { - gb->wy_triggered = true; - } + gb->disable_window_pixel_insertion_glitch = false; fifo_clear(&gb->bg_fifo); 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_sprite_at_0 = (gb->io_registers[GB_IO_SCX] & 7); - /* The actual rendering cycle */ - gb->fetcher_state = 0; + gb->fetcher_state = GB_FETCHER_GET_TILE_T1; + if ((gb->mode3_batching_length = mode3_batching_length(gb))) { + GB_BATCHPOINT(gb, display, 3, gb->mode3_batching_length); + if (GB_BATCHED_CYCLES(gb, display) >= gb->mode3_batching_length) { + // Successfully batched! + gb->lcd_x = gb->position_in_line = 160; + gb->cycles_for_line += gb->mode3_batching_length; + if (gb->sgb) { + render_line_sgb(gb); + } + else { + render_line(gb); + } + GB_SLEEP(gb, display, 4, gb->mode3_batching_length); + goto skip_slow_mode_3; + } + } while (true) { /* Handle window */ /* TODO: It appears that WX checks if the window begins *next* pixel, not *this* pixel. For this reason, @@ -1058,26 +1876,28 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) has weird artifacts (It appears to activate the window during HBlank, as PPU X is temporarily 160 at 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)) { + gb->wx_166_interrupt_glitch = false; + if (unlikely(gb->wy_just_checked)) { + gb->wy_just_checked = false; + } + else 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}; - if (gb->position_in_line == scx_to_wx0_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { + if (unlikely(gb->io_registers[GB_IO_WX] == 0)) { + if (gb->position_in_line == (uint8_t)-7) { + should_activate_window = true; + } + else if (gb->position_in_line == (uint8_t)-16 && (gb->io_registers[GB_IO_SCX] & 7)) { + should_activate_window = true; + } + else if (gb->position_in_line >= (uint8_t)-15 && gb->position_in_line <= (uint8_t)-8) { 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}; - if (gb->position_in_line == scx_to_wx166_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { - should_activate_window = true; - } - } - else if (gb->io_registers[GB_IO_WX] < 166 + GB_is_cgb(gb)) { + else if (gb->io_registers[GB_IO_WX] < 166 + GB_is_cgb(gb)) { // TODO: 166 on the CGB behaves a bit weird if (gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7)) { should_activate_window = true; } - else if (gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 6) && !gb->wx_just_changed) { + else if (!GB_is_cgb(gb) && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 6) && !gb->wx_just_changed) { should_activate_window = true; /* LCD-PPU horizontal desync! It only appears to happen on DMGs, but not all of them. This doesn't seem to be CPU revision dependent, but most revisions */ @@ -1091,49 +1911,50 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) if (should_activate_window) { gb->window_y++; - /* TODO: Verify fetcher access timings in this case */ - if (gb->io_registers[GB_IO_WX] == 0 && (gb->io_registers[GB_IO_SCX] & 7)) { - gb->cycles_for_line++; - GB_SLEEP(gb, display, 42, 1); - } - gb->wx_triggered = true; gb->window_tile_x = 0; fifo_clear(&gb->bg_fifo); - gb->fetcher_state = 0; + /* TODO: Verify fetcher access timings in this case */ + if (gb->io_registers[GB_IO_WX] == 0 && (gb->io_registers[GB_IO_SCX] & 7) && !GB_is_cgb(gb)) { + gb->cycles_for_line += 1; + GB_SLEEP(gb, display, 42, 1); + } + else if (gb->io_registers[GB_IO_WX] == 166) { + gb->wx_166_interrupt_glitch = true; + } + gb->wx_triggered = true; + gb->fetcher_state = GB_FETCHER_GET_TILE_T1; gb->window_is_being_fetched = true; } else if (!GB_is_cgb(gb) && gb->io_registers[GB_IO_WX] == 166 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7)) { 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; - } - /* Handle objects */ - /* When the sprite 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. */ + if (unlikely(gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7) && + (!GB_is_cgb(gb) || gb->io_registers[GB_IO_WX] == 0) && + gb->wx_triggered && !gb->window_is_being_fetched && + gb->fetcher_state == GB_FETCHER_GET_TILE_T1 && + 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->obj_comparators[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->obj_comparators[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); + while (gb->fetcher_state < GB_FETCHER_GET_TILE_DATA_HIGH_T2 || fifo_size(&gb->bg_fifo) == 0) { + advance_fetcher_state_machine(gb, &cycles); gb->cycles_for_line++; GB_SLEEP(gb, display, 27, 1); if (gb->object_fetch_aborted) { @@ -1141,20 +1962,8 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) } } - /* Todo: Measure if penalty occurs before or after waiting for the fetcher. */ - if (gb->extra_penalty_for_sprite_at_0 != 0) { - if (gb->obj_comparators[gb->n_visible_objs - 1] == 0) { - gb->cycles_for_line += gb->extra_penalty_for_sprite_at_0; - GB_SLEEP(gb, display, 28, gb->extra_penalty_for_sprite_at_0); - gb->extra_penalty_for_sprite_at_0 = 0; - if (gb->object_fetch_aborted) { - goto abort_fetching_object; - } - } - } - /* TODO: Can this be deleted? { */ - advance_fetcher_state_machine(gb); + advance_fetcher_state_machine(gb, &cycles); gb->cycles_for_line++; GB_SLEEP(gb, display, 41, 1); if (gb->object_fetch_aborted) { @@ -1162,43 +1971,57 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles) } /* } */ - advance_fetcher_state_machine(gb); - - gb->cycles_for_line += 3; - GB_SLEEP(gb, display, 20, 3); + advance_fetcher_state_machine(gb, &cycles); + dma_sync(gb, &cycles); + 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) { goto abort_fetching_object; } - gb->object_low_line_address = get_object_line_address(gb, &objects[gb->visible_objs[gb->n_visible_objs - 1]]); + /* 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); + - gb->cycles_for_line++; - GB_SLEEP(gb, display, 39, 1); + gb->cycles_for_line += 2; + GB_SLEEP(gb, display, 39, 2); if (gb->object_fetch_aborted) { goto abort_fetching_object; } 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); - const GB_object_t *object = &objects[gb->visible_objs[gb->n_visible_objs - 1]]; - uint16_t line_address = get_object_line_address(gb, object); - - uint8_t palette = (object->flags & 0x10) ? 1 : 0; + uint8_t palette = (gb->object_flags & 0x10) ? 1 : 0; if (gb->cgb_mode) { - palette = object->flags & 0x7; + palette = gb->object_flags & 0x7; } fifo_overlay_object_row(&gb->oam_fifo, - gb->vram_ppu_blocked? 0xFF : gb->vram[gb->object_low_line_address], - gb->vram_ppu_blocked? 0xFF : gb->vram[line_address + 1], + gb->object_tile_data[0], + gb->object_tile_data[1], palette, - object->flags & 0x80, + gb->object_flags & 0x80, gb->object_priority == GB_OBJECT_PRIORITY_INDEX? gb->visible_objs[gb->n_visible_objs - 1] : 0, - object->flags & 0x20); + gb->object_flags & 0x20); - gb->data_for_sel_glitch = gb->vram_ppu_blocked? 0xFF : gb->vram[line_address + 1]; + gb->data_for_sel_glitch = gb->vram_ppu_blocked? 0xFF : gb->vram[gb->object_low_line_address + 1]; gb->n_visible_objs--; } @@ -1207,23 +2030,37 @@ abort_fetching_object: gb->during_object_fetch = false; render_pixel_if_possible(gb); - advance_fetcher_state_machine(gb); - + advance_fetcher_state_machine(gb, &cycles); if (gb->position_in_line == 160) break; + gb->cycles_for_line++; GB_SLEEP(gb, display, 21, 1); + if (unlikely(gb->wx_166_interrupt_glitch)) { + gb->mode_for_interrupt = 0; + GB_STAT_update(gb); + } } +skip_slow_mode_3: + gb->position_in_line = -16; + gb->line_has_fractional_scrolling = false; - /* TODO: Verify */ - if (gb->fetcher_state == 4 || gb->fetcher_state == 5) { - gb->data_for_sel_glitch = gb->current_tile_data[0]; + + /* TODO: Commented code seems incorrect (glitches Tesserae), verify further */ + + if (gb->fetcher_state == GB_FETCHER_GET_TILE_DATA_HIGH_T1 || + gb->fetcher_state == GB_FETCHER_GET_TILE_DATA_HIGH_T2) { + // Make sure current_tile_data[1] holds the last tile data byte read + gb->current_tile_data[1] = gb->current_tile_data[0]; + + //gb->data_for_sel_glitch = gb->current_tile_data[0]; } + /* else { gb->data_for_sel_glitch = gb->current_tile_data[1]; } - + */ 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; @@ -1231,29 +2068,29 @@ abort_fetching_object: 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) { - gb->wx166_glitch = true; + /* TODO: Verify timing { */ + if (gb->current_line == 143) { + gb->window_y = -1; + } + 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->wx_triggered = true; + gb->window_tile_x = 1; + gb->window_y++; } else { - gb->wx166_glitch = false; - } - gb->wx_triggered = false; - - if (GB_is_cgb(gb) && gb->model <= GB_MODEL_CGB_C) { - gb->cycles_for_line++; - GB_SLEEP(gb, display, 30, 1); + gb->wx_triggered = false; } + /* } */ if (!gb->cgb_double_speed) { gb->io_registers[GB_IO_STAT] &= ~3; gb->mode_for_interrupt = 0; - gb->oam_read_blocked = false; + gb->oam_read_blocked = gb->model >= GB_MODEL_CGB_D; gb->vram_read_blocked = false; gb->oam_write_blocked = false; gb->vram_write_blocked = false; @@ -1275,25 +2112,27 @@ abort_fetching_object: GB_SLEEP(gb, display, 33, 2); gb->cgb_palettes_blocked = !gb->cgb_double_speed; + if (gb->hdma_on_hblank && !gb->halted && !gb->stopped) { + gb->hdma_on = true; + } + gb->cycles_for_line += 2; GB_SLEEP(gb, display, 36, 2); gb->cgb_palettes_blocked = false; - gb->cycles_for_line += 8; - GB_SLEEP(gb, display, 25, 8); + 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; + } - if (gb->hdma_on_hblank) { - gb->hdma_starting = true; - } - GB_SLEEP(gb, display, 11, LINE_LENGTH - gb->cycles_for_line - 2); - /* - TODO: Verify double speed timing - TODO: Timing differs on a DMG - */ - if ((gb->io_registers[GB_IO_LCDC] & 0x20) && - (gb->io_registers[GB_IO_WY] == gb->current_line)) { - gb->wy_triggered = true; + { + 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); } + + gb->cycles_for_line = 0; GB_SLEEP(gb, display, 31, 2); if (gb->current_line != LINES - 1) { gb->mode_for_interrupt = 2; @@ -1302,23 +2141,30 @@ abort_fetching_object: // Todo: unverified timing gb->current_lcd_line++; if (gb->current_lcd_line == LINES && GB_is_sgb(gb)) { - display_vblank(gb); + GB_display_vblank(gb, GB_VBLANK_TYPE_NORMAL_FRAME); } if (gb->icd_hreset_callback) { gb->icd_hreset_callback(gb); } } - gb->wx166_glitch = false; /* Lines 144 - 152 */ for (; gb->current_line < VIRTUAL_LINES - 1; gb->current_line++) { - gb->io_registers[GB_IO_LY] = gb->current_line; gb->ly_for_comparison = -1; + if (unlikely(gb->lcd_line_callback)) { + gb->lcd_line_callback(gb, gb->current_line); + } + GB_STAT_update(gb); GB_SLEEP(gb, display, 26, 2); + gb->io_registers[GB_IO_LY] = gb->current_line; if (gb->current_line == LINES && !gb->stat_interrupt_line && (gb->io_registers[GB_IO_STAT] & 0x20)) { 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); @@ -1336,43 +2182,46 @@ abort_fetching_object: if (gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON) { if (GB_is_cgb(gb)) { - GB_timing_sync(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; - 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; - 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->io_registers[GB_IO_LY] = 153; gb->ly_for_comparison = -1; GB_STAT_update(gb); - GB_SLEEP(gb, display, 14, (gb->model > GB_MODEL_CGB_C)? 4: 6); + GB_SLEEP(gb, display, 19, 2); + gb->io_registers[GB_IO_LY] = 153; + GB_SLEEP(gb, display, 14, (gb->model > GB_MODEL_CGB_C)? 2: 4); - if (!GB_is_cgb(gb)) { + if (gb->model <= GB_MODEL_CGB_C && !gb->cgb_double_speed) { gb->io_registers[GB_IO_LY] = 0; } gb->ly_for_comparison = 153; @@ -1380,7 +2229,7 @@ abort_fetching_object: GB_SLEEP(gb, display, 15, (gb->model > GB_MODEL_CGB_C)? 4: 2); gb->io_registers[GB_IO_LY] = 0; - gb->ly_for_comparison = (gb->model > GB_MODEL_CGB_C)? 153 : -1; + gb->ly_for_comparison = (gb->model > GB_MODEL_CGB_C || gb->cgb_double_speed)? 153 : -1; GB_STAT_update(gb); GB_SLEEP(gb, display, 16, 4); @@ -1419,7 +2268,7 @@ void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette palette = gb->background_palettes_rgb + (4 * (palette_index & 7)); break; case GB_PALETTE_OAM: - palette = gb->sprite_palettes_rgb + (4 * (palette_index & 7)); + palette = gb->object_palettes_rgb + (4 * (palette_index & 7)); break; } @@ -1469,18 +2318,18 @@ void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette palette = gb->background_palettes_rgb + (4 * (palette_index & 7)); break; case GB_PALETTE_OAM: - palette = gb->sprite_palettes_rgb + (4 * (palette_index & 7)); + palette = gb->object_palettes_rgb + (4 * (palette_index & 7)); break; case GB_PALETTE_AUTO: 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++) { @@ -1521,31 +2370,31 @@ 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 *sprite_height) +uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *object_height) { uint8_t count = 0; - *sprite_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++) { - GB_object_t *sprite = (GB_object_t *) &gb->oam; - uint8_t sprites_in_line = 0; - for (uint8_t i = 0; i < 40; i++, sprite++) { - signed sprite_y = sprite->y - 16; - bool obscured = false; - // Is sprite not in this line? - if (sprite_y > y || sprite_y + *sprite_height <= y) continue; - if (++sprites_in_line == 11) obscured = true; + 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; + // Is object not in this line? + if (object_y > y || object_y + *object_height <= y) continue; + if (++objects_in_line == 11) obscured = true; GB_oam_info_t *info = NULL; if (!oam_to_dest_index[i]) { info = dest + count; oam_to_dest_index[i] = ++count; - info->x = sprite->x; - info->y = sprite->y; - info->tile = *sprite_height == 16? sprite->tile & 0xFE : sprite->tile; - info->flags = sprite->flags; + info->x = object->x; + info->y = object->y; + info->tile = *object_height == 16? object->tile & 0xFE : object->tile; + info->flags = object->flags; info->obscured_by_line_limit = false; - info->oam_addr = 0xFE00 + i * sizeof(*sprite); + info->oam_addr = 0xFE00 + i * sizeof(*object); } else { info = dest + oam_to_dest_index[i] - 1; @@ -1561,16 +2410,24 @@ uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_h if (GB_is_cgb(gb) && (flags & 0x8)) { vram_address += 0x2000; } + + uint8_t dmg_palette = gb->io_registers[palette? GB_IO_OBP1:GB_IO_OBP0]; + if (dmg_palette == 0xFF) { + dmg_palette = 0xFC; + } + else if (dmg_palette == 0x00) { + dmg_palette = 0x03; + } - for (unsigned y = 0; y < *sprite_height; y++) { + for (unsigned y = 0; y < *object_height; y++) { unrolled for (unsigned x = 0; x < 8; x++) { uint8_t color = (((gb->vram[vram_address ] >> ((~x)&7)) & 1 ) | ((gb->vram[vram_address + 1] >> ((~x)&7)) & 1) << 1 ); if (!gb->cgb_mode) { - color = (gb->io_registers[palette? GB_IO_OBP1:GB_IO_OBP0] >> (color << 1)) & 3; + color = (dmg_palette >> (color << 1)) & 3; } - dest[i].image[((flags & 0x20)?7-x:x) + ((flags & 0x40)?*sprite_height - 1 -y:y) * 8] = gb->sprite_palettes_rgb[palette * 4 + color]; + dest[i].image[((flags & 0x20)?7-x:x) + ((flags & 0x40)?*object_height - 1 -y:y) * 8] = gb->object_palettes_rgb[palette * 4 + color]; } vram_address += 2; } @@ -1583,3 +2440,59 @@ bool GB_is_odd_frame(GB_gameboy_t *gb) { return gb->is_odd_frame; } + +void GB_set_object_rendering_disabled(GB_gameboy_t *gb, bool disabled) +{ + gb->objects_disabled = disabled; +} + +void GB_set_background_rendering_disabled(GB_gameboy_t *gb, bool disabled) +{ + gb->background_disabled = disabled; +} + +bool GB_is_object_rendering_disabled(GB_gameboy_t *gb) +{ + return gb->objects_disabled; +} + +bool GB_is_background_rendering_disabled(GB_gameboy_t *gb) +{ + return gb->background_disabled; +} + +unsigned GB_get_screen_width(GB_gameboy_t *gb) +{ + switch (gb->border_mode) { + default: + case GB_BORDER_SGB: + return GB_is_hle_sgb(gb)? 256 : 160; + case GB_BORDER_NEVER: + return 160; + case GB_BORDER_ALWAYS: + return 256; + } +} + +unsigned GB_get_screen_height(GB_gameboy_t *gb) +{ + switch (gb->border_mode) { + default: + case GB_BORDER_SGB: + return GB_is_hle_sgb(gb)? 224 : 144; + case GB_BORDER_NEVER: + return 144; + case GB_BORDER_ALWAYS: + return 224; + } +} + +double GB_get_usual_frame_rate(GB_gameboy_t *gb) +{ + return GB_get_clock_rate(gb) / (double)LCDC_PERIOD; +} + +void GB_set_enable_skipped_frame_vblank_callbacks(GB_gameboy_t *gb, bool enable) +{ + gb->enable_skipped_frame_vblank_callbacks = enable; +} diff --git a/bsnes/gb/Core/display.h b/bsnes/gb/Core/display.h index c9411dc8..b333df0d 100644 --- a/bsnes/gb/Core/display.h +++ b/bsnes/gb/Core/display.h @@ -1,22 +1,59 @@ -#ifndef display_h -#define display_h +#pragma once #include "gb.h" #include #include +typedef struct { + struct GB_color_s { + uint8_t r, g, b; + } colors[5]; +} GB_palette_t; + +extern const GB_palette_t GB_PALETTE_GREY; +extern const GB_palette_t GB_PALETTE_DMG; +extern const GB_palette_t GB_PALETTE_MGB; +extern const GB_palette_t GB_PALETTE_GBL; + +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_SKIPPED_FRAME, // If enabled via GB_set_enable_skipped_frame_vblank_callbacks, called on skipped frames during turbo mode +} GB_vblank_type_t; + +typedef void (*GB_vblank_callback_t)(GB_gameboy_t *gb, GB_vblank_type_t type); +typedef uint32_t (*GB_rgb_encode_callback_t)(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b); + +typedef struct { + uint8_t pixel; // Color, 0-3 + uint8_t palette; // Palette, 0 - 7 (CGB); 0-1 in DMG (or just 0 for BG) + uint8_t priority; // Object priority – 0 in DMG, OAM index in CGB + 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 8 +typedef struct { + GB_fifo_item_t fifo[GB_FIFO_LENGTH]; + uint8_t read_end; + uint8_t size; +} GB_fifo_t; + #ifdef GB_INTERNAL -void GB_display_run(GB_gameboy_t *gb, uint8_t cycles); -void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index); -void GB_STAT_update(GB_gameboy_t *gb); -void GB_lcd_off(GB_gameboy_t *gb); +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, GB_vblank_type_t type); +internal void GB_update_wx_glitch(GB_gameboy_t *gb); +internal void GB_update_dmg_palette(GB_gameboy_t *gb); +#define GB_display_sync(gb) GB_display_run(gb, 0, true) enum { - GB_OBJECT_PRIORITY_UNDEFINED, // For save state compatibility GB_OBJECT_PRIORITY_X, GB_OBJECT_PRIORITY_INDEX, }; - #endif typedef enum { @@ -48,17 +85,37 @@ 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_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback); +void GB_set_enable_skipped_frame_vblank_callbacks(GB_gameboy_t *gb, bool enable); +void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_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); +void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t mode); +void GB_set_light_temperature(GB_gameboy_t *gb, double temperature); +void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output); + +unsigned GB_get_screen_width(GB_gameboy_t *gb); +unsigned GB_get_screen_height(GB_gameboy_t *gb); +double GB_get_usual_frame_rate(GB_gameboy_t *gb); + +bool GB_is_odd_frame(GB_gameboy_t *gb); +uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border); + 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 *sprite_height); -uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border); -void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t mode); -void GB_set_light_temperature(GB_gameboy_t *gb, double temperature); -bool GB_is_odd_frame(GB_gameboy_t *gb); -#endif /* display_h */ +uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *object_height); + +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); diff --git a/bsnes/gb/Core/gb.c b/bsnes/gb/Core/gb.c index a845797a..8d115d11 100644 --- a/bsnes/gb/Core/gb.c +++ b/bsnes/gb/Core/gb.c @@ -13,14 +13,9 @@ #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) +void GB_attributed_logv(GB_gameboy_t *gb, GB_log_attributes_t attributes, const char *fmt, va_list args) { + int errno_backup = errno; char *string = NULL; vasprintf(&string, fmt, args); if (string) { @@ -33,9 +28,10 @@ void GB_attributed_logv(GB_gameboy_t *gb, GB_log_attributes attributes, const ch } } free(string); + errno = errno_backup; } -void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, ...) +void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes_t attributes, const char *fmt, ...) { va_list args; va_start(args, fmt); @@ -76,7 +72,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(""); } @@ -121,10 +117,21 @@ static void load_default_border(GB_gameboy_t *gb) } #endif - if (gb->model == GB_MODEL_AGB) { + if (gb->model > GB_MODEL_CGB_E) { #include "graphics/agb_border.inc" LOAD_BORDER(); } + else if (gb->model == GB_MODEL_MGB) { + #include "graphics/mgb_border.inc" + LOAD_BORDER(); + if (gb->dmg_palette && + gb->dmg_palette->colors[4].b > gb->dmg_palette->colors[4].r) { + for (unsigned i = 0; i < 7; i++) { + gb->borrowed_border.map[13 + 24 * 32 + i] = i + 1; + gb->borrowed_border.map[13 + 25 * 32 + i] = i + 8; + } + } + } else if (GB_is_cgb(gb)) { #include "graphics/cgb_border.inc" LOAD_BORDER(); @@ -135,7 +142,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; @@ -154,14 +173,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) @@ -171,6 +194,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); @@ -184,11 +208,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); @@ -196,16 +225,24 @@ 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]); - } + GB_remove_all_cheats(gb); #endif - memset(gb, 0, sizeof(*gb)); +#ifndef GB_DISABLE_CHEAT_SEARCH + GB_cheat_search_reset(gb); +#endif + 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) @@ -269,24 +306,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); @@ -327,7 +372,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); @@ -364,13 +409,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; @@ -388,18 +438,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); } @@ -426,12 +466,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 @@ -457,6 +497,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)); @@ -476,6 +518,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)); @@ -485,11 +529,8 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) #define READ(x) if (fread(&x, sizeof(x), 1, f) != 1) goto error fread(magic, 1, sizeof(magic), f); -#ifdef GB_BIG_ENDIAN - bool extended = *(uint32_t *)&magic == 'ISX '; -#else - bool extended = *(uint32_t *)&magic == __builtin_bswap32('ISX '); -#endif + + bool extended = *(uint32_t *)&magic == BE32('ISX '); fseek(f, extended? 0x20 : 0, SEEK_SET); @@ -512,19 +553,23 @@ 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 elaborate 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); -#ifdef GB_BIG_ENDIAN - address = __builtin_bswap16(address); -#endif - address &= 0x3FFF; + address = LE16(address); + if (bank) { + address &= 0x3FFF; + } + else { + address &= 0x7FFF; + } READ(length); -#ifdef GB_BIG_ENDIAN - length = __builtin_bswap16(length); -#endif + length = LE16(length); size_t needed_size = bank * 0x4000 + address + length; if (needed_size > 1024 * 1024 * 32) goto error; @@ -545,14 +590,10 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) uint32_t length; READ(address); -#ifdef GB_BIG_ENDIAN - address = __builtin_bswap32(address); -#endif + address = LE32(address); READ(length); -#ifdef GB_BIG_ENDIAN - length = __builtin_bswap32(length); -#endif + length = LE32(length); size_t needed_size = address + length; if (needed_size > 1024 * 1024 * 32) goto error; @@ -572,30 +613,20 @@ 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); -#ifdef GB_BIG_ENDIAN - count = __builtin_bswap16(count); -#endif + count = LE16(count); while (count--) { READ(length); if (fread(name, length, 1, f) != 1) goto error; name[length] = 0; READ(flag); // unused - READ(byte); - bank = byte; - if (byte >= 0x80) { - READ(byte); - bank |= byte << 8; - } + READ(bank); READ(address); -#ifdef GB_BIG_ENDIAN - address = __builtin_bswap16(address); -#endif + address = LE16(address); GB_debugger_add_symbol(gb, bank, address, name); } break; @@ -608,9 +639,7 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) uint8_t flag; uint32_t address; READ(count); -#ifdef GB_BIG_ENDIAN - count = __builtin_bswap16(count); -#endif + count = LE16(count); while (count--) { READ(length); if (fread(name, length + 1, 1, f) != 1) goto error; @@ -618,9 +647,7 @@ int GB_load_isx(GB_gameboy_t *gb, const char *path) READ(flag); // unused READ(address); -#ifdef GB_BIG_ENDIAN - address = __builtin_bswap32(address); -#endif + address = LE32(address); // TODO: How to convert 32-bit addresses to Bank:Address? Needs to tell RAM and ROM apart } break; @@ -690,6 +717,17 @@ done:; } } + // Inject a correct checksum, the official linker doesn't always fix it, which breaks the official boot ROMs + uint8_t original_checksum = gb->rom[0x14d]; + gb->rom[0x14d] = 0; + for (unsigned addr = 0x0134; addr <= 0x014C; addr++) { + gb->rom[0x14d] -= gb->rom[addr] + 1; + } + + if (original_checksum != gb->rom[0x14d]) { + GB_log(gb, "This ROM's header checksum has been automatically corrected\n"); + } + if (old_rom) { free(old_rom); } @@ -711,19 +749,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; @@ -742,7 +775,7 @@ typedef struct { uint8_t padding4[3]; uint8_t high; uint8_t padding5[3]; -} GB_vba_rtc_time_t; +} vba_rtc_time_t; typedef struct __attribute__((packed)) { uint32_t magic; @@ -751,7 +784,7 @@ typedef struct __attribute__((packed)) { uint8_t reserved; uint64_t last_rtc_second; uint8_t rtc_data[4]; -} GB_tpp1_rtc_save_t; +} tpp1_rtc_save_t; typedef union { struct __attribute__((packed)) { @@ -760,17 +793,17 @@ typedef union { } sameboy_legacy; struct { /* Used by VBA versions with 32-bit timestamp*/ - GB_vba_rtc_time_t rtc_real, rtc_latched; + vba_rtc_time_t rtc_real, rtc_latched; uint32_t last_rtc_second; /* Always little endian */ } vba32; struct { /* Used by BGB and VBA versions with 64-bit timestamp*/ - GB_vba_rtc_time_t rtc_real, rtc_latched; + vba_rtc_time_t rtc_real, rtc_latched; uint64_t last_rtc_second; /* Always little endian */ } vba64; -} GB_rtc_save_t; +} rtc_save_t; -static void GB_fill_tpp1_save_data(GB_gameboy_t *gb, GB_tpp1_rtc_save_t *data) +static void fill_tpp1_save_data(GB_gameboy_t *gb, tpp1_rtc_save_t *data) { data->magic = BE32('TPP1'); data->version = BE16(0x100); @@ -794,15 +827,17 @@ int GB_save_battery_size(GB_gameboy_t *gb) } if (gb->cartridge_type->mbc_type == GB_TPP1) { - return gb->mbc_ram_size + sizeof(GB_tpp1_rtc_save_t); + return gb->mbc_ram_size + sizeof(tpp1_rtc_save_t); } - GB_rtc_save_t rtc_save_size; + rtc_save_t rtc_save_size; return gb->mbc_ram_size + (gb->cartridge_type->has_rtc ? sizeof(rtc_save_size.vba64) : 0); } int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size) { + 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 */ @@ -813,36 +848,25 @@ int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size) if (gb->cartridge_type->mbc_type == GB_TPP1) { buffer += gb->mbc_ram_size; - GB_tpp1_rtc_save_t rtc_save; - GB_fill_tpp1_save_data(gb, &rtc_save); + tpp1_rtc_save_t rtc_save; + fill_tpp1_save_data(gb, &rtc_save); memcpy(buffer, &rtc_save, sizeof(rtc_save)); } else if (gb->cartridge_type->mbc_type == GB_HUC3) { buffer += gb->mbc_ram_size; -#ifdef GB_BIG_ENDIAN GB_huc3_rtc_time_t rtc_save = { - __builtin_bswap64(gb->last_rtc_second), - __builtin_bswap16(gb->huc3_minutes), - __builtin_bswap16(gb->huc3_days), - __builtin_bswap16(gb->huc3_alarm_minutes), - __builtin_bswap16(gb->huc3_alarm_days), - gb->huc3_alarm_enabled, + LE64(gb->last_rtc_second), + LE16(gb->huc3.minutes), + LE16(gb->huc3.days), + LE16(gb->huc3.alarm_minutes), + LE16(gb->huc3.alarm_days), + gb->huc3.alarm_enabled, }; -#else - GB_huc3_rtc_time_t rtc_save = { - gb->last_rtc_second, - gb->huc3_minutes, - gb->huc3_days, - gb->huc3_alarm_minutes, - gb->huc3_alarm_days, - gb->huc3_alarm_enabled, - }; -#endif memcpy(buffer, &rtc_save, sizeof(rtc_save)); } else if (gb->cartridge_type->has_rtc) { - GB_rtc_save_t rtc_save = {{{{0,}},},}; + rtc_save_t rtc_save = {{{{0,}},},}; rtc_save.vba64.rtc_real.seconds = gb->rtc_real.seconds; rtc_save.vba64.rtc_real.minutes = gb->rtc_real.minutes; rtc_save.vba64.rtc_real.hours = gb->rtc_real.hours; @@ -853,20 +877,17 @@ int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size) rtc_save.vba64.rtc_latched.hours = gb->rtc_latched.hours; rtc_save.vba64.rtc_latched.days = gb->rtc_latched.days; rtc_save.vba64.rtc_latched.high = gb->rtc_latched.high; -#ifdef GB_BIG_ENDIAN - rtc_save.vba64.last_rtc_second = __builtin_bswap64(time(NULL)); -#else - rtc_save.vba64.last_rtc_second = time(NULL); -#endif + rtc_save.vba64.last_rtc_second = LE64(time(NULL)); memcpy(buffer + gb->mbc_ram_size, &rtc_save.vba64, sizeof(rtc_save.vba64)); } - errno = 0; - return errno; + return 0; } 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 */ @@ -881,8 +902,8 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) return EIO; } if (gb->cartridge_type->mbc_type == GB_TPP1) { - GB_tpp1_rtc_save_t rtc_save; - GB_fill_tpp1_save_data(gb, &rtc_save); + tpp1_rtc_save_t rtc_save; + fill_tpp1_save_data(gb, &rtc_save); if (fwrite(&rtc_save, sizeof(rtc_save), 1, f) != 1) { fclose(f); @@ -890,25 +911,14 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) } } else if (gb->cartridge_type->mbc_type == GB_HUC3) { -#ifdef GB_BIG_ENDIAN GB_huc3_rtc_time_t rtc_save = { - __builtin_bswap64(gb->last_rtc_second), - __builtin_bswap16(gb->huc3_minutes), - __builtin_bswap16(gb->huc3_days), - __builtin_bswap16(gb->huc3_alarm_minutes), - __builtin_bswap16(gb->huc3_alarm_days), - gb->huc3_alarm_enabled, + LE64(gb->last_rtc_second), + LE16(gb->huc3.minutes), + LE16(gb->huc3.days), + LE16(gb->huc3.alarm_minutes), + LE16(gb->huc3.alarm_days), + gb->huc3.alarm_enabled, }; -#else - GB_huc3_rtc_time_t rtc_save = { - gb->last_rtc_second, - gb->huc3_minutes, - gb->huc3_days, - gb->huc3_alarm_minutes, - gb->huc3_alarm_days, - gb->huc3_alarm_enabled, - }; -#endif if (fwrite(&rtc_save, sizeof(rtc_save), 1, f) != 1) { fclose(f); @@ -916,7 +926,7 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) } } else if (gb->cartridge_type->has_rtc) { - GB_rtc_save_t rtc_save = {{{{0,}},},}; + rtc_save_t rtc_save = {{{{0,}},},}; rtc_save.vba64.rtc_real.seconds = gb->rtc_real.seconds; rtc_save.vba64.rtc_real.minutes = gb->rtc_real.minutes; rtc_save.vba64.rtc_real.hours = gb->rtc_real.hours; @@ -927,11 +937,7 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) rtc_save.vba64.rtc_latched.hours = gb->rtc_latched.hours; rtc_save.vba64.rtc_latched.days = gb->rtc_latched.days; rtc_save.vba64.rtc_latched.high = gb->rtc_latched.high; -#ifdef GB_BIG_ENDIAN - rtc_save.vba64.last_rtc_second = __builtin_bswap64(time(NULL)); -#else - rtc_save.vba64.last_rtc_second = time(NULL); -#endif + rtc_save.vba64.last_rtc_second = LE64(time(NULL)); if (fwrite(&rtc_save.vba64, 1, sizeof(rtc_save.vba64), f) != sizeof(rtc_save.vba64)) { fclose(f); return EIO; @@ -939,12 +945,11 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path) } - errno = 0; fclose(f); - return errno; + return 0; } -static void GB_load_tpp1_save_data(GB_gameboy_t *gb, const GB_tpp1_rtc_save_t *data) +static void load_tpp1_save_data(GB_gameboy_t *gb, const tpp1_rtc_save_t *data) { gb->last_rtc_second = LE64(data->last_rtc_second); unrolled for (unsigned i = 4; i--;) { @@ -954,19 +959,21 @@ static void GB_load_tpp1_save_data(GB_gameboy_t *gb, const GB_tpp1_rtc_save_t *d 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; } if (gb->cartridge_type->mbc_type == GB_TPP1) { - GB_tpp1_rtc_save_t rtc_save; + tpp1_rtc_save_t rtc_save; if (size - gb->mbc_ram_size < sizeof(rtc_save)) { goto reset_rtc; } memcpy(&rtc_save, buffer + gb->mbc_ram_size, sizeof(rtc_save)); - GB_load_tpp1_save_data(gb, &rtc_save); + load_tpp1_save_data(gb, &rtc_save); if (gb->last_rtc_second > time(NULL)) { /* We must reset RTC here, or it will not advance. */ @@ -981,21 +988,12 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t goto reset_rtc; } memcpy(&rtc_save, buffer + gb->mbc_ram_size, sizeof(rtc_save)); -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap64(rtc_save.last_rtc_second); - gb->huc3_minutes = __builtin_bswap16(rtc_save.minutes); - gb->huc3_days = __builtin_bswap16(rtc_save.days); - gb->huc3_alarm_minutes = __builtin_bswap16(rtc_save.alarm_minutes); - gb->huc3_alarm_days = __builtin_bswap16(rtc_save.alarm_days); - gb->huc3_alarm_enabled = rtc_save.alarm_enabled; -#else - gb->last_rtc_second = rtc_save.last_rtc_second; - gb->huc3_minutes = rtc_save.minutes; - gb->huc3_days = rtc_save.days; - gb->huc3_alarm_minutes = rtc_save.alarm_minutes; - gb->huc3_alarm_days = rtc_save.alarm_days; - gb->huc3_alarm_enabled = rtc_save.alarm_enabled; -#endif + gb->last_rtc_second = LE64(rtc_save.last_rtc_second); + gb->huc3.minutes = LE16(rtc_save.minutes); + gb->huc3.days = LE16(rtc_save.days); + gb->huc3.alarm_minutes = LE16(rtc_save.alarm_minutes); + gb->huc3.alarm_days = LE16(rtc_save.alarm_days); + gb->huc3.alarm_enabled = rtc_save.alarm_enabled; if (gb->last_rtc_second > time(NULL)) { /* We must reset RTC here, or it will not advance. */ goto reset_rtc; @@ -1003,7 +1001,7 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t return; } - GB_rtc_save_t rtc_save; + rtc_save_t rtc_save; memcpy(&rtc_save, buffer + gb->mbc_ram_size, MIN(sizeof(rtc_save), size)); switch (size - gb->mbc_ram_size) { case sizeof(rtc_save.sameboy_legacy): @@ -1023,11 +1021,7 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t gb->rtc_latched.hours = rtc_save.vba32.rtc_latched.hours; gb->rtc_latched.days = rtc_save.vba32.rtc_latched.days; gb->rtc_latched.high = rtc_save.vba32.rtc_latched.high; -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap32(rtc_save.vba32.last_rtc_second); -#else - gb->last_rtc_second = rtc_save.vba32.last_rtc_second; -#endif + gb->last_rtc_second = LE32(rtc_save.vba32.last_rtc_second); break; case sizeof(rtc_save.vba64): @@ -1041,11 +1035,7 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t gb->rtc_latched.hours = rtc_save.vba64.rtc_latched.hours; gb->rtc_latched.days = rtc_save.vba64.rtc_latched.days; gb->rtc_latched.high = rtc_save.vba64.rtc_latched.high; -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap64(rtc_save.vba64.last_rtc_second); -#else - gb->last_rtc_second = rtc_save.vba64.last_rtc_second; -#endif + gb->last_rtc_second = LE64(rtc_save.vba64.last_rtc_second); break; default: @@ -1061,23 +1051,29 @@ 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. */ - gb->huc3_days = 0xFFFF; - gb->huc3_minutes = 0xFFF; - gb->huc3_alarm_enabled = false; + if (gb->cartridge_type->mbc_type == GB_HUC3) { + gb->huc3.days = 0xFFFF; + gb->huc3.minutes = 0xFFF; + gb->huc3.alarm_enabled = false; + } exit: return; } /* Loading will silently stop if the format is incomplete */ -void GB_load_battery(GB_gameboy_t *gb, const char *path) +int 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; + return errno; } if (fread(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) { @@ -1085,18 +1081,18 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) } if (gb->cartridge_type->mbc_type == GB_TPP1) { - GB_tpp1_rtc_save_t rtc_save; + tpp1_rtc_save_t rtc_save; if (fread(&rtc_save, sizeof(rtc_save), 1, f) != 1) { goto reset_rtc; } - GB_load_tpp1_save_data(gb, &rtc_save); + load_tpp1_save_data(gb, &rtc_save); if (gb->last_rtc_second > time(NULL)) { /* We must reset RTC here, or it will not advance. */ goto reset_rtc; } - return; + return 0; } if (gb->cartridge_type->mbc_type == GB_HUC3) { @@ -1104,29 +1100,21 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) if (fread(&rtc_save, sizeof(rtc_save), 1, f) != 1) { goto reset_rtc; } -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap64(rtc_save.last_rtc_second); - gb->huc3_minutes = __builtin_bswap16(rtc_save.minutes); - gb->huc3_days = __builtin_bswap16(rtc_save.days); - gb->huc3_alarm_minutes = __builtin_bswap16(rtc_save.alarm_minutes); - gb->huc3_alarm_days = __builtin_bswap16(rtc_save.alarm_days); - gb->huc3_alarm_enabled = rtc_save.alarm_enabled; -#else - gb->last_rtc_second = rtc_save.last_rtc_second; - gb->huc3_minutes = rtc_save.minutes; - gb->huc3_days = rtc_save.days; - gb->huc3_alarm_minutes = rtc_save.alarm_minutes; - gb->huc3_alarm_days = rtc_save.alarm_days; - gb->huc3_alarm_enabled = rtc_save.alarm_enabled; -#endif + gb->last_rtc_second = LE64(rtc_save.last_rtc_second); + gb->huc3.minutes = LE16(rtc_save.minutes); + gb->huc3.days = LE16(rtc_save.days); + gb->huc3.alarm_minutes = LE16(rtc_save.alarm_minutes); + gb->huc3.alarm_days = LE16(rtc_save.alarm_days); + gb->huc3.alarm_enabled = rtc_save.alarm_enabled; + if (gb->last_rtc_second > time(NULL)) { /* We must reset RTC here, or it will not advance. */ goto reset_rtc; } - return; + return 0; } - GB_rtc_save_t rtc_save; + rtc_save_t rtc_save; switch (fread(&rtc_save, 1, sizeof(rtc_save), f)) { case sizeof(rtc_save.sameboy_legacy): memcpy(&gb->rtc_real, &rtc_save.sameboy_legacy.rtc_real, sizeof(gb->rtc_real)); @@ -1145,11 +1133,7 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) gb->rtc_latched.hours = rtc_save.vba32.rtc_latched.hours; gb->rtc_latched.days = rtc_save.vba32.rtc_latched.days; gb->rtc_latched.high = rtc_save.vba32.rtc_latched.high; -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap32(rtc_save.vba32.last_rtc_second); -#else - gb->last_rtc_second = rtc_save.vba32.last_rtc_second; -#endif + gb->last_rtc_second = LE32(rtc_save.vba32.last_rtc_second); break; case sizeof(rtc_save.vba64): @@ -1163,11 +1147,7 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path) gb->rtc_latched.hours = rtc_save.vba64.rtc_latched.hours; gb->rtc_latched.days = rtc_save.vba64.rtc_latched.days; gb->rtc_latched.high = rtc_save.vba64.rtc_latched.high; -#ifdef GB_BIG_ENDIAN - gb->last_rtc_second = __builtin_bswap64(rtc_save.vba64.last_rtc_second); -#else - gb->last_rtc_second = rtc_save.vba64.last_rtc_second; -#endif + gb->last_rtc_second = LE64(rtc_save.vba64.last_rtc_second); break; default: @@ -1183,40 +1163,56 @@ 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. */ - gb->huc3_days = 0xFFFF; - gb->huc3_minutes = 0xFFF; - gb->huc3_alarm_enabled = false; + if (gb->cartridge_type->mbc_type == GB_HUC3) { + gb->huc3.days = 0xFFFF; + gb->huc3.minutes = 0xFFF; + gb->huc3.alarm_enabled = false; + } exit: fclose(f); - return; + return 0; } -uint8_t GB_run(GB_gameboy_t *gb) +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_display_run(gb, 228); + + 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_update_faux_analog(gb); 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; } return gb->cycles_since_run; } @@ -1226,8 +1222,10 @@ uint64_t GB_run_frame(GB_gameboy_t *gb) /* Configure turbo temporarily, the user wants to handle FPS capping manually. */ bool old_turbo = gb->turbo; bool old_dont_skip = gb->turbo_dont_skip; + double old_turbo_cap = gb->turbo_cap_multiplier; gb->turbo = true; gb->turbo_dont_skip = true; + gb->turbo_cap_multiplier = 0; gb->cycles_since_last_sync = 0; while (true) { @@ -1238,27 +1236,29 @@ uint64_t GB_run_frame(GB_gameboy_t *gb) } gb->turbo = old_turbo; gb->turbo_dont_skip = old_dont_skip; + gb->turbo_cap_multiplier = old_turbo_cap; return gb->cycles_since_last_sync * 1000000000LL / 2 / GB_get_clock_rate(gb); /* / 2 because we use 8MHz units */ } -void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output) +uint32_t *GB_get_pixels_output(GB_gameboy_t *gb) { - gb->screen = output; -} - -void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback) -{ - gb->vblank_callback = callback; + return gb->screen; } void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->log_callback = callback; } void GB_set_input_callback(GB_gameboy_t *gb, GB_input_callback_t callback) { #ifndef GB_DISABLE_DEBUGGER + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } if (gb->input_callback == default_input_callback) { gb->async_input_callback = NULL; } @@ -1269,54 +1269,42 @@ 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) { #ifndef GB_DISABLE_DEBUGGER + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->async_input_callback = callback; #endif } -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) +void GB_set_execution_callback(GB_gameboy_t *gb, GB_execution_callback_t callback) { - const GB_palette_t *palette = gb->dmg_palette ?: &GB_PALETTE_GREY; - if (gb->rgb_encode_callback && !GB_is_cgb(gb)) { - gb->sprite_palettes_rgb[4] = gb->sprite_palettes_rgb[0] = gb->background_palettes_rgb[0] = - gb->rgb_encode_callback(gb, palette->colors[3].r, palette->colors[3].g, palette->colors[3].b); - gb->sprite_palettes_rgb[5] = gb->sprite_palettes_rgb[1] = gb->background_palettes_rgb[1] = - gb->rgb_encode_callback(gb, palette->colors[2].r, palette->colors[2].g, palette->colors[2].b); - gb->sprite_palettes_rgb[6] = gb->sprite_palettes_rgb[2] = gb->background_palettes_rgb[2] = - gb->rgb_encode_callback(gb, palette->colors[1].r, palette->colors[1].g, palette->colors[1].b); - gb->sprite_palettes_rgb[7] = gb->sprite_palettes_rgb[3] = gb->background_palettes_rgb[3] = - gb->rgb_encode_callback(gb, palette->colors[0].r, palette->colors[0].g, palette->colors[0].b); - - // LCD off color - gb->background_palettes_rgb[4] = - gb->rgb_encode_callback(gb, palette->colors[4].r, palette->colors[4].g, palette->colors[4].b); + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) } + gb->execution_callback = callback; } -void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette) +void GB_set_lcd_line_callback(GB_gameboy_t *gb, GB_lcd_line_callback_t callback) { - gb->dmg_palette = palette; - update_dmg_palette(gb); + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } + gb->lcd_line_callback = callback; } -void GB_set_rgb_encode_callback(GB_gameboy_t *gb, GB_rgb_encode_callback_t callback) +void GB_set_lcd_status_callback(GB_gameboy_t *gb, GB_lcd_status_callback_t callback) { - - gb->rgb_encode_callback = callback; - update_dmg_palette(gb); - - for (unsigned i = 0; i < 32; i++) { - GB_palette_changed(gb, true, i * 2); - GB_palette_changed(gb, false, i * 2); + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) } + gb->lcd_status_callback = callback; } void GB_set_infrared_callback(GB_gameboy_t *gb, GB_infrared_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->infrared_callback = callback; } @@ -1327,40 +1315,61 @@ void GB_set_infrared_input(GB_gameboy_t *gb, bool state) void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->rumble_callback = callback; } void GB_set_serial_transfer_bit_start_callback(GB_gameboy_t *gb, GB_serial_transfer_bit_start_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->serial_transfer_bit_start_callback = callback; } void GB_set_serial_transfer_bit_end_callback(GB_gameboy_t *gb, GB_serial_transfer_bit_end_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->serial_transfer_bit_end_callback = callback; } 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_SC] &= ~0x80; gb->io_registers[GB_IO_IF] |= 8; gb->serial_count = 0; } @@ -1368,22 +1377,32 @@ void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data) void GB_disconnect_serial(GB_gameboy_t *gb) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) gb->serial_transfer_bit_start_callback = NULL; 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(GB_gameboy_t *gb) +bool GB_is_cgb(const GB_gameboy_t *gb) { - return (gb->model & GB_MODEL_FAMILY_MASK) == GB_MODEL_CGB_FAMILY; + return gb->model >= GB_MODEL_CGB_0; +} + +bool GB_is_cgb_in_cgb_mode(GB_gameboy_t *gb) +{ + return gb->cgb_mode; } bool GB_is_sgb(GB_gameboy_t *gb) @@ -1402,6 +1421,11 @@ void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip) gb->turbo_dont_skip = no_frame_skip; } +void GB_set_turbo_cap(GB_gameboy_t *gb, double multiplier) +{ + gb->turbo_cap_multiplier = multiplier; +} + void GB_set_rendering_disabled(GB_gameboy_t *gb, bool disabled) { gb->disable_rendering = disabled; @@ -1420,8 +1444,10 @@ void GB_set_user_data(GB_gameboy_t *gb, void *data) static void reset_ram(GB_gameboy_t *gb) { switch (gb->model) { + case GB_MODEL_MGB: case GB_MODEL_CGB_E: - case GB_MODEL_AGB: /* Unverified */ + 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(); } @@ -1450,14 +1476,28 @@ static void reset_ram(GB_gameboy_t *gb) gb->ram[i] ^= GB_random() & GB_random() & GB_random(); } break; - + + case GB_MODEL_CGB_0: + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_B: case GB_MODEL_CGB_C: for (unsigned i = 0; i < gb->ram_size; i++) { if ((i & 0x808) == 0x800 || (i & 0x808) == 0x008) { gb->ram[i] = 0; } else { - gb->ram[i] = GB_random() | GB_random() | GB_random() | GB_random(); + gb->ram[i] = GB_random() | GB_random() | GB_random() | GB_random() | GB_random(); + } + } + break; + case GB_MODEL_CGB_D: + for (unsigned i = 0; i < gb->ram_size; i++) { + gb->ram[i] = GB_random(); + if (i & 0x800) { + gb->ram[i] &= GB_random(); + } + else { + gb->ram[i] |= GB_random(); } } break; @@ -1465,23 +1505,28 @@ static void reset_ram(GB_gameboy_t *gb) /* HRAM */ switch (gb->model) { + 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_D: case GB_MODEL_CGB_E: - case GB_MODEL_AGB: - for (unsigned i = 0; i < sizeof(gb->hram); i++) { + case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: + nounroll for (unsigned i = 0; i < sizeof(gb->hram); i++) { gb->hram[i] = GB_random(); } break; case GB_MODEL_DMG_B: + case GB_MODEL_MGB: case GB_MODEL_SGB_NTSC: /* Unverified*/ case GB_MODEL_SGB_PAL: /* Unverified */ case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ 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(); } @@ -1494,20 +1539,26 @@ static void reset_ram(GB_gameboy_t *gb) /* OAM */ switch (gb->model) { + 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: - case GB_MODEL_AGB: - /* Zero'd out by boot ROM anyway*/ + case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: + /* Zero'd out by boot ROM anyway */ break; case GB_MODEL_DMG_B: + case GB_MODEL_MGB: case GB_MODEL_SGB_NTSC: /* Unverified */ case GB_MODEL_SGB_PAL: /* Unverified */ case GB_MODEL_SGB_NTSC_NO_SFC: /* Unverified */ case GB_MODEL_SGB_PAL_NO_SFC: /* Unverified */ case GB_MODEL_SGB2: case GB_MODEL_SGB2_NO_SFC: - for (unsigned i = 0; i < 8; i++) { + for (unsigned i = 0; i < sizeof(gb->oam); i++) { if (i & 2) { gb->oam[i] = GB_random() & GB_random() & GB_random(); } @@ -1515,20 +1566,32 @@ 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++) { - gb->oam[i] = gb->oam[i - 8]; - } break; } /* Wave RAM */ switch (gb->model) { + 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: - case GB_MODEL_AGB: - /* Initialized by CGB-A and newer, 0s in CGB-0*/ + 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: { + 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(); + } + else { + gb->io_registers[GB_IO_WAV_START + i] = GB_random() | GB_random(); + } + } + break; + } case GB_MODEL_DMG_B: case GB_MODEL_SGB_NTSC: /* Unverified*/ case GB_MODEL_SGB_PAL: /* Unverified */ @@ -1536,7 +1599,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(); } @@ -1555,13 +1618,17 @@ static void reset_ram(GB_gameboy_t *gb) if (GB_is_cgb(gb)) { for (unsigned i = 0; i < 64; i++) { gb->background_palettes_data[i] = GB_random(); /* Doesn't really matter as the boot ROM overrides it anyway*/ - gb->sprite_palettes_data[i] = GB_random(); + gb->object_palettes_data[i] = GB_random(); } for (unsigned i = 0; i < 32; i++) { GB_palette_changed(gb, true, i * 2); 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) @@ -1572,6 +1639,9 @@ static void request_boot_rom(GB_gameboy_t *gb) case GB_MODEL_DMG_B: type = GB_BOOT_ROM_DMG; break; + case GB_MODEL_MGB: + type = GB_BOOT_ROM_MGB; + break; case GB_MODEL_SGB_NTSC: case GB_MODEL_SGB_PAL: case GB_MODEL_SGB_NTSC_NO_SFC: @@ -1582,11 +1652,20 @@ static void request_boot_rom(GB_gameboy_t *gb) case GB_MODEL_SGB2_NO_SFC: type = GB_BOOT_ROM_SGB2; break; + case GB_MODEL_CGB_0: + type = GB_BOOT_ROM_CGB_0; + break; + case GB_MODEL_CGB_A: + case GB_MODEL_CGB_B: case GB_MODEL_CGB_C: - case GB_MODEL_CGB_E: + case GB_MODEL_CGB_D: type = GB_BOOT_ROM_CGB; break; - case GB_MODEL_AGB: + 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; } @@ -1594,18 +1673,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->version = STRUCT_VERSION; + + GB_reset_mbc(gb); - gb->mbc_rom_bank = 1; gb->last_rtc_second = time(NULL); gb->cgb_ram_bank = 1; gb->io_registers[GB_IO_JOYP] = 0xCF; @@ -1621,22 +1723,15 @@ void GB_reset(GB_gameboy_t *gb) gb->ram_size = 0x2000; gb->vram_size = 0x2000; memset(gb->vram, 0, gb->vram_size); - gb->object_priority = GB_OBJECT_PRIORITY_X; - - update_dmg_palette(gb); + gb->object_priority = GB_OBJECT_PRIORITY_X; + 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; + if (GB_is_hle_sgb(gb)) { if (!gb->sgb) { gb->sgb = malloc(sizeof(*gb->sgb)); @@ -1658,22 +1753,60 @@ void GB_reset(GB_gameboy_t *gb) } } - /* Todo: Ugly, fixme, see comment in the timer state machine */ - gb->div_state = 3; + 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; - GB_apu_update_cycles_per_sample(gb); - +#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) + +#ifndef GB_DISABLE_CHEAT_SEARCH + GB_cheat_search_reset(gb); +#endif + gb->model = model; if (GB_is_cgb(gb)) { gb->ram = realloc(gb->ram, gb->ram_size = 0x1000 * 8); @@ -1683,11 +1816,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); } @@ -1709,7 +1844,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; @@ -1717,7 +1856,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; @@ -1744,9 +1883,9 @@ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t * *bank = 0; return &gb->background_palettes_data; case GB_DIRECT_ACCESS_OBP: - *size = sizeof(gb->sprite_palettes_data); + *size = sizeof(gb->object_palettes_data); *bank = 0; - return &gb->sprite_palettes_data; + return &gb->object_palettes_data; case GB_DIRECT_ACCESS_IE: *size = sizeof(gb->interrupt_enable); *bank = 0; @@ -1758,93 +1897,81 @@ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t * } } +GB_registers_t *GB_get_registers(GB_gameboy_t *gb) +{ + return (GB_registers_t *)&gb->registers; +} + void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier) { - gb->clock_multiplier = multiplier; - GB_apu_update_cycles_per_sample(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) { - return GB_get_unmultiplied_clock_rate(gb) * gb->clock_multiplier; + return gb->clock_rate; } - uint32_t GB_get_unmultiplied_clock_rate(GB_gameboy_t *gb) { - if (gb->model & GB_MODEL_PAL_BIT) { - return SGB_PAL_FREQUENCY; - } - if ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB) { - return SGB_NTSC_FREQUENCY; - } - return CPU_FREQUENCY; + return gb->unmultiplied_clock_rate; } + +void GB_update_clock_rate(GB_gameboy_t *gb) +{ + if (gb->model & GB_MODEL_PAL_BIT) { + gb->unmultiplied_clock_rate = SGB_PAL_FREQUENCY; + } + else if ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB) { + gb->unmultiplied_clock_rate = SGB_NTSC_FREQUENCY; + } + else { + gb->unmultiplied_clock_rate = CPU_FREQUENCY; + } + + 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) { if (gb->border_mode > GB_BORDER_ALWAYS) return; gb->border_mode = border_mode; } -unsigned GB_get_screen_width(GB_gameboy_t *gb) -{ - switch (gb->border_mode) { - default: - case GB_BORDER_SGB: - return GB_is_hle_sgb(gb)? 256 : 160; - case GB_BORDER_NEVER: - return 160; - case GB_BORDER_ALWAYS: - return 256; - } -} - -unsigned GB_get_screen_height(GB_gameboy_t *gb) -{ - switch (gb->border_mode) { - default: - case GB_BORDER_SGB: - return GB_is_hle_sgb(gb)? 224 : 144; - case GB_BORDER_NEVER: - return 144; - case GB_BORDER_ALWAYS: - return 224; - } -} - -unsigned GB_get_player_count(GB_gameboy_t *gb) -{ - return GB_is_hle_sgb(gb)? gb->sgb->player_count : 1; -} - -void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback) -{ - gb->update_input_hint_callback = callback; -} - -double GB_get_usual_frame_rate(GB_gameboy_t *gb) -{ - return GB_get_clock_rate(gb) / (double)LCDC_PERIOD; -} - void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->joyp_write_callback = callback; } void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->icd_pixel_callback = callback; } void GB_set_icd_hreset_callback(GB_gameboy_t *gb, GB_icd_hreset_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->icd_hreset_callback = callback; } void GB_set_icd_vreset_callback(GB_gameboy_t *gb, GB_icd_vreset_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->icd_vreset_callback = callback; } @@ -1857,21 +1984,33 @@ void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t unsigned GB_time_to_alarm(GB_gameboy_t *gb) { if (gb->cartridge_type->mbc_type != GB_HUC3) return 0; - if (!gb->huc3_alarm_enabled) return 0; - if (!(gb->huc3_alarm_days & 0x2000)) return 0; - unsigned current_time = (gb->huc3_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3_minutes * 60 + (time(NULL) % 60); - unsigned alarm_time = (gb->huc3_alarm_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3_alarm_minutes * 60; + if (!gb->huc3.alarm_enabled) return 0; + if (!(gb->huc3.alarm_days & 0x2000)) return 0; + unsigned current_time = (gb->huc3.days & 0x1FFF) * 24 * 60 * 60 + gb->huc3.minutes * 60 + (time(NULL) % 60); + unsigned alarm_time = (gb->huc3.alarm_days & 0x1FFF) * 24 * 60 * 60 + gb->huc3.alarm_minutes * 60; if (current_time > alarm_time) return 0; return alarm_time - current_time; } -void GB_set_rtc_mode(GB_gameboy_t *gb, GB_rtc_mode_t mode) +bool GB_rom_supports_alarms(GB_gameboy_t *gb) { - if (gb->rtc_mode != mode) { - gb->rtc_mode = mode; - gb->rtc_cycles = 0; - gb->last_rtc_second = time(NULL); - } + return gb->cartridge_type->mbc_type == GB_HUC3; +} + +bool GB_has_accelerometer(GB_gameboy_t *gb) +{ + return gb->cartridge_type->mbc_type == GB_MBC7; +} + +void GB_set_accelerometer_values(GB_gameboy_t *gb, double x, double y) +{ + gb->accelerometer_x = x; + 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) @@ -1888,49 +2027,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; @@ -1941,3 +2080,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/bsnes/gb/Core/gb.h b/bsnes/gb/Core/gb.h index 655346bf..447a1676 100644 --- a/bsnes/gb/Core/gb.h +++ b/bsnes/gb/Core/gb.h @@ -1,11 +1,26 @@ -#ifndef GB_h -#define GB_h -#define typeof __typeof__ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + #include #include +#include #include -#include "gb_struct_def.h" +#ifndef GB_DISABLE_CHEAT_SEARCH +#ifdef GB_DISABLE_CHEATS +#define GB_DISABLE_CHEAT_SEARCH +#else +#ifdef GB_DISABLE_DEBUGGER +#define GB_DISABLE_CHEAT_SEARCH +#endif +#endif +#endif + +#include "model.h" +#include "defs.h" #include "save_state.h" #include "apu.h" @@ -22,32 +37,16 @@ #include "symbol_hash.h" #include "sgb.h" #include "cheats.h" +#include "cheat_search.h" #include "rumble.h" #include "workboy.h" #include "random.h" -#define GB_STRUCT_VERSION 13 - -#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_MODEL_PAL_BIT_OLD 0x1000 -#define GB_MODEL_NO_SFC_BIT_OLD 0x2000 - #ifdef GB_INTERNAL -#if __clang__ -#define unrolled _Pragma("unroll") -#elif __GNUC__ >= 8 -#define unrolled _Pragma("GCC unroll 8") -#else -#define unrolled +#define STRUCT_VERSION 15 #endif -#endif +#define GB_REWIND_FRAMES_PER_KEY 255 #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ #define GB_BIG_ENDIAN @@ -57,39 +56,17 @@ #error Unable to detect endianess #endif -#ifdef GB_INTERNAL -/* Todo: similar macros are everywhere, clean this up and remove direct calls to bswap */ #ifdef GB_BIG_ENDIAN -#define LE16(x) __builtin_bswap16(x) -#define LE32(x) __builtin_bswap32(x) -#define LE64(x) __builtin_bswap64(x) -#define BE16(x) (x) -#define BE32(x) (x) -#define BE64(x) (x) +#define GB_REGISTER_ORDER a, f, \ + b, c, \ + d, e, \ + h, l #else -#define LE16(x) (x) -#define LE32(x) (x) -#define LE64(x) (x) -#define BE16(x) __builtin_bswap16(x) -#define BE32(x) __builtin_bswap32(x) -#define BE64(x) __builtin_bswap64(x) +#define GB_REGISTER_ORDER f, a, \ + c, b, \ + e, d, \ + l, h #endif -#endif - -#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 8) -#define __builtin_bswap16(x) ({ typeof(x) _x = (x); _x >> 8 | _x << 8; }) -#endif - -typedef struct { - struct { - uint8_t r, g, b; - } colors[5]; -} GB_palette_t; - -extern const GB_palette_t GB_PALETTE_GREY; -extern const GB_palette_t GB_PALETTE_DMG; -extern const GB_palette_t GB_PALETTE_MGB; -extern const GB_palette_t GB_PALETTE_GBL; typedef union { struct { @@ -117,44 +94,33 @@ 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 = 0x206, -} GB_model_t; - enum { GB_REGISTER_AF, GB_REGISTER_BC, GB_REGISTER_DE, GB_REGISTER_HL, GB_REGISTER_SP, + GB_REGISTER_PC, GB_REGISTERS_16_BIT /* Count */ }; /* 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 { @@ -179,7 +145,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) @@ -192,16 +158,16 @@ 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) GB_IO_NR43 = 0x22, // Channel 4 Polynomial Counter (R/W) - GB_IO_NR44 = 0x23, // Channel 4 Counter/consecutive, Inital (R/W) + GB_IO_NR44 = 0x23, // Channel 4 Counter/consecutive, Initial (R/W) GB_IO_NR50 = 0x24, // Channel control / ON-OFF / Volume (R/W) GB_IO_NR51 = 0x25, // Selection of Sound output terminal (R/W) GB_IO_NR52 = 0x26, // Sound on/off @@ -209,7 +175,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) @@ -222,21 +188,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) - // Has some undocumented compatibility flags written at boot. - // Unfortunately it is not readable or writable after boot has finished, so research of this - // register is quite limited. The value written to this register, however, can be controlled - // in some cases. - GB_IO_KEY0 = 0x4c, + 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, /* 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 @@ -250,48 +213,47 @@ 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 - Sprite Palette Index - GB_IO_OBPD = 0x6b, // CGB Mode Only - Sprite 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_UNKNOWN2 = 0x72, // (00h) - Bit 0-7 (Read/Write) - GB_IO_UNKNOWN3 = 0x73, // (00h) - Bit 0-7 (Read/Write) - GB_IO_UNKNOWN4 = 0x74, // (00h) - Bit 0-7 (Read/Write) - CGB Mode Only - GB_IO_UNKNOWN5 = 0x75, // (8Fh) - Bit 4-6 (Read/Write) - GB_IO_PCM_12 = 0x76, // Channels 1 and 2 amplitudes - GB_IO_PCM_34 = 0x77, // Channels 3 and 4 amplitudes - GB_IO_UNKNOWN8 = 0x7F, // Unknown, write only + 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 + GB_IO_PGB = 0x75, // Bits 0-2 control PHI, A15 and ¬CS, respectively. Bits 4-6 control the I/O directions of bits 0-2 (0 is R, 1 is W) + GB_IO_PCM12 = 0x76, // Channels 1 and 2 amplitudes + GB_IO_PCM34 = 0x77, // Channels 3 and 4 amplitudes }; +static const typeof(GB_IO_PGB) __attribute__((deprecated("Use GB_IO_PGB instead"))) GB_IO_UNKNOWN5 = GB_IO_PGB; + typedef enum { GB_LOG_BOLD = 1, GB_LOG_DASHED_UNDERLINE = 2, GB_LOG_UNDERLINE = 4, GB_LOG_UNDERLINE_MASK = GB_LOG_DASHED_UNDERLINE | GB_LOG_UNDERLINE -} GB_log_attributes; +} GB_log_attributes_t; typedef enum { - GB_BOOT_ROM_DMG0, + GB_BOOT_ROM_DMG_0, GB_BOOT_ROM_DMG, GB_BOOT_ROM_MGB, GB_BOOT_ROM_SGB, GB_BOOT_ROM_SGB2, - GB_BOOT_ROM_CGB0, + 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; -typedef enum { - GB_RTC_MODE_SYNC_TO_HOST, - GB_RTC_MODE_ACCURATE, -} GB_rtc_mode_t; - #ifdef GB_INTERNAL #define LCDC_PERIOD 70224 #define CPU_FREQUENCY 0x400000 @@ -299,47 +261,33 @@ 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_log_callback_t)(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes); +typedef void (*GB_log_callback_t)(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes); typedef char *(*GB_input_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); typedef void (*GB_serial_transfer_bit_start_callback_t)(GB_gameboy_t *gb, bool bit_to_send); typedef bool (*GB_serial_transfer_bit_end_callback_t)(GB_gameboy_t *gb); -typedef void (*GB_update_input_hint_callback_t)(GB_gameboy_t *gb); typedef void (*GB_joyp_write_callback_t)(GB_gameboy_t *gb, uint8_t value); typedef void (*GB_icd_pixel_callback_t)(GB_gameboy_t *gb, uint8_t row); typedef void (*GB_icd_hreset_callback_t)(GB_gameboy_t *gb); typedef void (*GB_icd_vreset_callback_t)(GB_gameboy_t *gb); 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; -typedef struct { - uint8_t pixel; // Color, 0-3 - uint8_t palette; // Palette, 0 - 7 (CGB); 0-1 in DMG (or just 0 for BG) - uint8_t priority; // Sprite priority – 0 in DMG, OAM index in CGB - bool bg_priority; // For sprite FIFO – the BG priority bit. For the BG FIFO – the CGB attributes priority bit -} GB_fifo_item_t; - -#define GB_FIFO_LENGTH 16 -typedef struct { - GB_fifo_item_t fifo[GB_FIFO_LENGTH]; - uint8_t read_end; - uint8_t write_end; -} GB_fifo_t; - typedef struct { uint32_t magic; uint8_t track_count; @@ -363,6 +311,28 @@ typedef struct { char copyright[33]; } GB_gbs_info_t; +/* Duplicated so it can remain anonymous in GB_gameboy_t */ +typedef union { + uint16_t registers[GB_REGISTERS_16_BIT]; + struct { + uint16_t af, + bc, + de, + hl, + sp, + pc; + }; + struct { + uint8_t GB_REGISTER_ORDER; + }; +} 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. @@ -382,35 +352,24 @@ struct GB_gameboy_internal_s { /* The version field makes sure we don't load save state files with a completely different structure. This happens when struct fields are removed/resized in an backward incompatible manner. */ uint32_t version; - ); + ) GB_SECTION(core_state, /* Registers */ - uint16_t pc; - union { - uint16_t registers[GB_REGISTERS_16_BIT]; - struct { - uint16_t af, - bc, - de, - hl, - sp; - }; - struct { -#ifdef GB_BIG_ENDIAN - uint8_t a, f, - b, c, - d, e, - h, l; -#else - uint8_t f, a, - c, b, - e, d, - l, h; -#endif - }; - - }; + union { + uint16_t registers[GB_REGISTERS_16_BIT]; + struct { + uint16_t af, + bc, + de, + hl, + sp, + pc; + }; + struct { + uint8_t GB_REGISTER_ORDER; + }; + }; uint8_t ime; uint8_t interrupt_enable; uint8_t cgb_ram_bank; @@ -428,36 +387,38 @@ 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 */ GB_SECTION(dma, bool hdma_on; bool hdma_on_hblank; uint8_t hdma_steps_left; - int16_t hdma_cycles; // in 8MHz units uint16_t hdma_current_src, hdma_current_dest; - uint8_t dma_steps_left; uint8_t dma_current_dest; + uint8_t last_dma_read; uint16_t dma_current_src; - int16_t dma_cycles; - bool is_dma_restarting; - uint8_t last_opcode_read; /* Required to emulte HDMA reads from Exxx */ - bool hdma_starting; - ); + uint16_t dma_cycles; + int8_t dma_cycles_modulo; + bool dma_ppu_vram_conflict; + uint16_t dma_ppu_vram_conflict_addr; + 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; @@ -475,18 +436,51 @@ struct GB_gameboy_internal_s { struct { uint8_t rom_bank:8; uint8_t ram_bank:3; + bool rtc_mapped:1; } mbc3; struct { uint8_t rom_bank_low; uint8_t rom_bank_high:1; uint8_t ram_bank:4; - } mbc5; + } mbc5; // Also used for GB_CAMERA + + 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; @@ -494,63 +488,75 @@ struct GB_gameboy_internal_s { uint8_t rom_bank:7; uint8_t padding:1; uint8_t ram_bank:4; + 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; } huc3; + + struct { + uint16_t rom_bank; + uint8_t ram_bank; + 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; - - // TODO: move to huc3/mbc3/tpp1 struct when breaking save compat - uint8_t huc3_mode; - uint8_t huc3_access_index; - uint16_t huc3_minutes, huc3_days; - uint16_t huc3_alarm_minutes, huc3_alarm_days; - bool huc3_alarm_enabled; - uint8_t huc3_read; - uint8_t huc3_access_flags; - bool mbc3_rtc_mapped; - uint16_t tpp1_rom_bank; - uint8_t tpp1_ram_bank; - uint8_t tpp1_mode; - ); - + + bool camera_registers_mapped; + uint8_t camera_registers[0x36]; + uint8_t camera_alignment; + int32_t camera_countdown; + ) /* HRAM and HW Registers */ GB_SECTION(hram, uint8_t hram[0xFFFF - 0xFF80]; uint8_t io_registers[0x80]; - ); + ) /* Timing */ GB_SECTION(timing, 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; uint8_t speed_switch_countdown; // To compensate for the lack of pipeline emulation uint8_t speed_switch_freeze; // Solely for realigning the PPU, should be removed when the odd modes are implemented - ); + /* For timing of the vblank callback */ + uint32_t cycles_since_vblank_callback; + 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 */ GB_SECTION(apu, GB_apu_t apu; - ); + ) /* RTC */ GB_SECTION(rtc, GB_rtc_time_t rtc_real, rtc_latched; uint64_t last_rtc_second; - GB_PADDING(bool, rtc_latch); uint32_t rtc_cycles; uint8_t tpp1_mr4; - ); + ) /* Video Display */ GB_SECTION(video, @@ -558,32 +564,28 @@ struct GB_gameboy_internal_s { bool cgb_vram_bank; uint8_t oam[0xA0]; uint8_t background_palettes_data[0x40]; - uint8_t sprite_palettes_data[0x40]; + 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; - GB_PADDING(uint8_t, fetcher_x); uint8_t fetcher_y; uint16_t cycles_for_line; uint8_t current_tile; @@ -591,19 +593,26 @@ struct GB_gameboy_internal_s { uint8_t current_tile_data[2]; uint8_t fetcher_state; bool window_is_being_fetched; - bool wx166_glitch; + GB_PADDING(bool, wx166_glitch); bool wx_triggered; uint8_t visible_objs[10]; - uint8_t obj_comparators[10]; + uint8_t objects_x[10]; + uint8_t objects_y[10]; + uint8_t object_tile_data[2]; + 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_sprite_at_0; uint8_t mode_for_interrupt; bool lyc_interrupt_line; bool cgb_palettes_blocked; uint8_t current_lcd_line; // The LCD can go out of sync since the vsync signal is skipped in some cases. - uint32_t cycles_in_stop_mode; uint8_t object_priority; bool oam_ppu_blocked; bool vram_ppu_blocked; @@ -617,9 +626,29 @@ 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; + bool last_tileset; + bool cgb_wx_glitch; + bool line_has_fractional_scrolling; + uint8_t wy_check_modulo; + bool wy_check_scheduled; + bool wy_just_checked; + bool wx_166_interrupt_glitch; + ) + + 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 */ /* This data is reserved on reset and must come last in the struct */ @@ -644,20 +673,37 @@ struct GB_gameboy_internal_s { /* I/O */ uint32_t *screen; uint32_t background_palettes_rgb[0x20]; - uint32_t sprite_palettes_rgb[0x20]; + uint32_t object_palettes_rgb[0x20]; const GB_palette_t *dmg_palette; GB_color_correction_mode_t color_correction_mode; double light_temperature; bool keys[4][GB_KEY_MAX]; + bool use_faux_analog[4]; + struct { + int8_t x, y; + } faux_analog_inputs[4]; + uint8_t faux_analog_ticks; + double accelerometer_x, accelerometer_y; GB_border_mode_t border_mode; GB_sgb_border_t borrowed_border; bool tried_loading_sgb_border; bool has_sgb_border; - + bool objects_disabled; + bool background_disabled; + bool joyp_accessed; + bool illegal_inputs_allowed; + bool no_bouncing_emulation; + bool joypad_is_stable; + /* Timing */ uint64_t last_sync; + uint64_t last_render; uint64_t cycles_since_last_sync; // In 8MHz units GB_rtc_mode_t rtc_mode; + 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; @@ -681,14 +727,23 @@ struct GB_gameboy_internal_s { GB_icd_vreset_callback_t icd_hreset_callback; GB_icd_vreset_callback_t icd_vreset_callback; GB_read_memory_callback_t read_memory_callback; + 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_workboy_set_time_callback workboy_set_time_callback; - GB_workboy_get_time_callback workboy_get_time_callback; - + GB_printer_done_callback_t printer_done_callback; + GB_workboy_set_time_callback_t workboy_set_time_callback; + GB_workboy_get_time_callback_t 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; + +#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; @@ -697,8 +752,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]; @@ -716,7 +769,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 */ @@ -726,16 +780,32 @@ struct GB_gameboy_internal_s { /* Undo */ uint8_t *undo_state; const char *undo_label; + + /* Callbacks */ + GB_debugger_reload_callback_t debugger_reload_callback; + + /* CPU usage */ + uint32_t current_frame_idle_cycles, current_frame_busy_cycles; + uint32_t last_frame_idle_cycles, last_frame_busy_cycles; + + uint32_t current_second_idle_cycles, current_second_busy_cycles; + uint32_t last_second_idle_cycles, last_second_busy_cycles; + uint8_t usage_frame_count; +#endif +#ifndef GB_DISABLE_REWIND /* Rewind */ -#define GB_REWIND_FRAMES_PER_KEY 255 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; @@ -744,19 +814,29 @@ 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 +#ifndef GB_DISABLE_CHEAT_SEARCH + uint8_t *cheat_search_data; + uint8_t *cheat_search_bitmap; + size_t cheat_search_count; + GB_cheat_search_data_type_t cheat_search_data_type; +#endif /* Misc */ bool turbo; bool turbo_dont_skip; + double turbo_cap_multiplier; + bool enable_skipped_frame_vblank_callbacks; bool disable_rendering; uint8_t boot_rom[0x900]; bool vblank_just_occured; // For slow operations involving syscalls; these should only run once per vblank - uint8_t cycles_since_run; // How many cycles have passed since the last call to GB_run(), in 8MHz units + unsigned cycles_since_run; // How many cycles have passed since the last call to GB_run(), in 8MHz units double clock_multiplier; GB_rumble_mode_t rumble_mode; uint32_t rumble_on_cycles; @@ -765,14 +845,23 @@ struct GB_gameboy_internal_s { /* Temporary state */ bool wx_just_changed; bool tile_sel_glitch; + 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; - ); + ) }; #ifndef GB_INTERNAL struct GB_gameboy_s { - char __internal[sizeof(struct GB_gameboy_internal_s)]; + alignas(struct GB_gameboy_internal_s) uint8_t __internal[sizeof(struct GB_gameboy_internal_s)]; }; #endif @@ -783,18 +872,43 @@ 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(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. */ -uint8_t GB_run(GB_gameboy_t *gb); +unsigned GB_run(GB_gameboy_t *gb); /* Returns the time passed since the last frame, in nanoseconds */ uint64_t GB_run_frame(GB_gameboy_t *gb); @@ -804,17 +918,19 @@ typedef enum { GB_DIRECT_ACCESS_CART_RAM, GB_DIRECT_ACCESS_VRAM, GB_DIRECT_ACCESS_HRAM, - GB_DIRECT_ACCESS_IO, /* Warning: Some registers can only be read/written correctly via GB_memory_read/write. */ + GB_DIRECT_ACCESS_IO, /* Warning: Some registers can only be read/written correctly via GB_read/write_memory. */ GB_DIRECT_ACCESS_BOOTROM, GB_DIRECT_ACCESS_OAM, 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 is returned at *bank, even if only a portion of the memory is banked. */ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t *size, uint16_t *bank); +GB_registers_t *GB_get_registers(GB_gameboy_t *gb); void *GB_get_user_data(GB_gameboy_t *gb); void GB_set_user_data(GB_gameboy_t *gb, void *data); @@ -833,31 +949,31 @@ 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); void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size); -void GB_load_battery(GB_gameboy_t *gb, const char *path); +int GB_load_battery(GB_gameboy_t *gb, const char *path); void GB_set_turbo_mode(GB_gameboy_t *gb, bool on, bool no_frame_skip); +void GB_set_turbo_cap(GB_gameboy_t *gb, double multiplier); // Use 0 to use no cap void GB_set_rendering_disabled(GB_gameboy_t *gb, bool disabled); 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_attributed_log(GB_gameboy_t *gb, GB_log_attributes_t 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); -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_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); -void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback); /* Called when a new boot ROM is needed. The callback should call GB_load_boot_rom or GB_load_boot_rom_from_buffer */ void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t callback); -void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette); +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); /* These APIs are used when using internal clock */ void GB_set_serial_transfer_bit_start_callback(GB_gameboy_t *gb, GB_serial_transfer_bit_start_callback_t callback); @@ -868,12 +984,20 @@ 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 */ +bool GB_has_accelerometer(GB_gameboy_t *gb); +// In units of g (gravity's acceleration). +// Values within ±4 recommended +void GB_set_accelerometer_values(GB_gameboy_t *gb, double x, double y); -/* RTC emulation mode */ -void GB_set_rtc_mode(GB_gameboy_t *gb, GB_rtc_mode_t mode); +// 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); @@ -885,18 +1009,39 @@ uint32_t GB_get_clock_rate(GB_gameboy_t *gb); uint32_t GB_get_unmultiplied_clock_rate(GB_gameboy_t *gb); void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier); -unsigned GB_get_screen_width(GB_gameboy_t *gb); -unsigned GB_get_screen_height(GB_gameboy_t *gb); -double GB_get_usual_frame_rate(GB_gameboy_t *gb); -unsigned GB_get_player_count(GB_gameboy_t *gb); - /* Handy ROM info APIs */ // `title` must be at least 17 bytes in size void GB_get_rom_title(GB_gameboy_t *gb, char *title); uint32_t GB_get_rom_crc32(GB_gameboy_t *gb); #ifdef GB_INTERNAL -void GB_borrow_sgb_border(GB_gameboy_t *gb); +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/bsnes/gb/Core/gb_struct_def.h b/bsnes/gb/Core/gb_struct_def.h deleted file mode 100644 index 0e0ebd12..00000000 --- a/bsnes/gb/Core/gb_struct_def.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef gb_struct_def_h -#define gb_struct_def_h -struct GB_gameboy_s; -typedef struct GB_gameboy_s GB_gameboy_t; -#endif diff --git a/bsnes/gb/Core/graphics/mgb_border.inc b/bsnes/gb/Core/graphics/mgb_border.inc new file mode 100644 index 00000000..f19ed8a1 --- /dev/null +++ b/bsnes/gb/Core/graphics/mgb_border.inc @@ -0,0 +1,477 @@ +static const uint16_t palette[] = { + 0x0000, 0x0000, 0x0011, 0x001A, 0x39CE, 0x6B5A, 0x739C, 0x5265, + 0x3DC5, 0x2924, 0x18A4, 0x20E6, 0x2D49, 0x1484, 0x5694, 0x20EC, +}; + + +static const uint16_t tilemap[] = { + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x0010, 0x0011, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, + 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, + 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, + 0x0012, 0x0012, 0x0012, 0x0012, 0x0012, 0x4011, 0x4010, 0x000F, + 0x000F, 0x0013, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x4013, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0016, 0x0017, 0x0017, + 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, + 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, 0x0017, + 0x0017, 0x0017, 0x4016, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0019, 0x001A, 0x4019, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x8019, 0x001B, 0xC019, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x0018, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x4018, 0x0014, 0x0014, 0x0014, 0x4015, 0x000F, + 0x000F, 0x0015, 0x0014, 0x0014, 0x0014, 0x001C, 0x001D, 0x001D, + 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, + 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, 0x001D, + 0x001D, 0x001D, 0x401C, 0x0014, 0x0014, 0x0014, 0x001E, 0x000F, + 0x000F, 0x0015, 0x0014, 0x001F, 0x0020, 0x0021, 0x0022, 0x0023, + 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0030, 0x0031, 0x000F, + 0x000F, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, + 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, 0x0040, + 0x0041, 0x0042, 0x0043, 0x0044, 0x0014, 0x0014, 0x0014, 0x0014, + 0x0014, 0x0014, 0x0014, 0x0014, 0x0045, 0x0046, 0x000F, 0x000F, + 0x000F, 0x0047, 0x0048, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, + 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, + 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, + 0x0049, 0x0049, 0x004A, 0x004B, 0x004C, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, + 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, 0x000F, +}; + + + +const uint8_t tiles[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x3D, + 0x42, 0x7F, 0x81, 0xFF, 0x01, 0xFD, 0x01, + 0xFD, 0x01, 0xFF, 0x03, 0xFF, 0x03, 0xFF, + 0xFF, 0xBC, 0x7F, 0xFD, 0xFE, 0xFD, 0xFE, + 0xFD, 0xFE, 0xFD, 0xFE, 0xFF, 0xFC, 0xFF, + 0xFC, 0xFF, 0x00, 0xBF, 0x41, 0xFE, 0xC0, + 0xBF, 0xC1, 0xFF, 0x81, 0x7D, 0x03, 0x7F, + 0x01, 0x7F, 0x01, 0xFF, 0xFF, 0x3E, 0xFF, + 0xBE, 0x7F, 0xBF, 0x7E, 0xFF, 0x7E, 0x7D, + 0xFE, 0x7D, 0xFE, 0x7D, 0xFE, 0xFF, 0x00, + 0xFF, 0x00, 0xBF, 0x83, 0xBF, 0x87, 0xFC, + 0x8D, 0xED, 0x8E, 0xDB, 0xF8, 0xBF, 0xD8, + 0xFF, 0xFF, 0x3E, 0xFF, 0xBB, 0x7C, 0xB7, + 0x78, 0xAC, 0x73, 0xAD, 0x73, 0x9B, 0x67, + 0x9B, 0x67, 0xFF, 0x00, 0xB7, 0x08, 0xFF, + 0xF8, 0x3F, 0x38, 0xFF, 0x08, 0xFE, 0x01, + 0x87, 0x00, 0xFB, 0x78, 0xFF, 0xFF, 0x07, + 0xFF, 0xFB, 0x07, 0x3B, 0xC7, 0xE7, 0xFF, + 0xFE, 0xFF, 0x82, 0xFF, 0xFA, 0x87, 0xFF, + 0x00, 0xFE, 0x81, 0x5F, 0x40, 0xDE, 0xC0, + 0xFE, 0xC0, 0xE0, 0xDE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1E, 0xFF, 0x5E, 0xBF, + 0xDE, 0x3F, 0xDE, 0x3F, 0xC0, 0x3F, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xCF, 0x30, + 0xDF, 0xEF, 0xFF, 0xCF, 0xFF, 0xC1, 0xBD, + 0x81, 0xBD, 0x81, 0xFF, 0x83, 0xFF, 0xFF, + 0x00, 0xFF, 0xCF, 0x30, 0xEF, 0x30, 0xFD, + 0x3E, 0xBD, 0x7E, 0xBD, 0x7E, 0xBF, 0x7C, + 0xFF, 0x00, 0xFF, 0x08, 0xF7, 0xF0, 0xFF, + 0xF0, 0xBF, 0xC0, 0xFF, 0x80, 0x7F, 0x00, + 0x7F, 0x00, 0xFF, 0xFF, 0x07, 0xFF, 0xF7, + 0x0F, 0xF7, 0x0F, 0xBF, 0x7F, 0xFF, 0x7F, + 0x7F, 0xFF, 0x7F, 0xFF, 0xFB, 0x07, 0xFF, + 0x03, 0xFF, 0x03, 0xFB, 0x03, 0xFB, 0x03, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFB, + 0xFC, 0xFB, 0xFC, 0xFB, 0xFC, 0xFB, 0xFC, + 0xFB, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFD, 0x01, 0xFD, 0x81, 0xFF, 0x0B, + 0xF7, 0xF3, 0xFB, 0xF7, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0x7D, 0xFE, 0x7D, 0xFE, + 0x07, 0xFC, 0xF7, 0x0C, 0xF3, 0x0C, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, + 0x7B, 0x38, 0x7C, 0x1D, 0xFF, 0x0F, 0xFB, + 0x0B, 0xFD, 0x03, 0xFF, 0x00, 0xFF, 0x00, + 0xDB, 0x67, 0x5B, 0xE7, 0x7C, 0xE3, 0x6F, + 0xF0, 0x73, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x9E, 0x18, 0xFE, 0x3C, 0x5A, + 0xDC, 0xFF, 0xF9, 0xED, 0xE3, 0xBF, 0xC0, + 0xFF, 0x00, 0xFF, 0x00, 0x9A, 0xE7, 0xDA, + 0xE7, 0x1A, 0xE7, 0xFF, 0x06, 0xE5, 0x1E, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, + 0xFD, 0xBF, 0x81, 0xBF, 0x81, 0xBD, 0x81, + 0xFD, 0x81, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xC1, 0x3E, 0xBD, 0x7E, 0xBD, 0x7E, + 0xBD, 0x7E, 0xBD, 0x7E, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xBB, 0xC7, + 0xFF, 0x83, 0xFF, 0x83, 0x7B, 0x03, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xBF, 0x7C, + 0xBB, 0x7C, 0xFB, 0x7C, 0xFB, 0x7C, 0x7B, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x80, 0x7F, + 0x80, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFE, 0x00, 0xF9, 0x00, + 0xF7, 0x00, 0xEE, 0x00, 0xDD, 0x04, 0xDF, + 0x04, 0xBF, 0x08, 0xFF, 0x00, 0xFF, 0x01, + 0xFF, 0x07, 0xFF, 0x0F, 0xFF, 0x1F, 0xFB, + 0x3F, 0xFB, 0x3F, 0xF7, 0x7F, 0x80, 0x00, + 0x7F, 0x00, 0xFF, 0x00, 0x80, 0x00, 0x7F, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, + 0x08, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, + 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, + 0x10, 0xF7, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, + 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, + 0x7F, 0xEF, 0x7F, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, + 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, + 0xFF, 0x10, 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, + 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, + 0xEF, 0x7F, 0xEF, 0x7F, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xFE, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, + 0xFF, 0x01, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xC3, 0x7E, + 0xBD, 0xFF, 0x66, 0xFF, 0x7E, 0xFF, 0x7E, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xC3, 0xFF, 0x81, 0xC3, 0x18, 0x81, 0x00, + 0x00, 0x00, 0x00, 0x7E, 0xFF, 0x3C, 0xFF, + 0x00, 0x7E, 0x81, 0xBD, 0xC3, 0x42, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x81, 0x00, 0xC3, 0x81, 0xFF, + 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x01, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x08, 0xFD, 0x12, 0xFD, 0x12, + 0xFD, 0x12, 0xFD, 0x12, 0xFB, 0x24, 0xFB, + 0x24, 0xFB, 0x24, 0xF7, 0xFE, 0xEF, 0xFC, + 0xEF, 0xFC, 0xEF, 0xFC, 0xEF, 0xFC, 0xDF, + 0xF8, 0xDF, 0xF8, 0xDF, 0xF8, 0xFF, 0x00, + 0xF0, 0x1E, 0xC0, 0x3F, 0x8D, 0x72, 0x0E, + 0xF3, 0x8F, 0xF0, 0x01, 0xFC, 0xA0, 0x1E, + 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xE0, 0xFF, + 0xC0, 0x7C, 0x9F, 0x7F, 0x9F, 0x7F, 0x87, + 0xFF, 0xC0, 0xFF, 0x00, 0xFC, 0x00, 0x78, + 0x87, 0x78, 0x87, 0xF0, 0x07, 0xF8, 0x07, + 0xE2, 0x0F, 0xE2, 0x1C, 0xFF, 0xFF, 0xFF, + 0xFE, 0xFB, 0xFC, 0x7F, 0xFC, 0xFF, 0xF8, + 0xFF, 0xF2, 0xFD, 0xF3, 0xFF, 0xE6, 0xFF, + 0x00, 0x7C, 0x02, 0xFC, 0x01, 0x3C, 0xC3, + 0x3C, 0x83, 0x3E, 0xC1, 0x3C, 0xC3, 0x18, + 0xC7, 0xFF, 0xFF, 0xFD, 0xFE, 0xFF, 0x7C, + 0xBF, 0x7E, 0xFF, 0x3E, 0xFF, 0x7C, 0xFF, + 0x3C, 0xFB, 0x3C, 0xFF, 0x00, 0x1E, 0x01, + 0x1E, 0xE1, 0x1E, 0xE3, 0x1C, 0xF3, 0x0C, + 0xE7, 0x08, 0xF7, 0x19, 0x6F, 0xFF, 0xFF, + 0xFE, 0x3F, 0xFF, 0x3F, 0xFD, 0x1E, 0xEF, + 0x1E, 0xFB, 0x8C, 0xFF, 0xDD, 0xF6, 0x49, + 0xFF, 0x00, 0x18, 0x14, 0x08, 0xE3, 0x08, + 0xE3, 0x18, 0xF7, 0x1D, 0xE2, 0x18, 0xE7, + 0xB8, 0x47, 0xFF, 0xFF, 0xEB, 0x1C, 0xFF, + 0x0C, 0xFF, 0x18, 0xEF, 0x1C, 0xFF, 0x98, + 0xFF, 0x98, 0xFF, 0x18, 0xFF, 0x00, 0x06, + 0x05, 0x02, 0xF8, 0x02, 0xF9, 0xFE, 0x01, + 0xFF, 0x00, 0x06, 0xF9, 0x04, 0xF3, 0xFF, + 0xFF, 0xFA, 0x07, 0xFF, 0x02, 0xFF, 0x07, + 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0x0E, 0xFD, + 0x06, 0xFF, 0x00, 0x03, 0x00, 0x01, 0xFF, + 0x30, 0xCF, 0x70, 0x8F, 0x30, 0xC6, 0x21, + 0xDC, 0x01, 0xFE, 0xFF, 0xFF, 0xFF, 0x0F, + 0xFE, 0x01, 0xFF, 0x01, 0xF7, 0x39, 0xFF, + 0x79, 0xFF, 0x01, 0xFF, 0x03, 0xFF, 0x00, + 0xF8, 0x01, 0xF0, 0x1F, 0xE1, 0x3E, 0x83, + 0x78, 0x87, 0x30, 0xCF, 0x30, 0x8F, 0x70, + 0xFF, 0xFF, 0xFF, 0xFD, 0xEF, 0xF8, 0xDF, + 0xE0, 0xBF, 0xC3, 0xFF, 0x87, 0xFF, 0x8F, + 0xFF, 0x9F, 0xFF, 0x00, 0x3C, 0xA0, 0x0C, + 0xE1, 0x07, 0xF0, 0x86, 0x38, 0xC7, 0x3C, + 0xC3, 0x18, 0xC7, 0x3C, 0xFF, 0xFF, 0xDF, + 0xBE, 0xFF, 0x0C, 0xFF, 0x06, 0xFF, 0x87, + 0xFB, 0xE7, 0xFF, 0xC7, 0xFB, 0xE7, 0xFF, + 0x00, 0x7C, 0x40, 0x78, 0x83, 0x39, 0xEE, + 0x19, 0xE7, 0x81, 0x7C, 0x03, 0x78, 0xC7, + 0x38, 0xFF, 0xFF, 0xBF, 0x7E, 0xFF, 0x38, + 0xD7, 0x38, 0xFE, 0x31, 0xFF, 0x13, 0xFF, + 0x83, 0xFF, 0x8F, 0xFF, 0x00, 0x3C, 0x43, + 0x7C, 0x83, 0xFC, 0x03, 0xFC, 0x03, 0xFC, + 0x03, 0xFD, 0x02, 0xFC, 0x03, 0xFF, 0xFF, + 0xBF, 0x7C, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, + 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0xFF, 0xFD, + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0xA3, 0xFD, 0xB6, 0x8C, 0x3A, 0x8B, 0x7C, + 0x99, 0x62, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0xF8, 0x4B, 0xFC, 0xF7, 0xCC, + 0xF3, 0xCD, 0xFF, 0xCB, 0xFF, 0x00, 0x00, + 0xFF, 0x00, 0xFF, 0x90, 0x3F, 0xD8, 0x46, + 0x09, 0xF6, 0x0D, 0xF1, 0x12, 0xF4, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xCF, 0x78, + 0xBF, 0xD9, 0x7F, 0x9F, 0xFE, 0x9B, 0xEF, + 0x9B, 0xFF, 0x00, 0x00, 0xFC, 0x02, 0xFC, + 0x46, 0x59, 0x13, 0xAC, 0x82, 0x68, 0x07, + 0xFC, 0x14, 0xE8, 0xFF, 0xFF, 0xFF, 0x03, + 0xFF, 0x02, 0xBF, 0x73, 0x5F, 0xB6, 0xFF, + 0x23, 0xFB, 0x07, 0xFF, 0x26, 0xFF, 0x00, + 0x00, 0xFF, 0x00, 0xFF, 0x03, 0x9F, 0x21, + 0x55, 0x48, 0xB7, 0x8F, 0x60, 0x80, 0x6F, + 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFC, + 0x27, 0xBA, 0xCD, 0x7F, 0x9D, 0xFF, 0x8F, + 0xF7, 0xD8, 0xFF, 0x00, 0x00, 0xFF, 0x0C, + 0xF3, 0x18, 0xF3, 0xD8, 0x2F, 0x90, 0x67, + 0xB0, 0x4F, 0x10, 0xEF, 0xFF, 0xFF, 0xFF, + 0x08, 0xFF, 0x18, 0xEF, 0xBC, 0xF7, 0xBC, + 0xFF, 0xD0, 0xFF, 0xD8, 0xFF, 0x30, 0xFF, + 0x00, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, + 0x80, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, + 0x7F, 0xFF, 0x7F, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFF, + 0x02, 0xFF, 0x04, 0xFF, 0x08, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xFF, 0xFD, 0xFF, 0xFB, 0xFF, 0xF7, 0xFF, + 0xF7, 0x48, 0xF7, 0x48, 0xEF, 0x90, 0xEF, + 0x90, 0xDF, 0x20, 0xBF, 0x40, 0xBF, 0x40, + 0x7F, 0x80, 0xBF, 0xF0, 0xBF, 0xF0, 0x7F, + 0xE0, 0x7F, 0xE0, 0xFF, 0xC0, 0xFF, 0x80, + 0xFF, 0x80, 0xFF, 0x00, 0xFF, 0x10, 0xFF, + 0x10, 0xFF, 0x10, 0xFF, 0x10, 0xFF, 0x10, + 0xFF, 0x10, 0xFF, 0x10, 0xBF, 0x48, 0xEF, + 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, + 0xEF, 0x7F, 0xEF, 0x7F, 0xEF, 0x7F, 0xF7, + 0x3F, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFE, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, + 0xF8, 0x07, 0x00, 0x57, 0x01, 0xFF, 0x05, + 0xF8, 0x87, 0x48, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xF8, 0xFF, 0xFC, 0xEF, 0x99, 0xFE, + 0x01, 0xFF, 0x83, 0xB7, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xC0, 0x3F, 0x80, 0x7F, 0x80, + 0x7F, 0x0F, 0x70, 0x8F, 0x70, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xE7, 0xBF, + 0xC0, 0xFF, 0xCF, 0xFF, 0x9F, 0xEF, 0x1F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x18, + 0xE3, 0x18, 0xE3, 0x98, 0x77, 0x08, 0x67, + 0x1D, 0xE2, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x3C, 0xFF, 0x18, 0xEF, 0x1C, + 0xFF, 0x0C, 0x7F, 0x88, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x01, 0x3E, 0xC2, 0xFF, + 0xC2, 0x3C, 0xE2, 0x18, 0xC6, 0x39, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x8B, + 0x3C, 0xC3, 0xFF, 0xC7, 0xFF, 0xC6, 0xFF, + 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x18, 0xEF, 0x10, 0xE6, 0x10, 0xC6, 0x30, + 0xEF, 0x30, 0xCF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xF7, 0x39, 0xFF, 0x39, 0xFF, + 0x10, 0xDF, 0x38, 0xFF, 0x38, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x0C, 0x09, 0xFC, + 0x01, 0x0C, 0x0B, 0x04, 0xFB, 0x06, 0xF1, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xF7, + 0x0E, 0xFF, 0xFC, 0xF7, 0x0E, 0xFF, 0x0E, + 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xCE, 0x70, 0xCF, 0x70, 0xFE, + 0x01, 0xFE, 0x0B, 0xF0, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x69, 0xB7, 0x79, + 0x8F, 0x79, 0xFF, 0x03, 0xFF, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0x70, + 0x87, 0x78, 0x88, 0x72, 0x80, 0x7F, 0xC0, + 0x3F, 0xFC, 0x05, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x9F, 0xF7, 0x8F, 0xFD, 0xC7, 0xBF, + 0xC0, 0xDF, 0xF0, 0xFA, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE7, 0x18, 0x87, 0x38, 0x17, + 0xE8, 0x1F, 0xF0, 0x3F, 0xC0, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xC7, 0xFF, + 0x8F, 0xF7, 0x8F, 0xEF, 0x1F, 0xFF, 0x7F, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x87, + 0x78, 0x8F, 0x70, 0xDF, 0x20, 0x8F, 0x70, + 0x8F, 0x70, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xF7, 0xCF, 0xFF, 0xCF, 0xFF, 0x8F, + 0xFF, 0x9F, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFC, 0x02, 0xFD, 0x03, + 0xFD, 0x02, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFC, + 0xFE, 0xFD, 0xFF, 0xFD, 0xFF, 0xFD, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x9F, 0x30, 0xA0, 0x9E, 0x00, 0x7F, 0x00, + 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xEF, 0xF9, 0x6F, 0xF8, 0xFF, + 0x00, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x90, 0xCF, 0xE1, + 0x1E, 0x40, 0xBF, 0x00, 0xFF, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x7C, + 0xDB, 0xFF, 0xF3, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x3D, 0x82, 0x86, 0x79, 0x80, 0x7F, + 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0xA6, 0xBF, 0xEC, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x88, 0x27, + 0xD6, 0xA0, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFE, 0xDF, 0x7F, 0xCE, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x18, 0x47, 0x10, 0xC7, 0x00, + 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x10, 0xFF, + 0x18, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, + 0x80, 0x7F, 0x80, 0x7F, 0x80, 0x7F, 0x80, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x7F, 0xFF, 0x7F, 0xFF, 0x7F, + 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFF, + 0x02, 0xFF, 0x0C, 0xFF, 0x10, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xEF, 0xFF, + 0xFE, 0x11, 0xFD, 0x22, 0xFB, 0x44, 0xF7, + 0x88, 0xEF, 0x10, 0xDF, 0x20, 0xBF, 0x40, + 0x7F, 0x80, 0xEF, 0xFE, 0xDF, 0xFC, 0xBF, + 0xF8, 0x7F, 0xF0, 0xFF, 0xE0, 0xFF, 0xC0, + 0xFF, 0x80, 0xFF, 0x00, 0xBF, 0x48, 0xDF, + 0x24, 0xDF, 0x22, 0xEF, 0x11, 0xF7, 0x08, + 0xF9, 0x06, 0xFE, 0x01, 0xFF, 0x00, 0xF7, + 0x3F, 0xFB, 0x1F, 0xFD, 0x1F, 0xFE, 0x0F, + 0xFF, 0x07, 0xFF, 0x01, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x80, 0xFF, 0x7F, 0xFF, 0x00, 0x7F, + 0x80, 0x80, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x7F, 0xFF, 0x80, 0xFF, 0xFF, + 0xFF, 0xFF, 0x7F, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x03, 0xFF, 0xFC, 0xFF, 0x00, + 0xFC, 0x03, 0x03, 0xFC, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0x03, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x03, 0xFF, 0x1C, 0xFF, 0xE0, + 0xFC, 0x03, 0xE3, 0x1C, 0x1F, 0xE0, 0xFF, + 0x00, 0xFF, 0xFF, 0xFC, 0xFF, 0xE3, 0xFF, + 0x1F, 0xFF, 0xFF, 0xFC, 0xFF, 0xE0, 0xFF, + 0x00, 0xFF, 0x00, 0xFC, 0xE3, 0xF3, 0x0C, + 0xEF, 0x10, 0x1F, 0xE0, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x1F, 0xFC, + 0xFF, 0xF0, 0xFF, 0xE0, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, +}; diff --git a/bsnes/gb/Core/joypad.c b/bsnes/gb/Core/joypad.c index c0655f08..66263a98 100644 --- a/bsnes/gb/Core/joypad.c +++ b/bsnes/gb/Core/joypad.c @@ -1,5 +1,82 @@ #include "gb.h" #include +#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 (gb->use_faux_analog[player] && key <= GB_KEY_DOWN) { + if (gb->keys[player][key]) return true; + uint8_t pattern = 0; + uint8_t index_in_pattern; + switch (key) { + // Most games only sample inputs in 30FPS, so we shift right once. + case GB_KEY_RIGHT: + if (gb->faux_analog_inputs[player].x <= 0) return false; + pattern = gb->faux_analog_inputs[player].x - 1; + index_in_pattern = gb->faux_analog_ticks; + break; + case GB_KEY_LEFT: + if (gb->faux_analog_inputs[player].x >= 0) return false; + pattern = -gb->faux_analog_inputs[player].x - 1; + index_in_pattern = gb->faux_analog_ticks; + break; + case GB_KEY_UP: + if (gb->faux_analog_inputs[player].y >= 0) return false; + pattern = -gb->faux_analog_inputs[player].y - 1; + index_in_pattern = gb->faux_analog_ticks + 2; + break; + case GB_KEY_DOWN: + if (gb->faux_analog_inputs[player].y <= 0) return false; + pattern = gb->faux_analog_inputs[player].y - 1; + index_in_pattern = gb->faux_analog_ticks + 2; + break; + nodefault; + } + if (pattern == 7) return true; + /* Dithering pattern */ + static const uint8_t patterns[] = { + 0x1, + 0x11, + 0x94, + 0x55, + 0x6d, + 0x77, + 0x7f + }; + return patterns[pattern] & (1 << (index_in_pattern & 6)); + } + 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) { @@ -8,7 +85,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,39 +103,41 @@ 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 (!(gb->io_registers[GB_IO_JOYP] & 1)) { - gb->io_registers[GB_IO_JOYP] |= 2; - } - if (!(gb->io_registers[GB_IO_JOYP] & 4)) { - gb->io_registers[GB_IO_JOYP] |= 8; + if (likely(!gb->illegal_inputs_allowed)) { + if (!(gb->io_registers[GB_IO_JOYP] & 1)) { + gb->io_registers[GB_IO_JOYP] |= 2; + } + if (!(gb->io_registers[GB_IO_JOYP] & 4)) { + gb->io_registers[GB_IO_JOYP] |= 8; + } } break; 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; - default: - __builtin_unreachable(); - 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. */ - gb->io_registers[GB_IO_IF] |= 0x10; + // 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; + } } gb->io_registers[GB_IO_JOYP] |= 0xC0; @@ -72,7 +150,10 @@ void GB_icd_set_joyp(GB_gameboy_t *gb, uint8_t value) gb->io_registers[GB_IO_JOYP] |= value & 0xF; if (previous_state & ~(gb->io_registers[GB_IO_JOYP] & 0xF)) { - gb->io_registers[GB_IO_IF] |= 0x10; + if (!(gb->io_registers[GB_IO_IF] & 0x10)) { + gb->joyp_accessed = true; + gb->io_registers[GB_IO_IF] |= 0x10; + } } gb->io_registers[GB_IO_JOYP] |= 0xC0; } @@ -80,6 +161,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); } @@ -88,6 +173,161 @@ 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) +{ + 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); + } + gb->keys[0][i] = pressed; + } + + GB_update_joyp(gb); +} + +void GB_set_key_mask_for_player(GB_gameboy_t *gb, GB_key_mask_t mask, unsigned player) +{ + 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); + } + 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 = gb->use_faux_analog[gb->sgb? gb->sgb->current_player : 0]; + 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; +} + +void GB_clear_joyp_accessed(GB_gameboy_t *gb) +{ + gb->joyp_accessed = false; +} + +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; +} + +void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback) +{ + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } + gb->update_input_hint_callback = callback; +} + +void GB_set_use_faux_analog_inputs(GB_gameboy_t *gb, unsigned player, bool use) +{ + if (gb->use_faux_analog[player] == use) return; + gb->use_faux_analog[player] = use; + for (GB_key_t key = GB_KEY_RIGHT; key <= GB_KEY_DOWN; key++) { + gb->keys[player][key] = false; + } + GB_update_joyp(gb); +} + +void GB_set_faux_analog_inputs(GB_gameboy_t *gb, unsigned player, double x, double y) +{ + + if (x > 1) x = 1; + else if (x < -1) x = -1; + if (y > 1) y = 1; + else if (y < -1) y = -1; + + double abs_x = fabs(x), abs_y = fabs(y); + if (abs_x <= 0.1) x = abs_x = 0; + if (abs_y <= 0.1) y = abs_y = 0; + if (!x && !y) { + gb->faux_analog_inputs[player].x = gb->faux_analog_inputs[player].y = 0; + } + else { + if (x) { + abs_x = (abs_x - 0.1) / 0.9; + x = x > 0? abs_x : -abs_x; + } + if (y) { + abs_y = (abs_y - 0.1) / 0.9; + y = y > 0? abs_y : -abs_y; + } + // Square the circle + double distance = MIN(sqrt(x * x + y * y), 1); + double multiplier = 8 * distance / MAX(abs_x, abs_y); + + gb->faux_analog_inputs[player].x = round(x * multiplier); + gb->faux_analog_inputs[player].y = round(y * multiplier); + } + GB_update_joyp(gb); +} + +void GB_update_faux_analog(GB_gameboy_t *gb) +{ + gb->faux_analog_ticks++; + for (unsigned i = 0; i < 4; i++) { + if (!gb->use_faux_analog[i]) continue; + if ((gb->faux_analog_inputs[i].x != 0 && + gb->faux_analog_inputs[i].x != 8 && + gb->faux_analog_inputs[i].x != -8) || + (gb->faux_analog_inputs[i].y != 0 && + gb->faux_analog_inputs[i].y != 8 && + gb->faux_analog_inputs[i].y != -8)) { + gb->joypad_is_stable = false; + return; + } + } +} diff --git a/bsnes/gb/Core/joypad.h b/bsnes/gb/Core/joypad.h index 21fad534..160f5f87 100644 --- a/bsnes/gb/Core/joypad.h +++ b/bsnes/gb/Core/joypad.h @@ -1,6 +1,5 @@ -#ifndef joypad_h -#define joypad_h -#include "gb_struct_def.h" +#pragma once +#include "defs.h" #include typedef enum { @@ -15,11 +14,34 @@ typedef enum { GB_KEY_MAX } GB_key_t; +typedef enum { + GB_KEY_RIGHT_MASK = 1 << GB_KEY_RIGHT, + GB_KEY_LEFT_MASK = 1 << GB_KEY_LEFT, + GB_KEY_UP_MASK = 1 << GB_KEY_UP, + GB_KEY_DOWN_MASK = 1 << GB_KEY_DOWN, + GB_KEY_A_MASK = 1 << GB_KEY_A, + GB_KEY_B_MASK = 1 << GB_KEY_B, + GB_KEY_SELECT_MASK = 1 << GB_KEY_SELECT, + GB_KEY_START_MASK = 1 << GB_KEY_START, +} GB_key_mask_t; + +typedef void (*GB_update_input_hint_callback_t)(GB_gameboy_t *gb); + void GB_set_key_state(GB_gameboy_t *gb, GB_key_t index, bool pressed); void GB_set_key_state_for_player(GB_gameboy_t *gb, GB_key_t index, unsigned player, bool pressed); +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); 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); +void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback); +void GB_set_use_faux_analog_inputs(GB_gameboy_t *gb, unsigned player, bool use); +void GB_set_faux_analog_inputs(GB_gameboy_t *gb, unsigned player, double x, double y); #ifdef GB_INTERNAL -void GB_update_joyp(GB_gameboy_t *gb); +internal void GB_update_joyp(GB_gameboy_t *gb); +internal void GB_joypad_run(GB_gameboy_t *gb, unsigned cycles); +internal void GB_update_faux_analog(GB_gameboy_t *gb); #endif -#endif /* joypad_h */ diff --git a/bsnes/gb/Core/mbc.c b/bsnes/gb/Core/mbc.c index a9e758eb..0e7f1bae 100644 --- a/bsnes/gb/Core/mbc.c +++ b/bsnes/gb/Core/mbc.c @@ -5,40 +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 , 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) @@ -75,6 +76,7 @@ void GB_update_mbc_mappings(GB_gameboy_t *gb) gb->mbc_rom_bank++; } break; + nodefault; } break; case GB_MBC2: @@ -94,60 +96,124 @@ 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_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_MBC7: + gb->mbc_rom_bank = gb->mbc7.rom_bank; + break; + 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; break; case GB_TPP1: - gb->mbc_rom_bank = gb->tpp1_rom_bank; - gb->mbc_ram_bank = gb->tpp1_ram_bank; - gb->mbc_ram_enable = (gb->tpp1_mode == 2) || (gb->tpp1_mode == 3); + gb->mbc_rom_bank = gb->tpp1.rom_bank; + gb->mbc_ram_bank = gb->tpp1.ram_bank; + gb->mbc_ram_enable = (gb->tpp1.mode == 2) || (gb->tpp1.mode == 3); break; + nodefault; } } 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}; - gb->cartridge_type = &tpp1; - gb->tpp1_rom_bank = 1; + 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); } - - 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; + 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->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->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; } + else if (gb->cartridge_type->mbc_type == GB_MBC7) { + gb->mbc_ram_size = 0x100; + } else if (gb->cartridge_type->mbc_type == GB_TPP1) { if (gb->rom[0x152] >= 1 && gb->rom[0x152] <= 9) { gb->mbc_ram_size = 0x2000 << (gb->rom[0x152] - 1); @@ -155,15 +221,30 @@ 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_size && gb->mbc_ram_size < 0x2000 && + gb->cartridge_type->mbc_type != GB_MBC2 && + gb->cartridge_type->mbc_type != GB_MBC7) { + GB_log(gb, "This ROM requests a RAM size smaller than a bank, it may misbehave if this was not done intentionally.\n"); + } + + 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). @@ -183,8 +264,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_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; + } + 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/bsnes/gb/Core/mbc.h b/bsnes/gb/Core/mbc.h index 3bbe7827..78dd09df 100644 --- a/bsnes/gb/Core/mbc.h +++ b/bsnes/gb/Core/mbc.h @@ -1,6 +1,5 @@ -#ifndef MBC_h -#define MBC_h -#include "gb_struct_def.h" +#pragma once +#include "defs.h" #include typedef struct { @@ -10,14 +9,13 @@ typedef struct { GB_MBC2, 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; @@ -25,9 +23,8 @@ typedef struct { } GB_cartridge_t; #ifdef GB_INTERNAL -extern const GB_cartridge_t GB_cart_defs[256]; -void GB_update_mbc_mappings(GB_gameboy_t *gb); -void GB_configure_cart(GB_gameboy_t *gb); +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/bsnes/gb/Core/memory.c b/bsnes/gb/Core/memory.c index 426e5d64..0c872db3 100644 --- a/bsnes/gb/Core/memory.c +++ b/bsnes/gb/Core/memory.c @@ -1,18 +1,18 @@ #include #include +#include #include "gb.h" -typedef uint8_t GB_read_function_t(GB_gameboy_t *gb, uint16_t addr); -typedef void GB_write_function_t(GB_gameboy_t *gb, uint16_t addr, uint8_t value); +typedef uint8_t read_function_t(GB_gameboy_t *gb, uint16_t addr); +typedef void write_function_t(GB_gameboy_t *gb, uint16_t addr, uint8_t value); typedef enum { GB_BUS_MAIN, /* In DMG: Cart and RAM. In CGB: Cart only */ GB_BUS_RAM, /* In CGB only. */ GB_BUS_VRAM, - GB_BUS_INTERNAL, /* Anything in highram. Might not be the most correct name. */ -} GB_bus_t; +} bus_t; -static GB_bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr) +static bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr) { if (addr < 0x8000) { return GB_BUS_MAIN; @@ -23,10 +23,7 @@ static GB_bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr) if (addr < 0xC000) { return GB_BUS_MAIN; } - if (addr < 0xFE00) { - return GB_is_cgb(gb)? GB_BUS_RAM : GB_BUS_MAIN; - } - return GB_BUS_INTERNAL; + return GB_is_cgb(gb)? GB_BUS_RAM : GB_BUS_MAIN; } static uint16_t bitwise_glitch(uint16_t a, uint16_t b, uint16_t c) @@ -98,7 +95,8 @@ void GB_trigger_oam_bug(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) { + GB_display_sync(gb); + 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], @@ -200,13 +198,17 @@ 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); } else if ((gb->accessed_oam_row & 0x18) == 0x00) { /* Everything in this specific case is *extremely* revision and instance specific. */ - if (gb->accessed_oam_row == 0x40) { + if (gb->model == GB_MODEL_MGB) { + /* TODO: This is rather simplified, research further */ + oam_bug_tertiary_read_corruption(gb, bitwise_glitch_tertiary_read_3); + } + else if (gb->accessed_oam_row == 0x40) { oam_bug_quaternary_read_corruption(gb, ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB2)? bitwise_glitch_quaternary_read_sgb2: @@ -240,6 +242,9 @@ void GB_trigger_oam_bug_read(GB_gameboy_t *gb, uint16_t address) if (gb->accessed_oam_row == 0x80) { memcpy(gb->oam, gb->oam + gb->accessed_oam_row, 8); } + else if (gb->model == GB_MODEL_MGB && gb->accessed_oam_row == 0x40) { + memcpy(gb->oam, gb->oam + gb->accessed_oam_row, 8); + } } } } @@ -247,7 +252,19 @@ 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->dma_steps_left || (gb->dma_cycles < 0 && !gb->is_dma_restarting) || 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 (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_is_cgb(gb)) { + if (addr >= 0xC000) { + return bus_for_addr(gb, gb->dma_current_src) != GB_BUS_VRAM; + } + if (gb->dma_current_src >= 0xE000) { + return bus_for_addr(gb, addr) != GB_BUS_VRAM; + } + } return bus_for_addr(gb, addr) == bus_for_addr(gb, gb->dma_current_src); } @@ -276,39 +293,84 @@ static uint8_t read_mbc_rom(GB_gameboy_t *gb, uint16_t addr) static uint8_t read_vram(GB_gameboy_t *gb, uint16_t addr) { - if (gb->vram_read_blocked) { + if (likely(!GB_is_dma_active(gb))) { + /* Prevent syncing from a DMA read. Batching doesn't happen during DMA anyway. */ + GB_display_sync(gb); + } + else { + if (unlikely((gb->dma_current_dest & 0xE000) == 0x8000)) { + // TODO: verify conflict behavior + 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 (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) +{ + if (!gb->mbc_ram_enable || !gb->mbc7.secondary_ram_enable) return 0xFF; + if (addr >= 0xB000) return 0xFF; + switch ((addr >> 4) & 0xF) { + case 2: return gb->mbc7.x_latch; + case 3: return gb->mbc7.x_latch >> 8; + case 4: return gb->mbc7.y_latch; + case 5: return gb->mbc7.y_latch >> 8; + case 6: return 0; + case 8: return gb->mbc7.eeprom_do | (gb->mbc7.eeprom_di << 1) | + (gb->mbc7.eeprom_clk << 6) | (gb->mbc7.eeprom_cs << 7); + } + return 0xFF; } static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) { + if (gb->cartridge_type->mbc_type == GB_MBC7) { + return read_mbc7_ram(gb, addr); + } + if (gb->cartridge_type->mbc_type == GB_HUC3) { - switch (gb->huc3_mode) { + switch (gb->huc3.mode) { case 0xC: // RTC read - if (gb->huc3_access_flags == 0x2) { + if (gb->huc3.access_flags == 0x2) { return 1; } - return gb->huc3_read; + return gb->huc3.read; case 0xD: // RTC status return 1; case 0xE: // IR mode return gb->effective_ir_input; // TODO: What are the other bits? default: - GB_log(gb, "Unsupported HuC-3 mode %x read: %04x\n", gb->huc3_mode, addr); + GB_log(gb, "Unsupported HuC-3 mode %x read: %04x\n", gb->huc3.mode, addr); return 1; // TODO: What happens in this case? case 0: // TODO: R/O RAM? (or is it disabled?) case 0xA: // RAM @@ -317,13 +379,14 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) } if (gb->cartridge_type->mbc_type == GB_TPP1) { - switch (gb->tpp1_mode) { + switch (gb->tpp1.mode) { case 0: switch (addr & 3) { - case 0: return gb->tpp1_rom_bank; - case 1: return gb->tpp1_rom_bank >> 8; - case 2: return gb->tpp1_ram_bank; + case 0: return gb->tpp1.rom_bank; + case 1: return gb->tpp1.rom_bank >> 8; + case 2: return gb->tpp1.ram_bank; case 3: return gb->rumble_strength | gb->tpp1_mr4; + nodefault; } case 2: case 3: @@ -331,22 +394,24 @@ 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 && - gb->mbc3_rtc_mapped) { + gb->mbc3.rtc_mapped) { /* RTC read */ if (gb->mbc_ram_bank <= 4) { gb->rtc_latched.seconds &= 0x3F; @@ -355,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) { @@ -363,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; @@ -394,47 +468,114 @@ static uint8_t read_banked_ram(GB_gameboy_t *gb, uint16_t addr) return gb->ram[(addr & 0x0FFF) + gb->cgb_ram_bank * 0x1000]; } -static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) +static inline void sync_ppu_if_needed(GB_gameboy_t *gb, uint8_t register_accessed) { + switch (register_accessed) { + case GB_IO_IF: + case GB_IO_LCDC: + case GB_IO_STAT: + case GB_IO_SCY: + case GB_IO_SCX: + case GB_IO_LY: + case GB_IO_LYC: + case GB_IO_DMA: + case GB_IO_BGP: + case GB_IO_OBP0: + case GB_IO_OBP1: + case GB_IO_WY: + case GB_IO_WX: + case GB_IO_HDMA1: + case GB_IO_HDMA2: + case GB_IO_HDMA3: + case GB_IO_HDMA4: + case GB_IO_HDMA5: + case GB_IO_BGPI: + case GB_IO_BGPD: + case GB_IO_OBPI: + case GB_IO_OBPD: + case GB_IO_OPRI: + GB_display_sync(gb); + break; + } +} - if (gb->hdma_on) { - return gb->last_opcode_read; +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) { return read_banked_ram(gb, addr); } if (addr < 0xFF00) { + GB_display_sync(gb); if (gb->oam_write_blocked && !GB_is_cgb(gb)) { - GB_trigger_oam_bug_read(gb, addr); - return 0xff; + if (!gb->disable_oam_corruption) { + GB_trigger_oam_bug_read(gb, addr); + } + return 0xFF; } - if ((gb->dma_steps_left && (gb->dma_cycles > 0 || gb->is_dma_restarting))) { + 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) { - if (!GB_is_cgb(gb)) { + if (!GB_is_cgb(gb) && !gb->disable_oam_corruption) { 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: @@ -449,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); @@ -463,65 +604,23 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) oam[target >> 1] = bitwise_glitch_read(a, b, c); break; - default: - __builtin_unreachable(); + nodefault; } 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: - 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_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) { + sync_ppu_if_needed(gb, addr); switch (addr & 0xFF) { case GB_IO_IF: return gb->io_registers[GB_IO_IF] | 0xE0; @@ -535,16 +634,23 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) } return gb->io_registers[GB_IO_OPRI] | 0xFE; - case GB_IO_PCM_12: + case GB_IO_PCM12: if (!GB_is_cgb(gb)) return 0xFF; + GB_apu_run(gb, true); return ((gb->apu.is_active[GB_SQUARE_2] ? (gb->apu.samples[GB_SQUARE_2] << 4) : 0) | (gb->apu.is_active[GB_SQUARE_1] ? (gb->apu.samples[GB_SQUARE_1]) : 0)) & (gb->model <= GB_MODEL_CGB_C? gb->apu.pcm_mask[0] : 0xFF); - case GB_IO_PCM_34: + case GB_IO_PCM34: if (!GB_is_cgb(gb)) return 0xFF; + GB_apu_run(gb, true); return ((gb->apu.is_active[GB_NOISE] ? (gb->apu.samples[GB_NOISE] << 4) : 0) | (gb->apu.is_active[GB_WAVE] ? (gb->apu.samples[GB_WAVE]) : 0)) & (gb->model <= GB_MODEL_CGB_C? gb->apu.pcm_mask[1] : 0xFF); 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: @@ -574,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; @@ -601,7 +708,7 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) uint8_t index_reg = (addr & 0xFF) - 1; return ((addr & 0xFF) == GB_IO_BGPD? gb->background_palettes_data : - gb->sprite_palettes_data)[gb->io_registers[index_reg] & 0x3F]; + gb->object_palettes_data)[gb->io_registers[index_reg] & 0x3F]; } case GB_IO_KEY1: @@ -609,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. */ @@ -617,17 +725,17 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) if (gb->model != GB_MODEL_CGB_E) { ret |= 0x10; } - if (((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && gb->effective_ir_input) && gb->model != GB_MODEL_AGB) { + if (((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && gb->effective_ir_input) && gb->model <= GB_MODEL_CGB_E) { ret &= ~2; } return ret; } - case GB_IO_UNKNOWN2: - case GB_IO_UNKNOWN3: + case GB_IO_PSWX: + case GB_IO_PSWY: return GB_is_cgb(gb)? gb->io_registers[addr & 0xFF] : 0xFF; - case GB_IO_UNKNOWN4: + case GB_IO_PSW: return gb->cgb_mode? gb->io_registers[addr & 0xFF] : 0xFF; - case GB_IO_UNKNOWN5: + case GB_IO_PGB: return GB_is_cgb(gb)? gb->io_registers[addr & 0xFF] | 0x8F : 0xFF; default: if ((addr & 0xFF) >= GB_IO_NR10 && (addr & 0xFF) <= GB_IO_WAV_END) { @@ -635,7 +743,7 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) } return 0xFF; } - __builtin_unreachable(); + unreachable(); } if (addr == 0xFFFF) { @@ -647,7 +755,7 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr) return gb->hram[addr - 0xFF80]; } -static GB_read_function_t * const read_map[] = +static read_function_t *const read_map[] = { read_rom, read_rom, read_rom, read_rom, /* 0XXX, 1XXX, 2XXX, 3XXX */ read_mbc_rom, read_mbc_rom, read_mbc_rom, read_mbc_rom, /* 4XXX, 5XXX, 6XXX, 7XXX */ @@ -659,20 +767,71 @@ static GB_read_function_t * const read_map[] = void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t callback) { + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } gb->read_memory_callback = callback; } uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr) { - if (gb->n_watchpoints) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + +#ifndef GB_DISABLE_DEBUGGER + if (unlikely(gb->n_watchpoints)) { GB_debugger_test_read_watchpoint(gb, addr); } - if (is_addr_in_dma_use(gb, addr)) { - addr = gb->dma_current_src; +#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) { + /* 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) { + // 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) { + // 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); + } } uint8_t data = read_map[addr >> 12](gb, addr); GB_apply_cheat(gb, addr, &data); - if (gb->read_memory_callback) { + 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; +} + +uint8_t GB_safe_read_memory(GB_gameboy_t *gb, uint16_t addr) +{ + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + + if (unlikely(addr == 0xFF00 + GB_IO_JOYP)) { + return gb->io_registers[GB_IO_JOYP]; + } + gb->disable_oam_corruption = true; + uint8_t data = read_map[addr >> 12](gb, addr); + gb->disable_oam_corruption = false; + GB_apply_cheat(gb, addr, &data); + if (unlikely(gb->read_memory_callback)) { data = gb->read_memory_callback(gb, addr, data); } return data; @@ -702,7 +861,7 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case 0x2000: case 0x3000: gb->mbc3.rom_bank = value; break; case 0x4000: case 0x5000: gb->mbc3.ram_bank = value; - gb->mbc3_rtc_mapped = value & 8; + gb->mbc3.rtc_mapped = value & 8; break; case 0x6000: case 0x7000: memcpy(&gb->rtc_latched, &gb->rtc_real, sizeof(gb->rtc_real)); @@ -722,7 +881,58 @@ 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; + case GB_MBC7: + switch (addr & 0xF000) { + case 0x0000: case 0x1000: gb->mbc_ram_enable = value == 0x0A; break; + case 0x2000: case 0x3000: gb->mbc7.rom_bank = value; break; + 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; @@ -731,14 +941,13 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) 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->mbc_ram_enable = gb->huc3_mode == 0xA; + gb->huc3.mode = value; + gb->mbc_ram_enable = gb->huc3.mode == 0xA; break; case 0x2000: case 0x3000: gb->huc3.rom_bank = value; break; case 0x4000: case 0x5000: gb->huc3.ram_bank = value; break; @@ -747,15 +956,15 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case GB_TPP1: switch (addr & 3) { case 0: - gb->tpp1_rom_bank &= 0xFF00; - gb->tpp1_rom_bank |= value; + gb->tpp1.rom_bank &= 0xFF00; + gb->tpp1.rom_bank |= value; break; case 1: - gb->tpp1_rom_bank &= 0xFF; - gb->tpp1_rom_bank |= value << 8; + gb->tpp1.rom_bank &= 0xFF; + gb->tpp1.rom_bank |= value << 8; break; case 2: - gb->tpp1_ram_bank = value; + gb->tpp1.ram_bank = value; break; case 3: switch (value) { @@ -763,7 +972,7 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case 2: case 3: case 5: - gb->tpp1_mode = value; + gb->tpp1.mode = value; break; case 0x10: memcpy(&gb->rtc_latched, &gb->rtc_real, sizeof(gb->rtc_real)); @@ -791,87 +1000,76 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } } break; + nodefault; } GB_update_mbc_mappings(gb); } static void write_vram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { - if (gb->vram_write_blocked) { + GB_display_sync(gb); + if (unlikely(gb->vram_write_blocked)) { //GB_log(gb, "Wrote %02x to %04x (VRAM) during mode 3\n", value, addr); return; } - /* TODO: not verified */ - if (gb->display_state == 22 && GB_is_cgb(gb) && !gb->cgb_double_speed) { - if (addr & 0x1000) { - addr = gb->last_tile_index_address; - } - 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; - } - } gb->vram[(addr & 0x1FFF) + (gb->cgb_vram_bank? 0x2000 : 0)] = value; } static bool huc3_write(GB_gameboy_t *gb, uint8_t value) { - switch (gb->huc3_mode) { + switch (gb->huc3.mode) { case 0xB: // RTC Write switch (value >> 4) { case 1: - if (gb->huc3_access_index < 3) { - gb->huc3_read = (gb->huc3_minutes >> (gb->huc3_access_index * 4)) & 0xF; + if (gb->huc3.access_index < 3) { + gb->huc3.read = (gb->huc3.minutes >> (gb->huc3.access_index * 4)) & 0xF; } - else if (gb->huc3_access_index < 7) { - gb->huc3_read = (gb->huc3_days >> ((gb->huc3_access_index - 3) * 4)) & 0xF; + else if (gb->huc3.access_index < 7) { + gb->huc3.read = (gb->huc3.days >> ((gb->huc3.access_index - 3) * 4)) & 0xF; } else { - // GB_log(gb, "Attempting to read from unsupported HuC-3 register: %03x\n", gb->huc3_access_index); + // GB_log(gb, "Attempting to read from unsupported HuC-3 register: %03x\n", gb->huc3.access_index); } - gb->huc3_access_index++; + gb->huc3.access_index++; break; case 2: case 3: - if (gb->huc3_access_index < 3) { - gb->huc3_minutes &= ~(0xF << (gb->huc3_access_index * 4)); - gb->huc3_minutes |= ((value & 0xF) << (gb->huc3_access_index * 4)); + if (gb->huc3.access_index < 3) { + gb->huc3.minutes &= ~(0xF << (gb->huc3.access_index * 4)); + gb->huc3.minutes |= ((value & 0xF) << (gb->huc3.access_index * 4)); } - else if (gb->huc3_access_index < 7) { - 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 < 7) { + 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) { - 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 >= 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) { - gb->huc3_alarm_enabled = value & 1; + else if (gb->huc3.access_index == 0x5F) { + gb->huc3.alarm_enabled = value & 1; } else { - // GB_log(gb, "Attempting to write %x to unsupported HuC-3 register: %03x\n", value & 0xF, gb->huc3_access_index); + // GB_log(gb, "Attempting to write %x to unsupported HuC-3 register: %03x\n", value & 0xF, gb->huc3.access_index); } if ((value >> 4) == 3) { - gb->huc3_access_index++; + gb->huc3.access_index++; } break; case 4: - gb->huc3_access_index &= 0xF0; - gb->huc3_access_index |= value & 0xF; + gb->huc3.access_index &= 0xF0; + gb->huc3.access_index |= value & 0xF; break; case 5: - gb->huc3_access_index &= 0x0F; - gb->huc3_access_index |= (value & 0xF) << 4; + gb->huc3.access_index &= 0x0F; + gb->huc3.access_index |= (value & 0xF) << 4; break; case 6: - gb->huc3_access_flags = (value & 0xF); + gb->huc3.access_flags = (value & 0xF); break; default: @@ -901,8 +1099,136 @@ static bool huc3_write(GB_gameboy_t *gb, uint8_t value) } } +static void write_mbc7_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) +{ + if (!gb->mbc_ram_enable || !gb->mbc7.secondary_ram_enable) return; + if (addr >= 0xB000) return; + switch ((addr >> 4) & 0xF) { + case 0: { + if (value == 0x55) { + gb->mbc7.latch_ready = true; + gb->mbc7.x_latch = gb->mbc7.y_latch = 0x8000; + } + break; + } + case 1: { + if (value == 0xAA) { + gb->mbc7.latch_ready = false; + gb->mbc7.x_latch = 0x81D0 + 0x70 * gb->accelerometer_x; + gb->mbc7.y_latch = 0x81D0 + 0x70 * gb->accelerometer_y; + } + break; + } + case 8: { + gb->mbc7.eeprom_cs = value & 0x80; + gb->mbc7.eeprom_di = value & 2; + if (gb->mbc7.eeprom_cs) { + if (!gb->mbc7.eeprom_clk && (value & 0x40)) { // Clocked + gb->mbc7.eeprom_do = gb->mbc7.read_bits >> 15; + gb->mbc7.read_bits <<= 1; + gb->mbc7.read_bits |= 1; + 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; + if (gb->mbc7.eeprom_command & 0x400) { + // Got full command + switch ((gb->mbc7.eeprom_command >> 6) & 0xF) { + case 0x8: + case 0x9: + case 0xA: + case 0xB: + // READ + gb->mbc7.read_bits = LE16(((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F]); + gb->mbc7.eeprom_command = 0; + break; + case 0x3: // EWEN (Eeprom Write ENable) + gb->mbc7.eeprom_write_enabled = true; + gb->mbc7.eeprom_command = 0; + break; + case 0x0: // EWDS (Eeprom Write DiSable) + gb->mbc7.eeprom_write_enabled = false; + gb->mbc7.eeprom_command = 0; + break; + case 0x4: + case 0x5: + case 0x6: + case 0x7: + // WRITE + if (gb->mbc7.eeprom_write_enabled) { + ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] = 0; + } + gb->mbc7.argument_bits_left = 16; + // We still need to process this command, don't erase eeprom_command + break; + case 0xC: + case 0xD: + case 0xE: + case 0xF: + // ERASE + if (gb->mbc7.eeprom_write_enabled) { + ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] = 0xFFFF; + gb->mbc7.read_bits = 0x3FFF; // Emulate some time to settle + } + gb->mbc7.eeprom_command = 0; + break; + case 0x2: + // ERAL (ERase ALl) + if (gb->mbc7.eeprom_write_enabled) { + memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size); + ((uint16_t *)gb->mbc_ram)[gb->mbc7.eeprom_command & 0x7F] = 0xFFFF; + gb->mbc7.read_bits = 0xFF; // Emulate some time to settle + } + gb->mbc7.eeprom_command = 0; + break; + case 0x1: + // WRAL (WRite ALl) + if (gb->mbc7.eeprom_write_enabled) { + memset(gb->mbc_ram, 0, gb->mbc_ram_size); + } + gb->mbc7.argument_bits_left = 16; + // We still need to process this command, don't erase eeprom_command + break; + } + } + } + else { + // We're shifting in extra bits for a WRITE/WRAL command + gb->mbc7.argument_bits_left--; + gb->mbc7.eeprom_do = true; + if (gb->mbc7.eeprom_di) { + 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; + } + else { + // WRAL + for (unsigned i = 0; i < 0x7F; i++) { + ((uint16_t *)gb->mbc_ram)[i] |= bit; + } + } + } + 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 + } + } + } + } + gb->mbc7.eeprom_clk = value & 0x40; + break; + } + } +} + static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { + if (gb->cartridge_type->mbc_type == GB_MBC7) { + write_mbc7_ram(gb, addr, value); + return; + } + if (gb->cartridge_type->mbc_type == GB_HUC3) { if (huc3_write(gb, value)) return; } @@ -913,7 +1239,7 @@ static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } if (gb->cartridge_type->mbc_type == GB_TPP1) { - switch (gb->tpp1_mode) { + switch (gb->tpp1.mode) { case 3: break; case 5: @@ -937,7 +1263,7 @@ static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) return; } - if (gb->cartridge_type->has_rtc && gb->mbc3_rtc_mapped) { + if (gb->cartridge_type->has_rtc && gb->mbc3.rtc_mapped) { if (gb->mbc_ram_bank <= 4) { if (gb->mbc_ram_bank == 0) { gb->rtc_cycles = 0; @@ -950,7 +1276,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) { @@ -972,6 +1303,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) { @@ -981,60 +1347,30 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } if (addr < 0xFF00) { + GB_display_sync(gb); if (gb->oam_write_blocked) { GB_trigger_oam_bug(gb, addr); return; } - if ((gb->dma_steps_left && (gb->dma_cycles > 0 || gb->is_dma_restarting))) { + if (GB_is_dma_active(gb)) { /* Todo: Does writing to OAM during DMA causes the OAM bug? */ return; } if (GB_is_cgb(gb)) { - if (addr < 0xFEA0) { - gb->oam[addr & 0xFF] = value; - } - 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_DMG_B: - 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: - case GB_MODEL_CGB_E: - case GB_MODEL_AGB: - break; - } + 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]); } } } @@ -1043,13 +1379,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]; } } } @@ -1062,13 +1398,16 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) /* Todo: Clean this code up: use a function table and move relevant code to display.c and timing.c (APU read and writes are already at apu.c) */ if (addr < 0xFF80) { + sync_ppu_if_needed(gb, addr); + /* Hardware registers */ switch (addr & 0xFF) { - case GB_IO_WY: - if (value == gb->current_line) { - gb->wy_triggered = true; - } + case GB_IO_WX: + gb->io_registers[addr & 0xFF] = value; + GB_update_wx_glitch(gb); + break; + case GB_IO_IF: case GB_IO_SCX: case GB_IO_SCY: @@ -1076,10 +1415,10 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) case GB_IO_OBP0: case GB_IO_OBP1: case GB_IO_SB: - case GB_IO_UNKNOWN2: - case GB_IO_UNKNOWN3: - case GB_IO_UNKNOWN4: - case GB_IO_UNKNOWN5: + case GB_IO_PSWX: + case GB_IO_PSWY: + case GB_IO_PSW: + case GB_IO_PGB: gb->io_registers[addr & 0xFF] = value; return; case GB_IO_OPRI: @@ -1092,8 +1431,12 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } return; - case GB_IO_LYC: + case GB_IO_WY: + gb->io_registers[addr & 0xFF] = value; + gb->wy_check_scheduled = true; + return; + case GB_IO_LYC: /* TODO: Probably completely wrong in double speed mode */ /* TODO: This hack is disgusting */ @@ -1107,7 +1450,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) /* These are the states when LY changes, let the display routine call GB_STAT_update for use so it correctly handles T-cycle accurate LYC writes */ - if (!GB_is_cgb(gb) || ( + if (!GB_is_cgb(gb) || ( gb->display_state != 35 && gb->display_state != 26 && gb->display_state != 15 && @@ -1145,51 +1488,78 @@ 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 & 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); + } + 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)) { - gb->wx_triggered = false; - gb->wx166_glitch = false; - } + gb->wy_check_scheduled = true; 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; @@ -1201,6 +1571,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); @@ -1208,7 +1603,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: @@ -1219,18 +1614,13 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) return; case GB_IO_DMA: - if (gb->dma_steps_left) { - /* This is not correct emulation, since we're not really delaying the second DMA. - One write that should have happened in the first DMA will not happen. However, - since that byte will be overwritten by the second DMA before it can actually be - read, it doesn't actually matter. */ - gb->is_dma_restarting = true; - } - gb->dma_cycles = -7; - gb->dma_current_dest = 0; + 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->dma_steps_left = 0xa0; 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)) { @@ -1238,6 +1628,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: @@ -1272,7 +1663,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } ((addr & 0xFF) == GB_IO_BGPD? gb->background_palettes_data : - gb->sprite_palettes_data)[gb->io_registers[index_reg] & 0x3F] = value; + gb->object_palettes_data)[gb->io_registers[index_reg] & 0x3F] = value; GB_palette_changed(gb, (addr & 0xFF) == GB_IO_BGPD, gb->io_registers[index_reg] & 0x3F); if (gb->io_registers[index_reg] & 0x80) { gb->io_registers[index_reg]++; @@ -1290,6 +1681,10 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) gb->hdma_current_src &= 0xF0; gb->hdma_current_src |= value << 8; } + /* Range 0xE*** like 0xF*** and can't overflow (with 0x800 bytes) to anything meaningful */ + if (gb->hdma_current_src >= 0xE000) { + gb->hdma_current_src |= 0xF000; + } return; case GB_IO_HDMA2: if (gb->cgb_mode) { @@ -1305,50 +1700,42 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) return; case GB_IO_HDMA4: if (gb->cgb_mode) { - gb->hdma_current_dest &= 0x1F00; + gb->hdma_current_dest &= 0xFF00; gb->hdma_current_dest |= value & 0xF0; } 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; } gb->hdma_on = (value & 0x80) == 0; gb->hdma_on_hblank = (value & 0x80) != 0; - 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->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; - /* Todo: Verify this. Gambatte's DMA tests require this. */ - if (gb->hdma_current_dest + (gb->hdma_steps_left << 4) > 0xFFFF) { - gb->hdma_steps_left = (0x10000 - gb->hdma_current_dest) >> 4; - } - gb->hdma_cycles = -12; 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: { @@ -1376,6 +1763,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } if (addr == 0xFFFF) { + GB_display_sync(gb); /* Interrupt mask */ gb->interrupt_enable = value; return; @@ -1387,7 +1775,7 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) -static GB_write_function_t * const write_map[] = +static write_function_t *const write_map[] = { write_mbc, write_mbc, write_mbc, write_mbc, /* 0XXX, 1XXX, 2XXX, 3XXX */ write_mbc, write_mbc, write_mbc, write_mbc, /* 4XXX, 5XXX, 6XXX, 7XXX */ @@ -1397,62 +1785,177 @@ static GB_write_function_t * const write_map[] = write_ram, write_high_memory, /* EXXX FXXX */ }; +void GB_set_write_memory_callback(GB_gameboy_t *gb, GB_write_memory_callback_t callback) +{ + if (!callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) + } + gb->write_memory_callback = callback; +} + void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { - if (gb->n_watchpoints) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) +#ifndef GB_DISABLE_DEBUGGER + if (unlikely(gb->n_watchpoints)) { GB_debugger_test_write_watchpoint(gb, addr, value); } - if (is_addr_in_dma_use(gb, addr)) { - /* Todo: What should happen? Will this affect DMA? Will data be written? What and where? */ - return; +#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; + } + + 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) { + /* This is cart specific! Everdrive 7X on a CGB-A or 0 behaves differently. */ + return; + } + + 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) { + // 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 >= 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)) { + gb->oam[gb->dma_current_dest - 1] &= 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 >= 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; +} + void GB_dma_run(GB_gameboy_t *gb) { - while (gb->dma_cycles >= 4 && gb->dma_steps_left) { - /* Todo: measure this value */ - gb->dma_cycles -= 4; - gb->dma_steps_left--; - - if (gb->dma_current_src < 0xe000) { + 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) { + gb->dma_current_dest++; + if (gb->display_state == 8) { + gb->io_registers[GB_IO_STAT] |= 2; + GB_STAT_update(gb); + } + break; + } + 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 { - /* Todo: Not correct on the CGB */ - gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src & ~0x2000); + if (GB_is_cgb(gb)) { + gb->oam[gb->dma_current_dest++] = 0xFF; + } + else { + gb->oam[gb->dma_current_dest++] = GB_read_memory(gb, gb->dma_current_src & ~0x2000); + } } /* dma_current_src must be the correct value during GB_read_memory */ gb->dma_current_src++; - if (!gb->dma_steps_left) { - gb->is_dma_restarting = false; - } + gb->dma_ppu_vram_conflict = false; } + gb->in_dma_read = false; + gb->dma_cycles_modulo = cycles; + gb->dma_cycles = 0; } void GB_hdma_run(GB_gameboy_t *gb) { - if (!gb->hdma_on) return; - - while (gb->hdma_cycles >= 0x4) { - gb->hdma_cycles -= 0x4; - - GB_write_memory(gb, 0x8000 | (gb->hdma_current_dest++ & 0x1FFF), GB_read_memory(gb, (gb->hdma_current_src++))); + 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->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->data_bus; + gb->addr_for_hdma_conflict = 0xFFFF; - if ((gb->hdma_current_dest & 0xf) == 0) { - if (--gb->hdma_steps_left == 0) { + 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++; + 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) { + /* + These corruptions revision (unit?) specific in single speed. They happen only on my CGB-E. + */ + gb->addr_for_hdma_conflict &= 0x1FFF; + // 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++; + } + + 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->hdma_starting = false; - gb->io_registers[GB_IO_HDMA5] &= 0x7F; - break; } - if (gb->hdma_on_hblank) { + else if (gb->hdma_on_hblank) { gb->hdma_on = false; - break; } } } + gb->hdma_in_progress = false; // TODO: timing? (affects VRAM reads) + if (!gb->cgb_double_speed) { + GB_advance_cycles(gb, 2); + } } diff --git a/bsnes/gb/Core/memory.h b/bsnes/gb/Core/memory.h index 80020f17..0bd8a435 100644 --- a/bsnes/gb/Core/memory.h +++ b/bsnes/gb/Core/memory.h @@ -1,17 +1,19 @@ -#ifndef memory_h -#define memory_h -#include "gb_struct_def.h" +#pragma once +#include "defs.h" #include typedef uint8_t (*GB_read_memory_callback_t)(GB_gameboy_t *gb, uint16_t addr, uint8_t data); +typedef bool (*GB_write_memory_callback_t)(GB_gameboy_t *gb, uint16_t addr, uint8_t data); // Return false to prevent the write void GB_set_read_memory_callback(GB_gameboy_t *gb, GB_read_memory_callback_t callback); +void GB_set_write_memory_callback(GB_gameboy_t *gb, GB_write_memory_callback_t callback); uint8_t GB_read_memory(GB_gameboy_t *gb, uint16_t addr); +uint8_t GB_safe_read_memory(GB_gameboy_t *gb, uint16_t addr); // Without side effects void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value); #ifdef GB_INTERNAL -void GB_dma_run(GB_gameboy_t *gb); -void GB_hdma_run(GB_gameboy_t *gb); -void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address); +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/bsnes/gb/Core/model.h b/bsnes/gb/Core/model.h new file mode 100644 index 00000000..fac525c6 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Core/printer.c b/bsnes/gb/Core/printer.c index c8514b41..3b86b5c0 100644 --- a/bsnes/gb/Core/printer.c +++ b/bsnes/gb/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,13 @@ 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) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) 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/bsnes/gb/Core/printer.h b/bsnes/gb/Core/printer.h index f5c9b277..2a76f309 100644 --- a/bsnes/gb/Core/printer.h +++ b/bsnes/gb/Core/printer.h @@ -1,8 +1,8 @@ -#ifndef printer_h -#define printer_h +#pragma once + #include #include -#include "gb_struct_def.h" +#include "defs.h" #define GB_PRINTER_MAX_COMMAND_LENGTH 0x280 #define GB_PRINTER_DATA_SIZE 0x280 @@ -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/bsnes/gb/Core/random.h b/bsnes/gb/Core/random.h index 8ab0e502..28676f0d 100644 --- a/bsnes/gb/Core/random.h +++ b/bsnes/gb/Core/random.h @@ -1,12 +1,12 @@ -#ifndef random_h -#define random_h +#pragma once #include #include +#include "defs.h" -uint8_t GB_random(void); -uint32_t GB_random32(void); +#ifdef GB_INTERNAL +internal uint8_t GB_random(void); +internal uint32_t GB_random32(void); +#endif void GB_random_seed(uint64_t seed); void GB_random_set_enabled(bool enable); - -#endif /* random_h */ diff --git a/bsnes/gb/Core/rewind.c b/bsnes/gb/Core/rewind.c index d3055284..9a1038ff 100644 --- a/bsnes/gb/Core/rewind.c +++ b/bsnes/gb/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/bsnes/gb/Core/rewind.h b/bsnes/gb/Core/rewind.h index ad548410..c0fb7026 100644 --- a/bsnes/gb/Core/rewind.h +++ b/bsnes/gb/Core/rewind.h @@ -1,14 +1,14 @@ -#ifndef rewind_h -#define rewind_h +#pragma once +#ifndef GB_DISABLE_REWIND #include -#include "gb_struct_def.h" +#include "defs.h" #ifdef GB_INTERNAL -void GB_rewind_push(GB_gameboy_t *gb); -void GB_rewind_free(GB_gameboy_t *gb); +internal void GB_rewind_push(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/bsnes/gb/Core/rumble.h b/bsnes/gb/Core/rumble.h index eae9f372..78a3da7f 100644 --- a/bsnes/gb/Core/rumble.h +++ b/bsnes/gb/Core/rumble.h @@ -1,7 +1,6 @@ -#ifndef rumble_h -#define rumble_h +#pragma once -#include "gb_struct_def.h" +#include "defs.h" typedef enum { GB_RUMBLE_DISABLED, @@ -10,8 +9,6 @@ typedef enum { } GB_rumble_mode_t; #ifdef GB_INTERNAL -void GB_handle_rumble(GB_gameboy_t *gb); +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/bsnes/gb/Core/save_state.c b/bsnes/gb/Core/save_state.c index b4780e8f..cc0330d2 100644 --- a/bsnes/gb/Core/save_state.c +++ b/bsnes/gb/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; @@ -67,7 +70,7 @@ typedef struct __attribute__((packed)) { BESS_buffer_t oam; BESS_buffer_t hram; BESS_buffer_t background_palettes; - BESS_buffer_t sprite_palettes; + BESS_buffer_t object_palettes; } BESS_CORE_t; typedef struct __attribute__((packed)) { @@ -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); } @@ -276,30 +308,6 @@ size_t GB_get_save_state_size(GB_gameboy_t *gb) static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save, bool *attempt_bess) { *attempt_bess = false; - if (save->ram_size == 0 && (&save->ram_size)[-1] == gb->ram_size) { - /* This is a save state with a bad printer struct from a 32-bit OS */ - memmove(save->extra_oam + 4, save->extra_oam, (uintptr_t)&save->ram_size - (uintptr_t)&save->extra_oam); - } - if (save->ram_size == 0) { - /* Save doesn't have ram size specified, it's a pre 0.12 save state with potentially - incorrect RAM amount if it's a CGB instance */ - if (GB_is_cgb(save)) { - save->ram_size = 0x2000 * 8; // Incorrect RAM size - } - else { - save->ram_size = gb->ram_size; - } - } - - if (save->model & GB_MODEL_PAL_BIT_OLD) { - save->model &= ~GB_MODEL_PAL_BIT_OLD; - save->model |= GB_MODEL_PAL_BIT; - } - - if (save->model & GB_MODEL_NO_SFC_BIT_OLD) { - save->model &= ~GB_MODEL_NO_SFC_BIT_OLD; - save->model |= GB_MODEL_NO_SFC_BIT; - } if (gb->version != save->version) { GB_log(gb, "The save state is for a different version of SameBoy.\n"); @@ -328,14 +336,12 @@ static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t } if (gb->ram_size != save->ram_size) { - if (gb->ram_size == 0x1000 * 8 && save->ram_size == 0x2000 * 8) { - /* A bug in versions prior to 0.12 made CGB instances allocate twice the ammount of RAM. - Ignore this issue to retain compatibility with older, 0.11, save states. */ - } - else { - GB_log(gb, "The save state has non-matching RAM size. Try changing the emulated model.\n"); - return false; - } + GB_log(gb, "The save state has non-matching RAM size. Try changing the emulated model.\n"); + return false; + } + + if (gb->accessory != save->accessory) { + memset(GB_GET_SECTION(save, accessory), 0, GB_SECTION_SIZE(accessory)); } switch (save->model) { @@ -344,16 +350,23 @@ static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t case GB_MODEL_SGB_PAL: return true; case GB_MODEL_SGB_NTSC_NO_SFC: return true; case GB_MODEL_SGB_PAL_NO_SFC: return true; + case GB_MODEL_MGB: return true; case GB_MODEL_SGB2: return true; case GB_MODEL_SGB2_NO_SFC: return true; + case GB_MODEL_CGB_0: return true; + case GB_MODEL_CGB_A: return true; + case GB_MODEL_CGB_B: return true; case GB_MODEL_CGB_C: return true; + case GB_MODEL_CGB_D: return true; case GB_MODEL_CGB_E: return true; - case GB_MODEL_AGB: 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; } @@ -365,15 +378,13 @@ 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; /* These are kind of DOS-ish if too large */ - if (abs(gb->display_cycles) > 0x8000) { + if (abs(gb->display_cycles) > 0x80000) { gb->display_cycles = 0; } @@ -403,61 +414,25 @@ static void sanitize_state(GB_gameboy_t *gb) gb->lcd_x = gb->position_in_line; } - if (gb->object_priority == GB_OBJECT_PRIORITY_UNDEFINED) { - gb->object_priority = gb->cgb_mode? GB_OBJECT_PRIORITY_INDEX : GB_OBJECT_PRIORITY_X; - } if (gb->sgb) { if (gb->sgb->player_count != 1 && gb->sgb->player_count != 2 && gb->sgb->player_count != 4) { gb->sgb->player_count = 1; } gb->sgb->current_player &= gb->sgb->player_count - 1; } - if (gb->sgb && !gb->sgb->v14_3) { -#ifdef GB_BIG_ENDIAN - for (unsigned i = 0; i < sizeof(gb->sgb->border.raw_data) / 2; i++) { - gb->sgb->border.raw_data[i] = LE16(gb->sgb->border.raw_data[i]); - } - - for (unsigned i = 0; i < sizeof(gb->sgb->pending_border.raw_data) / 2; i++) { - gb->sgb->pending_border.raw_data[i] = LE16(gb->sgb->pending_border.raw_data[i]); - } - - for (unsigned i = 0; i < sizeof(gb->sgb->effective_palettes) / 2; i++) { - gb->sgb->effective_palettes[i] = LE16(gb->sgb->effective_palettes[i]); - } - - for (unsigned i = 0; i < sizeof(gb->sgb->ram_palettes) / 2; i++) { - gb->sgb->ram_palettes[i] = LE16(gb->sgb->ram_palettes[i]); - } -#endif - uint8_t converted_tiles[sizeof(gb->sgb->border.tiles)] = {0,}; - for (unsigned tile = 0; tile < sizeof(gb->sgb->border.tiles_legacy) / 64; tile++) { - for (unsigned y = 0; y < 8; y++) { - unsigned base = tile * 32 + y * 2; - for (unsigned x = 0; x < 8; x++) { - uint8_t pixel = gb->sgb->border.tiles_legacy[tile * 8 * 8 + y * 8 + x]; - if (pixel & 1) converted_tiles[base] |= (1 << (7 ^ x)); - if (pixel & 2) converted_tiles[base + 1] |= (1 << (7 ^ x)); - if (pixel & 4) converted_tiles[base + 16] |= (1 << (7 ^ x)); - if (pixel & 8) converted_tiles[base + 17] |= (1 << (7 ^ x)); - } - } - } - memcpy(gb->sgb->border.tiles, converted_tiles, sizeof(converted_tiles)); - memset(converted_tiles, 0, sizeof(converted_tiles)); - for (unsigned tile = 0; tile < sizeof(gb->sgb->pending_border.tiles_legacy) / 64; tile++) { - for (unsigned y = 0; y < 8; y++) { - unsigned base = tile * 32 + y * 2; - for (unsigned x = 0; x < 8; x++) { - uint8_t pixel = gb->sgb->pending_border.tiles_legacy[tile * 8 * 8 + y * 8 + x]; - if (pixel & 1) converted_tiles[base] |= (1 << (7 ^ x)); - if (pixel & 2) converted_tiles[base + 1] |= (1 << (7 ^ x)); - if (pixel & 4) converted_tiles[base + 16] |= (1 << (7 ^ x)); - if (pixel & 8) converted_tiles[base + 17] |= (1 << (7 ^ x)); - } - } - } - memcpy(gb->sgb->pending_border.tiles, converted_tiles, sizeof(converted_tiles)); + 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; + } + + if (gb->n_visible_objs > 10) { + gb->n_visible_objs = 10; } } @@ -480,7 +455,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; @@ -499,7 +474,7 @@ static int save_bess_mbc_block(GB_gameboy_t *gb, virtual_file_t *file) case GB_MBC3: pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->mbc_ram_enable? 0xA : 0x0}; pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->mbc3.rom_bank}; - pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->mbc3.ram_bank | (gb->mbc3_rtc_mapped? 8 : 0)}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->mbc3.ram_bank | (gb->mbc3.rtc_mapped? 8 : 0)}; mbc_block.size = 3 * sizeof(pairs[0]); break; case GB_MBC5: @@ -509,25 +484,46 @@ 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[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}; - pairs[2] = (BESS_MBC_pair_t){LE16(0x0002), gb->tpp1_rom_bank}; - pairs[3] = (BESS_MBC_pair_t){LE16(0x0003), gb->tpp1_mode}; + 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}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x0002), gb->tpp1.rom_bank}; + pairs[3] = (BESS_MBC_pair_t){LE16(0x0003), gb->tpp1.mode}; mbc_block.size = 4 * sizeof(pairs[0]); break; } @@ -545,8 +541,17 @@ 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) { + errno = 0; if (file->write(file, GB_GET_SECTION(gb, header), GB_SECTION_SIZE(header)) != GB_SECTION_SIZE(header)) goto error; if (!DUMP_SECTION(gb, file, core_state)) goto error; if (!DUMP_SECTION(gb, file, dma )) goto error; @@ -558,11 +563,11 @@ 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)) { - gb->sgb->v14_3 = true; sgb_offset = file->tell(file) + 4; if (!dump_section(file, gb->sgb, sizeof(*gb->sgb))) goto error; } @@ -615,11 +620,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; } @@ -631,6 +638,7 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe switch (gb->model) { case GB_MODEL_DMG_B: bess_core.full_model = BE32('GDB '); break; + case GB_MODEL_MGB: bess_core.full_model = BE32('GM '); break; case GB_MODEL_SGB_NTSC: case GB_MODEL_SGB_NTSC_NO_SFC: @@ -644,10 +652,15 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe case GB_MODEL_SGB2: bess_core.full_model = BE32('S2 '); break; - + case GB_MODEL_CGB_0: bess_core.full_model = BE32('CC0 '); break; + case GB_MODEL_CGB_A: bess_core.full_model = BE32('CCA '); break; + case GB_MODEL_CGB_B: bess_core.full_model = BE32('CCB '); break; 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: bess_core.full_model = BE32('CA '); break; // SameBoy doesn't emulate a specific AGB revision yet + case GB_MODEL_AGB_A: + case GB_MODEL_GBP_A: + bess_core.full_model = BE32('CAA '); break; } bess_core.pc = LE16(gb->pc); @@ -678,8 +691,8 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe if (GB_is_cgb(gb)) { bess_core.background_palettes.size = LE32(sizeof(gb->background_palettes_data)); bess_core.background_palettes.offset = LE32(video_offset + offsetof(GB_gameboy_t, background_palettes_data) - GB_SECTION_OFFSET(video)); - bess_core.sprite_palettes.size = LE32(sizeof(gb->sprite_palettes_data)); - bess_core.sprite_palettes.offset = LE32(video_offset + offsetof(GB_gameboy_t, sprite_palettes_data) - GB_SECTION_OFFSET(video)); + bess_core.object_palettes.size = LE32(sizeof(gb->object_palettes_data)); + bess_core.object_palettes.offset = LE32(video_offset + offsetof(GB_gameboy_t, object_palettes_data) - GB_SECTION_OFFSET(video)); } if (file->write(file, &bess_core, sizeof(bess_core)) != sizeof(bess_core)) { @@ -739,11 +752,11 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe bess_huc3.data = (GB_huc3_rtc_time_t) { LE64(gb->last_rtc_second), - LE16(gb->huc3_minutes), - LE16(gb->huc3_days), - LE16(gb->huc3_alarm_minutes), - LE16(gb->huc3_alarm_days), - gb->huc3_alarm_enabled, + LE16(gb->huc3.minutes), + LE16(gb->huc3.days), + LE16(gb->huc3.alarm_minutes), + LE16(gb->huc3.alarm_days), + gb->huc3.alarm_enabled, }; if (file->write(file, &bess_huc3, sizeof(bess_huc3)) != sizeof(bess_huc3)) { goto error; @@ -751,6 +764,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 */ @@ -802,13 +839,15 @@ static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool appe goto error; } - errno = 0; + return 0;; error: + if (errno == 0) return EIO; return errno; } 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)); @@ -827,6 +866,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, @@ -1036,6 +1076,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) { @@ -1049,7 +1094,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; @@ -1057,7 +1103,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; @@ -1070,7 +1116,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)); @@ -1109,11 +1160,11 @@ static int load_bess_save(GB_gameboy_t *gb, virtual_file_t *file, bool is_samebo if (gb->rtc_mode == GB_RTC_MODE_SYNC_TO_HOST) { save.last_rtc_second = MIN(LE64(bess_huc3.data.last_rtc_second), time(NULL)); } - save.huc3_minutes = LE16(bess_huc3.data.minutes); - save.huc3_days = LE16(bess_huc3.data.days); - save.huc3_alarm_minutes = LE16(bess_huc3.data.alarm_minutes); - save.huc3_alarm_days = LE16(bess_huc3.data.alarm_days); - save.huc3_alarm_enabled = bess_huc3.data.alarm_enabled; + save.huc3.minutes = LE16(bess_huc3.data.minutes); + save.huc3.days = LE16(bess_huc3.data.days); + save.huc3.alarm_minutes = LE16(bess_huc3.data.alarm_minutes); + save.huc3.alarm_days = LE16(bess_huc3.data.alarm_days); + save.huc3.alarm_enabled = bess_huc3.data.alarm_enabled; break; case BE32('TPP1'): if (!found_core) goto parse_error; @@ -1129,6 +1180,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; @@ -1162,7 +1236,7 @@ done: read_bess_buffer(&core.oam, file, gb->oam, sizeof(gb->oam)); read_bess_buffer(&core.hram, file, gb->hram, sizeof(gb->hram)); read_bess_buffer(&core.background_palettes, file, gb->background_palettes_data, sizeof(gb->background_palettes_data)); - read_bess_buffer(&core.sprite_palettes, file, gb->sprite_palettes_data, sizeof(gb->sprite_palettes_data)); + read_bess_buffer(&core.object_palettes, file, gb->object_palettes_data, sizeof(gb->object_palettes_data)); if (gb->sgb) { memset(gb->sgb, 0, sizeof(*gb->sgb)); GB_sgb_load_default_data(gb); @@ -1253,8 +1327,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)) { @@ -1284,17 +1358,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)); @@ -1313,6 +1390,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, @@ -1325,6 +1403,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) { @@ -1333,7 +1514,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; } @@ -1341,7 +1522,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/bsnes/gb/Core/save_state.h b/bsnes/gb/Core/save_state.h index bf43a65c..ceb89c25 100644 --- a/bsnes/gb/Core/save_state.h +++ b/bsnes/gb/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 -#define GB_SECTION(name, ...) union __attribute__ ((aligned (8))) {uint8_t name##_section_start; struct {__VA_ARGS__};}; uint8_t name##_section_end[0] +// 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,16 +40,17 @@ 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'; } /* For internal in-memory save states (rewind, debugger) that do not need BESS */ -size_t GB_get_save_state_size_no_bess(GB_gameboy_t *gb); -void GB_save_state_to_buffer_no_bess(GB_gameboy_t *gb, uint8_t *buffer); +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/bsnes/gb/Core/sgb.c b/bsnes/gb/Core/sgb.c index 0e532668..16f9aa39 100644 --- a/bsnes/gb/Core/sgb.c +++ b/bsnes/gb/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]); @@ -262,9 +263,7 @@ static void command_ready(GB_gameboy_t *gb) } *command = (void *)(gb->sgb->command + 1); uint16_t count = command->length; -#ifdef GB_BIG_ENDIAN - count = __builtin_bswap16(count); -#endif + count = LE16(count); uint8_t x = command->x; uint8_t y = command->y; if (x >= 20 || y >= 18) { @@ -641,9 +640,9 @@ void GB_sgb_render(GB_gameboy_t *gb) for (unsigned x = 0; x < 8; x++) { *data |= pixel_to_bits[gb->sgb->screen_buffer[(tile_x + x) + (tile_y + y) * 160] & 3] >> x; } -#ifdef GB_BIG_ENDIAN - *data = __builtin_bswap16(*data); -#endif + if (gb->sgb->transfer_dest == TRANSFER_PALETTES || gb->sgb->transfer_dest == TRANSFER_BORDER_DATA) { + *data = LE16(*data); + } data++; } } @@ -832,19 +831,46 @@ void GB_sgb_load_default_data(GB_gameboy_t *gb) gb->sgb->effective_palettes[3] = LE16(built_in_palettes[3]); } +static double fm_sin(double phase) +{ +#define SIN_TABLE_LENGTH 128 + phase /= 2 * M_PI; + phase = fabs(phase); + phase -= floor(phase); + if (phase > 0.5) { + return -(fm_sin(1 - phase)); + } + if (phase > 0.25) { + return fm_sin(0.5 - phase); + } + + static bool once = false; + static double table[SIN_TABLE_LENGTH + 1]; + if (!once) { + for (unsigned i = 0; i < SIN_TABLE_LENGTH + 1; i++) { + table[i] = sin(i * M_PI / 2 / SIN_TABLE_LENGTH); + } + once = true; + } + + phase *= 4 * SIN_TABLE_LENGTH; + double fraction = phase - floor(phase); + return table[(unsigned)floor(phase)] * (1 - fraction) + table[(unsigned)ceil(phase)] * (fraction); +} + static double fm_synth(double phase) { - return (sin(phase * M_PI * 2) + - sin(phase * M_PI * 2 + sin(phase * M_PI * 2)) + - sin(phase * M_PI * 2 + sin(phase * M_PI * 3)) + - sin(phase * M_PI * 2 + sin(phase * M_PI * 4))) / 4; + return (fm_sin(phase * M_PI * 2) + + fm_sin(phase * M_PI * 2 + fm_sin(phase * M_PI * 2)) + + fm_sin(phase * M_PI * 2 + fm_sin(phase * M_PI * 3)) + + fm_sin(phase * M_PI * 2 + fm_sin(phase * M_PI * 4))) / 4; } static double fm_sweep(double phase) { double ret = 0; for (unsigned i = 0; i < 8; i++) { - ret += sin((phase * M_PI * 2 + sin(phase * M_PI * 8) / 4) * pow(1.25, i)) * (8 - i) / 36; + ret += fm_sin((phase * M_PI * 2 + fm_sin(phase * M_PI * 8) / 4) * pow(1.25, i)) * (8 - i) / 36; } return ret; } @@ -883,14 +909,29 @@ static void render_jingle(GB_gameboy_t *gb, size_t count) if (sweep_cutoff_ratio > 1) { sweep_cutoff_ratio = 1; } + + // Render at a lower resolution if our sample rate is too high + uint8_t downsample_mask = 0; + size_t temp_count = count; + while (temp_count > 2048) { + temp_count /= 2; + downsample_mask <<= 1; + downsample_mask |= 1; + sweep_phase_shift *= 2; + sweep_cutoff_ratio *= 2; + } - GB_sample_t stereo; + GB_sample_t stereo = {0, 0}; for (unsigned i = 0; i < count; i++) { + if ((i & downsample_mask)) { + gb->apu_output.sample_callback(gb, &stereo); + continue; + } double sample = 0; for (signed f = 0; f < 7 && f < jingle_stage; f++) { sample += fm_synth(gb->sgb_intro_jingle_phases[f]) * (0.75 * pow(0.5, jingle_stage - f) + 0.25) / 5.0; - gb->sgb_intro_jingle_phases[f] += frequencies[f] / gb->apu_output.sample_rate; + gb->sgb_intro_jingle_phases[f] += (frequencies[f] / gb->apu_output.sample_rate) * (downsample_mask + 1); } if (gb->sgb->intro_animation > 100) { sample *= pow((GB_SGB_INTRO_ANIMATION_LENGTH - gb->sgb->intro_animation) / (GB_SGB_INTRO_ANIMATION_LENGTH - 100.0), 3); @@ -912,3 +953,7 @@ static void render_jingle(GB_gameboy_t *gb, size_t count) return; } +unsigned GB_get_player_count(GB_gameboy_t *gb) +{ + return GB_is_hle_sgb(gb)? gb->sgb->player_count : 1; +} diff --git a/bsnes/gb/Core/sgb.h b/bsnes/gb/Core/sgb.h index 1e1a5c28..598b1b17 100644 --- a/bsnes/gb/Core/sgb.h +++ b/bsnes/gb/Core/sgb.h @@ -1,15 +1,13 @@ -#ifndef sgb_h -#define sgb_h -#include "gb_struct_def.h" +#pragma once + +#include "defs.h" #include #include typedef struct GB_sgb_s GB_sgb_t; typedef struct { - union { - uint8_t tiles[0x100 * 8 * 4]; - uint8_t tiles_legacy[0x100 * 8 * 8]; /* High nibble not used; TODO: Remove when breaking save-state compatibility! */ - }; + uint8_t tiles[0x100 * 8 * 4]; +#ifdef GB_INTERNAL union { struct { uint16_t map[32 * 32]; @@ -17,6 +15,9 @@ typedef struct { }; uint16_t raw_data[0x440]; }; +#else + uint16_t raw_data[0x440]; +#endif } GB_sgb_border_t; #ifdef GB_INTERNAL @@ -59,17 +60,11 @@ struct GB_sgb_s { /* GB Header */ uint8_t received_header[0x54]; - - /* Multiplayer (cont) */ - GB_PADDING(bool, mlt_lock); - - bool v14_3; // True on save states created on 0.14.3 or newer; Remove when breaking save state compatibility! }; -void GB_sgb_write(GB_gameboy_t *gb, uint8_t value); -void GB_sgb_render(GB_gameboy_t *gb); -void GB_sgb_load_default_data(GB_gameboy_t *gb); - -#endif +internal void GB_sgb_write(GB_gameboy_t *gb, uint8_t value); +internal void GB_sgb_render(GB_gameboy_t *gb); +internal void GB_sgb_load_default_data(GB_gameboy_t *gb); #endif +unsigned GB_get_player_count(GB_gameboy_t *gb); diff --git a/bsnes/gb/Core/sm83_cpu.c b/bsnes/gb/Core/sm83_cpu.c index d4829d58..77bc9926 100644 --- a/bsnes/gb/Core/sm83_cpu.c +++ b/bsnes/gb/Core/sm83_cpu.c @@ -4,7 +4,7 @@ #include "gb.h" -typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode); +typedef void opcode_t(GB_gameboy_t *gb, uint8_t opcode); typedef enum { /* Default behavior. If the CPU writes while another component reads, it reads the old value */ @@ -20,62 +20,66 @@ typedef enum { GB_CONFLICT_PALETTE_CGB, GB_CONFLICT_DMG_LCDC, GB_CONFLICT_SGB_LCDC, - GB_CONFLICT_WX, - GB_CONFLICT_CGB_LCDC, - GB_CONFLICT_NR10, -} GB_conflict_t; + GB_CONFLICT_WX_DMG, + GB_CONFLICT_LCDC_CGB, + GB_CONFLICT_LCDC_CGB_DOUBLE, + GB_CONFLICT_STAT_CGB_DOUBLE, + GB_CONFLICT_NR10_CGB_DOUBLE, + GB_CONFLICT_SCX_DMG_AND_CGB_DOUBLE, +} conflict_t; -/* Todo: How does double speed mode affect these? */ -static const GB_conflict_t cgb_conflict_map[0x80] = { - [GB_IO_LCDC] = GB_CONFLICT_CGB_LCDC, +static const conflict_t cgb_conflict_map[0x80] = { + [GB_IO_LCDC] = GB_CONFLICT_LCDC_CGB, [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, [GB_IO_LYC] = GB_CONFLICT_WRITE_CPU, + [GB_IO_WY] = GB_CONFLICT_READ_OLD, [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_READ_OLD, + [GB_IO_WX] = GB_CONFLICT_WRITE_CPU, +}; - /* 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_WY] = 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_DMG_AND_CGB_DOUBLE, + [GB_IO_WX] = GB_CONFLICT_READ_OLD, }; /* Todo: verify on an MGB */ -static const GB_conflict_t dmg_conflict_map[0x80] = { +static const conflict_t dmg_conflict_map[0x80] = { [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, [GB_IO_LYC] = GB_CONFLICT_READ_OLD, [GB_IO_LCDC] = GB_CONFLICT_DMG_LCDC, [GB_IO_SCY] = GB_CONFLICT_READ_NEW, [GB_IO_STAT] = GB_CONFLICT_STAT_DMG, - [GB_IO_BGP] = GB_CONFLICT_PALETTE_DMG, [GB_IO_OBP0] = GB_CONFLICT_PALETTE_DMG, [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, + [GB_IO_WX] = GB_CONFLICT_WX_DMG, + [GB_IO_SCX] = GB_CONFLICT_SCX_DMG_AND_CGB_DOUBLE, }; /* Todo: Verify on an SGB1 */ -static const GB_conflict_t sgb_conflict_map[0x80] = { +static const conflict_t sgb_conflict_map[0x80] = { [GB_IO_IF] = GB_CONFLICT_WRITE_CPU, [GB_IO_LYC] = GB_CONFLICT_READ_OLD, [GB_IO_LCDC] = GB_CONFLICT_SGB_LCDC, [GB_IO_SCY] = GB_CONFLICT_READ_NEW, [GB_IO_STAT] = GB_CONFLICT_STAT_DMG, - [GB_IO_BGP] = GB_CONFLICT_READ_NEW, [GB_IO_OBP0] = GB_CONFLICT_READ_NEW, [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, + [GB_IO_WX] = GB_CONFLICT_WX_DMG, + [GB_IO_SCX] = GB_CONFLICT_SCX_DMG_AND_CGB_DOUBLE, }; static uint8_t cycle_read(GB_gameboy_t *gb, uint16_t addr) @@ -109,11 +113,11 @@ static uint8_t cycle_write_if(GB_gameboy_t *gb, uint8_t value) static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) { assert(gb->pending_cycles); - GB_conflict_t conflict = GB_CONFLICT_READ_OLD; + conflict_t conflict = GB_CONFLICT_READ_OLD; if ((addr & 0xFF80) == 0xFF00) { - const GB_conflict_t *map = NULL; + 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 +149,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 +168,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 +176,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. @@ -188,32 +203,44 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) } case GB_CONFLICT_PALETTE_CGB: { - GB_advance_cycles(gb, gb->pending_cycles - 2); - GB_write_memory(gb, addr, value); - gb->pending_cycles = 6; + if (gb->model >= GB_MODEL_CGB_D) { + 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 - 1); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 5; + } break; } 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 sprite-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 && !(value & GB_LCDC_OBJ_EN)) { + old_value &= ~GB_LCDC_OBJ_EN; + } + else if (gb->during_object_fetch && !(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; } @@ -232,7 +259,7 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value) break; } - case GB_CONFLICT_WX: + case GB_CONFLICT_WX_DMG: GB_advance_cycles(gb, gb->pending_cycles); GB_write_memory(gb, addr, value); gb->wx_just_changed = true; @@ -241,27 +268,15 @@ 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 - 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->tile_sel_glitch = true; - GB_advance_cycles(gb, 1); - gb->tile_sel_glitch = false; - GB_write_memory(gb, addr, value); - gb->pending_cycles = 3; - } - else { - GB_advance_cycles(gb, gb->pending_cycles - 1); - GB_write_memory(gb, addr, value ^ 0x10); // Write with the old TILE_SET first - gb->tile_sel_glitch = true; - GB_advance_cycles(gb, 1); - gb->tile_sel_glitch = false; - GB_write_memory(gb, addr, value); - gb->pending_cycles = 4; - } + case GB_CONFLICT_LCDC_CGB: { + uint8_t old = gb->io_registers[GB_IO_LCDC]; + if ((~value & old) & GB_LCDC_TILE_SEL) { + GB_advance_cycles(gb, gb->pending_cycles); + GB_write_memory(gb, addr, value); + gb->tile_sel_glitch = true; + GB_advance_cycles(gb, 1); + gb->tile_sel_glitch = false; + gb->pending_cycles = 3; } else { GB_advance_cycles(gb, gb->pending_cycles); @@ -269,26 +284,36 @@ 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: Verify for CGB ≤ C for BG_EN and OBJ_EN. + GB_advance_cycles(gb, gb->pending_cycles - 2); + GB_write_memory(gb, addr, (value & ~(GB_LCDC_BG_EN | GB_LCDC_ENABLE)) | (old & (GB_LCDC_BG_EN | GB_LCDC_ENABLE))); + // TODO: This condition is different from single speed mode. Why? What about odd modes? + gb->tile_sel_glitch = ((value ^ old) & GB_LCDC_TILE_SEL); + GB_advance_cycles(gb, 2); + gb->tile_sel_glitch = false; GB_write_memory(gb, addr, value); gb->pending_cycles = 4; break; + } + + case GB_CONFLICT_SCX_DMG_AND_CGB_DOUBLE: + GB_advance_cycles(gb, gb->pending_cycles - 2); + GB_write_memory(gb, addr, value); + gb->pending_cycles = 6; + 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 +333,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 +361,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 +370,12 @@ 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); gb->oam_ppu_blocked = false; gb->vram_ppu_blocked = false; gb->cgb_palettes_ppu_blocked = false; @@ -363,12 +385,19 @@ 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; + } bool exit_by_joyp = ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF); bool speed_switch = (gb->io_registers[GB_IO_KEY1] & 0x1) && !exit_by_joyp; bool immediate_exit = speed_switch || exit_by_joyp; bool interrupt_pending = (gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F); // When entering with IF&IE, the 2nd byte of STOP is actually executed if (!exit_by_joyp) { + if (!immediate_exit) { + GB_dma_run(gb); + } enter_stop_mode(gb); } @@ -380,7 +409,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; @@ -389,6 +418,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; @@ -398,9 +430,7 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode) gb->speed_switch_freeze = 1; } - if (interrupt_pending) { - } - else { + if (!interrupt_pending) { gb->speed_switch_halt_countdown = 0x20008; gb->speed_switch_freeze = 5; } @@ -411,8 +441,10 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode) if (immediate_exit) { leave_stop_mode(gb); if (!interrupt_pending) { + 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; @@ -639,9 +671,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) @@ -655,8 +687,8 @@ static bool condition_code(GB_gameboy_t *gb, uint8_t opcode) return !(gb->af & GB_CARRY_FLAG); case 3: return (gb->af & GB_CARRY_FLAG); + nodefault; } - __builtin_unreachable(); return false; } @@ -666,7 +698,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); } } @@ -855,7 +887,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) @@ -867,9 +899,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) @@ -931,7 +965,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; @@ -997,11 +1031,10 @@ 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); - gb->halted = true; /* 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)) { if (gb->ime) { @@ -1013,6 +1046,10 @@ static void halt(GB_gameboy_t *gb, uint8_t opcode) gb->halt_bug = true; } } + else { + gb->halted = true; + gb->allow_hdma_on_wake = (gb->io_registers[GB_IO_STAT] & 3); + } gb->just_halted = true; } @@ -1354,7 +1391,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; } } @@ -1530,7 +1567,7 @@ static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode) } } -static GB_opcode_t *opcodes[256] = { +static opcode_t *opcodes[256] = { /* X0 X1 X2 X3 X4 X5 X6 X7 */ /* X8 X9 Xa Xb Xc Xd Xe Xf */ nop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rlca, /* 0X */ @@ -1568,13 +1605,12 @@ static GB_opcode_t *opcodes[256] = { }; void GB_cpu_run(GB_gameboy_t *gb) { - if (gb->hdma_on) { - GB_advance_cycles(gb, 4); - return; - } - 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) { + gb->joyp_accessed = true; + } if ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF) { leave_stop_mode(gb); GB_advance_cycles(gb, 8); @@ -1606,17 +1642,28 @@ 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 && gb->allow_hdma_on_wake) { + gb->hdma_on = true; + } + gb->dma_cycles = 4; + GB_dma_run(gb); gb->speed_switch_halt_countdown = 0; } /* 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 && gb->allow_hdma_on_wake) { + gb->hdma_on = true; + } + // TODO: verify the timing! + gb->dma_cycles = 4; + GB_dma_run(gb); gb->speed_switch_halt_countdown = 0; uint16_t call_addr = gb->pc; - gb->last_opcode_read = cycle_read(gb, gb->pc++); - cycle_oam_bug_pc(gb); + cycle_read(gb, gb->pc++); + 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); @@ -1639,6 +1686,10 @@ void GB_cpu_run(GB_gameboy_t *gb) interrupt_queue >>= 1; interrupt_bit++; } + assert(gb->pending_cycles > 2); + gb->pending_cycles -= 2; + flush_pending_cycles(gb); + gb->pending_cycles = 2; gb->io_registers[GB_IO_IF] &= ~(1 << interrupt_bit); gb->pc = interrupt_bit * 8 + 0x40; } @@ -1650,19 +1701,19 @@ void GB_cpu_run(GB_gameboy_t *gb) } /* Run mode */ else if (!gb->halted) { - gb->last_opcode_read = cycle_read(gb, gb->pc++); - if (gb->halt_bug) { + uint8_t opcode = cycle_read(gb, gb->pc++); + if (unlikely(gb->hdma_on)) { + GB_hdma_run(gb); + } + if (unlikely(gb->execution_callback)) { + gb->execution_callback(gb, gb->pc - 1, opcode); + } + if (unlikely(gb->halt_bug)) { gb->pc--; gb->halt_bug = false; } - opcodes[gb->last_opcode_read](gb, gb->last_opcode_read); + opcodes[opcode](gb, opcode); } flush_pending_cycles(gb); - - if (gb->hdma_starting) { - gb->hdma_starting = false; - gb->hdma_on = true; - gb->hdma_cycles = -8; - } } diff --git a/bsnes/gb/Core/sm83_cpu.h b/bsnes/gb/Core/sm83_cpu.h index 49fa80b5..7c188dfb 100644 --- a/bsnes/gb/Core/sm83_cpu.h +++ b/bsnes/gb/Core/sm83_cpu.h @@ -1,11 +1,10 @@ -#ifndef sm83_cpu_h -#define sm83_cpu_h -#include "gb_struct_def.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); -#ifdef GB_INTERNAL -void GB_cpu_run(GB_gameboy_t *gb); #endif - -#endif /* sm83_cpu_h */ +#ifdef GB_INTERNAL +internal void GB_cpu_run(GB_gameboy_t *gb); +#endif diff --git a/bsnes/gb/Core/sm83_disassembler.c b/bsnes/gb/Core/sm83_disassembler.c index 7dacd9eb..0e14a77c 100644 --- a/bsnes/gb/Core/sm83_disassembler.c +++ b/bsnes/gb/Core/sm83_disassembler.c @@ -2,8 +2,9 @@ #include #include "gb.h" +#define GB_read_memory GB_safe_read_memory -typedef void GB_opcode_t(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc); +typedef void opcode_t(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc); static void ill(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) { @@ -518,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); } @@ -531,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); } @@ -716,7 +717,7 @@ static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode, uint16_t *pc) } } -static GB_opcode_t *opcodes[256] = { +static opcode_t *opcodes[256] = { /* X0 X1 X2 X3 X4 X5 X6 X7 */ /* X8 X9 Xa Xb Xc Xd Xe Xf */ nop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rlca, /* 0X */ @@ -757,7 +758,7 @@ static GB_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; @@ -770,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/bsnes/gb/Core/symbol_hash.c b/bsnes/gb/Core/symbol_hash.c index a3718b83..995ad2d9 100644 --- a/bsnes/gb/Core/symbol_hash.c +++ b/bsnes/gb/Core/symbol_hash.c @@ -4,7 +4,7 @@ #include #include -static size_t GB_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 GB_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 GB_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 = GB_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 = GB_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/bsnes/gb/Core/symbol_hash.h b/bsnes/gb/Core/symbol_hash.h index 2a03c96b..bdc4a38b 100644 --- a/bsnes/gb/Core/symbol_hash.h +++ b/bsnes/gb/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 -void GB_reversed_map_add_symbol(GB_reversed_symbol_map_t *map, uint16_t bank, GB_bank_symbol_t *symbol); -const GB_symbol_t *GB_reversed_map_find_symbol(GB_reversed_symbol_map_t *map, const char *name); -GB_bank_symbol_t *GB_map_add_symbol(GB_symbol_map_t *map, uint16_t addr, const char *name); -const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr); -GB_symbol_map_t *GB_map_alloc(void); -void GB_map_free(GB_symbol_map_t *map); +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, bool prefer_local); +internal GB_symbol_map_t *GB_map_alloc(void); +internal void GB_map_free(GB_symbol_map_t *map); +#endif #endif - -#endif /* symbol_hash_h */ diff --git a/bsnes/gb/Core/timing.c b/bsnes/gb/Core/timing.c index 786dbc9b..8a88e3cc 100644 --- a/bsnes/gb/Core/timing.c +++ b/bsnes/gb/Core/timing.c @@ -8,7 +8,7 @@ #include #endif -static const unsigned GB_TAC_TRIGGER_BITS[] = {512, 8, 32, 128}; +static const unsigned TAC_TRIGGER_BITS[] = {512, 8, 32, 128}; #ifndef GB_DISABLE_TIMEKEEPING static int64_t get_nanoseconds(void) @@ -42,34 +42,53 @@ 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))) { + if (nanoseconds <= gb->last_render + 1000000000 / 60) { return true; } - gb->last_sync = nanoseconds; + gb->last_render = nanoseconds; } return false; } void GB_timing_sync(GB_gameboy_t *gb) { - if (gb->turbo) { - gb->cycles_since_last_sync = 0; - return; - } +#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; - uint64_t target_nanoseconds = gb->cycles_since_last_sync * 1000000000LL / 2 / GB_get_clock_rate(gb); /* / 2 because we use 8MHz units */ + unsigned target_clock_rate; + if (gb->turbo) { + if (gb->turbo_cap_multiplier) { + target_clock_rate = GB_get_clock_rate(gb) * gb->turbo_cap_multiplier; + } + else { + gb->cycles_since_last_sync = 0; + if (gb->update_input_hint_callback) { + gb->update_input_hint_callback(gb); + } + return; + } + } + else { + target_clock_rate = GB_get_clock_rate(gb); + } + + uint64_t target_nanoseconds = gb->cycles_since_last_sync * 1000000000LL / 2 / target_clock_rate; /* / 2 because we use 8MHz units */ int64_t nanoseconds = get_nanoseconds(); int64_t time_to_sleep = target_nanoseconds + gb->last_sync - nanoseconds; - if (time_to_sleep > 0 && time_to_sleep < LCDC_PERIOD * 1200000000LL / GB_get_clock_rate(gb)) { // +20% to be more forgiving + if (time_to_sleep > 0 && time_to_sleep < LCDC_PERIOD * 1200000000LL / target_clock_rate) { // +20% to be more forgiving nsleep(time_to_sleep); gb->last_sync += target_nanoseconds; } else { - if (time_to_sleep < 0 && -time_to_sleep < LCDC_PERIOD * 1200000000LL / GB_get_clock_rate(gb)) { + if (time_to_sleep < 0 && -time_to_sleep < LCDC_PERIOD * 1200000000LL / target_clock_rate) { // We're running a bit too slow, but the difference is small enough, // just skip this sync and let it even out return; @@ -91,28 +110,48 @@ 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; + + gb->cycles_since_last_sync = 0; + if (gb->update_input_hint_callback) { + gb->update_input_hint_callback(gb); + } + return; } #endif #define IR_DECAY 31500 -#define IR_THRESHOLD 19900 -#define IR_MAX IR_THRESHOLD * 2 + IR_DECAY +#define IR_WARMUP 19900 +#define IR_THRESHOLD 240 +#define IR_MAX IR_THRESHOLD * 2 + IR_DECAY + 268 -static void GB_ir_run(GB_gameboy_t *gb, uint32_t cycles) +static void ir_run(GB_gameboy_t *gb, uint32_t cycles) { - if (gb->model == GB_MODEL_AGB) return; - if (gb->infrared_input || gb->cart_ir || (gb->io_registers[GB_IO_RP] & 1)) { + /* 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 || + (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))) { gb->ir_sensor += cycles; if (gb->ir_sensor > IR_MAX) { gb->ir_sensor = IR_MAX; } - gb->effective_ir_input = gb->ir_sensor >= IR_THRESHOLD && gb->ir_sensor <= IR_THRESHOLD + IR_DECAY; + gb->effective_ir_input = gb->ir_sensor >= IR_WARMUP + IR_THRESHOLD && gb->ir_sensor <= IR_WARMUP + IR_THRESHOLD + IR_DECAY; } else { - if (gb->ir_sensor <= cycles) { - gb->ir_sensor = 0; + unsigned target = is_sensing? IR_WARMUP : 0; + if (gb->ir_sensor < target) { + gb->ir_sensor += cycles; + } + else if (gb->ir_sensor <= target + cycles) { + gb->ir_sensor = target; } else { gb->ir_sensor -= cycles; @@ -142,85 +181,31 @@ static void increase_tima(GB_gameboy_t *gb) } } -static void GB_set_internal_div_counter(GB_gameboy_t *gb, uint16_t value) +void GB_serial_master_edge(GB_gameboy_t *gb) { - /* TIMA increases when a specific high-bit becomes a low-bit. */ - uint16_t triggers = gb->div_counter & ~value; - if ((gb->io_registers[GB_IO_TAC] & 4) && (triggers & GB_TAC_TRIGGER_BITS[gb->io_registers[GB_IO_TAC] & 3])) { - increase_tima(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) { - GB_apu_run(gb); - GB_apu_div_event(gb); - } - else { - uint16_t secondary_triggers = ~gb->div_counter & value; - if (secondary_triggers & apu_bit) { - GB_apu_run(gb); - GB_apu_div_secondary_event(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->div_counter = value; -} - -static void GB_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; - } - return; - } - GB_STATE_MACHINE(gb, div, cycles, 1) { - GB_STATE(gb, div, 1); - GB_STATE(gb, div, 2); - GB_STATE(gb, div, 3); - } + gb->serial_master_clock ^= true; - GB_set_internal_div_counter(gb, 0); -main: - GB_SLEEP(gb, div, 1, 3); - 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_SLEEP(gb, div, 2, 4); - } - - /* Todo: This is ugly to allow compatibility with 0.11 save states. Fix me when breaking save compatibility */ - { - div3: - /* Compensate for lack of prefetch emulation, as well as DIV's internal initial value */ - GB_set_internal_div_counter(gb, 8); - goto main; - } -} - -static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) -{ - if (gb->printer.command_state || gb->printer.bits_received) { - gb->printer.idle_time += cycles; - } - if (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)) { + if (!gb->serial_master_clock && (gb->io_registers[GB_IO_SC] & 0x81) == 0x81) { 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; @@ -235,7 +220,7 @@ static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) gb->io_registers[GB_IO_SB] |= 1; } - if (gb->serial_length) { + 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); @@ -243,15 +228,161 @@ static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) } } - return; - } -static void GB_rtc_run(GB_gameboy_t *gb, uint8_t cycles) + +void GB_set_internal_div_counter(GB_gameboy_t *gb, uint16_t value) { - if (gb->cartridge_type->mbc_type != GB_HUC3 && !gb->cartridge_type->has_rtc) return; + /* TIMA increases when a specific high-bit becomes a low-bit. */ + uint16_t triggers = gb->div_counter & ~value; + if ((gb->io_registers[GB_IO_TAC] & 4) && unlikely(triggers & TAC_TRIGGER_BITS[gb->io_registers[GB_IO_TAC] & 3])) { + increase_tima(gb); + } + + if (unlikely(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 (unlikely(triggers & apu_bit)) { + GB_apu_div_event(gb); + } + else { + uint16_t secondary_triggers = ~gb->div_counter & value; + if (unlikely(secondary_triggers & apu_bit)) { + GB_apu_div_secondary_event(gb); + } + } + gb->div_counter = value; +} + +static void timers_run(GB_gameboy_t *gb, uint8_t cycles) +{ + if (gb->stopped) { + if (GB_is_cgb(gb)) { + 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; + } + + GB_STATE_MACHINE(gb, div, cycles, 1) { + GB_STATE(gb, div, 1); + GB_STATE(gb, div, 2); + } + + GB_SLEEP(gb, div, 1, 3); + while (true) { + advance_tima_state_machine(gb); + GB_set_internal_div_counter(gb, gb->div_counter + 4); + 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); + } +} + +void GB_set_rtc_mode(GB_gameboy_t *gb, GB_rtc_mode_t mode) +{ + if (gb->rtc_mode != mode) { + gb->rtc_mode = mode; + gb->rtc_cycles = 0; + gb->last_rtc_second = time(NULL); + } +} + + +void GB_set_rtc_multiplier(GB_gameboy_t *gb, double multiplier) +{ + if (multiplier == 1) { + gb->rtc_second_length = 0; + return; + } + + 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; gb->rtc_cycles += cycles; time_t current_time = 0; + uint32_t rtc_second_length = unlikely(gb->rtc_second_length)? gb->rtc_second_length : GB_get_unmultiplied_clock_rate(gb) * 2; switch (gb->rtc_mode) { case GB_RTC_MODE_SYNC_TO_HOST: @@ -265,88 +396,42 @@ static void GB_rtc_run(GB_gameboy_t *gb, uint8_t cycles) gb->rtc_cycles -= cycles; return; } - if (gb->rtc_cycles < GB_get_unmultiplied_clock_rate(gb) * 2) return; - gb->rtc_cycles -= GB_get_unmultiplied_clock_rate(gb) * 2; + if (gb->rtc_cycles < rtc_second_length) return; + gb->rtc_cycles -= rtc_second_length; current_time = gb->last_rtc_second + 1; 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); } } void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) { - if (gb->speed_switch_countdown) { + if (unlikely(gb->speed_switch_countdown)) { if (gb->speed_switch_countdown == cycles) { gb->cgb_double_speed ^= true; gb->speed_switch_countdown = 0; @@ -364,22 +449,22 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) } gb->apu.pcm_mask[0] = gb->apu.pcm_mask[1] = 0xFF; // Sort of hacky, but too many cross-component interactions to do it right // Affected by speed boost - gb->dma_cycles += cycles; + gb->dma_cycles = cycles; - GB_timers_run(gb, cycles); - if (!gb->stopped) { - advance_serial(gb, cycles); // TODO: Verify what happens in STOP mode - } + timers_run(gb, cycles); + camera_run(gb, cycles); - if (gb->speed_switch_halt_countdown) { + if (unlikely(gb->speed_switch_halt_countdown)) { gb->speed_switch_halt_countdown -= cycles; if (gb->speed_switch_halt_countdown <= 0) { gb->speed_switch_halt_countdown = 0; gb->halted = false; } } - + +#ifndef GB_DISABLE_DEBUGGER gb->debugger_ticks += cycles; +#endif if (gb->speed_switch_freeze) { if (gb->speed_switch_freeze >= cycles) { @@ -390,32 +475,44 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) gb->speed_switch_freeze = 0; } - if (!gb->cgb_double_speed) { + if (unlikely(!gb->cgb_double_speed)) { cycles <<= 1; } +#ifndef GB_DISABLE_DEBUGGER gb->absolute_debugger_ticks += cycles; + *((gb->halted || gb->stopped)? &gb->current_frame_idle_cycles : &gb->current_frame_busy_cycles) += cycles; + *((gb->halted || gb->stopped)? &gb->current_second_idle_cycles : &gb->current_second_busy_cycles) += cycles; +#endif // Not affected by speed boost - if (gb->io_registers[GB_IO_LCDC] & 0x80) { + if (likely(gb->io_registers[GB_IO_LCDC] & GB_LCDC_ENABLE)) { gb->double_speed_alignment += cycles; } - gb->hdma_cycles += cycles; - gb->apu_output.sample_cycles += cycles; 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 (!gb->stopped) { // TODO: Verify what happens in STOP mode - GB_dma_run(gb); - GB_hdma_run(gb); + + 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_apu_run(gb); - GB_display_run(gb, cycles); - GB_ir_run(gb, cycles); - GB_rtc_run(gb, 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 + GB_dma_run(gb); + } + ir_run(gb, cycles); + rtc_run(gb, cycles); } /* @@ -428,8 +525,8 @@ void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac) /* Glitch only happens when old_tac is enabled. */ if (!(old_tac & 4)) return; - unsigned old_clocks = GB_TAC_TRIGGER_BITS[old_tac & 3]; - unsigned new_clocks = GB_TAC_TRIGGER_BITS[new_tac & 3]; + unsigned old_clocks = TAC_TRIGGER_BITS[old_tac & 3]; + unsigned new_clocks = TAC_TRIGGER_BITS[new_tac & 3]; /* The bit used for overflow testing must have been 1 */ if (gb->div_counter & old_clocks) { diff --git a/bsnes/gb/Core/timing.h b/bsnes/gb/Core/timing.h index 07e04734..b89bf161 100644 --- a/bsnes/gb/Core/timing.h +++ b/bsnes/gb/Core/timing.h @@ -1,29 +1,46 @@ -#ifndef timing_h -#define timing_h -#include "gb_struct_def.h" +#pragma once + +#include "defs.h" + +typedef enum { + GB_RTC_MODE_SYNC_TO_HOST, + GB_RTC_MODE_ACCURATE, +} GB_rtc_mode_t; + +/* RTC emulation mode */ +void GB_set_rtc_mode(GB_gameboy_t *gb, GB_rtc_mode_t mode); + +/* Speed multiplier for the RTC, mostly for TAS syncing */ +void GB_set_rtc_multiplier(GB_gameboy_t *gb, double multiplier); #ifdef GB_INTERNAL -void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles); -void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac); -bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */ -void GB_timing_sync(GB_gameboy_t *gb); - -enum { - GB_TIMA_RUNNING = 0, - GB_TIMA_RELOADING = 1, - GB_TIMA_RELOADED = 2 -}; - +internal void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles); +internal void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac); +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); +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; \ - if ((gb)->unit##_cycles <= 0) {\ + if (unlikely((gb)->unit##_cycles <= 0)) {\ (gb)->unit##_state = state;\ return;\ unit##state:; \ }\ } while (0) +#define GB_BATCHPOINT(gb, unit, state, cycles) do {\ +unit##state:; \ +if (likely(__state_machine_allow_batching && (gb)->unit##_cycles < (cycles * 2))) {\ + (gb)->unit##_state = state;\ + return;\ +}\ +} while (0) + +#define GB_BATCHED_CYCLES(gb, unit) ((gb)->unit##_cycles / __state_machine_divisor) + #define GB_STATE_MACHINE(gb, unit, cycles, divisor) \ static const int __state_machine_divisor = divisor;\ (gb)->unit##_cycles += cycles; \ @@ -31,10 +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/bsnes/gb/Core/workboy.c b/bsnes/gb/Core/workboy.c index 3b103796..2315c840 100644 --- a/bsnes/gb/Core/workboy.c +++ b/bsnes/gb/Core/workboy.c @@ -1,5 +1,6 @@ #include "gb.h" #include +#include static inline uint8_t int_to_bcd(uint8_t i) { @@ -139,23 +140,27 @@ static bool serial_end(GB_gameboy_t *gb) } void GB_connect_workboy(GB_gameboy_t *gb, - GB_workboy_set_time_callback set_time_callback, - GB_workboy_get_time_callback get_time_callback) + GB_workboy_set_time_callback_t set_time_callback, + GB_workboy_get_time_callback_t get_time_callback) { + GB_ASSERT_NOT_RUNNING_OTHER_THREAD(gb) memset(&gb->workboy, 0, sizeof(gb->workboy)); GB_set_serial_transfer_bit_start_callback(gb, serial_start); 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/bsnes/gb/Core/workboy.h b/bsnes/gb/Core/workboy.h index d21f2731..c09a5990 100644 --- a/bsnes/gb/Core/workboy.h +++ b/bsnes/gb/Core/workboy.h @@ -1,9 +1,9 @@ -#ifndef workboy_h -#define workboy_h +#pragma once + #include #include #include -#include "gb_struct_def.h" +#include "defs.h" typedef struct { @@ -19,8 +19,8 @@ typedef struct { uint8_t buffer_index; // In nibbles during read, in bytes during write } GB_workboy_t; -typedef void (*GB_workboy_set_time_callback)(GB_gameboy_t *gb, time_t time); -typedef time_t (*GB_workboy_get_time_callback)(GB_gameboy_t *gb); +typedef void (*GB_workboy_set_time_callback_t)(GB_gameboy_t *gb, time_t time); +typedef time_t (*GB_workboy_get_time_callback_t)(GB_gameboy_t *gb); enum { GB_WORKBOY_NONE = 0xFF, @@ -110,9 +110,7 @@ enum { void GB_connect_workboy(GB_gameboy_t *gb, - GB_workboy_set_time_callback set_time_callback, - GB_workboy_get_time_callback get_time_callback); + GB_workboy_set_time_callback_t set_time_callback, + GB_workboy_get_time_callback_t 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/bsnes/gb/FreeDesktop/AppIcon/128x128.png b/bsnes/gb/FreeDesktop/AppIcon/128x128.png index 6303f237..b3259ed2 100644 Binary files a/bsnes/gb/FreeDesktop/AppIcon/128x128.png and b/bsnes/gb/FreeDesktop/AppIcon/128x128.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/16x16.png b/bsnes/gb/FreeDesktop/AppIcon/16x16.png index 6c3f81d0..335107ec 100644 Binary files a/bsnes/gb/FreeDesktop/AppIcon/16x16.png and b/bsnes/gb/FreeDesktop/AppIcon/16x16.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/256x256.png b/bsnes/gb/FreeDesktop/AppIcon/256x256.png index e2a6ceee..56ea0166 100644 Binary files a/bsnes/gb/FreeDesktop/AppIcon/256x256.png and b/bsnes/gb/FreeDesktop/AppIcon/256x256.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/32x32.png b/bsnes/gb/FreeDesktop/AppIcon/32x32.png index d7f2e4eb..ea65e5a6 100644 Binary files a/bsnes/gb/FreeDesktop/AppIcon/32x32.png and b/bsnes/gb/FreeDesktop/AppIcon/32x32.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/512x512.png b/bsnes/gb/FreeDesktop/AppIcon/512x512.png index 1608c71f..9004db20 100644 Binary files a/bsnes/gb/FreeDesktop/AppIcon/512x512.png and b/bsnes/gb/FreeDesktop/AppIcon/512x512.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/64x64.png b/bsnes/gb/FreeDesktop/AppIcon/64x64.png index 4a54e94e..a5675ca2 100644 Binary files a/bsnes/gb/FreeDesktop/AppIcon/64x64.png and b/bsnes/gb/FreeDesktop/AppIcon/64x64.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/128x128.png b/bsnes/gb/FreeDesktop/Cartridge/128x128.png index bc14d792..18c8898c 100644 Binary files a/bsnes/gb/FreeDesktop/Cartridge/128x128.png and b/bsnes/gb/FreeDesktop/Cartridge/128x128.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/16x16.png b/bsnes/gb/FreeDesktop/Cartridge/16x16.png index 3cbd9ae7..c6573987 100644 Binary files a/bsnes/gb/FreeDesktop/Cartridge/16x16.png and b/bsnes/gb/FreeDesktop/Cartridge/16x16.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/256x256.png b/bsnes/gb/FreeDesktop/Cartridge/256x256.png index 14258ea8..8c260efb 100644 Binary files a/bsnes/gb/FreeDesktop/Cartridge/256x256.png and b/bsnes/gb/FreeDesktop/Cartridge/256x256.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/32x32.png b/bsnes/gb/FreeDesktop/Cartridge/32x32.png index c8ef62fd..cc202622 100644 Binary files a/bsnes/gb/FreeDesktop/Cartridge/32x32.png and b/bsnes/gb/FreeDesktop/Cartridge/32x32.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/512x512.png b/bsnes/gb/FreeDesktop/Cartridge/512x512.png index 71314f72..20222b19 100644 Binary files a/bsnes/gb/FreeDesktop/Cartridge/512x512.png and b/bsnes/gb/FreeDesktop/Cartridge/512x512.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/64x64.png b/bsnes/gb/FreeDesktop/Cartridge/64x64.png index 8835f79c..30eef076 100644 Binary files a/bsnes/gb/FreeDesktop/Cartridge/64x64.png and b/bsnes/gb/FreeDesktop/Cartridge/64x64.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/128x128.png b/bsnes/gb/FreeDesktop/ColorCartridge/128x128.png index da4757e8..d2956752 100644 Binary files a/bsnes/gb/FreeDesktop/ColorCartridge/128x128.png and b/bsnes/gb/FreeDesktop/ColorCartridge/128x128.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/16x16.png b/bsnes/gb/FreeDesktop/ColorCartridge/16x16.png index 50e6b2b4..f1046eeb 100644 Binary files a/bsnes/gb/FreeDesktop/ColorCartridge/16x16.png and b/bsnes/gb/FreeDesktop/ColorCartridge/16x16.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/256x256.png b/bsnes/gb/FreeDesktop/ColorCartridge/256x256.png index 186f5d30..0154b257 100644 Binary files a/bsnes/gb/FreeDesktop/ColorCartridge/256x256.png and b/bsnes/gb/FreeDesktop/ColorCartridge/256x256.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/32x32.png b/bsnes/gb/FreeDesktop/ColorCartridge/32x32.png index 47e45b50..0579d44b 100644 Binary files a/bsnes/gb/FreeDesktop/ColorCartridge/32x32.png and b/bsnes/gb/FreeDesktop/ColorCartridge/32x32.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/512x512.png b/bsnes/gb/FreeDesktop/ColorCartridge/512x512.png index 715d68f3..03d5f650 100644 Binary files a/bsnes/gb/FreeDesktop/ColorCartridge/512x512.png and b/bsnes/gb/FreeDesktop/ColorCartridge/512x512.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/64x64.png b/bsnes/gb/FreeDesktop/ColorCartridge/64x64.png index 403e307a..62c78705 100644 Binary files a/bsnes/gb/FreeDesktop/ColorCartridge/64x64.png and b/bsnes/gb/FreeDesktop/ColorCartridge/64x64.png differ diff --git a/bsnes/gb/FreeDesktop/sameboy.xml b/bsnes/gb/FreeDesktop/sameboy.xml index 18123edc..2e15fded 100644 --- a/bsnes/gb/FreeDesktop/sameboy.xml +++ b/bsnes/gb/FreeDesktop/sameboy.xml @@ -1,23 +1,38 @@ - - Game Boy ROM - - - - - - - Game Boy Color ROM - - - - - - - Game Boy ISX binary - - - - + + Game Boy ROM + + + + + + + + + + + + + Game Boy Color ROM + + + + + + + + + + + + + Game Boy ISX binary + + + + + + + diff --git a/bsnes/gb/GNUmakefile b/bsnes/gb/GNUmakefile index e88ac418..fa3eb0f9 100644 --- a/bsnes/gb/GNUmakefile +++ b/bsnes/gb/GNUmakefile @@ -1,13 +1,13 @@ include gb/version.mk export VERSION -flags += -DGB_INTERNAL -DGB_DISABLE_DEBUGGER -DGB_DISABLE_CHEATS -D_GNU_SOURCE -DGB_VERSION=\"$(VERSION)\" -Wno-multichar +flags += -DGB_INTERNAL -DGB_DISABLE_DEBUGGER -DGB_DISABLE_CHEATS -D_GNU_SOURCE -DGB_VERSION=\"$(VERSION)\" -DNDEBUG -Wno-multichar options += -I../sameboy objects += gb-apu gb-camera gb-rumble gb-display gb-gb gb-joypad gb-mbc objects += gb-memory gb-printer gb-random gb-rewind gb-save_state gb-sgb -objects += gb-sm83_cpu gb-symbol_hash gb-timing -#objects+= gb-debugger gb-sm83_disassembler +objects += gb-sm83_cpu gb-timing +#objects+= gb-debugger gb-sm83_disassembler gb-symbol_hash obj/gb-apu.o: gb/Core/apu.c obj/gb-camera.o: gb/Core/camera.c diff --git a/bsnes/gb/HexFiend/HFController.m b/bsnes/gb/HexFiend/HFController.m index 74e033c0..1bb20572 100644 --- a/bsnes/gb/HexFiend/HFController.m +++ b/bsnes/gb/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]; @@ -468,7 +473,7 @@ static inline Class preferredByteArrayClass(void) { } - (void)_setSingleSelectedContentsRange:(HFRange)newSelection { - HFASSERT(HFRangeIsSubrangeOfRange(newSelection, HFRangeMake(0, [self contentsLength]))); + if (!HFRangeIsSubrangeOfRange(newSelection, HFRangeMake(0, [self contentsLength]))) return; BOOL selectionChanged; if ([selectedContentsRanges count] == 1) { selectionChanged = ! HFRangeEqualsRange([selectedContentsRanges[0] HFRange], newSelection); diff --git a/bsnes/gb/HexFiend/HFHexTextRepresenter.m b/bsnes/gb/HexFiend/HFHexTextRepresenter.m index f98382b6..4ff9f202 100644 --- a/bsnes/gb/HexFiend/HFHexTextRepresenter.m +++ b/bsnes/gb/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/bsnes/gb/HexFiend/HFHexTextView.h b/bsnes/gb/HexFiend/HFHexTextView.h deleted file mode 100644 index 3222b65e..00000000 --- a/bsnes/gb/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/bsnes/gb/HexFiend/HFHexTextView.m b/bsnes/gb/HexFiend/HFHexTextView.m deleted file mode 100644 index 9e6ae47b..00000000 --- a/bsnes/gb/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/bsnes/gb/HexFiend/HFLineCountingView.m b/bsnes/gb/HexFiend/HFLineCountingView.m index 080599b0..d65e3692 100644 --- a/bsnes/gb/HexFiend/HFLineCountingView.m +++ b/bsnes/gb/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/bsnes/gb/HexFiend/HFPasteboardOwner.m b/bsnes/gb/HexFiend/HFPasteboardOwner.m index 0ca341d6..5140fc67 100755 --- a/bsnes/gb/HexFiend/HFPasteboardOwner.m +++ b/bsnes/gb/HexFiend/HFPasteboardOwner.m @@ -44,7 +44,7 @@ NSString *const HFPrivateByteArrayPboardType = @"HFPrivateByteArrayPboardType"; [[NSNotificationCenter defaultCenter] removeObserver:self name:HFPrepareForChangeInFileNotification object:nil]; } if (retainedSelfOnBehalfOfPboard) { - CFRelease(self); + [self release]; retainedSelfOnBehalfOfPboard = NO; } } diff --git a/bsnes/gb/HexFiend/HFRepresenterTextView.m b/bsnes/gb/HexFiend/HFRepresenterTextView.m index 7fcbd0c7..463a6fdd 100644 --- a/bsnes/gb/HexFiend/HFRepresenterTextView.m +++ b/bsnes/gb/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/bsnes/gb/HexFiend/HFStatusBarRepresenter.h b/bsnes/gb/HexFiend/HFStatusBarRepresenter.h deleted file mode 100644 index e70b893f..00000000 --- a/bsnes/gb/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/bsnes/gb/HexFiend/HFStatusBarRepresenter.m b/bsnes/gb/HexFiend/HFStatusBarRepresenter.m deleted file mode 100644 index 883677f9..00000000 --- a/bsnes/gb/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/bsnes/gb/HexFiend/HexFiend.h b/bsnes/gb/HexFiend/HexFiend.h index 60d69a7e..0253cb8d 100644 --- a/bsnes/gb/HexFiend/HexFiend.h +++ b/bsnes/gb/HexFiend/HexFiend.h @@ -28,7 +28,6 @@ #import #import #import -#import #import #import #import diff --git a/bsnes/gb/JoyKit/ControllerConfiguration.inc b/bsnes/gb/JoyKit/ControllerConfiguration.inc index fb7df630..9b97a201 100644 --- a/bsnes/gb/JoyKit/ControllerConfiguration.inc +++ b/bsnes/gb/JoyKit/ControllerConfiguration.inc @@ -101,15 +101,31 @@ 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}, + + @{@"reportID": @(0x31), @"size":@16, @"offset":@0x80, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0x90, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0xA0, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0xB0, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisX)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0xC0, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisY)}, + @{@"reportID": @(0x31), @"size":@16, @"offset":@0xD0, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ)}, + ], + + @(1): @[ + @{@"reportID": @(1), @"size":@16, @"offset":@0x78, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0x88, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0x98, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0xA8, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisX)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0xB8, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisY)}, + @{@"reportID": @(1), @"size":@16, @"offset":@0xC8, @"usagePage":@(kHIDPage_Sensor), @"min": @-0x7FFF, @"max": @0x7FFF, @"usage":@(kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ)}, ], }, @@ -156,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: @{ @@ -406,8 +422,6 @@ hacksByName = @{ JOYCustomReports: @{ @(0x30): @[ - // For USB mode, which uses the wrong report descriptor - @{@"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}, @@ -440,11 +454,155 @@ hacksByName = @{ @{@"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}, + + @{@"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 (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/bsnes/gb/JoyKit/JOYAxes2D.h b/bsnes/gb/JoyKit/JOYAxes2D.h index b6f6d152..71fdb422 100644 --- a/bsnes/gb/JoyKit/JOYAxes2D.h +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYAxes2D.m b/bsnes/gb/JoyKit/JOYAxes2D.m index 272d34f9..dad59bba 100644 --- a/bsnes/gb/JoyKit/JOYAxes2D.m +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYAxes3D.h b/bsnes/gb/JoyKit/JOYAxes3D.h new file mode 100644 index 00000000..e30d4658 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYAxes3D.h @@ -0,0 +1,26 @@ +#import +#import "JOYInput.h" + +typedef enum { + JOYAxes3DUsageNone, + JOYAxes3DUsageAcceleration, + JOYAxes3DUsageOrientation, + JOYAxes3DUsageGyroscope, + JOYAxes3DUsageNonGenericMax, + + JOYAxes3DUsageGeneric0 = 0x10000, +} JOYAxes3DUsage; + +typedef struct { + double x, y, z; +} JOYPoint3D; + +@interface JOYAxes3D : JOYInput ++ (NSString *)usageToString: (JOYAxes3DUsage) usage; +- (JOYPoint3D)rawValue; +- (JOYPoint3D)normalizedValue; // For orientation +- (JOYPoint3D)gUnitsValue; // For acceleration +@property JOYAxes3DUsage usage; +@end + + diff --git a/bsnes/gb/JoyKit/JOYAxes3D.m b/bsnes/gb/JoyKit/JOYAxes3D.m new file mode 100644 index 00000000..9c7b86b9 --- /dev/null +++ b/bsnes/gb/JoyKit/JOYAxes3D.m @@ -0,0 +1,129 @@ +#import "JOYAxes3D.h" +#import "JOYElement.h" + +@interface JOYAxes3D() +@property unsigned rotation; // in 90 degrees units, clockwise +@end + +@implementation JOYAxes3D +{ + JOYElement *_element1, *_element2, *_element3; + double _state1, _state2, _state3; + int32_t _minX, _minY, _minZ; + int32_t _maxX, _maxY, _maxZ; + double _gApproximation; +} + ++ (NSString *)usageToString: (JOYAxes3DUsage) usage +{ + if (usage < JOYAxes3DUsageNonGenericMax) { + return inline_const(NSString *[], { + @"None", + @"Acceleretion", + @"Orientation", + @"Gyroscope", + })[usage]; + } + if (usage >= JOYAxes3DUsageGeneric0) { + return [NSString stringWithFormat:@"Generic 3D Analog Control %d", usage - JOYAxes3DUsageGeneric0]; + } + + return [NSString stringWithFormat:@"Unknown Usage 3D Axes %d", usage]; +} + +- (NSString *)usageString +{ + return [self.class usageToString:_usage]; +} + +- (uint64_t)uniqueID +{ + return _element1.uniqueID | (uint64_t)self.combinedIndex << 32; +} + +- (NSString *)description +{ + 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 +{ + self = [super init]; + if (!self) return self; + + _element1 = element1; + _element2 = element2; + _element3 = element3; + + _maxX = element1? element1.max : 1; + _maxY = element2? element2.max : 1; + _maxZ = element3? element3.max : 1; + _minX = element1? element1.min : -1; + _minY = element2? element2.min : -1; + _minZ = element3? element3.min : -1; + + return self; +} + +- (JOYPoint3D)rawValue +{ + return (JOYPoint3D){_state1, _state2, _state3}; +} + +- (JOYPoint3D)normalizedValue +{ + double distance = sqrt(_state1 * _state1 + _state2 * _state2 + _state3 * _state3); + if (distance == 0) { + distance = 1; + } + return (JOYPoint3D){_state1 / distance, _state2 / distance, _state3 / distance}; +} + +- (JOYPoint3D)gUnitsValue +{ + double distance = _gApproximation ?: 1; + return (JOYPoint3D){_state1 / distance, _state2 / distance, _state3 / distance}; +} + +- (bool)updateState +{ + int32_t x = [_element1 value]; + int32_t y = [_element2 value]; + int32_t z = [_element3 value]; + + if (x == 0 && y == 0 && z == 0) return false; + + double old1 = _state1, old2 = _state2, old3 = _state3; + _state1 = (x - _minX) / (double)(_maxX - _minX) * 2 - 1; + _state2 = (y - _minY) / (double)(_maxY - _minY) * 2 - 1; + _state3 = (z - _minZ) / (double)(_maxZ - _minZ) * 2 - 1; + + double distance = sqrt(_state1 * _state1 + _state2 * _state2 + _state3 * _state3); + if (_gApproximation == 0) { + _gApproximation = distance; + } + 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; +} + +@end diff --git a/bsnes/gb/JoyKit/JOYAxis.h b/bsnes/gb/JoyKit/JOYAxis.h index 8d4b7abe..06c0917c 100644 --- a/bsnes/gb/JoyKit/JOYAxis.h +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYAxis.m b/bsnes/gb/JoyKit/JOYAxis.m index afe90d26..9e15295e 100644 --- a/bsnes/gb/JoyKit/JOYAxis.m +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYButton.h b/bsnes/gb/JoyKit/JOYButton.h index 6a67c6c1..c3f13dea 100644 --- a/bsnes/gb/JoyKit/JOYButton.h +++ b/bsnes/gb/JoyKit/JOYButton.h @@ -1,6 +1,5 @@ #import - - +#import "JOYInput.h" typedef enum { JOYButtonUsageNone, @@ -41,12 +40,18 @@ typedef enum { JOYButtonUsageGeneric0 = 0x10000, } JOYButtonUsage; -@interface JOYButton : NSObject -- (NSString *)usageString; +typedef enum { + JOYButtonTypeNormal, + JOYButtonTypeAxisEmulated, + JOYButtonTypeAxes2DEmulated, + JOYButtonTypeHatEmulated, +} JOYButtonType; + +@interface JOYButton : JOYInput + (NSString *)usageToString: (JOYButtonUsage) usage; -- (uint64_t)uniqueID; - (bool) isPressed; @property JOYButtonUsage usage; +@property (readonly) JOYButtonType type; @end diff --git a/bsnes/gb/JoyKit/JOYButton.m b/bsnes/gb/JoyKit/JOYButton.m index 18970cde..ff26a730 100644 --- a/bsnes/gb/JoyKit/JOYButton.m +++ b/bsnes/gb/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; } @@ -105,4 +111,9 @@ } return false; } + +- (JOYButtonType)type +{ + return JOYButtonTypeNormal; +} @end diff --git a/bsnes/gb/JoyKit/JOYController.h b/bsnes/gb/JoyKit/JOYController.h index 8f5f6f44..5bd63385 100644 --- a/bsnes/gb/JoyKit/JOYController.h +++ b/bsnes/gb/JoyKit/JOYController.h @@ -2,6 +2,7 @@ #import "JOYButton.h" #import "JOYAxis.h" #import "JOYAxes2D.h" +#import "JOYAxes3D.h" #import "JOYHat.h" static NSString const *JOYAxes2DEmulateButtonsKey = @"JOYAxes2DEmulateButtons"; @@ -17,25 +18,51 @@ static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons"; -(void) controller:(JOYController *)controller buttonChangedState:(JOYButton *)button; -(void) controller:(JOYController *)controller movedAxis:(JOYAxis *)axis; -(void) controller:(JOYController *)controller movedAxes2D:(JOYAxes2D *)axes; +-(void) controller:(JOYController *)controller movedAxes3D:(JOYAxes3D *)axes; -(void) controller:(JOYController *)controller movedHat:(JOYHat *)hat; @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/bsnes/gb/JoyKit/JOYController.m b/bsnes/gb/JoyKit/JOYController.m index 1097ef65..c473ec68 100644 --- a/bsnes/gb/JoyKit/JOYController.m +++ b/bsnes/gb/JoyKit/JOYController.m @@ -3,11 +3,11 @@ #import "JOYElement.h" #import "JOYSubElement.h" #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,20 @@ 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 () +{ + @public JOYElement *_element1, *_element2, *_element3; +} +- (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) @@ -164,14 +180,17 @@ typedef union { @implementation JOYController { + @public // Let JOYCombinedController access everything IOHIDDeviceRef _device; NSMutableDictionary *_buttons; NSMutableDictionary *_axes; NSMutableDictionary *_axes2D; + NSMutableDictionary *_axes3D; NSMutableDictionary *_hats; NSMutableDictionary *_fullReportElements; NSMutableDictionary *> *_multiElements; - + JOYAxes3D *_lastAxes3D; + // Button emulation NSMutableDictionary *_axisEmulatedButtons; NSMutableDictionary *> *_axes2DEmulatedButtons; @@ -203,6 +222,7 @@ typedef union { unsigned _rumbleCounter; bool _deviceCantSendReports; dispatch_queue_t _rumbleQueue; + JOYCombinedController *_parent; } - (instancetype)initWithDevice:(IOHIDDeviceRef) device hacks:(NSDictionary *)hacks @@ -246,6 +266,69 @@ typedef union { @(kHIDUsage_GD_Rz): @(1), }; + if (element.usagePage == kHIDPage_Sensor) { + JOYAxes3DUsage usage; + JOYElement *element1 = nil, *element2 = nil, *element3 = nil; + + switch (element.usage) { + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisX: + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisY: + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ: + usage = JOYAxes3DUsageAcceleration; + break; + case kHIDUsage_Snsr_Data_Motion_AngularPositionXAxis: + case kHIDUsage_Snsr_Data_Motion_AngularPositionYAxis: + case kHIDUsage_Snsr_Data_Motion_AngularPositionZAxis: + usage = JOYAxes3DUsageOrientation; + break; + case kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis: + usage = JOYAxes3DUsageGyroscope; + break; + default: + return; + } + + switch (element.usage) { + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisX: + case kHIDUsage_Snsr_Data_Motion_AngularPositionXAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityXAxis: + element1 = element; + if (_lastAxes3D && !_lastAxes3D->_element1 && _lastAxes3D.usage == usage) { + element2 = _lastAxes3D->_element2; + element3 = _lastAxes3D->_element3; + } + break; + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisY: + case kHIDUsage_Snsr_Data_Motion_AngularPositionYAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityYAxis: + element2 = element; + if (_lastAxes3D && !_lastAxes3D->_element2 && _lastAxes3D.usage == usage) { + element1 = _lastAxes3D->_element1; + element3 = _lastAxes3D->_element3; + } + break; + case kHIDUsage_Snsr_Data_Motion_AccelerationAxisZ: + case kHIDUsage_Snsr_Data_Motion_AngularPositionZAxis: + case kHIDUsage_Snsr_Data_Motion_AngularVelocityZAxis: + element3 = element; + if (_lastAxes3D && !_lastAxes3D->_element3 && _lastAxes3D.usage == usage) { + element1 = _lastAxes3D->_element1; + element2 = _lastAxes3D->_element2; + } + break; + } + + _lastAxes3D = [[JOYAxes3D alloc] initWithFirstElement:element1 secondElement:element2 thirdElement:element3]; + _lastAxes3D.usage = usage; + if (element1) _axes3D[element1] = _lastAxes3D; + if (element2) _axes3D[element2] = _lastAxes3D; + if (element3) _axes3D[element3] = _lastAxes3D; + + return; + } + axisGroups = _hacks[JOYAxisGroups] ?: axisGroups; if (element.usagePage == kHIDPage_Button || @@ -256,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; } @@ -284,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; @@ -307,29 +390,12 @@ typedef union { if (axes2DEmulateButtons) { _axes2DEmulatedButtons[@(axes.uniqueID)] = @[ - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft uniqueID:axes.uniqueID | 0x100000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight uniqueID:axes.uniqueID | 0x200000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp uniqueID:axes.uniqueID | 0x300000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown uniqueID:axes.uniqueID | 0x400000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft type:JOYButtonTypeAxes2DEmulated uniqueID:axes.uniqueID | 0x100000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight type:JOYButtonTypeAxes2DEmulated uniqueID:axes.uniqueID | 0x200000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp type:JOYButtonTypeAxes2DEmulated uniqueID:axes.uniqueID | 0x300000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown type:JOYButtonTypeAxes2DEmulated uniqueID:axes.uniqueID | 0x400000000L], ]; } - - /* - for (NSArray *group in axes2d) { - break; - IOHIDElementRef first = (__bridge IOHIDElementRef)group[0]; - IOHIDElementRef second = (__bridge IOHIDElementRef)group[1]; - if (IOHIDElementGetUsage(first) > element.usage) continue; - if (IOHIDElementGetUsage(second) > element.usage) continue; - if (IOHIDElementGetReportID(first) != IOHIDElementGetReportID(element)) continue; - if ((IOHIDElementGetUsage(first) - kHIDUsage_GD_X) / 3 != (element.usage - kHIDUsage_GD_X) / 3) continue; - if (IOHIDElementGetParent(first) != IOHIDElementGetParent(element)) continue; - - [axes2d removeObject:group]; - [axes3d addObject:@[(__bridge id)first, (__bridge id)second, _element]]; - found = true; - break; - }*/ break; } case kHIDUsage_GD_Slider: @@ -346,7 +412,7 @@ typedef union { if ([_hacks[JOYEmulateAxisButtons] boolValue]) { _axisEmulatedButtons[@(axis.uniqueID)] = - [[JOYEmulatedButton alloc] initWithUsage:axis.equivalentButtonUsage uniqueID:axis.uniqueID]; + [[JOYEmulatedButton alloc] initWithUsage:axis.equivalentButtonUsage type:JOYButtonTypeAxisEmulated uniqueID:axis.uniqueID]; } @@ -366,10 +432,10 @@ typedef union { [_hats setObject:hat forKey:element]; if (hatsEmulateButtons) { _hatEmulatedButtons[@(hat.uniqueID)] = @[ - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft uniqueID:hat.uniqueID | 0x100000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight uniqueID:hat.uniqueID | 0x200000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp uniqueID:hat.uniqueID | 0x300000000L], - [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown uniqueID:hat.uniqueID | 0x400000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft type:JOYButtonTypeHatEmulated uniqueID:hat.uniqueID | 0x100000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight type:JOYButtonTypeHatEmulated uniqueID:hat.uniqueID | 0x200000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp type:JOYButtonTypeHatEmulated uniqueID:hat.uniqueID | 0x300000000L], + [[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown type:JOYButtonTypeHatEmulated uniqueID:hat.uniqueID | 0x400000000L], ]; } break; @@ -388,6 +454,7 @@ typedef union { _device = (IOHIDDeviceRef)CFRetain(device); _serialSuffix = suffix; _playerLEDs = -1; + [self obtainInfo]; IOHIDDeviceRegisterInputValueCallback(device, HIDInput, (void *)self); IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); @@ -396,6 +463,7 @@ typedef union { _buttons = [NSMutableDictionary dictionary]; _axes = [NSMutableDictionary dictionary]; _axes2D = [NSMutableDictionary dictionary]; + _axes3D = [NSMutableDictionary dictionary]; _hats = [NSMutableDictionary dictionary]; _axisEmulatedButtons = [NSMutableDictionary dictionary]; _axes2DEmulatedButtons = [NSMutableDictionary dictionary]; @@ -403,12 +471,11 @@ typedef union { _iokitToJOY = [NSMutableDictionary dictionary]; - //NSMutableArray *axes3d = [NSMutableArray array]; - _hacks = hacks; _isSwitch = [_hacks[JOYIsSwitch] boolValue]; _isDualShock3 = [_hacks[JOYIsDualShock3] boolValue]; _isSony = [_hacks[JOYIsSony] boolValue]; + _joyconType = [_hacks[JOYJoyCon] unsignedIntValue]; NSDictionary *customReports = hacks[JOYCustomReports]; _lastReport = [NSMutableData dataWithLength:MAX( @@ -520,24 +587,46 @@ 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++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _lastVendorSpecificOutput.switchPacket.command = 3; // Set input report mode + _lastVendorSpecificOutput.switchPacket.commandData[0] = 0x30; // Standard full mode + [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; + + _lastVendorSpecificOutput.switchPacket.sequence++; + _lastVendorSpecificOutput.switchPacket.sequence &= 0xF; + _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) { @@ -569,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", @@ -586,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 @@ -619,6 +709,11 @@ typedef union { return [[NSSet setWithArray:[_axes2D allValues]] allObjects]; } +- (NSArray *)axes3D +{ + return [[NSSet setWithArray:[_axes3D allValues]] allObjects]; +} + - (NSArray *)hats { return [_hats allValues]; @@ -666,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]; @@ -681,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]; } } } @@ -696,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]; } } } @@ -718,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]; } } } @@ -736,22 +832,36 @@ typedef union { } } + { + JOYAxes3D *axes = _axes3D[element]; + if (axes) { + if ([axes updateState]) { + for (id listener in listeners) { + if ([listener respondsToSelector:@selector(controller:movedAxes3D:)]) { + [listener controller:_parent ?: self movedAxes3D:axes]; + } + } + } + return; + } + } + { JOYHat *hat = _hats[element]; if (hat) { 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]; } } } @@ -764,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:)]) { @@ -771,7 +883,6 @@ typedef union { } } } - _physicallyConnected = false; [exposedControllers removeObject:self]; [self setRumbleAmplitude:0]; dispatch_sync(_rumbleQueue, ^{ @@ -801,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); @@ -865,7 +976,7 @@ typedef union { if (_isDualShock3) { return 2 << player; } - if (_isUSBDualSense) { + if (_isDualSense) { switch (player) { case 0: return 0x04; case 1: return 0x0A; @@ -962,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 { @@ -989,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)); @@ -1008,8 +1184,6 @@ typedef union { } [controllers setObject:controller forKey:[NSValue valueWithPointer:device]]; - - } + (void)controllerRemoved:(IOHIDDeviceRef) device @@ -1025,7 +1199,7 @@ typedef union { + (void)load { -#include "ControllerConfiguration.inc" +#import "ControllerConfiguration.inc" } +(void)registerListener:(id)listener @@ -1079,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)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/bsnes/gb/JoyKit/JOYElement.h b/bsnes/gb/JoyKit/JOYElement.h index 0e917dd0..9f2fe464 100644 --- a/bsnes/gb/JoyKit/JOYElement.h +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYElement.m b/bsnes/gb/JoyKit/JOYElement.m index 2432002a..175cf161 100644 --- a/bsnes/gb/JoyKit/JOYElement.m +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYEmulatedButton.h b/bsnes/gb/JoyKit/JOYEmulatedButton.h index 491e0c73..05ccde82 100644 --- a/bsnes/gb/JoyKit/JOYEmulatedButton.h +++ b/bsnes/gb/JoyKit/JOYEmulatedButton.h @@ -4,7 +4,7 @@ #import "JOYHat.h" @interface JOYEmulatedButton : JOYButton -- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID; +- (instancetype)initWithUsage:(JOYButtonUsage)usage type:(JOYButtonType)type uniqueID:(uint64_t)uniqueID; - (bool)updateStateFromAxis:(JOYAxis *)axis; - (bool)updateStateFromAxes2D:(JOYAxes2D *)axes; - (bool)updateStateFromHat:(JOYHat *)hat; diff --git a/bsnes/gb/JoyKit/JOYEmulatedButton.m b/bsnes/gb/JoyKit/JOYEmulatedButton.m index b62670a6..841617ed 100644 --- a/bsnes/gb/JoyKit/JOYEmulatedButton.m +++ b/bsnes/gb/JoyKit/JOYEmulatedButton.m @@ -10,20 +10,22 @@ @implementation JOYEmulatedButton { uint64_t _uniqueID; + JOYButtonType _type; } -- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID; +- (instancetype)initWithUsage:(JOYButtonUsage)usage type:(JOYButtonType)type uniqueID:(uint64_t)uniqueID; { self = [super init]; self.usage = usage; _uniqueID = uniqueID; + _type = type; return self; } - (uint64_t)uniqueID { - return _uniqueID; + return _uniqueID | (uint64_t)self.combinedIndex << 32; } - (bool)updateStateFromAxis:(JOYAxis *)axis @@ -89,4 +91,9 @@ return _state != old; } +- (JOYButtonType)type +{ + return _type; +} + @end diff --git a/bsnes/gb/JoyKit/JOYFullReportElement.h b/bsnes/gb/JoyKit/JOYFullReportElement.h index 808644e7..75fce1f6 100644 --- a/bsnes/gb/JoyKit/JOYFullReportElement.h +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYFullReportElement.m b/bsnes/gb/JoyKit/JOYFullReportElement.m index c8efb270..e4f464cd 100644 --- a/bsnes/gb/JoyKit/JOYFullReportElement.m +++ b/bsnes/gb/JoyKit/JOYFullReportElement.m @@ -1,5 +1,5 @@ #import "JOYFullReportElement.h" -#include +#import @implementation JOYFullReportElement { @@ -46,7 +46,7 @@ { [self updateValue:value]; - return IOHIDDeviceSetReport(_device, kIOHIDReportTypeOutput, _reportID, [_data bytes], [_data length]);; + return IOHIDDeviceSetReport(_device, kIOHIDReportTypeOutput, _reportID, [_data bytes], [_data length]); } - (void)updateValue:(NSData *)value @@ -61,12 +61,13 @@ return self.uniqueID; } -- (BOOL)isEqual:(id)object +- (BOOL)isEqual:(JOYFullReportElement *)object { - return self.uniqueID == self.uniqueID; + if ([object isKindOfClass:self.class]) return false; + return self.uniqueID == object.uniqueID; } -- (id)copyWithZone:(nullable NSZone *)zone; +- (id)copyWithZone:(NSZone *)zone; { return self; } diff --git a/bsnes/gb/JoyKit/JOYHat.h b/bsnes/gb/JoyKit/JOYHat.h index 05a58292..f430beb2 100644 --- a/bsnes/gb/JoyKit/JOYHat.h +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYHat.m b/bsnes/gb/JoyKit/JOYHat.m index b5a18f0b..9cebe65c 100644 --- a/bsnes/gb/JoyKit/JOYHat.m +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYInput.h b/bsnes/gb/JoyKit/JOYInput.h new file mode 100644 index 00000000..ea45b59a --- /dev/null +++ b/bsnes/gb/JoyKit/JOYInput.h @@ -0,0 +1,8 @@ +#import + +@interface JOYInput : NSObject +@property (readonly) unsigned combinedIndex; +- (NSString *)usageString; +- (uint64_t)uniqueID; +@end + diff --git a/bsnes/gb/JoyKit/JOYInput.m b/bsnes/gb/JoyKit/JOYInput.m new file mode 100644 index 00000000..0de83bf2 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYMultiplayerController.h b/bsnes/gb/JoyKit/JOYMultiplayerController.h index 44d74219..34c4d4c9 100644 --- a/bsnes/gb/JoyKit/JOYMultiplayerController.h +++ b/bsnes/gb/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/bsnes/gb/JoyKit/JOYSubElement.m b/bsnes/gb/JoyKit/JOYSubElement.m index c94badc7..186caf9e 100644 --- a/bsnes/gb/JoyKit/JOYSubElement.m +++ b/bsnes/gb/JoyKit/JOYSubElement.m @@ -57,6 +57,12 @@ memcpy(temp, bytes + _offset / 8, (_offset + _size - 1) / 8 - _offset / 8 + 1); uint32_t ret = (*(uint32_t *)temp) >> (_offset % 8); ret &= (1 << _size) - 1; + // + if (_min < 0 || _max < 0) { // Uses unsigned values + if (ret & (1 << (_size - 1)) ) { // Is negative + ret |= ~((1 << _size) - 1); // Fill with 1s + } + } if (_max < _min) { return _max + _min - ret; diff --git a/bsnes/gb/JoyKit/JoyKit.h b/bsnes/gb/JoyKit/JoyKit.h index d56b5051..f96659c6 100644 --- a/bsnes/gb/JoyKit/JoyKit.h +++ b/bsnes/gb/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/bsnes/gb/LICENSE b/bsnes/gb/LICENSE index 3303e0d7..7c45dc19 100644 --- a/bsnes/gb/LICENSE +++ b/bsnes/gb/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-2025 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/bsnes/gb/Makefile b/bsnes/gb/Makefile index 7bfe5803..603690aa 100644 --- a/bsnes/gb/Makefile +++ b/bsnes/gb/Makefile @@ -13,27 +13,42 @@ ifneq ($(findstring MSYS,$(PLATFORM)),) PLATFORM := windows32 endif +DL_EXT := so + 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 +DL_EXT := dylib else DEFAULT := sdl endif -ifneq ($(shell which xdg-open)$(FREEDESKTOP),) + +NULL := /dev/null +ifeq ($(PLATFORM),windows32) +ifneq ($(shell echo /dev/null*),/dev/null) +# Windows shell is not "aware" of /dev/null, use NUL and pray +NULL := NUL +endif +endif + +PREFIX ?= /usr/local +ifneq ($(shell which xdg-open 2> $(NULL))$(FREEDESKTOP),) # Running on an FreeDesktop environment, configure for (optional) installation DESTDIR ?= -PREFIX ?= /usr/local DATA_DIR ?= $(PREFIX)/share/sameboy/ FREEDESKTOP ?= true endif @@ -44,13 +59,67 @@ 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 +DISABLE_CHEAT_SEARCH := 1 +else +CPPP_FLAGS += -UGB_DISABLE_DEBUGGER +endif + +ifneq ($(DISABLE_CHEATS),) +CFLAGS += -DGB_DISABLE_CHEATS +CPPP_FLAGS += -DGB_DISABLE_CHEATS +CORE_FILTER += Core/cheats.c +DISABLE_CHEAT_SEARCH := 1 +else +CPPP_FLAGS += -UGB_DISABLE_CHEATS +endif + +ifneq ($(DISABLE_CHEAT_SEARCH),) +CFLAGS += -DGB_DISABLE_CHEAT_SEARCH +CPPP_FLAGS += -DGB_DISABLE_CHEAT_SEARCH +CORE_FILTER += Core/cheat_search.c +else +CPPP_FLAGS += -UGB_DISABLE_CHEAT_SEARCH +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 +PKGCONF_DIR := $(LIBDIR)/pkgconfig +PKGCONF_FILE := $(PKGCONF_DIR)/sameboy.pc + +ifneq ($(CORE_FILTER)$(DISABLE_TIMEKEEPING),) +ifneq ($(filter-out lib headers $(LIBDIR)/% $(INC)/%,$(MAKECMDGOALS)),) +$(error SameBoy features can only be disabled when compiling the 'lib' target) +endif +endif + BOOTROMS_DIR ?= $(BIN)/BootROMs ifdef DATA_DIR @@ -61,15 +130,20 @@ 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 +IBTOOL ?= ibtool + # 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, and not on Haiku, where OpenGL is broken +ifeq ($(filter Darwin Haiku,$(PLATFORM)),) PKG_CONFIG := pkg-config endif +endif ifeq ($(PLATFORM),windows32) # To force use of the Unix version instead of the Windows version @@ -89,16 +163,26 @@ override CONF := release FAT_FLAGS += -arch x86_64 -arch arm64 endif +IOS_MIN := 11.0 + +IOS_PNGS := $(shell ls iOS/*.png iOS/*.car) +# 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 +191,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 -Wno-nullability-completeness # 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,76 +205,187 @@ 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 ($(PLATFORM),windows32) +CFLAGS += -fPIC +endif + ifneq (,$(UPDATE_SUPPORT)) CFLAGS += -DUPDATE_SUPPORT endif ifeq (,$(PKG_CONFIG)) +ifneq ($(PLATFORM),windows32) SDL_CFLAGS := $(shell sdl2-config --cflags) SDL_LDFLAGS := $(shell sdl2-config --libs) -lpthread +endif +ifeq ($(PLATFORM),Darwin) +SDL_LDFLAGS += -framework AppKit +endif + +# 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 # ifneq ($(PKG_CONFIG),) 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) +ifneq ($(shell $(PKG_CONFIG) --exists openal && echo 0),) +SDL_CFLAGS += $(shell $(PKG_CONFIG) --cflags openal) -DENABLE_OPENAL +SDL_LDFLAGS += $(shell $(PKG_CONFIG) --libs openal) +SDL_AUDIO_DRIVERS += openal endif +endif + +ifneq ($(shell $(PKG_CONFIG) --exists gio-unix-2.0 || echo 0),) +GIO_CFLAGS = $(error The Gio library could not be found) +GIO_LDFLAGS = $(error The Gio library could not be found) +else +GIO_CFLAGS := $(shell $(PKG_CONFIG) --cflags gio-unix-2.0) -DG_LOG_USE_STRUCTURED +GIO_LDFLAGS := $(shell $(PKG_CONFIG) --libs gio-unix-2.0) +ifeq ($(CONF),debug) +GIO_CFLAGS += -DG_ENABLE_DEBUG +else +GIO_CFLAGS += -DG_DISABLE_ASSERT +endif +endif + +ifneq ($(shell $(PKG_CONFIG) --exists gdk-pixbuf-2.0 || echo 0),) +GDK_PIXBUF_CFLAGS = $(error The Gdk-Pixbuf library could not be found) +GDK_PIXBUF_LDFLAGS = $(error The Gdk-Pixbuf library could not be found) +else +GDK_PIXBUF_CFLAGS := $(shell $(PKG_CONFIG) --cflags gdk-pixbuf-2.0) +GDK_PIXBUF_LDFLAGS := $(shell $(PKG_CONFIG) --libs gdk-pixbuf-2.0) +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 -SDL_LDFLAGS := -lSDL2 -GL_LDFLAGS := -lopengl32 +CFLAGS += -IWindows -Drandom=rand --target=x86_64-pc-windows +LDFLAGS += -lmsvcrt -lkernel32 -Wl,/MANIFESTFILE:NUL --target=x86_64-pc-windows +SDL_LDFLAGS := -lSDL2 -lcomdlg32 -luser32 -lshell32 -lole32 -ladvapi32 -ldwmapi -lSDL2main +GL_LDFLAGS := -lopengl32 +ifneq ($(REDIST_XAUDIO),) +CFLAGS += -DREDIST_XAUDIO +LDFLAGS += -lxaudio2_9redist +sdl: $(BIN)/SDL/xaudio2_9redist.dll +endif else -LDFLAGS += -lc -lm -ldl +LDFLAGS += -lc -lm +# libdl is not available as a standalone library in Haiku or OpenBSD +ifneq ($(PLATFORM),Haiku) +ifneq ($(PLATFORM),OpenBSD) +LDFLAGS += -ldl +endif +endif 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 Core/cheat_search.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 -lcompression +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 += -mmacosx-version-min=10.9 -isysroot $(SYSROOT) GL_LDFLAGS := -framework OpenGL endif CFLAGS += -Wno-deprecated-declarations ifeq ($(PLATFORM),windows32) CFLAGS += -Wno-deprecated-declarations # Seems like Microsoft deprecated every single LIBC function LDFLAGS += -Wl,/NODEFAULTLIB:libcmt.lib + +ifneq ($(USE_MSVCRT_DLL),) +CFLAGS += -D_NO_CRT_STDIO_INLINE -DUSE_MSVCRT_DLL +$(BIN)/SDL/sameboy.exe: $(OBJ)/Windows/msvcrt.lib +$(LIBDIR)/libsameboy.dll: $(OBJ)/Windows/msvcrt.lib +endif + +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 -STRIP := strip -ifeq ($(PLATFORM),Darwin) -LDFLAGS += -Wl,-exported_symbols_list,$(NULL) -STRIP := -@true +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 -ifneq ($(PLATFORM),windows32) + +# 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 LDFLAGS += -flto CFLAGS += -flto LDFLAGS += -Wno-lto-type-mismatch # For GCC's LTO -endif else $(error Invalid value for CONF: $(CONF). Use "debug", "release" or "native_release") endif +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 +endif + + # Define our targets ifeq ($(PLATFORM),windows32) -SDL_TARGET := $(BIN)/SDL/sameboy.exe $(BIN)/SDL/sameboy_debugger.exe $(BIN)/SDL/SDL2.dll +SDL_TARGET := $(BIN)/SDL/sameboy.exe $(BIN)/SDL/SDL2.dll $(BIN)/SDL/sameboy_debugger.txt TESTER_TARGET := $(BIN)/tester/sameboy_tester.exe else SDL_TARGET := $(BIN)/SDL/sameboy @@ -198,36 +393,56 @@ TESTER_TARGET := $(BIN)/tester/sameboy_tester endif cocoa: $(BIN)/SameBoy.app -quicklook: $(BIN)/SameBoy.qlgenerator -sdl: $(SDL_TARGET) $(BIN)/SDL/dmg_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/dmg_boot.bin $(BIN)/BootROMs/sgb_boot.bin $(BIN)/BootROMs/sgb2_boot.bin +xdg-thumbnailer: $(BIN)/XdgThumbnailer/sameboy-thumbnailer +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: $(LIBDIR)/libsameboy.dll +else +lib: $(LIBDIR)/libsameboy.o $(LIBDIR)/libsameboy.a $(LIBDIR)/libsameboy.$(DL_EXT) +endif +all: sdl tester libretro lib +ifeq ($(PLATFORM),Darwin) +all: cocoa ios-ipa ios-deb +endif +ifneq ($(FREEDESKTOP),) +all: xdg-thumbnailer +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 +XDG_THUMBNAILER_SOURCES := $(shell ls XdgThumbnailer/*.c) 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)) +XDG_THUMBNAILER_OBJECTS := $(patsubst %,$(OBJ)/%.o,$(XDG_THUMBNAILER_SOURCES)) $(OBJ)/XdgThumbnailer/resources.c.o + +lib: headers +headers: $(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) @@ -238,15 +453,22 @@ 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 $@ + $(CC) $(CFLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -MT $(OBJ)/$^.o -M $^ -o $@ + +$(OBJ)/OpenDialog/%.dep: OpenDialog/% + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(SDL_CFLAGS) $(GL_CFLAGS) -MT $(OBJ)/$^.o -M $^ -o $@ $(OBJ)/%.dep: % -@$(MKDIR) -p $(dir $@) - $(CC) $(CFLAGS) -MT $(OBJ)/$^.o -M $^ -c -o $@ + $(CC) $(CFLAGS) -MT $(OBJ)/$^.o -M $^ -o $@ # Compilation rules @@ -256,77 +478,185 @@ $(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)/XdgThumbnailer/%.c.o: XdgThumbnailer/%.c + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(GIO_CFLAGS) $(GDK_PIXBUF_CFLAGS) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -c $< -o $@ +# Make sure not to attempt compiling this before generating the resource code. +$(OBJ)/XdgThumbnailer/emulate.c.o: $(OBJ)/XdgThumbnailer/resources.h +# Silence warnings for this. It is code generated not by us, so we do not want `-Werror` to break +# compilation with some version of the generator and/or compiler. +$(OBJ)/XdgThumbnailer/%.c.o: $(OBJ)/XdgThumbnailer/%.c + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(GIO_CFLAGS) $(GDK_PIXBUF_CFLAGS) -DG_LOG_DOMAIN='"sameboy-thumbnailer"' -w -c $< -o $@ + +$(OBJ)/XdgThumbnailer/resources.c $(OBJ)/XdgThumbnailer/resources.h: %: XdgThumbnailer/resources.gresource.xml $(BIN)/BootROMs/cgb_boot_fast.bin + -@$(MKDIR) -p $(dir $@) + CC=$(CC) glib-compile-resources --dependency-file $@.mk --generate-phony-targets --generate --target $@ $< +-include $(OBJ)/XdgThumbnailer/resources.c.mk $(OBJ)/XdgThumbnailer/resources.h.mk + +$(OBJ)/OpenDialog/%.c.o: OpenDialog/%.c + -@$(MKDIR) -p $(dir $@) + $(CC) $(CFLAGS) $(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 $@ -# Cocoa Port +# iOS Port -$(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \ - $(shell ls Cocoa/*.icns Cocoa/*.png) \ - Cocoa/License.html \ - Cocoa/Info.plist \ - Misc/registers.sym \ - $(BIN)/SameBoy.app/Contents/Resources/dmg_boot.bin \ - $(BIN)/SameBoy.app/Contents/Resources/cgb_boot.bin \ - $(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))) \ - $(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 - $(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/ +$(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.app/Contents/MacOS/SameBoy: $(CORE_OBJECTS) $(COCOA_OBJECTS) +$(BIN)/SameBoy-iOS.app/SameBoy: $(CORE_OBJECTS) $(IOS_OBJECTS) -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework IOKit + $(CC) $^ -o $@ $(LDFLAGS) ifeq ($(CONF), release) $(STRIP) $@ endif -$(BIN)/SameBoy.app/Contents/Resources/Base.lproj/%.nib: Cocoa/%.xib - ibtool --compile $@ $^ 2>&1 | cat - - -# Quick Look generator +$(OBJ)/installer: iOS/installer.m + $(CC) $< -o $@ $(IOS_INSTALLER_LDFLAGS) $(CFLAGS) -$(BIN)/SameBoy.qlgenerator: $(BIN)/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL \ - $(shell ls QuickLook/*.png) \ - QuickLook/Info.plist \ - $(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 +# Cocoa Port -# 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) +$(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \ + $(shell ls Cocoa/*.icns Cocoa/*.png Cocoa/*.car) \ + Cocoa/License.html \ + Cocoa/Info.plist \ + Cocoa/SameBoy.entitlements \ + Misc/registers.sym \ + $(BIN)/SameBoy.app/Contents/Resources/dmg_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/mgb_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/cgb0_boot.bin \ + $(BIN)/SameBoy.app/Contents/Resources/cgb_boot.bin \ + $(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/,$(shell cd Cocoa;ls *.xib))) \ + $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator \ + $(BIN)/SameBoy.app/Contents/PlugIns/Thumbnailer.appex \ + $(BIN)/SameBoy.app/Contents/PlugIns/Previewer.appex \ + Shaders + $(MKDIR) -p $(BIN)/SameBoy.app/Contents/Resources + cp Cocoa/*.icns Cocoa/*.png Cocoa/*.car Misc/registers.sym $(BIN)/SameBoy.app/Contents/Resources/ + 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/ + $(CODESIGN) $@ --entitlements Cocoa/SameBoy.entitlements + +# We place the dylib inside the Quick Look plugin, because Quick Look plugins run in a very strict sandbox + +$(BIN)/SameBoy.app/Contents/MacOS/SameBoy: $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/MacOS/SameBoy.dylib -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) -Wl,-exported_symbols_list,QuickLook/exports.sym -bundle -framework Cocoa -framework Quicklook + $(CC) -o $@ $(LDFLAGS) $(FAT_FLAGS) -rpath @executable_path/../Library/QuickLook/SameBoy.qlgenerator/ -Wl,-reexport_library,$^ + +$(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/MacOS/SameBoy.dylib: $(COCOA_OBJECTS) $(CORE_OBJECTS) $(QUICKLOOK_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) $(FAT_FLAGS) -shared -install_name @rpath/Contents/MacOS/SameBoy.dylib -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 -weak_framework QuickLookThumbnailing -weak_framework QuickLookUI -framework Quicklook -framework AppKit -Wl,-exported_symbols_list,QuickLook/exports.sym -Wl,-exported_symbol,_main +ifeq ($(CONF), release) + $(STRIP) $@ + $(CODESIGN) $@ +endif + +$(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 generators + +$(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator: $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL \ + $(shell ls QuickLook/*.png) \ + QuickLook/Info.plist \ + $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin + $(MKDIR) -p $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/Resources + cp QuickLook/*.png $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/Resources/ + sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < QuickLook/Info.plist > $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/Info.plist +ifeq ($(CONF), release) + $(CODESIGN) $@ +endif + +$(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL: $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/MacOS/SameBoy.dylib + -@$(MKDIR) -p $(dir $@) + $(CC) -o $@ $(LDFLAGS) $(FAT_FLAGS) -bundle -Wl,-reexport_library,$^ -rpath @loader_path/../../ +ifeq ($(CONF), release) + $(STRIP) $@ +endif + +$(BIN)/SameBoy.app/Contents/PlugIns/Thumbnailer.appex: $(BIN)/SameBoy.app/Contents/PlugIns/Thumbnailer.appex/Contents/MacOS/Thumbnailer \ + QuickLook/Thumbnailer.plist \ + QuickLook/plugin.entitlements + sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < QuickLook/Thumbnailer.plist > $(BIN)/SameBoy.app/Contents/PlugIns/Thumbnailer.appex/Contents/Info.plist + $(CODESIGN) --entitlements QuickLook/plugin.entitlements $@ + +$(BIN)/SameBoy.app/Contents/PlugIns/Thumbnailer.appex/Contents/MacOS/Thumbnailer: $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/MacOS/SameBoy.dylib + -@$(MKDIR) -p $(dir $@) + $(CC) -o $@ $(LDFLAGS) $(FAT_FLAGS) -e _NSExtensionMain -framework Foundation -Wl,-reexport_library,$^ -rpath @loader_path/../../../../Library/QuickLook/SameBoy.qlgenerator/ +ifeq ($(CONF), release) + $(STRIP) $@ +endif + +$(BIN)/SameBoy.app/Contents/PlugIns/Previewer.appex: $(BIN)/SameBoy.app/Contents/PlugIns/Previewer.appex/Contents/MacOS/Previewer \ + QuickLook/Previewer.plist \ + QuickLook/plugin.entitlements + sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < QuickLook/Previewer.plist > $(BIN)/SameBoy.app/Contents/PlugIns/Previewer.appex/Contents/Info.plist + $(CODESIGN) --entitlements QuickLook/plugin.entitlements $@ + +$(BIN)/SameBoy.app/Contents/PlugIns/Previewer.appex/Contents/MacOS/Previewer: $(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/MacOS/SameBoy.dylib + -@$(MKDIR) -p $(dir $@) + $(CC) -o $@ $(LDFLAGS) $(FAT_FLAGS) -e _NSExtensionMain -framework Foundation -Wl,-reexport_library,$^ -rpath @loader_path/../../../../Library/QuickLook/SameBoy.qlgenerator/ +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. -$(BIN)/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin: $(BIN)/BootROMs/cgb_boot_fast.bin +$(BIN)/SameBoy.app/Contents/Library/QuickLook/SameBoy.qlgenerator/Contents/Resources/cgb_boot_fast.bin: $(BIN)/BootROMs/cgb_boot_fast.bin -@$(MKDIR) -p $(dir $@) cp -f $^ $@ - + +# XDG thumbnailer + +$(BIN)/XdgThumbnailer/sameboy-thumbnailer: $(CORE_OBJECTS) $(XDG_THUMBNAILER_OBJECTS) + -@$(MKDIR) -p $(dir $@) + $(CC) $^ -o $@ $(LDFLAGS) $(GIO_LDFLAGS) $(GDK_PIXBUF_LDFLAGS) +ifeq ($(CONF), release) + $(STRIP) $@ +endif + # SDL Port # Unix versions build only one binary @@ -335,33 +665,39 @@ $(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 $(BIN)/SDL/sameboy.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(OBJ)/Windows/resources.o -@$(MKDIR) -p $(dir $@) $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) -Wl,/subsystem:windows - -$(BIN)/SDL/sameboy_debugger.exe: $(CORE_OBJECTS) $(SDL_OBJECTS) $(OBJ)/Windows/resources.o - -@$(MKDIR) -p $(dir $@) - $(CC) $^ -o $@ $(LDFLAGS) $(SDL_LDFLAGS) $(GL_LDFLAGS) -Wl,/subsystem:console + +$(BIN)/SDL/sameboy_debugger.txt: + echo Looking for sameboy_debugger.exe? > $@ + echo >> $@ + echo Starting with SameBoy v1.0.1, sameboy.exe and sameboy_debugger.exe >> $@ + echo have been merged into a single executable. You can open a debugger >> $@ + echo console at any time by pressing Ctrl+C to interrupt the currently >> $@ + echo open ROM. Once you\'re done debugging, you can close the debugger >> $@ + echo console and resume normal execution. >> $@ 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:"$@" $^ + cvtres /MACHINE:X64 /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 @@ -371,112 +707,217 @@ $(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) +$(BIN)/tester/sameboy_tester.exe: $(CORE_OBJECTS) -@$(MKDIR) -p $(dir $@) $(CC) $^ -o $@ $(LDFLAGS) -Wl,/subsystem:console -$(BIN)/SDL/%.bin: $(BOOTROMS_DIR)/%.bin - -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ - $(BIN)/tester/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ $(BIN)/SameBoy.app/Contents/Resources/%.bin: $(BOOTROMS_DIR)/%.bin -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ + +$(BIN)/SameBoy-iOS.app/%.bin: $(BOOTROMS_DIR)/%.bin + -@$(MKDIR) -p $(dir $@) + cp -f $< $@ + +$(BIN)/SDL/%.bin: $(BOOTROMS_DIR)/%.bin + -@$(MKDIR) -p $(dir $@) + cp -f $< $@ $(BIN)/SDL/LICENSE: LICENSE -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + grep -v "^ " $< > $@ $(BIN)/SDL/registers.sym: Misc/registers.sym -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ $(BIN)/SDL/background.bmp: SDL/background.bmp -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + cp -f $< $@ -$(BIN)/SDL/Shaders: Shaders +$(BIN)/SDL/Shaders: $(wildcard Shaders/*.fsh) -@$(MKDIR) -p $@ - cp -rf Shaders/*.fsh $@ + cp -f $^ $@ + touch $@ + +$(BIN)/SDL/Palettes: Misc/Palettes + -@$(MKDIR) -p $@ + cp -f $ $@ + -@$(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 + CC=$(CC) 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. -# If you somehow find a reasonable way to make associate an icon with an extension in this dumpster -# fire of a desktop environment, open an issue or a pull request +# Install for Linux, and other FreeDesktop platforms. ifneq ($(FREEDESKTOP),) -ICON_NAMES := apps/sameboy mimetypes/x-gameboy-rom mimetypes/x-gameboy-color-rom -ICON_SIZES := 16x16 32x32 64x64 128x128 256x256 512x512 -ICONS := $(foreach name,$(ICON_NAMES), $(foreach size,$(ICON_SIZES),$(DESTDIR)$(PREFIX)/share/icons/hicolor/$(size)/$(name).png)) -install: sdl $(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml $(ICONS) FreeDesktop/sameboy.desktop - -@$(MKDIR) -p $(dir $(DESTDIR)$(PREFIX)) - mkdir -p $(DESTDIR)$(DATA_DIR)/ $(DESTDIR)$(PREFIX)/bin/ - cp -rf $(BIN)/SDL/* $(DESTDIR)$(DATA_DIR)/ - mv $(DESTDIR)$(DATA_DIR)/sameboy $(DESTDIR)$(PREFIX)/bin/sameboy +install: $(BIN)/XdgThumbnailer/sameboy-thumbnailer sdl $(shell find FreeDesktop) XdgThumbnailer/sameboy.thumbnailer + install -d $(DESTDIR)$(DATA_DIR)/Shaders + install -d $(DESTDIR)$(DATA_DIR)/Palettes + install -d $(DESTDIR)$(DATA_DIR)/BootROMs + install -d $(DESTDIR)$(PREFIX)/bin + install -d $(DESTDIR)$(PREFIX)/share/thumbnailers + install -d $(DESTDIR)$(PREFIX)/share/mime/packages + install -d $(DESTDIR)$(PREFIX)/share/applications + + (cd $(BIN)/SDL && find . \! -name sameboy -type f -exec install -m 644 {} "$(abspath $(DESTDIR))$(DATA_DIR)/{}" \; ) + install -m 755 $(BIN)/SDL/sameboy $(DESTDIR)$(PREFIX)/bin/sameboy + install -m 755 $(BIN)/XdgThumbnailer/sameboy-thumbnailer $(DESTDIR)$(PREFIX)/bin/sameboy-thumbnailer + install -m 644 XdgThumbnailer/sameboy.thumbnailer $(DESTDIR)$(PREFIX)/share/thumbnailers/sameboy.thumbnailer ifeq ($(DESTDIR),) - -update-mime-database -n $(PREFIX)/share/mime - -xdg-desktop-menu install --novendor --mode system FreeDesktop/sameboy.desktop - -xdg-icon-resource forceupdate --mode system - -xdg-desktop-menu forceupdate --mode system -ifneq ($(SUDO_USER),) - -su $(SUDO_USER) -c "xdg-desktop-menu forceupdate --mode system" -endif + xdg-mime install --novendor FreeDesktop/sameboy.xml + xdg-desktop-menu install --novendor FreeDesktop/sameboy.desktop + for size in 16 32 64 128 256 512; do \ + xdg-icon-resource install --novendor --theme hicolor --size $$size --context apps FreeDesktop/AppIcon/$${size}x$${size}.png sameboy; \ + xdg-icon-resource install --novendor --theme hicolor --size $$size --context mimetypes FreeDesktop/Cartridge/$${size}x$${size}.png x-gameboy-rom; \ + xdg-icon-resource install --novendor --theme hicolor --size $$size --context mimetypes FreeDesktop/ColorCartridge/$${size}x$${size}.png x-gameboy-color-rom; \ + done else - -@$(MKDIR) -p $(DESTDIR)$(PREFIX)/share/applications/ - cp FreeDesktop/sameboy.desktop $(DESTDIR)$(PREFIX)/share/applications/sameboy.desktop + install -m 644 FreeDesktop/sameboy.xml $(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml + install -m 644 FreeDesktop/sameboy.desktop $(DESTDIR)$(PREFIX)/share/applications/sameboy.desktop + for size in 16x16 32x32 64x64 128x128 256x256 512x512; do \ + install -d $(DESTDIR)$(PREFIX)/share/icons/hicolor/$$size/apps; \ + install -d $(DESTDIR)$(PREFIX)/share/icons/hicolor/$$size/mimetypes; \ + install -m 644 FreeDesktop/AppIcon/$$size.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/$$size/apps/sameboy.png; \ + install -m 644 FreeDesktop/Cartridge/$$size.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/$$size/mimetypes/x-gameboy-rom.png; \ + install -m 644 FreeDesktop/ColorCartridge/$$size.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/$$size/mimetypes/x-gameboy-color-rom.png; \ + done +endif endif -$(DESTDIR)$(PREFIX)/share/icons/hicolor/%/apps/sameboy.png: FreeDesktop/AppIcon/%.png +ios: bootroms + @$(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 $@) - cp -f $^ $@ + (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 > $@ -$(DESTDIR)$(PREFIX)/share/icons/hicolor/%/mimetypes/x-gameboy-rom.png: FreeDesktop/Cartridge/%.png +$(LIBDIR)/libsameboy.o: $(CORE_OBJECTS) -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + @# 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)) $(FAT_FLAGS) -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 -$(DESTDIR)$(PREFIX)/share/icons/hicolor/%/mimetypes/x-gameboy-color-rom.png: FreeDesktop/ColorCartridge/%.png +$(LIBDIR)/libsameboy.a: $(LIBDIR)/libsameboy.o -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ - -$(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml: FreeDesktop/sameboy.xml + -@rm -f $@ + ar -crs $@ $^ + +$(LIBDIR)/libsameboy.$(DL_EXT): $(CORE_OBJECTS) -@$(MKDIR) -p $(dir $@) - cp -f $^ $@ + $(CC) $(LDFLAGS) -shared $(FAT_FLAGS) $(CFLAGS) $^ -o $@ +ifeq ($(CONF), release) + $(STRIP) $@ + $(CODESIGN) $@ endif +$(PKGCONF_FILE): sameboy.pc.in + -@$(MKDIR) -p $(dir $@) + -@rm -f $@ + sed -e 's,@prefix@,$(PREFIX),' \ + -e 's/@version@/$(VERSION)/' $< > $@ + +lib-install: lib $(PKGCONF_FILE) + install -d $(DESTDIR)$(PREFIX)/lib/pkgconfig + install -d $(DESTDIR)$(PREFIX)/include/sameboy + install -m 644 $(LIBDIR)/libsameboy.a $(LIBDIR)/libsameboy.$(DL_EXT) $(DESTDIR)$(PREFIX)/lib/ + install -m 644 $(INC)/* $(DESTDIR)$(PREFIX)/include/sameboy/ + install -m 644 $(PKGCONF_FILE) $(DESTDIR)$(PREFIX)/lib/pkgconfig + +# Windows dll + +# To avoid Windows' sort.exe +SORT = $(dir $(shell which grep))\sort.exe + +$(OBJ)/names: $(CORE_OBJECTS) + llvm-nm -gU $(CORE_OBJECTS) -P | grep -Eo "^GB_[^ ]+" | $(SORT) -u > $@ + +$(OBJ)/exports: $(PUBLIC_HEADERS) + grep -Eho "\bGB_[a-zA-Z0-9_]+\b" $^ | $(dir $(shell which grep))\sort.exe -u > $@ + +$(OBJ)/exports.def: $(OBJ)/exports $(OBJ)/names + echo LIBRARY libsameboy > $@ + echo EXPORTS >> $@ + comm -12 $^ >> $@ + +$(LIBDIR)/libsameboy.dll: $(CORE_OBJECTS) | $(OBJ)/exports.def + -@$(MKDIR) -p $(dir $@) + $(CC) $(LDFLAGS) -Wl,/def:$(OBJ)/exports.def -shared $(CFLAGS) $^ -o $@ + +# CPPP doesn't like multibyte characters, so we replace the single quote character before processing so it doesn't complain +$(INC)/%.h: Core/%.h + -@$(MKDIR) -p $(dir $@) + sed "s/'/@SINGLE_QUOTE@/g" $^ | cppp $(CPPP_FLAGS) | sed "s/@SINGLE_QUOTE@/'/g" > $@ + +# Generate msvcrt.lib so we can use the always-present msvcrt.dll +$(OBJ)/Windows/msvcrt.lib: Windows/msvcrt.def + lib.exe /MACHINE:X64 /def:$< /out:$@ + # Clean clean: rm -rf build -.PHONY: libretro tester +.PHONY: libretro tester cocoa ios _ios ios-ipa ios-deb liblib-unsupported bootroms diff --git a/bsnes/gb/Misc/Palettes/Canyon.sbp b/bsnes/gb/Misc/Palettes/Canyon.sbp new file mode 100644 index 00000000..a0eaf37c Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Canyon.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Desert.sbp b/bsnes/gb/Misc/Palettes/Desert.sbp new file mode 100644 index 00000000..28625ad3 Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Desert.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Evening.sbp b/bsnes/gb/Misc/Palettes/Evening.sbp new file mode 100644 index 00000000..e11998ab --- /dev/null +++ b/bsnes/gb/Misc/Palettes/Evening.sbp @@ -0,0 +1 @@ +LPBS&6UiS䦻}^LH+ \ No newline at end of file diff --git a/bsnes/gb/Misc/Palettes/Fog.sbp b/bsnes/gb/Misc/Palettes/Fog.sbp new file mode 100644 index 00000000..a79fe00f Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Fog.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Green Slate.sbp b/bsnes/gb/Misc/Palettes/Green Slate.sbp new file mode 100644 index 00000000..b260c967 --- /dev/null +++ b/bsnes/gb/Misc/Palettes/Green Slate.sbp @@ -0,0 +1 @@ +LPBS14ojP$)I \ No newline at end of file diff --git a/bsnes/gb/Misc/Palettes/Green Tea.sbp b/bsnes/gb/Misc/Palettes/Green Tea.sbp new file mode 100644 index 00000000..dbc2effd --- /dev/null +++ b/bsnes/gb/Misc/Palettes/Green Tea.sbp @@ -0,0 +1 @@ +LPBS1Rt;@W^|[. \ No newline at end of file diff --git a/bsnes/gb/Misc/Palettes/Lavender.sbp b/bsnes/gb/Misc/Palettes/Lavender.sbp new file mode 100644 index 00000000..1e0bd53d --- /dev/null +++ b/bsnes/gb/Misc/Palettes/Lavender.sbp @@ -0,0 +1 @@ +LPBS:*+|PμCFrTNe \ No newline at end of file diff --git a/bsnes/gb/Misc/Palettes/Magic Eggplant.sbp b/bsnes/gb/Misc/Palettes/Magic Eggplant.sbp new file mode 100644 index 00000000..6bd59291 Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Magic Eggplant.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Mystic Blue.sbp b/bsnes/gb/Misc/Palettes/Mystic Blue.sbp new file mode 100644 index 00000000..a9f55d54 Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Mystic Blue.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Pink Pop.sbp b/bsnes/gb/Misc/Palettes/Pink Pop.sbp new file mode 100644 index 00000000..956e3d0d Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Pink Pop.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Radioactive Pea.sbp b/bsnes/gb/Misc/Palettes/Radioactive Pea.sbp new file mode 100644 index 00000000..57f9d6a3 Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Radioactive Pea.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Rose.sbp b/bsnes/gb/Misc/Palettes/Rose.sbp new file mode 100644 index 00000000..735598da Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Rose.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Seaweed.sbp b/bsnes/gb/Misc/Palettes/Seaweed.sbp new file mode 100644 index 00000000..3718efd7 Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Seaweed.sbp differ diff --git a/bsnes/gb/Misc/Palettes/Twilight.sbp b/bsnes/gb/Misc/Palettes/Twilight.sbp new file mode 100644 index 00000000..a5decc10 Binary files /dev/null and b/bsnes/gb/Misc/Palettes/Twilight.sbp differ diff --git a/bsnes/gb/Misc/registers.sym b/bsnes/gb/Misc/registers.sym index 3b31b745..1da88717 100644 --- a/bsnes/gb/Misc/registers.sym +++ b/bsnes/gb/Misc/registers.sym @@ -1,67 +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_UNKNOWN2 -00:FF73 IO_UNKNOWN3 -00:FF74 IO_UNKNOWN4 -00:FF75 IO_UNKNOWN5 -00:FF76 IO_PCM_12 -00:FF77 IO_PCM_34 -00:FF7F IO_UNKNOWN8 -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 rPGB +00:FF76 rPCM12 +00:FF77 rPCM34 +00:FFFF rIE diff --git a/bsnes/gb/OpenDialog/cocoa.m b/bsnes/gb/OpenDialog/cocoa.m index cfb2553a..fd9af3ca 100644 --- a/bsnes/gb/OpenDialog/cocoa.m +++ b/bsnes/gb/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/bsnes/gb/OpenDialog/gtk.c b/bsnes/gb/OpenDialog/gtk.c index 378dcb4e..2b08f1df 100644 --- a/bsnes/gb/OpenDialog/gtk.c +++ b/bsnes/gb/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/bsnes/gb/OpenDialog/open_dialog.h b/bsnes/gb/OpenDialog/open_dialog.h index 6d7fb5b2..b1f4ff72 100644 --- a/bsnes/gb/OpenDialog/open_dialog.h +++ b/bsnes/gb/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/bsnes/gb/OpenDialog/windows.c b/bsnes/gb/OpenDialog/windows.c index e7110320..79133338 100644 --- a/bsnes/gb/OpenDialog/windows.c +++ b/bsnes/gb/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/bsnes/gb/QuickLook/CartridgeTemplate.png b/bsnes/gb/QuickLook/CartridgeTemplate.png index 5bf1a2fb..816de91d 100644 Binary files a/bsnes/gb/QuickLook/CartridgeTemplate.png and b/bsnes/gb/QuickLook/CartridgeTemplate.png differ diff --git a/bsnes/gb/QuickLook/ColorCartridgeTemplate.png b/bsnes/gb/QuickLook/ColorCartridgeTemplate.png index 5eac5622..d21692e2 100644 Binary files a/bsnes/gb/QuickLook/ColorCartridgeTemplate.png and b/bsnes/gb/QuickLook/ColorCartridgeTemplate.png differ diff --git a/bsnes/gb/QuickLook/GBPreviewProvider.h b/bsnes/gb/QuickLook/GBPreviewProvider.h new file mode 100644 index 00000000..ae0dfd2b --- /dev/null +++ b/bsnes/gb/QuickLook/GBPreviewProvider.h @@ -0,0 +1,6 @@ +#import +#import + +API_AVAILABLE(macos(12.0)) +@interface GBPreviewProvider : QLPreviewProvider +@end diff --git a/bsnes/gb/QuickLook/GBPreviewProvider.m b/bsnes/gb/QuickLook/GBPreviewProvider.m new file mode 100644 index 00000000..dd48db66 --- /dev/null +++ b/bsnes/gb/QuickLook/GBPreviewProvider.m @@ -0,0 +1,20 @@ +#import "GBPreviewProvider.h" + +extern OSStatus GBQuickLookRender(CGContextRef cgContext, CFURLRef url, bool showBorder); + +@implementation GBPreviewProvider + +- (void)providePreviewForFileRequest:(QLFilePreviewRequest *)request completionHandler:(void (^)(QLPreviewReply *reply, NSError *error))handler +{ + QLPreviewReply* reply = [[QLPreviewReply alloc] initWithContextSize:CGSizeMake(640, 576) + isBitmap:true + drawingBlock:^BOOL (CGContextRef context, QLPreviewReply *reply, NSError **error) { + return !GBQuickLookRender(context, (__bridge CFURLRef)request.fileURL, false); + + }]; + + handler(reply, nil); +} + +@end + diff --git a/bsnes/gb/QuickLook/GBThumbnailProvider.h b/bsnes/gb/QuickLook/GBThumbnailProvider.h new file mode 100644 index 00000000..4a8268f2 --- /dev/null +++ b/bsnes/gb/QuickLook/GBThumbnailProvider.h @@ -0,0 +1,5 @@ +#import + +API_AVAILABLE(macos(10.15)) +@interface GBThumbnailProvider : QLThumbnailProvider +@end diff --git a/bsnes/gb/QuickLook/GBThumbnailProvider.m b/bsnes/gb/QuickLook/GBThumbnailProvider.m new file mode 100644 index 00000000..34c16db6 --- /dev/null +++ b/bsnes/gb/QuickLook/GBThumbnailProvider.m @@ -0,0 +1,27 @@ +#import "GBThumbnailProvider.h" + +extern OSStatus GBQuickLookRender(CGContextRef cgContext, CFURLRef url, bool showBorder); + +@interface QLThumbnailReply (private) +@property unsigned iconFlavor; +@end + +@implementation GBThumbnailProvider + +- (void)provideThumbnailForFileRequest:(QLFileThumbnailRequest *)request completionHandler:(void (^)(QLThumbnailReply *, NSError *))handler +{ + CGSize size = {64, 64}; + CGSize maximumSize = request.maximumSize; + while (size.width <= maximumSize.width / 2 && + size.width <= maximumSize.height / 2) { + size.width *= 2; + } + size.height = size.width; + QLThumbnailReply *reply = [QLThumbnailReply replyWithContextSize:size drawingBlock: ^BOOL(CGContextRef context) { + return !GBQuickLookRender(context, (__bridge CFURLRef)request.fileURL, true); + }]; + reply.iconFlavor = 0; + handler(reply, nil); +} + +@end diff --git a/bsnes/gb/QuickLook/Info.plist b/bsnes/gb/QuickLook/Info.plist index 9b369ec4..924f9f5b 100644 --- a/bsnes/gb/QuickLook/Info.plist +++ b/bsnes/gb/QuickLook/Info.plist @@ -14,6 +14,7 @@ com.github.liji32.sameboy.gb com.github.liji32.sameboy.gbc com.github.liji32.sameboy.isx + public.gbrom @@ -26,7 +27,7 @@ CFBundleName SameBoy CFBundleShortVersionString - Version @VERSION + @VERSION CFBundleSignature ???? CFPlugInDynamicRegisterFunction @@ -48,7 +49,7 @@ CFPlugInUnloadFunction NSHumanReadableCopyright - Copyright © 2015-2021 Lior Halphon + Copyright © 2015-@COPYRIGHT_YEAR Lior Halphon QLNeedsToBeRunInMainThread QLPreviewHeight diff --git a/bsnes/gb/QuickLook/Previewer.plist b/bsnes/gb/QuickLook/Previewer.plist new file mode 100644 index 00000000..c35a4834 --- /dev/null +++ b/bsnes/gb/QuickLook/Previewer.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDisplayName + Previewer + CFBundleExecutable + Previewer + CFBundleIdentifier + com.github.liji32.sameboy.ios.Previewer + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Previewer + CFBundlePackageType + XPC! + CFBundleShortVersionString + @VERSION + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + 12.0 + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + com.github.liji32.sameboy.gb + com.github.liji32.sameboy.gbc + com.github.liji32.sameboy.isx + public.gbrom + + QLIsDataBasedPreview + + + NSExtensionPointIdentifier + com.apple.quicklook.preview + NSExtensionPrincipalClass + GBPreviewProvider + + + diff --git a/bsnes/gb/QuickLook/Thumbnailer.plist b/bsnes/gb/QuickLook/Thumbnailer.plist new file mode 100644 index 00000000..6e180297 --- /dev/null +++ b/bsnes/gb/QuickLook/Thumbnailer.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDisplayName + Thumbnailer + CFBundleExecutable + Thumbnailer + CFBundleIdentifier + com.github.liji32.sameboy.ios.Thumbnailer + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Thumbnailer + CFBundlePackageType + XPC! + CFBundleShortVersionString + @VERSION + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + 12.0 + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + com.github.liji32.sameboy.gb + com.github.liji32.sameboy.gbc + com.github.liji32.sameboy.isx + public.gbrom + + QLThumbnailMinimumDimension + 64 + + NSExtensionPointIdentifier + com.apple.quicklook.thumbnail + NSExtensionPrincipalClass + GBThumbnailProvider + + + diff --git a/bsnes/gb/QuickLook/UniversalCartridgeTemplate.png b/bsnes/gb/QuickLook/UniversalCartridgeTemplate.png index 1bf4903a..78e7414b 100644 Binary files a/bsnes/gb/QuickLook/UniversalCartridgeTemplate.png and b/bsnes/gb/QuickLook/UniversalCartridgeTemplate.png differ diff --git a/bsnes/gb/QuickLook/generator.m b/bsnes/gb/QuickLook/generator.m index f2651d28..442423cf 100644 --- a/bsnes/gb/QuickLook/generator.m +++ b/bsnes/gb/QuickLook/generator.m @@ -2,7 +2,7 @@ #include #include "get_image_for_rom.h" -static OSStatus render(CGContextRef cgContext, CFURLRef url, bool showBorder) +OSStatus GBQuickLookRender(CGContextRef cgContext, CFURLRef url, bool showBorder) { /* Load the template NSImages when generating the first thumbnail */ static NSImage *template = nil; @@ -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 bundleForClass:NSClassFromString(@"GBPanel")]; + }); 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, @@ -50,11 +53,13 @@ static OSStatus render(CGContextRef cgContext, CFURLRef url, bool showBorder) [NSGraphicsContext setCurrentContext:context]; + double ratio = CGBitmapContextGetWidth(cgContext) / 1024.0; + /* Convert the screenshot to a magnified NSImage */ NSImage *screenshot = [[NSImage alloc] initWithCGImage:iref size:NSMakeSize(160, 144)]; /* Draw the screenshot */ if (showBorder) { - [screenshot drawInRect:NSMakeRect(192, 150, 640, 576)]; + [screenshot drawInRect:NSMakeRect(192 * ratio, 150 * ratio, 640 * ratio, 576 * ratio)]; } else { [screenshot drawInRect:NSMakeRect(0, 0, 640, 576)]; @@ -78,8 +83,9 @@ static OSStatus render(CGContextRef cgContext, CFURLRef url, bool showBorder) effectiveTemplate = template; } + CGContextSetInterpolationQuality(cgContext, kCGInterpolationMedium); /* Mask it with the template (The middle part of the template image is transparent) */ - [effectiveTemplate drawInRect:(NSRect){{0, 0}, template.size}]; + [effectiveTemplate drawInRect:(NSRect){{0, 0}, {CGBitmapContextGetWidth(cgContext), CGBitmapContextGetHeight(cgContext)}}]; } CGColorSpaceRelease(colorSpaceRef); @@ -93,7 +99,7 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, { @autoreleasepool { CGContextRef cgContext = QLPreviewRequestCreateContext(preview, ((NSSize){640, 576}), true, nil); - if (render(cgContext, url, false) == noErr) { + if (GBQuickLookRender(cgContext, url, false) == noErr) { QLPreviewRequestFlushContext(preview, cgContext); CGContextRelease(cgContext); return noErr; @@ -108,7 +114,7 @@ OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thum extern NSString *kQLThumbnailPropertyIconFlavorKey; @autoreleasepool { CGContextRef cgContext = QLThumbnailRequestCreateContext(thumbnail, ((NSSize){1024, 1024}), true, (__bridge CFDictionaryRef)(@{kQLThumbnailPropertyIconFlavorKey : @(0)})); - if (render(cgContext, url, true) == noErr) { + if (GBQuickLookRender(cgContext, url, true) == noErr) { QLThumbnailRequestFlushContext(thumbnail, cgContext); CGContextRelease(cgContext); return noErr; diff --git a/bsnes/gb/QuickLook/get_image_for_rom.c b/bsnes/gb/QuickLook/get_image_for_rom.c index b9f87edb..1c9366a3 100755 --- a/bsnes/gb/QuickLook/get_image_for_rom.c +++ b/bsnes/gb/QuickLook/get_image_for_rom.c @@ -19,13 +19,13 @@ static char *async_input_callback(GB_gameboy_t *gb) return NULL; } -static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes_t 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/bsnes/gb/QuickLook/get_image_for_rom.h b/bsnes/gb/QuickLook/get_image_for_rom.h index 598486a5..183bba48 100644 --- a/bsnes/gb/QuickLook/get_image_for_rom.h +++ b/bsnes/gb/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/bsnes/gb/QuickLook/plugin.entitlements b/bsnes/gb/QuickLook/plugin.entitlements new file mode 100644 index 00000000..f2ef3ae0 --- /dev/null +++ b/bsnes/gb/QuickLook/plugin.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/bsnes/gb/README.md b/bsnes/gb/README.md index 00283a06..ea1a8dcf 100644 --- a/bsnes/gb/README.md +++ b/bsnes/gb/README.md @@ -38,22 +38,33 @@ 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/LIJI32/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) + * [Git Bash](https://git-scm.com/downloads/win) or another distribution of basic Unix utilities + * Git Bash does not include make, you can get it [here](https://sourceforge.net/projects/ezwinports/files/make-4.4.1-without-guile-w32-bin.zip/download). + * Running `vcvars64.bat` or `vcvarsx86_amd64.bat` before running make. Make sure all required tools, libraries, and headers are in %PATH%, %lib%, and %include%`, 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/bsnes/gb/SDL/audio.c b/bsnes/gb/SDL/audio.c new file mode 100644 index 00000000..c1f2fc7e --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/SDL/audio/audio.h b/bsnes/gb/SDL/audio/audio.h index acaa011d..6d42c12a 100644 --- a/bsnes/gb/SDL/audio/audio.h +++ b/bsnes/gb/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/bsnes/gb/SDL/audio/openal.c b/bsnes/gb/SDL/audio/openal.c new file mode 100644 index 00000000..fdcaeade --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/SDL/audio/sdl.c b/bsnes/gb/SDL/audio/sdl.c index 12ee69ae..9c0cd98b 100644 --- a/bsnes/gb/SDL/audio/sdl.c +++ b/bsnes/gb/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/bsnes/gb/SDL/audio/xaudio2.c b/bsnes/gb/SDL/audio/xaudio2.c new file mode 100644 index 00000000..e7fc4f98 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/SDL/background.bmp b/bsnes/gb/SDL/background.bmp index 0f6192d6..d356d24e 100644 Binary files a/bsnes/gb/SDL/background.bmp and b/bsnes/gb/SDL/background.bmp differ diff --git a/bsnes/gb/SDL/configuration.c b/bsnes/gb/SDL/configuration.c new file mode 100644 index 00000000..b59f4701 --- /dev/null +++ b/bsnes/gb/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_AUTO, + .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/bsnes/gb/SDL/configuration.h b/bsnes/gb/SDL/configuration.h new file mode 100644 index 00000000..487acc2b --- /dev/null +++ b/bsnes/gb/SDL/configuration.h @@ -0,0 +1,171 @@ +#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, +}; + +enum { + GB_CONF_KEYS_RIGHT = GB_KEY_RIGHT, + GB_CONF_KEYS_LEFT = GB_KEY_LEFT, + GB_CONF_KEYS_UP = GB_KEY_UP, + GB_CONF_KEYS_DOWN = GB_KEY_DOWN, + GB_CONF_KEYS_A = GB_KEY_A, + GB_CONF_KEYS_B = GB_KEY_B, + GB_CONF_KEYS_SELECT = GB_KEY_SELECT, + GB_CONF_KEYS_START = GB_KEY_START, + GB_CONF_KEYS_TURBO, + GB_CONF_KEYS_COUNT, +}; + +enum { + GB_CONF_KEYS2_REWIND, + GB_CONF_KEYS2_UNDERCLOCK, + GB_CONF_KEYS2_RAPID_A, + GB_CONF_KEYS2_RAPID_B, + GB_CONF_KEYS2_COUNT = 32, +}; + +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_BUTTON_RAPID_A, + JOYPAD_BUTTON_RAPID_B, + 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[GB_CONF_KEYS_COUNT]; + 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_AUTO, + MODEL_MAX, + } model; + + /* v0.11 */ + uint32_t rewind_length; + SDL_Scancode keys_2[GB_CONF_KEYS2_COUNT]; /* 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_palette_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; + + /* v1.0 */ + bool windows_associations_prompted; // Windows only + + /* v1.0.1 */ + bool disable_rounded_corners; // Windows only + bool use_faux_analog_inputs; + + /* v1.0.2 */ + int8_t vsync_mode; + uint8_t turbo_cap; + }; +} configuration_t; + +extern configuration_t configuration; diff --git a/bsnes/gb/SDL/console.c b/bsnes/gb/SDL/console.c index ad9c2b54..ff361a21 100644 --- a/bsnes/gb/SDL/console.c +++ b/bsnes/gb/SDL/console.c @@ -17,6 +17,7 @@ #define SGR(x) CSI(x "m") static bool initialized = false; +static bool no_csi = false; typedef struct listent_s listent_t; struct listent_s { @@ -85,35 +86,38 @@ static bool is_term(void) { if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) return false; #ifdef _WIN32 - if (AllocConsole()) { - FreeConsole(); - return false; + unsigned long input_mode, output_mode; + bool has_con_output; + + HANDLE stdin_handle = GetStdHandle(STD_INPUT_HANDLE); + HANDLE stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE); + + GetConsoleMode(stdin_handle, &input_mode); + has_con_output = GetConsoleMode(stdout_handle, &output_mode); + if (!has_con_output) { + return false; // stdout has been redirected to a file or pipe } - unsigned long input_mode, output_mode; - - GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &input_mode); - GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &output_mode); - SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_VIRTUAL_TERMINAL_INPUT); - SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + SetConsoleMode(stdin_handle, ENABLE_VIRTUAL_TERMINAL_INPUT); + SetConsoleMode(stdout_handle, ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); CONSOLE_SCREEN_BUFFER_INFO before = {0,}; - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &before); + GetConsoleScreenBufferInfo(stdout_handle, &before); printf(SGR("0")); CONSOLE_SCREEN_BUFFER_INFO after = {0,}; - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &after); + GetConsoleScreenBufferInfo(stdout_handle, &after); - SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), input_mode); - SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), output_mode); - + SetConsoleMode(stdin_handle, input_mode); + SetConsoleMode(stdout_handle, output_mode); if (before.dwCursorPosition.X != after.dwCursorPosition.X || before.dwCursorPosition.Y != after.dwCursorPosition.Y) { printf("\r \r"); - return false; + no_csi = true; } + return true; #else return getenv("TERM"); @@ -127,7 +131,7 @@ static char raw_getc(void) #ifdef _WIN32 char c; unsigned long ret; - ReadConsole(GetStdHandle(STD_INPUT_HANDLE), &c, 1, &ret, NULL); + ReadFile(GetStdHandle(STD_INPUT_HANDLE), &c, 1, &ret, NULL); #else ssize_t ret; char c; @@ -155,10 +159,12 @@ static unsigned long input_mode, output_mode; static void cleanup(void) { - printf(CSI("!p")); // reset + if (!no_csi) { + printf(CSI("!p")); // reset + } + fflush(stdout); SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), input_mode); SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), output_mode); - fflush(stdout); } static bool initialize(void) @@ -176,10 +182,14 @@ static bool initialize(void) GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &output_mode); once = true; } + if (no_csi) { + initialized = true; + return true; + } SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_VIRTUAL_TERMINAL_INPUT); SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); - printf(CSI("%dB") "\n" CSI("A") ESC("7") CSI("B"), height); + fprintf(stdout, CSI("%dB") "\n" CSI("A") ESC("7") CSI("B"), height); fflush(stdout); initialized = true; @@ -207,7 +217,9 @@ static struct termios terminal; static void cleanup(void) { - printf(CSI("!p")); // reset + if (!no_csi) { + printf(CSI("!p")); // reset + } tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminal); fflush(stdout); } @@ -261,6 +273,8 @@ static bool repeat_empty = false; static bool redraw_prompt(bool force) { + if (no_csi) return true; + if (line.reverse_search) { if (!force) return false; if (line.length == 0) { @@ -522,6 +536,7 @@ restart: static pthread_mutex_t terminal_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t lines_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t lines_cond = PTHREAD_COND_INITIALIZER; +static void (*line_ready_callback)(void) = NULL; static char reverse_search_mainloop(void) { @@ -546,14 +561,6 @@ static char reverse_search_mainloop(void) delete_word(false); redraw_prompt(true); break; -#ifndef _WIN32 - case CTL('Z'): - set_line(""); - raise(SIGSTOP); - initialize(); // Reinitialize - redraw_prompt(true); - break; -#endif case CTL('H'): case 0x7F: // Backspace delete(1, false); @@ -580,6 +587,39 @@ static char reverse_search_mainloop(void) } } +static void no_csi_mainloop(void) +{ + while (true) { + char *expression = NULL; + size_t size = 0; + + errno = 0; + if (getline(&expression, &size, stdin) <= 0) { + if (!errno) { + continue; + } + return; + } + + pthread_mutex_lock(&lines_lock); + if (!expression) { + add_entry(&lines, ""); + } + else { + size_t length = strlen(expression); + if (expression[length - 1] == '\n') { + expression[length - 1] = 0; + } + add_entry(&lines, expression); + free(expression); + } + pthread_cond_signal(&lines_cond); + pthread_mutex_unlock(&lines_lock); + if (line_ready_callback) { + line_ready_callback(); + } + } +} static @@ -590,6 +630,11 @@ void * #endif mainloop(char *(*completer)(const char *substring, uintptr_t *context)) { + if (no_csi) { + no_csi_mainloop(); + return 0; + } + listent_t *history_line = NULL; uintptr_t complete_context = 0; size_t completion_length = 0; @@ -611,6 +656,10 @@ mainloop(char *(*completer)(const char *substring, uintptr_t *context)) else { c = raw_getc(); } + if (c == EOF) { + return 0; + } + pthread_mutex_lock(&terminal_lock); switch (c) { @@ -646,6 +695,9 @@ mainloop(char *(*completer)(const char *substring, uintptr_t *context)) add_entry(&lines, CON_EOF); pthread_cond_signal(&lines_cond); pthread_mutex_unlock(&lines_lock); + if (line_ready_callback) { + line_ready_callback(); + } } break; case CTL('E'): @@ -692,14 +744,6 @@ mainloop(char *(*completer)(const char *substring, uintptr_t *context)) delete_word(false); complete_context = completion_length = 0; break; -#ifndef _WIN32 - case CTL('Z'): - set_line(""); - complete_context = completion_length = 0; - raise(SIGSTOP); - initialize(); // Reinitialize - break; -#endif case '\r': case '\n': pthread_mutex_lock(&lines_lock); @@ -711,6 +755,9 @@ mainloop(char *(*completer)(const char *substring, uintptr_t *context)) } pthread_cond_signal(&lines_cond); pthread_mutex_unlock(&lines_lock); + if (line_ready_callback) { + line_ready_callback(); + } if (line.length) { listent_t *dup = reverse_find(history.last, line.content, true); if (dup) { @@ -794,7 +841,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; @@ -808,22 +855,24 @@ mainloop(char *(*completer)(const char *substring, uintptr_t *context)) } break; case '\t': { - char temp = line.content[line.position - completion_length]; - line.content[line.position - completion_length] = 0; - char *completion = completer? completer(line.content, &complete_context) : NULL; - line.content[line.position - completion_length] = temp; - if (completion) { - if (completion_length) { - delete(completion_length, false); + if (!no_csi) { + char temp = line.content[line.position - completion_length]; + line.content[line.position - completion_length] = 0; + char *completion = completer? completer(line.content, &complete_context) : NULL; + line.content[line.position - completion_length] = temp; + if (completion) { + if (completion_length) { + delete(completion_length, false); + } + insert(completion); + completion_length = strlen(completion); + free(completion); } - insert(completion); - completion_length = strlen(completion); - free(completion); + else { + printf("\a"); + } + break; } - else { - printf("\a"); - } - break; } default: if (c >= ' ') { @@ -842,6 +891,12 @@ mainloop(char *(*completer)(const char *substring, uintptr_t *context)) return 0; } + +void CON_set_line_ready_callback(void (*callback)(void)) +{ + line_ready_callback = callback; +} + char *CON_readline(const char *new_prompt) { pthread_mutex_lock(&terminal_lock); @@ -893,8 +948,8 @@ bool CON_start(char *(*completer)(const char *substring, uintptr_t *context)) void CON_attributed_print(const char *string, CON_attributes_t *attributes) { - if (!initialized) { - printf("%s", string); + if (!initialized || no_csi) { + fprintf(stdout, "%s", string); return; } static bool pending_newline = false; @@ -1018,3 +1073,8 @@ void CON_set_repeat_empty(bool repeat) { repeat_empty = repeat; } + +bool CON_no_csi_mode(void) +{ + return no_csi; +} diff --git a/bsnes/gb/SDL/console.h b/bsnes/gb/SDL/console.h index d1589888..e925b8ac 100644 --- a/bsnes/gb/SDL/console.h +++ b/bsnes/gb/SDL/console.h @@ -47,3 +47,5 @@ void CON_printf(const char *fmt, ...) __printflike(1, 2); void CON_attributed_printf(const char *fmt, CON_attributes_t *attributes,...) __printflike(1, 3); void CON_set_async_prompt(const char *string); void CON_set_repeat_empty(bool repeat); +void CON_set_line_ready_callback(void (*callback)(void)); +bool CON_no_csi_mode(void); diff --git a/bsnes/gb/SDL/font.c b/bsnes/gb/SDL/font.c index ea2c590c..b94dc2da 100644 --- a/bsnes/gb/SDL/font.c +++ b/bsnes/gb/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/bsnes/gb/SDL/font.h b/bsnes/gb/SDL/font.h index f2111c3f..aec1f4c9 100644 --- a/bsnes/gb/SDL/font.h +++ b/bsnes/gb/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/bsnes/gb/SDL/gui.c b/bsnes/gb/SDL/gui.c index d9a58e54..030e11f3 100644 --- a/bsnes/gb/SDL/gui.c +++ b/bsnes/gb/SDL/gui.c @@ -5,9 +5,18 @@ #include #include #include +#include +#include #include "utils.h" #include "gui.h" #include "font.h" +#include "audio/audio.h" + +#ifdef _WIN32 +#include +#include "windows_associations.h" +#include +#endif 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 +29,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 +43,10 @@ shader_t shader; static SDL_Rect rect; static unsigned factor; +static SDL_Surface *converted_background = NULL; + +bool screen_manually_resized = false; + void render_texture(void *pixels, void *previous) { if (renderer) { @@ -67,57 +84,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 +98,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 +159,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 +237,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 +290,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 +314,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 +343,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 +369,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,31 +408,631 @@ static void return_to_root_menu(unsigned index) recalculate_menu_height(); } +#ifdef _WIN32 +static void associate_rom_files(unsigned index); +#endif + +static +#ifndef _WIN32 +const +#endif +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}, +#ifdef _WIN32 + {"Associate ROM Files", associate_rom_files}, +#endif + {"Back", return_to_root_menu}, + {NULL,} +}; + +#ifdef _WIN32 +static void associate_rom_files(unsigned index) +{ + if (GB_do_windows_association()) { + options_menu[index].string = "ROM Files Associated"; + } + else { + options_menu[index].string = "Files Association Failed"; + } +} +#endif + +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 Game Genie Code"); + } + +} + + +static void import_cheat(unsigned index) +{ + strcpy(text_input_title, "Enter a GameShark"); + strcpy(text_input_title2, "or Game Genie 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) { - - configuration.model++; - if (configuration.model == MODEL_MAX) { - configuration.model = 0; + switch (configuration.model) { + case MODEL_DMG: configuration.model = MODEL_MGB; break; + case MODEL_MGB: configuration.model = MODEL_SGB; break; + case MODEL_SGB: configuration.model = MODEL_CGB; break; + case MODEL_CGB: configuration.model = MODEL_AGB; break; + case MODEL_AGB: configuration.model = MODEL_AUTO; break; + case MODEL_AUTO: configuration.model = MODEL_DMG; break; + default: configuration.model = MODEL_AUTO; } pending_command = GB_SDL_RESET_COMMAND; } static void cycle_model_backwards(unsigned index) { - if (configuration.model == 0) { - configuration.model = MODEL_MAX; + switch (configuration.model) { + case MODEL_MGB: configuration.model = MODEL_DMG; break; + case MODEL_SGB: configuration.model = MODEL_MGB; break; + case MODEL_CGB: configuration.model = MODEL_SGB; break; + case MODEL_AGB: configuration.model = MODEL_CGB; break; + case MODEL_AUTO: configuration.model = MODEL_AGB; break; + case MODEL_DMG: configuration.model = MODEL_AUTO; break; + default: configuration.model = MODEL_AUTO; } - configuration.model--; 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"} + return GB_inline_const(const char *[], {"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy", "Game Boy Pocket", "Pick Automatically"}) [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", + "CPU CGB A", + "CPU CGB B", + "CPU CGB C", + "CPU CGB D", + "CPU CGB E", + }) + [configuration.cgb_revision]; +} + static void cycle_sgb_revision(unsigned index) { @@ -411,10 +1052,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 +1094,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 +1104,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 +1149,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 +1158,70 @@ 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 void cycle_turbo_cap(unsigned index) +{ + + if (configuration.turbo_cap >= 16) { // 400% + configuration.turbo_cap = 0; // uncapped + } + else if (configuration.turbo_cap == 0) { // uncapped + configuration.turbo_cap = 6; // 150% + } + else { + configuration.turbo_cap++; + } +} + +static void cycle_turbo_cap_backwards(unsigned index) +{ + + if (configuration.turbo_cap == 0) { // uncapped + configuration.turbo_cap = 16; // 400% + } + else if (configuration.turbo_cap == 6) { // 150% + configuration.turbo_cap = 0; // uncapped + } + else { + configuration.turbo_cap--; + } +} + +static const char *current_turbo_cap_string(unsigned index) +{ + if (configuration.turbo_cap == 0) { + return "Uncapped"; + } + static char ret[5]; + snprintf(ret, sizeof(ret), "%d%%", configuration.turbo_cap * 25); + return ret; +} + + 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}, + {"Turbo speed cap:", cycle_turbo_cap, current_turbo_cap_string, cycle_turbo_cap_backwards}, + {"Back", enter_options_menu}, {NULL,} }; @@ -535,21 +1233,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 +1262,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 +1285,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; @@ -594,9 +1295,10 @@ void cycle_scaling_backwards(unsigned index) } update_viewport(); render_texture(NULL, NULL); + screen_manually_resized = false; } -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; @@ -607,9 +1309,10 @@ void cycle_default_scale(unsigned index) rescale_window(); update_viewport(); + screen_manually_resized = false; } -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; @@ -620,6 +1323,7 @@ void cycle_default_scale_backwards(unsigned index) rescale_window(); update_viewport(); + screen_manually_resized = false; } static void cycle_color_correction(unsigned index) @@ -627,6 +1331,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 +1347,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 +1372,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_palette_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_palette_enabled = true; + update_gui_palette(); } static void cycle_border_mode(unsigned index) @@ -708,6 +1525,7 @@ struct shader_name { {"MonoLCD", "Monochrome LCD"}, {"LCD", "LCD Display"}, {"CRT", "CRT Display"}, + {"FlatCRT", "Flat CRT Display"}, {"Scale2x", "Scale2x"}, {"Scale4x", "Scale4x"}, {"AAScale2x", "Anti-aliased Scale2x"}, @@ -805,8 +1623,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) @@ -820,6 +1638,84 @@ static const char *current_osd_mode(unsigned index) return configuration.osd? "Enabled" : "Disabled"; } +static const char *current_vsync_mode(unsigned index) +{ + switch (configuration.vsync_mode) { + default: + case 0: return "Disabled"; + case 1: return "Enabled"; + case -1: return "Adaptive"; + } +} + +static void cycle_vsync(unsigned index) +{ +retry: + configuration.vsync_mode++; + if (configuration.vsync_mode == 2) { + configuration.vsync_mode = -1; + } + if (SDL_GL_SetSwapInterval(configuration.vsync_mode) && configuration.vsync_mode != 0) { + goto retry; + } +} + +static void cycle_vsync_backwards(unsigned index) +{ +retry: + configuration.vsync_mode--; + if (configuration.vsync_mode == -2) { + configuration.vsync_mode = 1; + } + if (SDL_GL_SetSwapInterval(configuration.vsync_mode) && configuration.vsync_mode != 0) { + goto retry; + } +} + +#ifdef _WIN32 + +// Don't use the standard header definitions because we might not have the newest headers +typedef enum { + DWM_CORNER_DEFAULT = 0, + DWM_CORNER_SQUARE = 1, + DWM_CORNER_ROUND = 2, + DWM_CORNER_ROUNDSMALL = 3 +} DMW_corner_settings_t; + +#define DWM_CORNER_PREFERENCE 33 + +void configure_window_corners(void) +{ + SDL_SysWMinfo wmInfo; + SDL_VERSION(&wmInfo.version); + SDL_GetWindowWMInfo(window, &wmInfo); + HWND hwnd = wmInfo.info.win.window; + DMW_corner_settings_t pref = configuration.disable_rounded_corners? DWM_CORNER_SQUARE : DWM_CORNER_DEFAULT; + DwmSetWindowAttribute(hwnd, DWM_CORNER_PREFERENCE, &pref, sizeof(pref)); +} + +static void toggle_corners(unsigned index) +{ + configuration.disable_rounded_corners = !configuration.disable_rounded_corners; + configure_window_corners(); +} + +static const char *current_corner_mode(unsigned index) +{ + SDL_SysWMinfo wmInfo; + SDL_VERSION(&wmInfo.version); + SDL_GetWindowWMInfo(window, &wmInfo); + HWND hwnd = wmInfo.info.win.window; + DMW_corner_settings_t pref; + + if (DwmGetWindowAttribute(hwnd, DWM_CORNER_PREFERENCE, &pref, sizeof(pref)) || + pref == DWM_CORNER_SQUARE) { + return "Square"; + } + return "Rounded"; +} +#endif + static const struct menu_item graphics_menu[] = { {"Scaling Mode:", cycle_scaling, current_scaling_mode, cycle_scaling_backwards}, {"Default Window Scale:", cycle_default_scale, current_default_scale, cycle_default_scale_backwards}, @@ -830,8 +1726,11 @@ 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}, + {"Vsync Mode:", cycle_vsync, current_vsync_mode, cycle_vsync_backwards}, +#ifdef _WIN32 + {"Window Corners:", toggle_corners, current_corner_mode, toggle_corners}, +#endif + {"Back", enter_options_menu}, {NULL,} }; @@ -843,13 +1742,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 +1756,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 +1766,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 +1781,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 +1789,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 +1804,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 +1812,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 +1910,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,32 +1922,33 @@ 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}, + {"Rapid A:", modify_key, key_name,}, + {"Rapid B:", modify_key, key_name,}, + {"Back", enter_controls_menu}, {NULL,} }; static const char *key_name(unsigned index) { - if (index > 8) { - return SDL_GetScancodeName(configuration.keys_2[index - 9]); - } - return SDL_GetScancodeName(configuration.keys[index]); + SDL_Scancode code = index >= GB_CONF_KEYS_COUNT? configuration.keys_2[index - GB_CONF_KEYS_COUNT] : configuration.keys[index]; + if (!code) return "Not Set"; + return SDL_GetScancodeName(code); } -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 +1998,8 @@ static void cycle_joypads(unsigned index) } if (joystick) { haptic = SDL_HapticOpenFromJoystick(joystick); - }} + } +} static void cycle_joypads_backwards(unsigned index) { @@ -1082,17 +2058,95 @@ 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_use_faux_analog_inputs(unsigned index) +{ + configuration.use_faux_analog_inputs ^= true; +} + +static const char *current_faux_analog_inputs(unsigned index) +{ + return configuration.use_faux_analog_inputs? "Faux Analog" : "Digital"; +} + +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}, + {"Analog Stick Behavior:", toggle_use_faux_analog_inputs, current_faux_analog_inputs, toggle_use_faux_analog_inputs}, + {"Enable Control:", toggle_allow_background_controllers, current_background_control_mode, toggle_allow_background_controllers}, + {"Back", enter_controls_menu}, {NULL,} }; @@ -1151,28 +2205,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_palette_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 +2371,50 @@ 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: + case SDL_SCANCODE_H: + case SDL_SCANCODE_J: + case SDL_SCANCODE_K: + case SDL_SCANCODE_L: + 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 +2425,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 +2487,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 +2552,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); @@ -1340,6 +2569,14 @@ void run_gui(bool is_running) if (event.window.event == SDL_WINDOWEVENT_RESIZED) { update_viewport(); render_texture(NULL, NULL); + screen_manually_resized = true; + } + if (event.window.type == SDL_WINDOWEVENT_MOVED +#if SDL_COMPILEDVERSION > 2018 + || event.window.type == SDL_WINDOWEVENT_DISPLAY_CHANGED +#endif + ) { + update_swap_interval(); } break; } @@ -1360,14 +2597,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,11 +2655,59 @@ 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; + configuration.keys_2[current_selection - GB_CONF_KEYS_COUNT] = event.key.keysym.scancode; } else { configuration.keys[current_selection] = event.key.keysym.scancode; @@ -1428,7 +2722,9 @@ void run_gui(bool is_running) else { SDL_SetWindowFullscreen(window, 0); } + update_swap_interval(); update_viewport(); + screen_manually_resized = true; } else if (event_hotkey_code(&event) == SDL_SCANCODE_O) { if (event.key.keysym.mod & MODIFIER) { @@ -1457,10 +2753,17 @@ 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); + goto handle_pending; + break; + } + } should_render = true; } else if (is_running) { + SDL_StopTextInput(); return; } else { @@ -1479,12 +2782,16 @@ void run_gui(bool is_running) } } else if (gui_state == SHOWING_MENU) { - if (event.key.keysym.scancode == SDL_SCANCODE_DOWN && current_menu[current_selection + 1].string) { + if ((event.key.keysym.scancode == SDL_SCANCODE_DOWN || + event.key.keysym.scancode == SDL_SCANCODE_J) && + current_menu[current_selection + 1].string) { current_selection++; mouse_scroling = false; should_render = true; } - else if (event.key.keysym.scancode == SDL_SCANCODE_UP && current_selection) { + else if ((event.key.keysym.scancode == SDL_SCANCODE_UP || + event.key.keysym.scancode == SDL_SCANCODE_K) && + current_selection) { current_selection--; mouse_scroling = false; should_render = true; @@ -1492,6 +2799,7 @@ void run_gui(bool is_running) else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && !current_menu[current_selection].backwards_handler) { if (current_menu[current_selection].handler) { current_menu[current_selection].handler(current_selection); + handle_pending: if (pending_command == GB_SDL_RESET_COMMAND && !is_running) { pending_command = GB_SDL_NO_COMMAND; } @@ -1507,20 +2815,21 @@ void run_gui(bool is_running) return; } } - else if (event.key.keysym.scancode == SDL_SCANCODE_RIGHT && current_menu[current_selection].backwards_handler) { + else if ((event.key.keysym.scancode == SDL_SCANCODE_RIGHT || + event.key.keysym.scancode == SDL_SCANCODE_L) && + current_menu[current_selection].backwards_handler) { current_menu[current_selection].handler(current_selection); should_render = true; } - else if (event.key.keysym.scancode == SDL_SCANCODE_LEFT && current_menu[current_selection].backwards_handler) { + else if ((event.key.keysym.scancode == SDL_SCANCODE_LEFT || + event.key.keysym.scancode == SDL_SCANCODE_H) && + current_menu[current_selection].backwards_handler) { current_menu[current_selection].backwards_handler(current_selection); should_render = true; } } 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 +2838,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 +2882,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 +2924,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 +2937,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 +2957,19 @@ void run_gui(bool is_running) "Turbo", "Rewind", "Slow-Motion", + "Hotkey 1", + "Hotkey 2", + "Rapid A", + "Rapid B", "", - } [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 +2979,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/bsnes/gb/SDL/gui.h b/bsnes/gb/SDL/gui.h index 1764c8b9..a3091cd1 100644 --- a/bsnes/gb/SDL/gui.h +++ b/bsnes/gb/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,11 @@ enum pending_command { GB_SDL_NEW_FILE_COMMAND, GB_SDL_QUIT_COMMAND, GB_SDL_LOAD_STATE_FROM_FILE_COMMAND, + GB_SDL_CART_SWAP_COMMAND, + GB_SDL_DEBUGGER_INTERRUPT_COMMAND, +#ifdef _WIN32 + GB_SDL_HIDE_DEBUGGER_COMMAND, +#endif }; #define GB_SDL_DEFAULT_SCALE_MAX 8 @@ -47,84 +45,7 @@ enum pending_command { 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_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; +extern bool screen_manually_resized; void update_viewport(void); void run_gui(bool is_running); @@ -148,5 +69,10 @@ void show_osd_text(const char *text); extern const char *osd_text; extern unsigned osd_countdown; extern unsigned osd_text_lines; +void convert_mouse_coordinates(signed *x, signed *y); +const GB_palette_t *current_dmg_palette(void); +void update_swap_interval(void); +#ifdef _WIN32 +void configure_window_corners(void); #endif diff --git a/bsnes/gb/SDL/main.c b/bsnes/gb/SDL/main.c index ccfa906d..5d05b218 100644 --- a/bsnes/gb/SDL/main.c +++ b/bsnes/gb/SDL/main.c @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -11,11 +13,11 @@ #include "shader.h" #include "audio/audio.h" #include "console.h" +#include -#ifndef _WIN32 -#include -#else +#ifdef _WIN32 #include +#include "windows_associations.h" #endif static bool stop_on_start = false; @@ -24,9 +26,11 @@ static bool paused = false; static uint32_t pixel_buffer_1[256 * 224], pixel_buffer_2[256 * 224]; static uint32_t *active_pixel_buffer = pixel_buffer_1, *previous_pixel_buffer = pixel_buffer_2; static bool underclock_down = false, rewind_down = false, do_rewind = false, rewind_paused = false, turbo_down = false; +static bool rapid_a = false, rapid_b = false; +static uint8_t rapid_a_count = 0, rapid_b_count = 0; 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; @@ -37,6 +41,15 @@ bool uses_gl(void) return gl_context; } +void rerender_screen(void) +{ + render_texture(active_pixel_buffer, configuration.blending_mode? previous_pixel_buffer : NULL); +#ifdef _WIN32 + /* Required for some Windows 10 machines, god knows why */ + render_texture(active_pixel_buffer, configuration.blending_mode? previous_pixel_buffer : NULL); +#endif +} + void set_filename(const char *new_filename, typeof(free) *new_free_function) { if (filename && free_function) { @@ -44,6 +57,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) @@ -55,7 +69,7 @@ static char *completer(const char *substring, uintptr_t *context) return ret; } -static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes) { CON_attributes_t con_attributes = {0,}; con_attributes.bold = attributes & GB_LOG_BOLD; @@ -87,22 +101,78 @@ static void handle_eof(void) static char *input_callback(GB_gameboy_t *gb) { + if (CON_no_csi_mode()) { + fprintf(stdout, "> "); + fflush(stdout); + } +#ifdef _WIN32 + DWORD pid; + GetWindowThreadProcessId(GetForegroundWindow(), &pid); + if (pid == GetCurrentProcessId()) { + BringWindowToTop(GetConsoleWindow()); + } +#endif retry: { - char *ret = CON_readline("Stopped> "); - if (strcmp(ret, CON_EOF) == 0) { - handle_eof(); - free(ret); + CON_set_async_prompt("Stopped> "); + char *ret = CON_readline_async(); + if (!ret) { +#ifdef _WIN32 + HWND window = GetConsoleWindow(); + if (pending_command == GB_SDL_HIDE_DEBUGGER_COMMAND || !window) return strdup("c"); + ShowWindow(window, SW_SHOW); +#endif + SDL_Event event; + SDL_WaitEvent(&event); + if (pending_command == GB_SDL_QUIT_COMMAND) { + return strdup("c"); + } + switch (event.type) { + case SDL_DISPLAYEVENT: + update_swap_interval(); + break; + + case SDL_WINDOWEVENT: { + if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + screen_manually_resized = true; + update_viewport(); + } + if (event.window.type == SDL_WINDOWEVENT_MOVED +#if SDL_COMPILEDVERSION > 2018 + || event.window.type == SDL_WINDOWEVENT_DISPLAY_CHANGED +#endif + ) { + update_swap_interval(); + } + rerender_screen(); + break; + } + case SDL_QUIT: + pending_command = GB_SDL_QUIT_COMMAND; + return strdup("c"); + case SDL_KEYDOWN: + fputc('\a', stdout); + fflush(stdout); + break; + default: + break; + } goto retry; } - else { + if (strcmp(ret, CON_EOF) == 0) { + free(ret); + handle_eof(); + goto retry; + } + else if (!CON_no_csi_mode()) { CON_attributes_t attributes = {.bold = true}; CON_attributed_printf("> %s\n", &attributes, ret); } + CON_set_async_prompt("> "); return ret; } } -static char *asyc_input_callback(GB_gameboy_t *gb) +static char *async_input_callback(GB_gameboy_t *gb) { retry: { char *ret = CON_readline_async(); @@ -122,7 +192,7 @@ retry: { static char *captured_log = NULL; -static void log_capture_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +static void log_capture_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes) { size_t current_len = strlen(captured_log); size_t len_to_add = strlen(string); @@ -131,24 +201,21 @@ static void log_capture_callback(GB_gameboy_t *gb, const char *string, GB_log_at captured_log[current_len + len_to_add] = 0; } -static void start_capturing_logs(void) +static void *start_capturing_logs(void) { - if (captured_log != NULL) { - free(captured_log); - } + void *previous = captured_log; captured_log = malloc(1); captured_log[0] = 0; GB_set_log_callback(&gb, log_capture_callback); + return previous; } -static const char *end_capturing_logs(bool show_popup, bool should_exit, uint32_t popup_flags, const char *title) +static void end_capturing_logs(bool show_popup, bool should_exit, uint32_t popup_flags, const char *title, void *previous) { - GB_set_log_callback(&gb, console_supported? log_callback : NULL); - if (captured_log[0] == 0) { - free(captured_log); - captured_log = NULL; + if (!previous) { + GB_set_log_callback(&gb, console_supported? log_callback : NULL); } - else { + if (captured_log[0] != 0) { if (show_popup) { SDL_ShowSimpleMessageBox(popup_flags, title, captured_log, window); } @@ -156,30 +223,16 @@ static const char *end_capturing_logs(bool show_popup, bool should_exit, uint32_ exit(1); } } - return captured_log; + free(captured_log); + captured_log = previous; } 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) +static void screen_size_changed(bool resize_window) { SDL_DestroyTexture(texture); texture = SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), SDL_TEXTUREACCESS_STREAMING, @@ -187,6 +240,18 @@ static void screen_size_changed(void) SDL_SetWindowMinimumSize(window, GB_get_screen_width(&gb), GB_get_screen_height(&gb)); + if (resize_window) { + signed current_window_width, current_window_height; + SDL_GetWindowSize(window, ¤t_window_width, ¤t_window_height); + + signed width = GB_get_screen_width(&gb) * configuration.default_scale; + signed height = GB_get_screen_height(&gb) * configuration.default_scale; + signed x, y; + SDL_GetWindowPosition(window, &x, &y); + SDL_SetWindowSize(window, width, height); + SDL_SetWindowPosition(window, x - (width - current_window_width) / 2, y - (height - current_window_height) / 2); + } + update_viewport(); } @@ -197,7 +262,9 @@ static void open_menu(void) GB_audio_set_paused(true); } size_t previous_width = GB_get_screen_width(&gb); + size_t previous_height = GB_get_screen_height(&gb); run_gui(true); + rerender_screen(); SDL_ShowCursor(SDL_DISABLE); if (audio_playing) { GB_audio_set_paused(false); @@ -210,16 +277,42 @@ static void open_menu(void) GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); GB_set_rewind_length(&gb, configuration.rewind_length); GB_set_rtc_mode(&gb, configuration.rtc_mode); + GB_set_turbo_cap(&gb, configuration.turbo_cap / 4.0); if (previous_width != GB_get_screen_width(&gb)) { - screen_size_changed(); + signed current_window_width, current_window_height; + SDL_GetWindowSize(window, ¤t_window_width, ¤t_window_height); + + screen_size_changed(current_window_width == previous_width * configuration.default_scale && + current_window_height == previous_height * configuration.default_scale); } } +static void console_line_ready(void) +{ + static SDL_Event event = { + .type = SDL_USEREVENT + }; + SDL_PushEvent(&event); +} + +static void configure_console(void) +{ + CON_set_async_prompt("> "); + CON_set_repeat_empty(true); + CON_set_line_ready_callback(console_line_ready); + GB_set_log_callback(&gb, log_callback); + GB_set_input_callback(&gb, input_callback); + GB_set_async_input_callback(&gb, async_input_callback); +} + 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; @@ -238,10 +331,46 @@ static void handle_events(GB_gameboy_t *gb) case SDL_WINDOWEVENT: { if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + screen_manually_resized = true; 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: { @@ -253,6 +382,7 @@ static void handle_events(GB_gameboy_t *gb) GB_audio_clear_queue(); turbo_down = event.type == SDL_JOYBUTTONDOWN; GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); + SDL_GL_SetSwapInterval(turbo_down? 0 : configuration.vsync_mode); } else if (button == JOYPAD_BUTTON_SLOW_MOTION) { underclock_down = event.type == SDL_JOYBUTTONDOWN; @@ -263,24 +393,77 @@ static void handle_events(GB_gameboy_t *gb) rewind_paused = false; } GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); + SDL_GL_SetSwapInterval(turbo_down? 0 : configuration.vsync_mode); } 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; + } + } + else if (button == JOYPAD_BUTTON_RAPID_A) { + rapid_a = event.type == SDL_JOYBUTTONDOWN; + rapid_a_count = 0; + GB_set_key_state(gb, GB_KEY_A, event.type == SDL_JOYBUTTONDOWN); + } + else if (button == JOYPAD_BUTTON_RAPID_B) { + rapid_b = event.type == SDL_JOYBUTTONDOWN; + rapid_b_count = 0; + GB_set_key_state(gb, GB_KEY_B, event.type == SDL_JOYBUTTONDOWN); + } } - break; + break; case SDL_JOYAXISMOTION: { static bool axis_active[2] = {false, false}; + static double accel_values[2] = {0, 0}; + static double axis_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 (configuration.use_faux_analog_inputs) { + axis_values[0] = event.jaxis.value / (double)32768.0; + } + else if (event.jaxis.value > JOYSTICK_HIGH) { axis_active[0] = true; + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_RIGHT, true); GB_set_key_state(gb, GB_KEY_LEFT, false); } else if (event.jaxis.value < -JOYSTICK_HIGH) { axis_active[0] = true; + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_RIGHT, false); GB_set_key_state(gb, GB_KEY_LEFT, true); } @@ -291,13 +474,22 @@ 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 (configuration.use_faux_analog_inputs) { + axis_values[1] = event.jaxis.value / (double)32768.0; + } + else if (event.jaxis.value > JOYSTICK_HIGH) { axis_active[1] = true; + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_DOWN, true); GB_set_key_state(gb, GB_KEY_UP, false); } else if (event.jaxis.value < -JOYSTICK_HIGH) { axis_active[1] = true; + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_DOWN, false); GB_set_key_state(gb, GB_KEY_UP, true); } @@ -307,8 +499,12 @@ static void handle_events(GB_gameboy_t *gb) GB_set_key_state(gb, GB_KEY_UP, false); } } - } + if (configuration.use_faux_analog_inputs && !GB_has_accelerometer(gb)) { + GB_set_use_faux_analog_inputs(gb, 0, true); + GB_set_faux_analog_inputs(gb, 0, axis_values[0], axis_values[1]); + } break; + } case SDL_JOYHATMOTION: { uint8_t value = event.jhat.value; @@ -317,6 +513,7 @@ static void handle_events(GB_gameboy_t *gb) int8_t leftright = value == SDL_HAT_LEFTUP || value == SDL_HAT_LEFT || value == SDL_HAT_LEFTDOWN ? -1 : (value == SDL_HAT_RIGHTUP || value == SDL_HAT_RIGHT || value == SDL_HAT_RIGHTDOWN ? 1 : 0); + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_LEFT, leftright == -1); GB_set_key_state(gb, GB_KEY_RIGHT, leftright == 1); GB_set_key_state(gb, GB_KEY_UP, updown == -1); @@ -332,14 +529,14 @@ static void handle_events(GB_gameboy_t *gb) } case SDL_SCANCODE_C: if (event.type == SDL_KEYDOWN && (event.key.keysym.mod & KMOD_CTRL)) { - CON_print("^C\a\n"); - GB_debugger_break(gb); + pending_command = GB_SDL_DEBUGGER_INTERRUPT_COMMAND; } break; case SDL_SCANCODE_R: if (event.key.keysym.mod & MODIFIER) { pending_command = GB_SDL_RESET_COMMAND; + paused = false; } break; @@ -373,13 +570,15 @@ 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(); + screen_manually_resized = true; } break; @@ -396,28 +595,53 @@ 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; } case SDL_KEYUP: // Fallthrough - if (event.key.keysym.scancode == configuration.keys[8]) { + if (event.key.keysym.scancode == configuration.keys[GB_CONF_KEYS_TURBO]) { turbo_down = event.type == SDL_KEYDOWN; GB_audio_clear_queue(); GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); + SDL_GL_SetSwapInterval(turbo_down? 0 : configuration.vsync_mode); } - else if (event.key.keysym.scancode == configuration.keys_2[0]) { + else if (event.key.keysym.scancode == configuration.keys_2[GB_CONF_KEYS2_REWIND]) { rewind_down = event.type == SDL_KEYDOWN; if (event.type == SDL_KEYUP) { rewind_paused = false; } GB_set_turbo_mode(gb, turbo_down, turbo_down && rewind_down); + SDL_GL_SetSwapInterval(turbo_down? 0 : configuration.vsync_mode); } - else if (event.key.keysym.scancode == configuration.keys_2[1]) { + else if (event.key.keysym.scancode == configuration.keys_2[GB_CONF_KEYS2_UNDERCLOCK]) { underclock_down = event.type == SDL_KEYDOWN; } + else if (event.key.keysym.scancode == configuration.keys_2[GB_CONF_KEYS2_RAPID_A]) { + rapid_a = event.type == SDL_KEYDOWN; + rapid_a_count = 0; + GB_set_key_state(gb, GB_KEY_A, event.type == SDL_KEYDOWN); + } + else if (event.key.keysym.scancode == configuration.keys_2[GB_CONF_KEYS2_RAPID_B]) { + rapid_b = event.type == SDL_KEYDOWN; + rapid_b_count = 0; + GB_set_key_state(gb, GB_KEY_B, event.type == SDL_KEYDOWN); + } else { for (unsigned i = 0; i < GB_KEY_MAX; i++) { if (event.key.keysym.scancode == configuration.keys[i]) { + if (i <= GB_KEY_DOWN) { + GB_set_use_faux_analog_inputs(gb, 0, false); + } GB_set_key_state(gb, i, event.type == SDL_KEYDOWN); } } @@ -434,7 +658,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; @@ -445,6 +669,15 @@ static void vblank(GB_gameboy_t *gb) GB_set_clock_multiplier(gb, clock_mutliplier); } + if (rapid_a) { + rapid_a_count++; + GB_set_key_state(gb, GB_KEY_A, !(rapid_a_count & 2)); + } + if (rapid_b) { + rapid_b_count++; + GB_set_key_state(gb, GB_KEY_B, !(rapid_b_count & 2)); + } + if (turbo_down) { show_osd_text("Fast forward..."); } @@ -464,15 +697,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); @@ -485,32 +720,49 @@ static void rumble(GB_gameboy_t *gb, double amp) static void debugger_interrupt(int ignore) { - if (!GB_is_inited(&gb)) exit(0); - /* ^C twice to exit */ - if (GB_debugger_is_stopped(&gb)) { - GB_save_battery(&gb, battery_save_path_ptr); +#ifndef _WIN32 + if (!GB_is_inited(&gb)) { exit(0); } - if (console_supported) { - CON_print("^C\n"); + if (GB_debugger_is_stopped(&gb)) { + pending_command = GB_SDL_QUIT_COMMAND; + console_line_ready(); // Force the debugger wait-loop to process the command + return; } - GB_debugger_break(&gb); +#endif + pending_command = GB_SDL_DEBUGGER_INTERRUPT_COMMAND; + } +#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) { static unsigned skip = 0; skip++; if (skip == GB_audio_get_frequency() / 8) { skip = 0; } - if (skip > GB_audio_get_frequency() / 16) { - return; + if (configuration.turbo_cap) { + if (skip > GB_audio_get_frequency() / 8 * 4 / configuration.turbo_cap) { + return; + } + } + else { + + if (skip > GB_audio_get_frequency() / 16) { + return; + } } } - 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; } @@ -522,7 +774,77 @@ static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) GB_audio_queue_sample(sample); } + +#ifdef _WIN32 +static BOOL windows_console_handler(DWORD signal) +{ + /* + Hack: prevents process termination on console close + https://twitter.com/yo_yo_yo_jbo/status/1904592584326218069 + Thanks JBO! + */ + if (signal == CTRL_C_EVENT) { + /* Only happens in no-csi mode */ + pending_command = GB_SDL_DEBUGGER_INTERRUPT_COMMAND; + TerminateThread(GetCurrentThread(), 0); + } + pending_command = GB_SDL_HIDE_DEBUGGER_COMMAND; + console_line_ready(); + TerminateThread(GetCurrentThread(), 0); + return false; +} + +static void initialize_windows_console(void) +{ + if (AllocConsole()) { + SetConsoleTitle("SameBoy Debugger Console"); + freopen("CONIN$", "r", stdin); + setvbuf(stdin, NULL, _IONBF, 0); + freopen("CONOUT$", "w", stdout); + setvbuf(stdout, NULL, _IONBF, 0); + freopen("CONOUT$", "w", stderr); + setvbuf(stderr, NULL, _IONBF, 0); + + console_supported = CON_start(completer); + if (console_supported) { + configure_console(); + } + + /* I would set a callback via SetConsoleCtrlHandler, but the function (CtrlRoutine) that + eventually calls our callback takes a lock and doesn't release it (as it expects the + process to exit afterwards). The solution is to take a more violent approach and hook + it instead. */ + +#if defined(__x86_64__) || defined(__i386__) + uint8_t *patch_address = (void *)(GetProcAddress(GetModuleHandleA("KernelBase.dll"), "CtrlRoutine") ?: + GetProcAddress(GetModuleHandleA("Kernel32.dll"), "CtrlRoutine")); +#else + uint8_t *patch_address = NULL; +#endif + if (!patch_address) { + EnableMenuItem(GetSystemMenu(GetConsoleWindow(), false), SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); + } + else { + DWORD old_protection; + VirtualProtect(patch_address, 0x20, PAGE_READWRITE, &old_protection); + if (sizeof(&windows_console_handler) == 8) { + *(patch_address++) = 0x48; // movabs + } + *(patch_address++) = 0xb8; // mov + (*(uintptr_t *)patch_address) = (uintptr_t)&windows_console_handler; + patch_address += sizeof(&windows_console_handler); + // jmp rax/eax + *(patch_address++) = 0xff; + *(patch_address++) = 0xe0; + VirtualProtect(patch_address, 0x20, old_protection, &old_protection); + } + } +} + +#endif + +static bool doing_hot_swap = false; static bool handle_pending_command(void) { switch (pending_command) { @@ -533,7 +855,7 @@ static bool handle_pending_command(void) save_extension[2] += command_parameter; replace_extension(filename, strlen(filename), save_path, save_extension); - start_capturing_logs(); + void *previous = start_capturing_logs(); bool success; if (pending_command == GB_SDL_LOAD_STATE_COMMAND) { int result = GB_load_state(&gb, save_path); @@ -552,29 +874,34 @@ static bool handle_pending_command(void) end_capturing_logs(true, false, success? SDL_MESSAGEBOX_INFORMATION : SDL_MESSAGEBOX_ERROR, - success? "Notice" : "Error"); + success? "Notice" : "Error", + previous); if (success) { show_osd_text(pending_command == GB_SDL_LOAD_STATE_COMMAND? "State loaded" : "State saved"); } return false; } - case GB_SDL_LOAD_STATE_FROM_FILE_COMMAND: - start_capturing_logs(); + case GB_SDL_LOAD_STATE_FROM_FILE_COMMAND: { + void *previous = start_capturing_logs(); bool success = GB_load_state(&gb, dropped_state_file) == 0; end_capturing_logs(true, false, success? SDL_MESSAGEBOX_INFORMATION : SDL_MESSAGEBOX_ERROR, - success? "Notice" : "Error"); + success? "Notice" : "Error", + previous); SDL_free(dropped_state_file); if (success) { show_osd_text("State loaded"); } return false; + } 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); @@ -583,6 +910,36 @@ static bool handle_pending_command(void) case GB_SDL_QUIT_COMMAND: GB_save_battery(&gb, battery_save_path_ptr); exit(0); + case GB_SDL_DEBUGGER_INTERRUPT_COMMAND: + if (!GB_is_inited(&gb)) exit(0); + +#ifdef _WIN32 + initialize_windows_console(); +#endif + + /* ^C twice to exit */ + if (GB_debugger_is_stopped(&gb)) { +#ifndef _WIN32 + GB_save_battery(&gb, battery_save_path_ptr); + exit(0); +#else + break; +#endif + } + if (console_supported) { + CON_print("^C\n"); + } + GB_debugger_break(&gb); + break; +#if _WIN32 + case GB_SDL_HIDE_DEBUGGER_COMMAND: + HWND console_window = GetConsoleWindow(); + ShowWindow(console_window, SW_HIDE); + FreeConsole(); + SDL_RaiseWindow(window); + break; +#endif + } return false; } @@ -590,49 +947,146 @@ static bool handle_pending_command(void) static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) { static const char *const names[] = { - [GB_BOOT_ROM_DMG0] = "dmg0_boot.bin", + [GB_BOOT_ROM_DMG_0] = "dmg0_boot.bin", [GB_BOOT_ROM_DMG] = "dmg_boot.bin", [GB_BOOT_ROM_MGB] = "mgb_boot.bin", [GB_BOOT_ROM_SGB] = "sgb_boot.bin", [GB_BOOT_ROM_SGB2] = "sgb2_boot.bin", - [GB_BOOT_ROM_CGB0] = "cgb0_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])); - end_capturing_logs(true, false, SDL_MESSAGEBOX_ERROR, "Error"); + void *previous = start_capturing_logs(); + if (GB_load_boot_rom(gb, resource_path(names[type]))) { + if (type == GB_BOOT_ROM_CGB_E) { + end_capturing_logs(false, false, 0, NULL, previous); + load_boot_rom(gb, GB_BOOT_ROM_CGB); + return; + } + if (type == GB_BOOT_ROM_AGB_0) { + end_capturing_logs(false, false, 0, NULL, previous); + load_boot_rom(gb, GB_BOOT_ROM_AGB); + return; + } + } + end_capturing_logs(true, false, SDL_MESSAGEBOX_ERROR, "Error", previous); } } +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 GB_model_t model_to_use(void) +{ + typeof(configuration.model) gui_model = configuration.model; + if (gui_model == MODEL_AUTO) { + uint8_t *rom = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_ROM, NULL, NULL); + if (!rom) { + gui_model = MODEL_CGB; + } + else if (rom[0x143] & 0x80) { // Has CGB features + gui_model = MODEL_CGB; + } + else if (rom[0x146] == 3) { // Has SGB features + gui_model = MODEL_SGB; + } + else if (rom[0x14B] == 1) { // Nintendo-licensed (most likely has boot ROM palettes) + gui_model = MODEL_CGB; + } + else if (rom[0x14B] == 0x33 && + rom[0x144] == '0' && + rom[0x145] == '1') { // Ditto + gui_model = MODEL_CGB; + } + else { + gui_model = MODEL_DMG; + } + } + + return (GB_model_t []) + { + [MODEL_DMG] = GB_MODEL_DMG_B, + [MODEL_CGB] = GB_MODEL_CGB_0 + configuration.cgb_revision, + [MODEL_AGB] = configuration.agb_revision, + [MODEL_MGB] = GB_MODEL_MGB, + [MODEL_SGB] = (GB_model_t []) + { + [SGB_NTSC] = GB_MODEL_SGB_NTSC, + [SGB_PAL] = GB_MODEL_SGB_PAL, + [SGB_2] = GB_MODEL_SGB2, + }[configuration.sgb_revision], + }[gui_model]; +} + static void run(void) { SDL_ShowCursor(SDL_DISABLE); GB_model_t model; pending_command = GB_SDL_NO_COMMAND; -restart: - model = (GB_model_t []) - { - [MODEL_DMG] = GB_MODEL_DMG_B, - [MODEL_CGB] = GB_MODEL_CGB_E, - [MODEL_AGB] = GB_MODEL_AGB, - [MODEL_SGB] = (GB_model_t []) - { - [SGB_NTSC] = GB_MODEL_SGB_NTSC, - [SGB_PAL] = GB_MODEL_SGB_PAL, - [SGB_2] = GB_MODEL_SGB2, - }[configuration.sgb_revision], - }[configuration.model]; +restart:; + model = model_to_use(); + bool should_resize = !screen_manually_resized; + signed current_window_width, current_window_height; + SDL_GetWindowSize(window, ¤t_window_width, ¤t_window_height); + if (GB_is_inited(&gb)) { - GB_switch_model_and_reset(&gb, model); + should_resize = + current_window_width == GB_get_screen_width(&gb) * configuration.default_scale && + current_window_height == GB_get_screen_height(&gb) * configuration.default_scale; + + if (doing_hot_swap) { + doing_hot_swap = false; + } + else { + GB_switch_model_and_reset(&gb, model); + } } else { GB_init(&gb, model); @@ -654,15 +1108,15 @@ restart: GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); GB_set_rewind_length(&gb, configuration.rewind_length); GB_set_rtc_mode(&gb, configuration.rtc_mode); + GB_set_turbo_cap(&gb, configuration.turbo_cap / 4.0); GB_set_update_input_hint_callback(&gb, handle_events); GB_apu_set_sample_callback(&gb, gb_audio_callback); if (console_supported) { - CON_set_async_prompt("> "); - GB_set_log_callback(&gb, log_callback); - GB_set_input_callback(&gb, input_callback); - GB_set_async_input_callback(&gb, asyc_input_callback); + configure_console(); } + + GB_debugger_set_reload_callback(&gb, debugger_reload_callback); } if (stop_on_start) { stop_on_start = false; @@ -671,7 +1125,7 @@ restart: bool error = false; GB_debugger_clear_symbols(&gb); - start_capturing_logs(); + void *previous = start_capturing_logs(); size_t path_length = strlen(filename); char extension[4] = {0,}; if (path_length > 4) { @@ -692,7 +1146,39 @@ restart: else { GB_load_rom(&gb, filename); } - end_capturing_logs(true, error, SDL_MESSAGEBOX_WARNING, "Warning"); + GB_model_t updated_model = model_to_use(); // Could change after loading ROM with auto setting + if (model != updated_model) { + model = updated_model; + GB_switch_model_and_reset(&gb, model); + } + + if (should_resize) { + signed width = GB_get_screen_width(&gb) * configuration.default_scale; + signed height = GB_get_screen_height(&gb) * configuration.default_scale; + signed x, y; + SDL_GetWindowPosition(window, &x, &y); + SDL_SetWindowSize(window, width, height); + SDL_SetWindowPosition(window, x - (width - current_window_width) / 2, y - (height - current_window_height) / 2); + } + + /* 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"); + // Remove all cheats before loading, so they're cleared even if loading fails. + GB_remove_all_cheats(&gb); + GB_load_cheats(&gb, cheat_path, false); + + end_capturing_logs(true, error, SDL_MESSAGEBOX_WARNING, "Warning", previous); static char start_text[64]; static char title[17]; @@ -700,13 +1186,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")); @@ -714,7 +1193,7 @@ restart: replace_extension(filename, path_length, symbols_path, ".sym"); GB_debugger_load_symbol_file(&gb, symbols_path); - screen_size_changed(); + screen_size_changed(false); /* Run emulation */ while (true) { @@ -745,7 +1224,7 @@ restart: } } -static char prefs_path[1024] = {0, }; +static char prefs_path[PATH_MAX + 1] = {0, }; static void save_configuration(void) { @@ -756,6 +1235,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++) { @@ -768,6 +1252,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) @@ -776,6 +1273,99 @@ 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[] = { + {"auto", -1, "Pick automatically"}, + {"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"}, + {"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: + configuration.model = MODEL_AUTO; + break; + } +} + +#ifdef _WIN32 +/* raise is buggy and for some reason not always go through our signal handler, so + let's just place the implementation with a direct call to debugger_interrupt. */ +int raise(int signal) +{ + debugger_interrupt(signal); + return 0; +} +#endif + int main(int argc, char **argv) { #ifdef _WIN32 @@ -785,13 +1375,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); } @@ -800,8 +1392,17 @@ int main(int argc, char **argv) } signal(SIGINT, debugger_interrupt); +#ifndef _WIN32 + signal(SIGUSR1, debugger_reset); +#endif + + if (SDL_Init(SDL_INIT_EVERYTHING & ~SDL_INIT_AUDIO) < 0) { + fprintf(stderr, "Couldn't initialize SDL: %s\n", SDL_GetError()); + } + // 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"); @@ -823,18 +1424,30 @@ 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 %= 3; + configuration.dmg_palette %= 5; + if (configuration.dmg_palette) { + configuration.gui_palette_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) { @@ -845,14 +1458,24 @@ 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); + (configuration.border_mode == GB_BORDER_ALWAYS? 256 : 160) * configuration.default_scale, + (configuration.border_mode == GB_BORDER_ALWAYS? 224 : 144) * configuration.default_scale, + SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); if (window == NULL) { fputs(SDL_GetError(), stderr); exit(1); @@ -862,6 +1485,10 @@ int main(int argc, char **argv) if (fullscreen) { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); } + +#ifdef _WIN32 + configure_window_corners(); +#endif gl_context = nogl? NULL : SDL_GL_CreateContext(window); @@ -869,6 +1496,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) { @@ -894,6 +1522,38 @@ int main(int argc, char **argv) } update_viewport(); +#ifdef _WIN32 + if (!configuration.windows_associations_prompted) { + configuration.windows_associations_prompted = true; + save_configuration(); + SDL_MessageBoxButtonData buttons[2] = { + { + .flags = SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, + .buttonid = 0, + .text = "No", + }, + { + .flags = SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, + .buttonid = 1, + .text = "Yes", + }, + }; + SDL_MessageBoxData box = { + .title = "Associate SameBoy with Game Boy ROMs", + .message = "Would you like to associate SameBoy with Game Boy ROMs?\nThis can be also done later in the Options menu.", + .numbuttons = 2, + .buttons = buttons, + }; + int button; + SDL_ShowMessageBox(&box, &button); + if (button) { + GB_do_windows_association(); + } + } +#endif + + SDL_GL_SetSwapInterval(configuration.vsync_mode); + if (filename == NULL) { stop_on_start = false; run_gui(false); diff --git a/bsnes/gb/SDL/opengl_compat.h b/bsnes/gb/SDL/opengl_compat.h index 4b79b0c7..da2e128d 100644 --- a/bsnes/gb/SDL/opengl_compat.h +++ b/bsnes/gb/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/bsnes/gb/SDL/shader.h b/bsnes/gb/SDL/shader.h index 149958d5..a31ccd1c 100644 --- a/bsnes/gb/SDL/shader.h +++ b/bsnes/gb/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/bsnes/gb/SDL/utils.c b/bsnes/gb/SDL/utils.c index 603e34a8..32945fd6 100644 --- a/bsnes/gb/SDL/utils.c +++ b/bsnes/gb/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/bsnes/gb/SDL/utils.h b/bsnes/gb/SDL/utils.h index 5c0383d3..1599913b 100644 --- a/bsnes/gb/SDL/utils.h +++ b/bsnes/gb/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/bsnes/gb/SDL/windows_associations.c b/bsnes/gb/SDL/windows_associations.c new file mode 100755 index 00000000..f5ee66d8 --- /dev/null +++ b/bsnes/gb/SDL/windows_associations.c @@ -0,0 +1,92 @@ +#ifdef _WIN32 +#include +#include +#include +#include "windows_associations.h" + +static bool set_registry_string(HKEY hive, const char *folder, const char *name, const char *value) +{ + HKEY hkey; + LONG status = RegCreateKeyExA(hive, folder, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hkey, NULL); + if (status != ERROR_SUCCESS || hkey == NULL) { + return false; + } + status = RegSetValueExA(hkey, name, 0, REG_SZ, (void *)value, strlen(value) + 1); + RegCloseKey(hkey); + return status == ERROR_SUCCESS; +} + +static bool delete_registry_key(HKEY hive, const char *folder, const char *name) +{ + HKEY hkey; + LONG status = RegCreateKeyExA(hive, folder, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hkey, NULL); + if (status != ERROR_SUCCESS || hkey == NULL) { + return false; + } + status = RegDeleteTreeA(hkey, name); + RegCloseKey(hkey); + return status == ERROR_SUCCESS; +} + +static bool set_registry_string_unicode(HKEY hive, const char *folder, const char *name, const wchar_t *value) +{ + HKEY hkey; + LONG status = RegCreateKeyExA(hive, folder, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hkey, NULL); + if (status != ERROR_SUCCESS || hkey == NULL) { + return false; + } + + wchar_t wide_name[strlen(name) + 1]; + MultiByteToWideChar(CP_UTF8, 0, name, -1, wide_name, sizeof(wide_name) / sizeof(wide_name[0])); + status = RegSetValueExW(hkey, wide_name, 0, REG_SZ, (void *)value, (wcslen(value) + 1) * 2); + + RegCloseKey(hkey); + return status == ERROR_SUCCESS; +} + + +static bool associate(const char *extension, const char *class, const char *description, signed icon) +{ + char path[128] = "Software\\Classes\\"; + strcat(path, extension); + if (!set_registry_string(HKEY_CURRENT_USER, path, "", class)) return false; + + strcpy(path, "Software\\Classes\\"); + strcat(path, class); + if (!set_registry_string(HKEY_CURRENT_USER, path, "", description)) return false; + + strcat(path, "\\shell\\open\\command"); + + wchar_t exe[MAX_PATH]; + GetModuleFileNameW(NULL, exe, MAX_PATH); + + wchar_t temp[sizeof(exe) + 32]; + wsprintfW(temp, L"\"\%s\" \"%%1\"", exe); + if (!set_registry_string_unicode(HKEY_CURRENT_USER, path, "", temp)) return false; + + strcpy(path, "Software\\Classes\\"); + strcat(path, class); + strcat(path, "\\DefaultIcon"); + + wsprintfW(temp, L"\%s,%d", exe, icon); + if (!set_registry_string_unicode(HKEY_CURRENT_USER, path, "", temp)) return false; + + strcpy(path, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + strcat(path, extension); + delete_registry_key(HKEY_CURRENT_USER, path, "UserChoice"); // Might not exist, do not check return value + + return true; +} + +bool GB_do_windows_association(void) +{ + bool ret = true; + ret &= associate(".gb", "SameBoy.gb", "Game Boy Game", 1); + ret &= associate(".gbc", "SameBoy.gbc", "Game Boy Color Game", 2); + ret &= associate(".isx", "SameBoy.isx", "Game Boy ISX File", 2); + + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); + + return ret; +} +#endif diff --git a/bsnes/gb/SDL/windows_associations.h b/bsnes/gb/SDL/windows_associations.h new file mode 100755 index 00000000..d705c1a4 --- /dev/null +++ b/bsnes/gb/SDL/windows_associations.h @@ -0,0 +1,5 @@ +#ifndef _WIN32 +#error windows_associations.h included while building for a different platform +#endif +#include +bool GB_do_windows_association(void); diff --git a/bsnes/gb/Shaders/AAScale2x.fsh b/bsnes/gb/Shaders/AAScale2x.fsh index d51a9a6a..b1b35cef 100644 --- a/bsnes/gb/Shaders/AAScale2x.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/AAScale4x.fsh b/bsnes/gb/Shaders/AAScale4x.fsh index b59b80e9..738a38ff 100644 --- a/bsnes/gb/Shaders/AAScale4x.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/CRT.fsh b/bsnes/gb/Shaders/CRT.fsh index 4cbab721..154f0a2f 100644 --- a/bsnes/gb/Shaders/CRT.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/FlatCRT.fsh b/bsnes/gb/Shaders/FlatCRT.fsh new file mode 100644 index 00000000..53122459 --- /dev/null +++ b/bsnes/gb/Shaders/FlatCRT.fsh @@ -0,0 +1,146 @@ +#define COLOR_LOW 0.45 +#define COLOR_HIGH 1.0 +#define VERTICAL_BORDER_DEPTH 0.6 +#define SCANLINE_DEPTH 0.55 + +STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 output_resolution) +{ + /* Setting up common vars */ + vec2 pos = fract(position * input_resolution); + 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 (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 (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.0) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); + } + else { + scanline_multiplier = ((1.0 - pos.y) * 2.0) * SCANLINE_DEPTH + (1.0 - SCANLINE_DEPTH); + } + + center *= scanline_multiplier; + left *= scanline_multiplier; + right *= scanline_multiplier; + + /* Vertical separator for shadow masks */ + bool odd = bool(int((position * input_resolution).x) & 1); + if (odd) { + pos.y += 0.5; + pos.y = fract(pos.y); + } + + if (pos.y < 1.0 / 3.0) { + float gradient_position = 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); + } + else if (pos.y > 2.0 / 3.0) { + 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 (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.0 - (1.0 - gradient_position) * (1.0 - (pos.x) * 6.0); + } + else { + gradient_position = 1.0 - (1.0 - gradient_position) * (1.0 - (1.0 - pos.x) * 6.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); + } + else if (pos.y > 2.0 / 3.0) { + float gradient_position = (1.0 - pos.y) * 3.0; + if (pos.x < 0.5) { + gradient_position = 1.0 - (1.0 - gradient_position) * (1.0 - (pos.x) * 6.0); + } + else { + gradient_position = 1.0 - (1.0 - gradient_position) * (1.0 - (1.0 - pos.x) * 6.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); + } + } + + + /* Subpixel blurring, like LCD filter*/ + + vec4 midleft = mix(left, center, 0.5); + vec4 midright = mix(right, center, 0.5); + + vec4 ret; + 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 (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 - 1.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 - 2.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 - 3.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 - 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 - 5.0); + } + + /* Anti alias the curve */ + vec2 pixel_position = position * output_resolution; + if (pixel_position.x < 1.0) { + ret *= pixel_position.x; + } + else if (pixel_position.x > output_resolution.x - 1.0) { + ret *= output_resolution.x - pixel_position.x; + } + if (pixel_position.y < 1.0) { + ret *= pixel_position.y; + } + else if (pixel_position.y > output_resolution.y - 1.0) { + ret *= output_resolution.y - pixel_position.y; + } + + return ret; +} diff --git a/bsnes/gb/Shaders/HQ2x.fsh b/bsnes/gb/Shaders/HQ2x.fsh index 7ae80637..0baf9e14 100644 --- a/bsnes/gb/Shaders/HQ2x.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/LCD.fsh b/bsnes/gb/Shaders/LCD.fsh index d20a7c93..c8fde24f 100644 --- a/bsnes/gb/Shaders/LCD.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/MasterShader.fsh b/bsnes/gb/Shaders/MasterShader.fsh index 3f891d5d..220bac70 100644 --- a/bsnes/gb/Shaders/MasterShader.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/MasterShader.metal b/bsnes/gb/Shaders/MasterShader.metal index 2f3113e3..45207231 100644 --- a/bsnes/gb/Shaders/MasterShader.metal +++ b/bsnes/gb/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/bsnes/gb/Shaders/MonoLCD.fsh b/bsnes/gb/Shaders/MonoLCD.fsh index 009e1db1..00b63c25 100644 --- a/bsnes/gb/Shaders/MonoLCD.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/OmniScale.fsh b/bsnes/gb/Shaders/OmniScale.fsh index eab27ae8..960d08f7 100644 --- a/bsnes/gb/Shaders/OmniScale.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/Scale2x.fsh b/bsnes/gb/Shaders/Scale2x.fsh index 17b6edb8..44bcfc4d 100644 --- a/bsnes/gb/Shaders/Scale2x.fsh +++ b/bsnes/gb/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/bsnes/gb/Shaders/Scale4x.fsh b/bsnes/gb/Shaders/Scale4x.fsh index da1ff148..f76e0ece 100644 --- a/bsnes/gb/Shaders/Scale4x.fsh +++ b/bsnes/gb/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/bsnes/gb/Tester/main.c b/bsnes/gb/Tester/main.c index a3add107..35196877 100755 --- a/bsnes/gb/Tester/main.c +++ b/bsnes/gb/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) { @@ -195,7 +195,7 @@ static void vblank(GB_gameboy_t *gb) } } -static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes) { if (!log_file) log_file = fopen(log_filename, "w"); fprintf(log_file, "%s", string); @@ -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/bsnes/gb/Windows/Cartridge.ico b/bsnes/gb/Windows/Cartridge.ico new file mode 100644 index 00000000..fa76c39c Binary files /dev/null and b/bsnes/gb/Windows/Cartridge.ico differ diff --git a/bsnes/gb/Windows/ColorCartridge.ico b/bsnes/gb/Windows/ColorCartridge.ico new file mode 100644 index 00000000..7ed8e438 Binary files /dev/null and b/bsnes/gb/Windows/ColorCartridge.ico differ diff --git a/bsnes/gb/Windows/crt.c b/bsnes/gb/Windows/crt.c new file mode 100755 index 00000000..23b4f194 --- /dev/null +++ b/bsnes/gb/Windows/crt.c @@ -0,0 +1,105 @@ +#include +#include + +#ifdef USE_MSVCRT_DLL +/* Stub-out functions imported by VS's msvcrt.lib but are not implemented in msvcrt.dll */ + +/* We don't use exceptions, these are used by the uncaught exception handler and can be replaced with aborts */ + +void *__current_exception(void) +{ + abort(); + return NULL; +} + +void *__current_exception_context(void) +{ + abort(); + return NULL; +} + +/* terminate uses a mangled symbol on msvcrt.dll */ + +void msvcrt_terminate(void) __asm__("?terminate@@YAXXZ"); +void terminate(void) +{ + msvcrt_terminate(); +} + +/* We need to redirect these to msvcrt.dll's atexit */ + +int _crt_atexit(void (*function)(void)) +{ + return ((typeof(atexit) *)GetProcAddress(GetModuleHandleA("msvcrt.dll"), "atexit"))(function); +} + +int _register_onexit_function(void *table, void (*function)(void)) +{ + return ((typeof(atexit) *)GetProcAddress(GetModuleHandleA("msvcrt.dll"), "atexit"))(function); +} + +/* Various imported function we don't need and can nop-out */ + +int *__p__commode(void) +{ + static int dummy; + return &dummy; +} + +int _configthreadlocale(int flag) +{ + return 0; +} + +errno_t _configure_narrow_argv(unsigned mode) +{ + return 0; +} + +char *_get_narrow_winmain_command_line(void) +{ + return NULL; +} + +int _initialize_narrow_environment() +{ + return 0; +} + +int _initialize_onexit_table(void *table) +{ + return 0; +} + +void _register_thread_local_exe_atexit_callback(const void *callback) +{ +} + +int _seh_filter_exe(unsigned exception_num, void *exception) +{ + return 0; +} + +int _seh_filter_dll(unsigned ExceptionNum, struct _EXCEPTION_POINTERS *ExceptionPtr) +{ + return 0; +} + +void _set_app_type(unsigned type) +{ +} + +int _set_new_mode(int new_mode) +{ + return 0; +} + +int _execute_onexit_table(void *Table) +{ + return 0; +} + +void __std_type_info_destroy_list(void *const root_node) +{ +} +#endif diff --git a/bsnes/gb/Windows/dirent.c b/bsnes/gb/Windows/dirent.c new file mode 100755 index 00000000..f5fd8b3b --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Windows/dirent.h b/bsnes/gb/Windows/dirent.h new file mode 100755 index 00000000..7102995b --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/Windows/limits.h b/bsnes/gb/Windows/limits.h new file mode 100644 index 00000000..1ef24fb0 --- /dev/null +++ b/bsnes/gb/Windows/limits.h @@ -0,0 +1,2 @@ +#include_next +#define PATH_MAX 1024 diff --git a/bsnes/gb/Windows/manifest.xml b/bsnes/gb/Windows/manifest.xml new file mode 100644 index 00000000..177a8c28 --- /dev/null +++ b/bsnes/gb/Windows/manifest.xml @@ -0,0 +1,22 @@ + + + + SameBoy + + + + + + diff --git a/bsnes/gb/Windows/math.c b/bsnes/gb/Windows/math.c new file mode 100755 index 00000000..8590ace4 --- /dev/null +++ b/bsnes/gb/Windows/math.c @@ -0,0 +1,18 @@ +#include + +#ifdef USE_MSVCRT_DLL + +/* "Old" (Pre-2015) Windows headers/libc don't have round and exp2. */ + +__attribute__((no_builtin)) double round(double f) +{ + return f >= 0? (int)(f + 0.5) : (int)(f - 0.5); +} + + +__attribute__((no_builtin)) double exp2(double f) +{ + return pow(2, f); +} + +#endif diff --git a/bsnes/gb/Windows/math.h b/bsnes/gb/Windows/math.h deleted file mode 100755 index 2b934f90..00000000 --- a/bsnes/gb/Windows/math.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include_next -#ifndef __MINGW32__ -/* "Old" (Pre-2015) Windows headers/libc don't have round. */ -static inline double round(double f) -{ - return f >= 0? (int)(f + 0.5) : (int)(f - 0.5); -} -#endif \ No newline at end of file diff --git a/bsnes/gb/Windows/msvcrt.def b/bsnes/gb/Windows/msvcrt.def new file mode 100644 index 00000000..4f6ba5dd --- /dev/null +++ b/bsnes/gb/Windows/msvcrt.def @@ -0,0 +1,1276 @@ +EXPORTS +?terminate@@YAXXZ +__ExceptionPtrAssign +__ExceptionPtrCompare +__ExceptionPtrCopy +__ExceptionPtrCopyException +__ExceptionPtrCreate +__ExceptionPtrCurrentException +__ExceptionPtrDestroy +__ExceptionPtrRethrow +__ExceptionPtrSwap +__ExceptionPtrToBool +__uncaught_exception +_CrtCheckMemory +_CrtDbgBreak +_CrtDbgReport +_CrtDbgReportV +_CrtDbgReportW +_CrtDbgReportWV +_CrtDoForAllClientObjects +_CrtDumpMemoryLeaks +_CrtIsMemoryBlock +_CrtIsValidHeapPointer +_CrtIsValidPointer +_CrtMemCheckpoint +_CrtMemDifference +_CrtMemDumpAllObjectsSince +_CrtMemDumpStatistics +_CrtReportBlockType +_CrtSetAllocHook +_CrtSetBreakAlloc +_CrtSetDbgBlockType +_CrtSetDbgFlag +_CrtSetDumpClient +_CrtSetReportFile +_CrtSetReportHook +_CrtSetReportHook2 +_CrtSetReportMode +_CxxThrowException +_Getdays +_Getmonths +_Gettnames +_HUGE +_Strftime +_W_Getdays +_W_Getmonths +_W_Gettnames +_Wcsftime +_XcptFilter +__AdjustPointer +__BuildCatchObject +__BuildCatchObjectHelper +__C_specific_handler +__CppXcptFilter +__CxxFrameHandler +__CxxFrameHandler2 +__CxxFrameHandler3 +__CxxFrameHandler4 +__DestructExceptionObject +__RTCastToVoid +__RTDynamicCast +__RTtypeid +__STRINGTOLD +__TypeMatch +___lc_codepage_func +___lc_collate_cp_func +___lc_handle_func +___mb_cur_max_func +___setlc_active_func +___unguarded_readlc_active_add_func +__argc +__argv +__badioinfo +__crtCompareStringA +__crtCompareStringW +__crtGetLocaleInfoW +__crtGetStringTypeW +__crtLCMapStringA +__crtLCMapStringW +__daylight +__dllonexit +__doserrno +__dstbias +__fpecode +__getmainargs +__initenv +__iob_func +__isascii +__iscsym +__iscsymf +__lc_codepage +__lc_collate_cp +__lc_handle +__lconv_init +__mb_cur_max +__pctype_func +__pioinfo +__pwctype_func +__pxcptinfoptrs +__set_app_type +__setlc_active +__setusermatherr +__strncnt +__threadhandle +__threadid +__toascii +__unDName +__unDNameEx +__unguarded_readlc_active +__wargv +__wcserror +__wcserror_s +__wcsncnt +__wgetmainargs +__winitenv +_abs64 +_access +_access_s +_acmdln +_aexit_rtn +_aligned_free +_aligned_free_dbg +_aligned_malloc +_aligned_malloc_dbg +_aligned_offset_malloc +_aligned_offset_malloc_dbg +_aligned_offset_realloc +_aligned_offset_realloc_dbg +_aligned_realloc +_aligned_realloc_dbg +_amsg_exit +_assert +_atodbl +_atodbl_l +_atof_l +_atoflt_l +_atoi64 +_atoi64_l +_atoi_l +_atol_l +_atoldbl +_atoldbl_l +_beep +_beginthread +_beginthreadex +_c_exit +_cabs +_callnewh +_calloc_dbg +_cexit +_cgets +_cgets_s +_cgetws +_cgetws_s +_chdir +_chdrive +_chgsign +_chgsignf +_chmod +_chsize +_chsize_s +_chvalidator +_chvalidator_l +_clearfp +_close +_commit +_commode +_control87 +_controlfp +_controlfp_s +_copysign +_copysignf +_cprintf +_cprintf_l +_cprintf_p +_cprintf_p_l +_cprintf_s +_cprintf_s_l +_cputs +_cputws +_creat +_create_locale +_crtAssertBusy +_crtBreakAlloc +_crtDbgFlag +_cscanf +_cscanf_l +_cscanf_s +_cscanf_s_l +_ctime32 +_ctime32_s +_ctime64 +_ctime64_s +_ctype +_cwait +_cwprintf +_cwprintf_l +_cwprintf_p +_cwprintf_p_l +_cwprintf_s +_cwprintf_s_l +_cwscanf +_cwscanf_l +_cwscanf_s +_cwscanf_s_l +_daylight +_difftime32 +_difftime64 +_dstbias +_dup +_dup2 +_ecvt +_ecvt_s +_endthread +_endthreadex +_environ +_eof +_errno +_execl +_execle +_execlp +_execlpe +_execv +_execve +_execvp +_execvpe +_exit +_expand +_expand_dbg +_fcloseall +_fcvt +_fcvt_s +_fdopen +_fgetchar +_fgetwchar +_filbuf +_fileinfo +_filelength +_filelengthi64 +_fileno +_findclose +_findfirst +_findfirst64 +_findfirsti64 +_findnext +_findnext64 +_findnexti64 +_finite +_finitef +_flsbuf +_flushall +_fmode +_fpclass +_fpclassf +_fpreset +_fprintf_l +_fprintf_p +_fprintf_p_l +_fprintf_s_l +_fputchar +_fputwchar +_free_dbg +_free_locale +_freea +_freea_s +_freefls +_fscanf_l +_fscanf_s_l +_fseeki64 +_fsopen +_fstat +_fstat64 +_fstati64 +_ftime +_ftime32 +_ftime32_s +_ftime64 +_ftime64_s +_fullpath +_fullpath_dbg +_futime +_futime32 +_futime64 +_fwprintf_l +_fwprintf_p +_fwprintf_p_l +_fwprintf_s_l +_fwscanf_l +_fwscanf_s_l +_gcvt +_gcvt_s +_get_current_locale +_get_doserrno +_get_environ +_get_errno +_get_fileinfo +_get_fmode +_get_heap_handle +_get_osfhandle +_get_osplatform +_get_osver +_get_output_format +_get_pgmptr +_get_sbh_threshold +_get_terminate +_get_unexpected +_get_wenviron +_get_winmajor +_get_winminor +_get_winver +_get_wpgmptr +_getch +_getche +_getcwd +_getdcwd +_getdiskfree +_getdrive +_getdrives +_getmaxstdio +_getmbcp +_getpid +_getsystime +_getw +_getwch +_getwche +_getws +_gmtime32 +_gmtime32_s +_gmtime64 +_gmtime64_s +_heapchk +_heapmin +_heapset +_heapwalk +_hypot +_hypotf +_i64toa +_i64toa_s +_i64tow +_i64tow_s +_initterm +_initterm_e +_invalid_parameter +_iob +_isalnum_l +_isalpha_l +_isatty +_iscntrl_l +_isctype +_isctype_l +_isdigit_l +_isgraph_l +_isleadbyte_l +_islower_l +_ismbbalnum +_ismbbalnum_l +_ismbbalpha +_ismbbalpha_l +_ismbbgraph +_ismbbgraph_l +_ismbbkalnum +_ismbbkalnum_l +_ismbbkana +_ismbbkana_l +_ismbbkprint +_ismbbkprint_l +_ismbbkpunct +_ismbbkpunct_l +_ismbblead +_ismbblead_l +_ismbbprint +_ismbbprint_l +_ismbbpunct +_ismbbpunct_l +_ismbbtrail +_ismbbtrail_l +_ismbcalnum +_ismbcalnum_l +_ismbcalpha +_ismbcalpha_l +_ismbcdigit +_ismbcdigit_l +_ismbcgraph +_ismbcgraph_l +_ismbchira +_ismbchira_l +_ismbckata +_ismbckata_l +_ismbcl0 +_ismbcl0_l +_ismbcl1 +_ismbcl1_l +_ismbcl2 +_ismbcl2_l +_ismbclegal +_ismbclegal_l +_ismbclower +_ismbclower_l +_ismbcprint +_ismbcprint_l +_ismbcpunct +_ismbcpunct_l +_ismbcspace +_ismbcspace_l +_ismbcsymbol +_ismbcsymbol_l +_ismbcupper +_ismbcupper_l +_ismbslead +_ismbslead_l +_ismbstrail +_ismbstrail_l +_isnan +_isnanf +_isprint_l +_isspace_l +_isupper_l +_iswalnum_l +_iswalpha_l +_iswcntrl_l +_iswctype_l +_iswdigit_l +_iswgraph_l +_iswlower_l +_iswprint_l +_iswpunct_l +_iswspace_l +_iswupper_l +_iswxdigit_l +_isxdigit_l +_itoa +_itoa_s +_itow +_itow_s +_j0 +_j1 +_jn +_kbhit +_lfind +_lfind_s +_local_unwind +_localtime32 +_localtime32_s +_localtime64 +_localtime64_s +_lock +_locking +_logb +_logbf +_lrotl +_lrotr +_lsearch +_lsearch_s +_lseek +_lseeki64 +_ltoa +_ltoa_s +_ltow +_ltow_s +_makepath +_makepath_s +_malloc_dbg +_mbbtombc +_mbbtombc_l +_mbbtype +_mbcasemap +_mbccpy +_mbccpy_l +_mbccpy_s +_mbccpy_s_l +_mbcjistojms +_mbcjistojms_l +_mbcjmstojis +_mbcjmstojis_l +_mbclen +_mbclen_l +_mbctohira +_mbctohira_l +_mbctokata +_mbctokata_l +_mbctolower +_mbctolower_l +_mbctombb +_mbctombb_l +_mbctoupper +_mbctoupper_l +_mbctype +_mblen_l +_mbsbtype +_mbsbtype_l +_mbscat +_mbscat_s +_mbscat_s_l +_mbschr +_mbschr_l +_mbscmp +_mbscmp_l +_mbscoll +_mbscoll_l +_mbscpy +_mbscpy_s +_mbscpy_s_l +_mbscspn +_mbscspn_l +_mbsdec +_mbsdec_l +_mbsdup +_mbsicmp +_mbsicmp_l +_mbsicoll +_mbsicoll_l +_mbsinc +_mbsinc_l +_mbslen +_mbslen_l +_mbslwr +_mbslwr_l +_mbslwr_s +_mbslwr_s_l +_mbsnbcat +_mbsnbcat_l +_mbsnbcat_s +_mbsnbcat_s_l +_mbsnbcmp +_mbsnbcmp_l +_mbsnbcnt +_mbsnbcnt_l +_mbsnbcoll +_mbsnbcoll_l +_mbsnbcpy +_mbsnbcpy_l +_mbsnbcpy_s +_mbsnbcpy_s_l +_mbsnbicmp +_mbsnbicmp_l +_mbsnbicoll +_mbsnbicoll_l +_mbsnbset +_mbsnbset_l +_mbsnbset_s +_mbsnbset_s_l +_mbsncat +_mbsncat_l +_mbsncat_s +_mbsncat_s_l +_mbsnccnt +_mbsnccnt_l +_mbsncmp +_mbsncmp_l +_mbsncoll +_mbsncoll_l +_mbsncpy +_mbsncpy_l +_mbsncpy_s +_mbsncpy_s_l +_mbsnextc +_mbsnextc_l +_mbsnicmp +_mbsnicmp_l +_mbsnicoll +_mbsnicoll_l +_mbsninc +_mbsninc_l +_mbsnlen +_mbsnlen_l +_mbsnset +_mbsnset_l +_mbsnset_s +_mbsnset_s_l +_mbspbrk +_mbspbrk_l +_mbsrchr +_mbsrchr_l +_mbsrev +_mbsrev_l +_mbsset +_mbsset_l +_mbsset_s +_mbsset_s_l +_mbsspn +_mbsspn_l +_mbsspnp +_mbsspnp_l +_mbsstr +_mbsstr_l +_mbstok +_mbstok_l +_mbstok_s +_mbstok_s_l +_mbstowcs_l +_mbstowcs_s_l +_mbstrlen +_mbstrlen_l +_mbstrnlen +_mbstrnlen_l +_mbsupr +_mbsupr_l +_mbsupr_s +_mbsupr_s_l +_mbtowc_l +_memccpy +_memicmp +_memicmp_l +_mkdir +_mkgmtime +_mkgmtime32 +_mkgmtime64 +_mktemp +_mktemp_s +_mktime32 +_mktime64 +_msize +_msize_dbg +_nextafter +_nextafterf +_onexit +_open +_open_osfhandle +_osplatform +_osver +_pclose +_pctype +_pgmptr +_pipe +_popen +_printf_l +_printf_p +_printf_p_l +_printf_s_l +_purecall +_putch +_putenv +_putenv_s +_putw +_putwch +_putws +_pwctype +_read +_realloc_dbg +_resetstkoflw +_rmdir +_rmtmp +_rotl +_rotl64 +_rotr +_rotr64 +_scalb +_scalbf +_scanf_l +_scanf_s_l +_scprintf +_scprintf_l +_scprintf_p_l +_scwprintf +_scwprintf_l +_scwprintf_p_l +_searchenv +_searchenv_s +_set_controlfp +_set_doserrno +_set_errno +_set_error_mode +_set_fileinfo +_set_fmode +_set_output_format +_set_sbh_threshold +_seterrormode +_setjmp +_setjmpex +_setmaxstdio +_setmbcp +_setmode +_setsystime +_sleep +_snprintf +_snprintf_c +_snprintf_c_l +_snprintf_l +_snprintf_s +_snprintf_s_l +_snscanf +_snscanf_l +_snscanf_s +_snscanf_s_l +_snwprintf +_snwprintf_l +_snwprintf_s +_snwprintf_s_l +_snwscanf +_snwscanf_l +_snwscanf_s +_snwscanf_s_l +_sopen +_sopen_s +_spawnl +_spawnle +_spawnlp +_spawnlpe +_spawnv +_spawnve +_spawnvp +_spawnvpe +_splitpath +_splitpath_s +_sprintf_l +_sprintf_p_l +_sprintf_s_l +_sscanf_l +_sscanf_s_l +_stat +_stat64 +_stati64 +_statusfp +_strcmpi +_strcoll_l +_strdate +_strdate_s +_strdup +_strdup_dbg +_strerror +_strerror_s +_stricmp +_stricmp_l +_stricoll +_stricoll_l +_strlwr +_strlwr_l +_strlwr_s +_strlwr_s_l +_strncoll +_strncoll_l +_strnicmp +_strnicmp_l +_strnicoll +_strnicoll_l +_strnset +_strnset_s +_strrev +_strset +_strset_s +_strtime +_strtime_s +_strtod_l +_strtoi64 +_strtoi64_l +_strtol_l +_strtoui64 +_strtoui64_l +_strtoul_l +_strupr +_strupr_l +_strupr_s +_strupr_s_l +_strxfrm_l +_swab +_swprintf +_swprintf_c +_swprintf_c_l +_swprintf_p_l +_swprintf_s_l +_swscanf_l +_swscanf_s_l +_sys_errlist +_sys_nerr +_tell +_telli64 +_tempnam +_tempnam_dbg +_time32 +_time64 +_timezone +_tolower +_tolower_l +_toupper +_toupper_l +_towlower_l +_towupper_l +_tzname +_tzset +_ui64toa +_ui64toa_s +_ui64tow +_ui64tow_s +_ultoa +_ultoa_s +_ultow +_ultow_s +_umask +_umask_s +_ungetc_nolock +_ungetch +_ungetwch +_unlink +_unlock +_utime +_utime32 +_utime64 +_vcprintf +_vcprintf_l +_vcprintf_p +_vcprintf_p_l +_vcprintf_s +_vcprintf_s_l +_vcwprintf +_vcwprintf_l +_vcwprintf_p +_vcwprintf_p_l +_vcwprintf_s +_vcwprintf_s_l +_vfprintf_l +_vfprintf_p +_vfprintf_p_l +_vfprintf_s_l +_vfwprintf_l +_vfwprintf_p +_vfwprintf_p_l +_vfwprintf_s_l +_vprintf_l +_vprintf_p +_vprintf_p_l +_vprintf_s_l +_vscprintf +_vscprintf_l +_vscprintf_p_l +_vscwprintf +_vscwprintf_l +_vscwprintf_p_l +_vsnprintf +_vsnprintf_c +_vsnprintf_c_l +_vsnprintf_l +_vsnprintf_s +_vsnprintf_s_l +_vsnwprintf +_vsnwprintf_l +_vsnwprintf_s +_vsnwprintf_s_l +_vsprintf_l +_vsprintf_p +_vsprintf_p_l +_vsprintf_s_l +_vswprintf +_vswprintf_c +_vswprintf_c_l +_vswprintf_l +_vswprintf_p_l +_vswprintf_s_l +_vwprintf_l +_vwprintf_p +_vwprintf_p_l +_vwprintf_s_l +_waccess +_waccess_s +_wasctime +_wasctime_s +_wassert +_wchdir +_wchmod +_wcmdln +_wcreat +_wcscoll_l +_wcsdup +_wcsdup_dbg +_wcserror +_wcserror_s +_wcsftime_l +_wcsicmp +_wcsicmp_l +_wcsicoll +_wcsicoll_l +_wcslwr +_wcslwr_l +_wcslwr_s +_wcslwr_s_l +_wcsncoll +_wcsncoll_l +_wcsnicmp +_wcsnicmp_l +_wcsnicoll +_wcsnicoll_l +_wcsnset +_wcsnset_s +_wcsrev +_wcsset +_wcsset_s +_wcstod_l +_wcstoi64 +_wcstoi64_l +_wcstol_l +_wcstombs_l +_wcstombs_s_l +_wcstoui64 +_wcstoui64_l +_wcstoul_l +_wcsupr +_wcsupr_l +_wcsupr_s +_wcsupr_s_l +_wcsxfrm_l +_wctime +_wctime32 +_wctime32_s +_wctime64 +_wctime64_s +_wctomb_l +_wctomb_s_l +_wctype +_wenviron +_wexecl +_wexecle +_wexeclp +_wexeclpe +_wexecv +_wexecve +_wexecvp +_wexecvpe +_wfdopen +_wfindfirst +_wfindfirst64 +_wfindfirsti64 +_wfindnext +_wfindnext64 +_wfindnexti64 +_wfopen +_wfopen_s +_wfreopen +_wfreopen_s +_wfsopen +_wfullpath +_wfullpath_dbg +_wgetcwd +_wgetdcwd +_wgetenv +_wgetenv_s +_winmajor +_winminor +_winput_s +_winver +_wmakepath +_wmakepath_s +_wmkdir +_wmktemp +_wmktemp_s +_wopen +_woutput_s +_wperror +_wpgmptr +_wpopen +_wprintf_l +_wprintf_p +_wprintf_p_l +_wprintf_s_l +_wputenv +_wputenv_s +_wremove +_wrename +_write +_wrmdir +_wscanf_l +_wscanf_s_l +_wsearchenv +_wsearchenv_s +_wsetlocale +_wsopen +_wsopen_s +_wspawnl +_wspawnle +_wspawnlp +_wspawnlpe +_wspawnv +_wspawnve +_wspawnvp +_wspawnvpe +_wsplitpath +_wsplitpath_s +_wstat +_wstat64 +_wstati64 +_wstrdate +_wstrdate_s +_wstrtime +_wstrtime_s +_wsystem +_wtempnam +_wtempnam_dbg +_wtmpnam +_wtmpnam_s +_wtof +_wtof_l +_wtoi +_wtoi64 +_wtoi64_l +_wtoi_l +_wtol +_wtol_l +_wunlink +_wutime +_wutime32 +_wutime64 +_y0 +_y1 +_yn +abort +abs +acos +acosf +asctime +asctime_s +asin +asinf +atan +atan2 +atan2f +atanf +atexit +atof +atoi +atol +bsearch +bsearch_s +btowc +calloc +ceil +ceilf +clearerr +clearerr_s +clock +cos +cosf +cosh +coshf +ctime +difftime +div +exit +exp +expf +fabs +fclose +feof +ferror +fflush +fgetc +fgetpos +fgets +fgetwc +fgetws +floor +floorf +fmod +fmodf +fopen +fopen_s +fprintf +fprintf_s +fputc +fputs +fputwc +fputws +fread +free +freopen +freopen_s +frexp +fscanf +fscanf_s +fseek +fsetpos +ftell +fwprintf +fwprintf_s +fwrite +fwscanf +fwscanf_s +getc +getchar +getenv +getenv_s +gets +getwc +getwchar +gmtime +is_wctype +isalnum +isalpha +iscntrl +isdigit +isgraph +isleadbyte +islower +isprint +ispunct +isspace +isupper +iswalnum +iswalpha +iswascii +iswcntrl +iswctype +iswdigit +iswgraph +iswlower +iswprint +iswpunct +iswspace +iswupper +iswxdigit +isxdigit +labs +ldexp +ldiv +localeconv +localtime +log +log10 +log10f +logf +longjmp +malloc +mblen +mbrlen +mbrtowc +mbsdup_dbg +mbsrtowcs +mbsrtowcs_s +mbstowcs +mbstowcs_s +mbtowc +memchr +memcmp +memcpy +memcpy_s +memmove +memmove_s +memset +mktime +modf +modff +perror +pow +powf +printf +printf_s +putc +putchar +puts +putwc +putwchar +qsort +qsort_s +raise +rand +rand_s +realloc +remove +rename +rewind +scanf +scanf_s +setbuf +setjmp +setlocale +setvbuf +signal +sin +sinf +sinh +sinhf +sprintf +sprintf_s +sqrt +sqrtf +srand +sscanf +sscanf_s +strcat +strcat_s +strchr +strcmp +strcoll +strcpy +strcpy_s +strcspn +strerror +strerror_s +strftime +strlen +strncat +strncat_s +strncmp +strncpy +strncpy_s +strnlen +strpbrk +strrchr +strspn +strstr +strtod +strtok +strtok_s +strtol +strtoul +strxfrm +swprintf +swprintf_s +swscanf +swscanf_s +system +tan +tanf +tanh +tanhf +time +tmpfile +tmpfile_s +tmpnam +tmpnam_s +tolower +toupper +towlower +towupper +ungetc +ungetwc +utime +vfprintf +vfprintf_s +vfwprintf +vfwprintf_s +vprintf +vprintf_s +vsnprintf +vsprintf +vsprintf_s +vswprintf +vswprintf_s +vwprintf +vwprintf_s +wcrtomb +wcrtomb_s +wcscat +wcscat_s +wcschr +wcscmp +wcscoll +wcscpy +wcscpy_s +wcscspn +wcsftime +wcslen +wcsncat +wcsncat_s +wcsncmp +wcsncpy +wcsncpy_s +wcsnlen +wcspbrk +wcsrchr +wcsrtombs +wcsrtombs_s +wcsspn +wcsstr +wcstod +wcstok +wcstok_s +wcstol +wcstombs +wcstombs_s +wcstoul +wcsxfrm +wctob +wctomb +wctomb_s +wprintf +wprintf_s +wscanf +wscanf_s \ No newline at end of file diff --git a/bsnes/gb/Windows/resources.rc b/bsnes/gb/Windows/resources.rc index 73c12139..75b4c00f 100644 Binary files a/bsnes/gb/Windows/resources.rc and b/bsnes/gb/Windows/resources.rc differ diff --git a/bsnes/gb/Windows/sameboy.ico b/bsnes/gb/Windows/sameboy.ico index bd8e372d..17f072c1 100644 Binary files a/bsnes/gb/Windows/sameboy.ico and b/bsnes/gb/Windows/sameboy.ico differ diff --git a/bsnes/gb/Windows/stdio.c b/bsnes/gb/Windows/stdio.c new file mode 100755 index 00000000..2b6d459c --- /dev/null +++ b/bsnes/gb/Windows/stdio.c @@ -0,0 +1,96 @@ +#include +#include + +#ifdef USE_MSVCRT_DLL + +FILE *__acrt_iob_func(unsigned index) +{ + static FILE *files[3]; + if (index > sizeof(files) / sizeof(files[0])) return NULL; + if (files[index]) return files[index]; + return (files[index] = fdopen(index, index == STDIN_FILENO? "r" : "w")); +} + +#endif + +#ifndef __MINGW32__ +#ifndef __LIBRETRO__ +int vasprintf(char **str, const char *fmt, va_list args) +{ + size_t size = _vscprintf(fmt, args) + 1; + *str = (char*)malloc(size); + int ret = vsprintf(*str, fmt, args); + if (ret != size - 1) { + free(*str); + *str = NULL; + return -1; + } + return ret; +} + +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 + +/* This code is public domain -- Will Hartung 4/9/09 */ +intptr_t getline(char **lineptr, size_t *n, FILE *stream) +{ + char *bufptr = NULL; + char *p = bufptr; + size_t size; + int c; + + if (lineptr == NULL) { + return -1; + } + if (stream == NULL) { + return -1; + } + if (n == NULL) { + return -1; + } + bufptr = *lineptr; + size = *n; + + errno = 0; + c = fgetc(stream); + if (c == EOF) { + return -1; + } + if (bufptr == NULL) { + bufptr = (char*)malloc(128); + if (bufptr == NULL) { + return -1; + } + size = 128; + } + p = bufptr; + while (c != EOF) { + if ((p - bufptr) > (size - 1)) { + size = size + 128; + bufptr = (char*)realloc(bufptr, size); + if (bufptr == NULL) { + return -1; + } + } + *p++ = c; + if (c == '\n') { + break; + } + c = fgetc(stream); + } + + *p++ = '\0'; + *lineptr = bufptr; + *n = size; + + return p - bufptr - 1; +} diff --git a/bsnes/gb/Windows/stdio.h b/bsnes/gb/Windows/stdio.h index 1e6ec02f..e2f1c9dc 100755 --- a/bsnes/gb/Windows/stdio.h +++ b/bsnes/gb/Windows/stdio.h @@ -1,80 +1,32 @@ #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__ -static inline int vasprintf(char **str, const char *fmt, va_list args) -{ - size_t size = _vscprintf(fmt, args) + 1; - *str = (char*)malloc(size); - int ret = vsprintf(*str, fmt, args); - if (ret != size - 1) { - free(*str); - *str = NULL; - return -1; - } - return ret; -} +int vasprintf(char **str, const char *fmt, va_list args); +int asprintf(char **strp, const char *fmt, ...); #endif #endif -/* This code is public domain -- Will Hartung 4/9/09 */ -static inline size_t getline(char **lineptr, size_t *n, FILE *stream) -{ - char *bufptr = NULL; - char *p = bufptr; - size_t size; - int c; - - if (lineptr == NULL) { - return -1; - } - if (stream == NULL) { - return -1; - } - if (n == NULL) { - return -1; - } - bufptr = *lineptr; - size = *n; - - c = fgetc(stream); - if (c == EOF) { - return -1; - } - if (bufptr == NULL) { - bufptr = (char*)malloc(128); - if (bufptr == NULL) { - return -1; - } - size = 128; - } - p = bufptr; - while (c != EOF) { - if ((p - bufptr) > (size - 1)) { - size = size + 128; - bufptr = (char*)realloc(bufptr, size); - if (bufptr == NULL) { - return -1; - } - } - *p++ = c; - if (c == '\n') { - break; - } - c = fgetc(stream); - } - - *p++ = '\0'; - *lineptr = bufptr; - *n = size; - - return p - bufptr - 1; -} +intptr_t getline(char **lineptr, size_t *n, FILE *stream); #define snprintf _snprintf +#define printf(...) fprintf(stdout, __VA_ARGS__) diff --git a/bsnes/gb/Windows/stdlib.h b/bsnes/gb/Windows/stdlib.h new file mode 100755 index 00000000..7d35615f --- /dev/null +++ b/bsnes/gb/Windows/stdlib.h @@ -0,0 +1,3 @@ +#pragma once +#include_next +#define alloca _alloca diff --git a/bsnes/gb/Windows/string.h b/bsnes/gb/Windows/string.h index b899ca97..f1cf6b1e 100755 --- a/bsnes/gb/Windows/string.h +++ b/bsnes/gb/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/bsnes/gb/Windows/unistd.h b/bsnes/gb/Windows/unistd.h index 62e2337c..c17587e4 100644 --- a/bsnes/gb/Windows/unistd.h +++ b/bsnes/gb/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/bsnes/gb/Windows/utf8_compat.c b/bsnes/gb/Windows/utf8_compat.c index 03472115..9264e2e8 100755 --- a/bsnes/gb/Windows/utf8_compat.c +++ b/bsnes/gb/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/bsnes/gb/XdgThumbnailer/emulate.c b/bsnes/gb/XdgThumbnailer/emulate.c new file mode 100644 index 00000000..dd111ffc --- /dev/null +++ b/bsnes/gb/XdgThumbnailer/emulate.c @@ -0,0 +1,101 @@ +#include "emulate.h" + +#include +#include +#include +#include +#include + +#include "Core/gb.h" + +// Auto-generated via `glib-compile-resources` from `resources.gresource.xml`. +#include "build/obj/XdgThumbnailer/resources.h" + +#define NB_FRAMES_TO_EMULATE (60 * 10) + +#define BOOT_ROM_SIZE (0x100 + 0x800) // The two "parts" of it, which are stored contiguously. + +/* --- */ + +static char *async_input_callback(GB_gameboy_t *gb) +{ + (void)gb; + return NULL; +} + +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes) +{ + (void)gb, (void)string, (void)attributes; // Swallow any logs. +} + +static void vblank_callback(GB_gameboy_t *gb, GB_vblank_type_t type) +{ + (void)type; // Ignore the type, we use VBlank counting as a kind of pacing (and to avoid tearing). + + unsigned *nb_frames_left = GB_get_user_data(gb); + (*nb_frames_left)--; + + // *Do* render the very last frame. + if (*nb_frames_left == 1) { + GB_set_rendering_disabled(gb, false); + } +} + +static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + uint32_t rgba; + // The GdkPixbuf that will be created from the screen buffer later, expects components in the + // order [red, green, blue, alpha], from a uint8_t[] buffer. + // But SameBoy requires a uint32_t[] buffer, and don't know the endianness of `uint32_t`. + // So we treat each uint32_t as a 4-byte buffer, and write the bytes accordingly. + // This is guaranteed to not be UB, because casting a `T*` to any flavour of `char*` accesses + // and modifies the `T`'s "object representation". + uint8_t *bytes = (uint8_t *)&rgba; + bytes[0] = r; + bytes[1] = g; + bytes[2] = b; + bytes[3] = 0xFF; + return rgba; +} + +uint8_t emulate(const char *path, uint32_t screen[static GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]) +{ + GB_gameboy_t gb; + GB_init(&gb, GB_MODEL_CGB_E); + + const char *last_dot = strrchr(path, '.'); + bool is_isx = last_dot && strcmp(last_dot + 1, "isx") == 0; + if (is_isx ? GB_load_isx(&gb, path) : GB_load_rom(&gb, path)) { + exit(EXIT_FAILURE); + } + + GError *error = NULL; + GBytes *boot_rom = g_resource_lookup_data(resources_get_resource(), "/thumbnailer/cgb_boot_fast.bin", + G_RESOURCE_LOOKUP_FLAGS_NONE, &error); + g_assert_no_error(error); // This shouldn't be able to fail. + size_t boot_rom_size; + const uint8_t *boot_rom_data = g_bytes_get_data(boot_rom, &boot_rom_size); + g_assert_cmpuint(boot_rom_size, ==, BOOT_ROM_SIZE); + GB_load_boot_rom_from_buffer(&gb, boot_rom_data, boot_rom_size); + g_bytes_unref(boot_rom); + + GB_set_vblank_callback(&gb, vblank_callback); + GB_set_pixels_output(&gb, screen); + GB_set_rgb_encode_callback(&gb, rgb_encode); + GB_set_async_input_callback(&gb, async_input_callback); + GB_set_log_callback(&gb, log_callback); // Anything bizarre the ROM does during emulation, we don't care about. + GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_MODERN_BALANCED); + + unsigned nb_frames_left = NB_FRAMES_TO_EMULATE; + GB_set_user_data(&gb, &nb_frames_left); + + GB_set_rendering_disabled(&gb, true); + GB_set_turbo_mode(&gb, true, true); + while (nb_frames_left) { + GB_run(&gb); + } + + int cgb_flag = GB_read_memory(&gb, 0x143) & 0xC0; + GB_free(&gb); + return cgb_flag; +} diff --git a/bsnes/gb/XdgThumbnailer/emulate.h b/bsnes/gb/XdgThumbnailer/emulate.h new file mode 100644 index 00000000..3e75d4a9 --- /dev/null +++ b/bsnes/gb/XdgThumbnailer/emulate.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#define GB_SCREEN_WIDTH 160 +#define GB_SCREEN_HEIGHT 144 + +uint8_t emulate(const char *path, uint32_t screen[static GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]); diff --git a/bsnes/gb/XdgThumbnailer/main.c b/bsnes/gb/XdgThumbnailer/main.c new file mode 100644 index 00000000..2a263fbd --- /dev/null +++ b/bsnes/gb/XdgThumbnailer/main.c @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "emulate.h" + +static const char dmg_only_resource_path[] = "/thumbnailer/CartridgeTemplate.png"; +static const char dual_resource_path[] = "/thumbnailer/UniversalCartridgeTemplate.png"; +static const char cgb_only_resource_path[] = "/thumbnailer/ColorCartridgeTemplate.png"; + +static GdkPixbuf *generate_thumbnail(const char *input_path) +{ + uint32_t screen_raw[GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT]; + uint8_t cgb_flag = emulate(input_path, screen_raw); + + // Generate the thumbnail from `screen_raw` and `cgb_flag`. + + // `screen_raw` is properly formatted for this operation; see the comment in `rgb_encode` for a + // discussion of why and how. + GdkPixbuf *screen = gdk_pixbuf_new_from_data((uint8_t *)screen_raw, GDK_COLORSPACE_RGB, + true, // Yes, we have alpha! + 8, // bpp + GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT, // Size. + GB_SCREEN_WIDTH * sizeof(screen_raw[0]), // Row stride. + NULL, NULL); // Do not free the buffer. + // Scale the screen and position it in the appropriate place for compositing the cartridge templates. + GdkPixbuf *scaled_screen = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 1024, 1024); + gdk_pixbuf_scale(screen, // Source. + scaled_screen, // Destination. + 192, 298, // Match the displacement below. + GB_SCREEN_WIDTH * 4, GB_SCREEN_HEIGHT * 4, // How the scaled rectangle should be cropped. + 192, 298, // Displace the scaled screen so it lines up with the template. + 4, 4, // Scaling factors. + GDK_INTERP_NEAREST); + g_object_unref(screen); + + GError *error = NULL; + GdkPixbuf *template; + switch (cgb_flag) { + case 0xC0: + template = gdk_pixbuf_new_from_resource(cgb_only_resource_path, &error); + break; + case 0x80: + template = gdk_pixbuf_new_from_resource(dual_resource_path, &error); + break; + default: + template = gdk_pixbuf_new_from_resource(dmg_only_resource_path, &error); + break; + } + g_assert_no_error(error); + g_assert_cmpint(gdk_pixbuf_get_width(template), ==, 1024); + g_assert_cmpint(gdk_pixbuf_get_height(template), ==, 1024); + gdk_pixbuf_composite(template, // Source. + scaled_screen, // Destination. + 0, 0, // Match the displacement below. + 1024, 1024, // Crop of the scaled rectangle. + 0, 0, // Displacement of the scaled rectangle. + 1, 1, // Scaling factors. + GDK_INTERP_NEAREST, // Doesn't really matter, but should be a little faster. + 255); // Blending factor of the source onto the destination. + g_object_unref(template); + + return scaled_screen; +} + +static GdkPixbuf *enforce_max_size(GdkPixbuf *thumbnail, unsigned max_size) +{ + g_assert_cmpuint(gdk_pixbuf_get_width(thumbnail), ==, gdk_pixbuf_get_height(thumbnail)); + g_assert_cmpuint(gdk_pixbuf_get_width(thumbnail), ==, 1024); + // This is only a *max* size; don't bother scaling up. + // (This also prevents any overflow errors—notice that the scale function takes `int` size parameters!) + if (max_size > 1024) return thumbnail; + GdkPixbuf *scaled = gdk_pixbuf_scale_simple(thumbnail, max_size, max_size, GDK_INTERP_BILINEAR); + g_object_unref(thumbnail); + return scaled; +} + +static void write_thumbnail(GdkPixbuf *thumbnail, const char *output_path) +{ + GError *error = NULL; + // Intentionally be "not a good citizen": + // - Write directly to the provided path, instead of atomically replacing it with a fully-formed file; + // this is necessary for at least Tumbler (XFCE's thumbnailer daemon), which creates the file **and** keeps the + // returned FD—which keeps pointing to the deleted file... which is still empty! + // - Do not save any metadata to the PNG, since the thumbnailer daemon (again, at least XFCE's, the only one I have + // tested with) appears to read the PNG's pixels, and write a new one with the appropriate metadata. + // (Thank you! Saves me all that work.) + gdk_pixbuf_save(thumbnail, output_path, "png", &error, NULL); + if (error) { + g_error("Failed to save thumbnail: %s", error->message); + // NOTREACHED + } +} + +int main(int argc, char *argv[]) +{ + if (argc != 3 && argc != 4) { + g_error("Usage: %s []", argv[0] ? argv[0] : "sameboy-thumbnailer"); + // NOTREACHED + } + const char *input_path = argv[1]; + char *output_path = argv[2]; // Gets mutated in-place. + const char *max_size = argv[3]; // May be NULL. + + g_debug("%s -> %s [%s]", input_path, output_path, max_size ? max_size : "(none)"); + + GdkPixbuf *thumbnail = generate_thumbnail(input_path); + if (max_size) { + char *endptr; + errno = 0; + /* This will implicitly truncate, but enforce_max_size will cap size to 1024 anyway. + (Not that 4 billion pixels wide icons make sense to begin with)*/ + unsigned size = strtoul(max_size, &endptr, 10); + if (errno != 0 || *max_size == '\0' || *endptr != '\0') { + g_error("Invalid size parameter \"%s\": %s", max_size, strerror(errno == 0 ? EINVAL : errno)); + // NOTREACHED + } + + thumbnail = enforce_max_size(thumbnail, size); + } + write_thumbnail(thumbnail, output_path); + g_object_unref(thumbnail); + + return 0; +} diff --git a/bsnes/gb/XdgThumbnailer/resources.gresource.xml b/bsnes/gb/XdgThumbnailer/resources.gresource.xml new file mode 100644 index 00000000..f30ec174 --- /dev/null +++ b/bsnes/gb/XdgThumbnailer/resources.gresource.xml @@ -0,0 +1,9 @@ + + + + QuickLook/CartridgeTemplate.png + QuickLook/ColorCartridgeTemplate.png + QuickLook/UniversalCartridgeTemplate.png + build/bin/BootROMs/cgb_boot_fast.bin + + diff --git a/bsnes/gb/XdgThumbnailer/sameboy.thumbnailer b/bsnes/gb/XdgThumbnailer/sameboy.thumbnailer new file mode 100644 index 00000000..eee621a9 --- /dev/null +++ b/bsnes/gb/XdgThumbnailer/sameboy.thumbnailer @@ -0,0 +1,4 @@ +[Thumbnailer Entry] +TryExec=sameboy-thumbnailer +Exec=sameboy-thumbnailer %i %o %s +MimeType=application/x-gameboy-rom;application/x-gameboy-color-rom;application/x-gameboy-isx diff --git a/bsnes/gb/build-faq.md b/bsnes/gb/build-faq.md index 09214363..6cf1a4b7 100644 --- a/bsnes/gb/build-faq.md +++ b/bsnes/gb/build-faq.md @@ -15,43 +15,30 @@ 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 After downloading [rgbds](https://github.com/gbdev/rgbds/releases/), ensure that it is added to the `%PATH%`. This may be done by adding it to the user's or SYSTEM's Environment Variables, or may be added to the command line at compilation time via `set path=%path%;C:\path\to\rgbds`. -### GnuWin +### Git Bash & Make -Ensure that the `gnuwin32\bin\` directory is included in `%PATH%`. Like rgbds above, this may instead be manually included on the command line before installation: `set path=%path%;C:\path\to\gnuwin32\bin`. +Ensure that the `Git\usr\bin` directory is included in `%PATH%`. Like rgbds above, this may instead be manually included on the command line before installation: `set path=%path%;C:\path\to\Git\usr\bin`. Similarly, make sure that the directory containing `make.exe` is also included. ## Building 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 ``` -Please note that these directories (`C:\SDL2\*`) are the examples given within the "SDL Port" section above. Ensure that your `%PATH%` properly includes `rgbds` and `gnuwin32\bin`, and that the `lib` and `include` paths include the appropriate SDL2 directories. +On some versions of Visual Studio, you might need to use `vcvarsx86_amd64` instead of `vcvars64`. Please note that these directories (`C:\SDL2\*`) are the examples given within the "SDL Port" section above. Ensure that your `%PATH%` properly includes `rgbds` and `Git\usr\bin`, and that the `lib` and `include` paths include the appropriate SDL2 directories. -## Common Errors - -### Error -1073741819 - -If encountering an error that appears as follows: - -``` make: *** [build/bin/BootROMs/dmg_boot.bin] Error -1073741819``` - -Simply run `make` again, and the process will continue. This appears to happen occasionally with `build/bin/BootROMs/dmg_boot.bin` and `build/bin/BootROMs/sgb2_boot.bin`. It does not affect the compiled output. This appears to be an issue with GnuWin. - -### The system cannot find the file specified (`usr/bin/mkdir`) - -If errors arise (i.e., particularly with the `CREATE_PROCESS('usr/bin/mkdir')` calls, also verify that Git for Windows has not been installed with full Linux support. If it has, remove `C:\Program Files\Git\usr\bin` from the SYSTEM %PATH% until after compilation. This happens because the Git for Windows version of `which` is used instead of the GnuWin one, and it returns a Unix-style path instead of a Windows one. diff --git a/bsnes/gb/iOS/AppIcon60x60@2x.png b/bsnes/gb/iOS/AppIcon60x60@2x.png new file mode 100644 index 00000000..6a1d9f73 Binary files /dev/null and b/bsnes/gb/iOS/AppIcon60x60@2x.png differ diff --git a/bsnes/gb/iOS/AppIcon76x76@2x.png b/bsnes/gb/iOS/AppIcon76x76@2x.png new file mode 100644 index 00000000..2f3f8dcb Binary files /dev/null and b/bsnes/gb/iOS/AppIcon76x76@2x.png differ diff --git a/bsnes/gb/iOS/Assets.car b/bsnes/gb/iOS/Assets.car new file mode 100644 index 00000000..d5d92102 Binary files /dev/null and b/bsnes/gb/iOS/Assets.car differ diff --git a/bsnes/gb/iOS/CameraRotateTemplate@2x.png b/bsnes/gb/iOS/CameraRotateTemplate@2x.png new file mode 100644 index 00000000..2862152d Binary files /dev/null and b/bsnes/gb/iOS/CameraRotateTemplate@2x.png differ diff --git a/bsnes/gb/iOS/CameraRotateTemplate@3x.png b/bsnes/gb/iOS/CameraRotateTemplate@3x.png new file mode 100644 index 00000000..ece8ad90 Binary files /dev/null and b/bsnes/gb/iOS/CameraRotateTemplate@3x.png differ diff --git a/bsnes/gb/iOS/Cartridge.png b/bsnes/gb/iOS/Cartridge.png new file mode 100644 index 00000000..b637d791 Binary files /dev/null and b/bsnes/gb/iOS/Cartridge.png differ diff --git a/bsnes/gb/iOS/Cartridge64.png b/bsnes/gb/iOS/Cartridge64.png new file mode 100644 index 00000000..add728c7 Binary files /dev/null and b/bsnes/gb/iOS/Cartridge64.png differ diff --git a/bsnes/gb/iOS/CheatsTemplate@2x.png b/bsnes/gb/iOS/CheatsTemplate@2x.png new file mode 100644 index 00000000..b5d773f2 Binary files /dev/null and b/bsnes/gb/iOS/CheatsTemplate@2x.png differ diff --git a/bsnes/gb/iOS/CheatsTemplate@3x.png b/bsnes/gb/iOS/CheatsTemplate@3x.png new file mode 100644 index 00000000..b7fb3be3 Binary files /dev/null and b/bsnes/gb/iOS/CheatsTemplate@3x.png differ diff --git a/bsnes/gb/iOS/ColorCartridge.png b/bsnes/gb/iOS/ColorCartridge.png new file mode 100644 index 00000000..96447dd7 Binary files /dev/null and b/bsnes/gb/iOS/ColorCartridge.png differ diff --git a/bsnes/gb/iOS/ColorCartridge64.png b/bsnes/gb/iOS/ColorCartridge64.png new file mode 100644 index 00000000..9342cf3a Binary files /dev/null and b/bsnes/gb/iOS/ColorCartridge64.png differ diff --git a/bsnes/gb/iOS/FolderTemplate@2x.png b/bsnes/gb/iOS/FolderTemplate@2x.png new file mode 100644 index 00000000..b868fcb0 Binary files /dev/null and b/bsnes/gb/iOS/FolderTemplate@2x.png differ diff --git a/bsnes/gb/iOS/FolderTemplate@3x.png b/bsnes/gb/iOS/FolderTemplate@3x.png new file mode 100644 index 00000000..c24e7f6a Binary files /dev/null and b/bsnes/gb/iOS/FolderTemplate@3x.png differ diff --git a/bsnes/gb/iOS/GBAboutController.h b/bsnes/gb/iOS/GBAboutController.h new file mode 100644 index 00000000..97573090 --- /dev/null +++ b/bsnes/gb/iOS/GBAboutController.h @@ -0,0 +1,5 @@ +#import + +@interface GBAboutController : UIViewController + +@end diff --git a/bsnes/gb/iOS/GBAboutController.m b/bsnes/gb/iOS/GBAboutController.m new file mode 100644 index 00000000..3941a4e6 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBActivityViewController.h b/bsnes/gb/iOS/GBActivityViewController.h new file mode 100644 index 00000000..adf5af67 --- /dev/null +++ b/bsnes/gb/iOS/GBActivityViewController.h @@ -0,0 +1,5 @@ +#import + +@interface GBActivityViewController : UIActivityViewController + +@end diff --git a/bsnes/gb/iOS/GBActivityViewController.m b/bsnes/gb/iOS/GBActivityViewController.m new file mode 100644 index 00000000..0c436a52 --- /dev/null +++ b/bsnes/gb/iOS/GBActivityViewController.m @@ -0,0 +1,10 @@ +#import "GBActivityViewController.h" + +@implementation GBActivityViewController + +- (UIModalPresentationStyle)modalPresentationStyle +{ + return UIModalPresentationFormSheet; +} + +@end diff --git a/bsnes/gb/iOS/GBBackgroundView.h b/bsnes/gb/iOS/GBBackgroundView.h new file mode 100644 index 00000000..41ed47ad --- /dev/null +++ b/bsnes/gb/iOS/GBBackgroundView.h @@ -0,0 +1,16 @@ +#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; +@property (nonatomic) bool fullScreenMode; + +- (void)enterPreviewMode:(bool)showLabel; +- (void)reloadThemeImages; +- (void)fadeOverlayOut; +@end diff --git a/bsnes/gb/iOS/GBBackgroundView.m b/bsnes/gb/iOS/GBBackgroundView.m new file mode 100644 index 00000000..13f532d0 --- /dev/null +++ b/bsnes/gb/iOS/GBBackgroundView.m @@ -0,0 +1,635 @@ +#import "GBBackgroundView.h" +#import "GBViewMetal.h" +#import "GBHapticManager.h" +#import "GBMenuViewController.h" +#import "GBViewController.h" +#import "GBROMManager.h" + +static double CGPointSquaredDistance(CGPoint a, CGPoint b) +{ + double deltaX = a.x - b.x; + double deltaY = a.y - b.y; + return deltaX * deltaX + deltaY * deltaY; +} + +static 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; + + UIView *_fadeView; + 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; + bool _fullScreenMode; +} + +- (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; +} + +- (void)setDefaultScreenLabel +{ + _screenLabel.text = @"Tap the Game Boy screen to open the menu and load a ROM from the library."; +} + + +- (instancetype)initWithLayout:(GBLayout *)layout; +{ + self = [super initWithImage:nil]; + if (!self) return nil; + + _layout = layout; + _touches = [NSMutableSet set]; + + _screenLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _screenLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightMedium]; + _screenLabel.textAlignment = NSTextAlignmentCenter; + _screenLabel.textColor = [UIColor whiteColor]; + _screenLabel.lineBreakMode = NSLineBreakByWordWrapping; + _screenLabel.numberOfLines = 0; + [self setDefaultScreenLabel]; + [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]; + + _fadeView = [[UIView alloc] initWithFrame:self.frame]; + _fadeView.backgroundColor = [UIColor colorWithWhite:0 alpha:0]; + _fadeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _fadeView.multipleTouchEnabled = true; + + [self addSubview:_dpadView]; + [self addSubview:_aButtonView]; + [self addSubview:_bButtonView]; + [self addSubview:_startButtonView]; + [self addSubview:_selectButtonView]; + [self addSubview:_fadeView]; + [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; + if (_fullScreenMode) { + self.fullScreenMode = false; + 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) { + GB_set_use_faux_analog_inputs(_gbView.gb, 0, false); + 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)) { + GB_set_use_faux_analog_inputs(_gbView.gb, 0, false); + 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; + + if (_fullScreenMode) { + CGRect fullScreenFrame = layout.fullScreenRect; + fullScreenFrame.origin.x /= [UIScreen mainScreen].scale; + fullScreenFrame.origin.y /= [UIScreen mainScreen].scale; + fullScreenFrame.size.width /= [UIScreen mainScreen].scale; + fullScreenFrame.size.height /= [UIScreen mainScreen].scale; + _gbView.frame = fullScreenFrame; + } + else { + _gbView.frame = screenFrame; + } + + screenFrame.origin.x += 8; + screenFrame.origin.y += 8; + screenFrame.size.width -= 16; + screenFrame.size.height -= 16; + + if (@available(iOS 13.0, *)) { + self.overrideUserInterfaceStyle = layout.theme.isDark? UIUserInterfaceStyleDark : UIUserInterfaceStyleLight; + self.tintColor = layout.theme.buttonColor; + } + + _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; +} + +- (bool)fullScreenMode +{ + return _fullScreenMode; +} + +- (void)setFullScreenMode:(bool)fullScreenMode +{ + if (fullScreenMode == _fullScreenMode) return; + _fullScreenMode = fullScreenMode; + [UIView animateWithDuration:1.0/3 animations:^{ + // Animating alpha has some weird quirks for some reason + _fadeView.backgroundColor = [UIColor colorWithWhite:0 alpha:fullScreenMode]; + [self setLayout:_layout]; + }]; + [self.window.rootViewController setNeedsStatusBarAppearanceUpdate]; +} + +@end diff --git a/bsnes/gb/iOS/GBCheatsController.h b/bsnes/gb/iOS/GBCheatsController.h new file mode 100644 index 00000000..81179665 --- /dev/null +++ b/bsnes/gb/iOS/GBCheatsController.h @@ -0,0 +1,7 @@ +#import +#import + +@interface GBCheatsController : UITableViewController +- (instancetype)initWithGameBoy:(GB_gameboy_t *)gb; +@end + diff --git a/bsnes/gb/iOS/GBCheatsController.m b/bsnes/gb/iOS/GBCheatsController.m new file mode 100644 index 00000000..beebc57e --- /dev/null +++ b/bsnes/gb/iOS/GBCheatsController.m @@ -0,0 +1,369 @@ +#import "GBCheatsController.h" +#import "GBROMManager.h" +#import "UIToolbar+disableCompact.h" +#import +#import + +@interface GBCheatsController() +@end + +@implementation GBCheatsController +{ + GB_gameboy_t *_gb; + NSIndexPath *_renamingPath; + __weak UITextField *_editingField; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 2; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + if (section == 0) return 1; + size_t count; + GB_get_cheats(_gb, &count); + self.toolbarItems[0].enabled = count; + ((UIButton *)(self.toolbarItems[0].customView.subviews[0])).enabled = count; + return count; +} + + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil]; + + UISwitch *button = [[UISwitch alloc] init]; + cell.accessoryView = button; + const GB_cheat_t *cheat = NULL; + if (indexPath.section == 0) { + button.on = GB_cheats_enabled(_gb); + cell.textLabel.text = @"Enable Cheats"; + } + else { + cheat = GB_get_cheats(_gb, NULL)[indexPath.row]; + button.on = cheat->enabled; + cell.textLabel.text = @(cheat->description) ?: @"Unnamed Cheat"; + button.enabled = GB_cheats_enabled(_gb); + } + + id block = ^(){ + if (!cheat) { + GB_set_cheats_enabled(_gb, button.on); + [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationNone]; + } + else { + GB_update_cheat(_gb, cheat, cheat->description, + cheat->address, cheat->bank, + cheat->value, cheat->old_value, cheat->use_old_value, + button.on); + } + }; + objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN); + + [button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + return cell; +} + +- (void)addCheat +{ + [self setEditing:false animated:true]; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle: @"Add Cheat" + message: @"Add a GameShark or Game Genie cheat code" + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.placeholder = @"Description"; + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + }]; + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.placeholder = @"Cheat Code"; + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + textField.keyboardType = UIKeyboardTypeASCIICapable; + }]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Add" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + size_t index = [self tableView:self.tableView numberOfRowsInSection:1]; + NSString *name = alertController.textFields[0].text; + if (GB_import_cheat(_gb, alertController.textFields[1].text.UTF8String, name.length? name.UTF8String : "Unnamed Cheat", true)) { + [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:1]] withRowAnimation:UITableViewRowAnimationAutomatic]; + } + else { + alertController.title = @"Invalid cheat code entered"; + [self presentViewController:alertController animated:true completion:nil]; + } + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; + [self presentViewController:alertController animated:true completion:nil]; +} + ++ (UIBarButtonItem *)buttonWithLabel:(NSString *)label + imageWithName:(NSString *)imageName + target:(id)target + action:(SEL)action +{ + if (@available(iOS 13.0, *)) { + UIImage *image = [UIImage systemImageNamed:imageName + withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]; + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + [button setImage:image forState:UIControlStateNormal]; + button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside]; + if (label) { + [button setTitle:label forState:UIControlStateNormal]; + [button setTitleColor:button.tintColor forState:UIControlStateNormal]; + button.titleEdgeInsets = UIEdgeInsetsMake(0, 4, 0, 0); + button.contentEdgeInsets = UIEdgeInsetsMake(0, 12, 0, 0); + } + [button sizeToFit]; + CGRect frame = button.frame; + frame.size.width = ceil(frame.size.width + (label? 4 : 0)); + if (@available(iOS 19.0, *)) { + if (label) { + frame.size.width += 12; + } + } + frame.size.height = 28; + button.frame = frame; + UIView *wrapper = [[UIView alloc] initWithFrame:button.bounds]; + [wrapper addSubview:button]; + UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:wrapper]; + return item; + } + return [[UIBarButtonItem alloc] initWithTitle:label style:UIBarButtonItemStylePlain target:target action:action]; +} + +- (void)importCheats +{ + [self setEditing:false animated:true]; + NSString *chtUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"cht", NULL); + + + UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[chtUTI] + inMode:UIDocumentPickerModeImport]; + if (@available(iOS 13.0, *)) { + picker.shouldShowFileExtensions = true; + } + picker.delegate = self; + [self presentViewController:picker animated:true completion:nil]; + + return; + +} + +- (void)exportCheats +{ + [self setEditing:false animated:true]; + NSString *cheatsFile = [[GBROMManager sharedManager] cheatsFile]; + GB_save_cheats(_gb, cheatsFile.UTF8String); + NSURL *url = [NSURL fileURLWithPath:cheatsFile]; + UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:@[url] + applicationActivities:nil]; + + controller.popoverPresentationController.barButtonItem = self.toolbarItems.firstObject; + + [self presentViewController:controller + animated:true + completion:nil]; + +} + +- (instancetype)initWithGameBoy:(GB_gameboy_t *)gb +{ + UITableViewStyle style = UITableViewStyleGrouped; + if (@available(iOS 13.0, *)) { + style = UITableViewStyleInsetGrouped; + } + self = [super initWithStyle:style]; + self.tableView.allowsSelectionDuringEditing = true; + self.navigationItem.rightBarButtonItem = self.editButtonItem; + + bool hasSFSymbols = false; + if (@available(iOS 13.0, *)) { + hasSFSymbols = true; + } + + UIBarButtonItem *export = hasSFSymbols? + [self.class buttonWithLabel:nil + imageWithName:@"square.and.arrow.up" + target:self + action:@selector(exportCheats)] : + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction + target:self + action:@selector(exportCheats)]; + + UIBarButtonItem *flexItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:NULL]; + UIBarButtonItem *import = [self.class buttonWithLabel:@"Import" + imageWithName:@"square.and.arrow.down" + target:self + action:@selector(importCheats)]; + + UIBarButtonItem *add = [self.class buttonWithLabel:@"Add" + imageWithName:@"plus" + target:self + action:@selector(addCheat)]; + + if (@available(iOS 19.0, *)) { + self.toolbarItems = @[export, + flexItem, + import, [UIBarButtonItem fixedSpaceItemOfWidth:0], add]; + } + else { + self.toolbarItems = @[export, + flexItem, + import, add]; + } + + _gb = gb; + return self; +} + +- (NSString *)title +{ + return @"Cheats"; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + return indexPath.section == 1; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section != 1) return; + if (editingStyle != UITableViewCellEditingStyleDelete) return; + + const GB_cheat_t *cheat = GB_get_cheats(_gb, NULL)[indexPath.row]; + GB_remove_cheat(_gb, cheat); + [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [self.navigationController setToolbarHidden:false animated:false]; + self.navigationController.toolbar.disableCompactLayout = true; +} + + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url +{ + [url startAccessingSecurityScopedResource]; + NSString *tempDir = NSTemporaryDirectory(); + NSString *newPath = [tempDir stringByAppendingPathComponent:@"import.cht"]; + [[NSFileManager defaultManager] copyItemAtPath:url.path toPath:newPath error:nil]; + [url stopAccessingSecurityScopedResource]; + unsigned count = [self tableView:self.tableView numberOfRowsInSection:1]; + + void (^load)(bool) = ^(bool replace) { + if (GB_load_cheats(_gb, newPath.UTF8String, replace)) { + [[NSFileManager defaultManager] removeItemAtPath:newPath error:nil]; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Import Failed" + message:@"The imported cheats file is invalid." + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alertController animated:true completion:nil]; + return; + } + + [[NSFileManager defaultManager] removeItemAtPath:newPath error:nil]; + unsigned newCount = [self tableView:self.tableView numberOfRowsInSection:1]; + if (!replace) { + NSMutableArray *paths = [NSMutableArray arrayWithCapacity:newCount - count]; + for (unsigned i = count; i < newCount; i++) { + [paths addObject:[NSIndexPath indexPathForRow:i inSection:1]]; + } + if (paths.count) { + [self.tableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationAutomatic]; + } + } + else { + NSMutableArray *paths = [NSMutableArray arrayWithCapacity:abs((signed)newCount - (signed)count)]; + for (unsigned i = MIN(newCount, count); i < count || i < newCount; i++) { + [paths addObject:[NSIndexPath indexPathForRow:i inSection:1]]; + } + if (newCount > count) { + [self.tableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationAutomatic]; + } + else { + [self.tableView deleteRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationAutomatic]; + } + + paths = [NSMutableArray arrayWithCapacity:MIN(newCount, count)]; + for (unsigned i = 0; i < count && i < newCount; i++) { + [paths addObject:[NSIndexPath indexPathForRow:i inSection:1]]; + } + [self.tableView reloadRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationRight]; + } + }; + + if (count) { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Replace Existing Cheats?" + message:@"Append the newly imported cheats or replace the existing ones?" + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Append" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + load(false); + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Replace" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { + load(true); + }]]; + [self presentViewController:alertController animated:true completion:nil]; + } + else { + load(true); + } +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 0) return; + if (!self.editing) return; + + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; + CGRect frame = cell.textLabel.frame; + frame.size.width = cell.textLabel.superview.frame.size.width - 8 - frame.origin.x; + UITextField *field = [[UITextField alloc] initWithFrame: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]; + _editingField = field; +} + +- (void)doneRename:(UITextField *)sender +{ + if (!_renamingPath) return; + const GB_cheat_t *cheat = GB_get_cheats(_gb, NULL)[_renamingPath.row]; + GB_update_cheat(_gb, cheat, sender.text.length? sender.text.UTF8String : "Unnamed Cheat", + cheat->address, cheat->bank, + cheat->value, cheat->old_value, cheat->use_old_value, + cheat->enabled); + [self.tableView reloadRowsAtIndexPaths:@[_renamingPath] withRowAnimation:UITableViewRowAnimationNone]; + _renamingPath = nil; +} + +- (void)setEditing:(BOOL)editing animated:(BOOL)animated +{ + [super setEditing:editing animated:animated]; + if (!editing && _editingField) { + [self doneRename:_editingField]; + } +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + NSString *cheatsFile = [[GBROMManager sharedManager] cheatsFile]; + [[NSFileManager defaultManager] removeItemAtPath:cheatsFile error:nil]; + GB_save_cheats(_gb, cheatsFile.UTF8String); +} + +@end diff --git a/bsnes/gb/iOS/GBCheckableAlertController.h b/bsnes/gb/iOS/GBCheckableAlertController.h new file mode 100644 index 00000000..74235657 --- /dev/null +++ b/bsnes/gb/iOS/GBCheckableAlertController.h @@ -0,0 +1,5 @@ +#import + +@interface GBCheckableAlertController : UIAlertController +@property UIAlertAction *selectedAction; +@end diff --git a/bsnes/gb/iOS/GBCheckableAlertController.m b/bsnes/gb/iOS/GBCheckableAlertController.m new file mode 100644 index 00000000..9b9ae738 --- /dev/null +++ b/bsnes/gb/iOS/GBCheckableAlertController.m @@ -0,0 +1,43 @@ +#import "GBCheckableAlertController.h" + +/* Private API */ +@interface UIAlertAction() +- (bool)_isChecked; +- (void)_setChecked:(bool)checked; +@end + +@implementation GBCheckableAlertController +{ + bool _addedChecks; +} + +- (void)viewWillAppear:(BOOL)animated +{ + if (@available(iOS 13.0, *)) { + if (!_addedChecks && _selectedAction) { + _addedChecks = true; + NSMutableSet *set = [NSMutableSet setWithObject:self.view]; + while (set.count) { + UIView *view = [set anyObject]; + [set removeObject:view]; + if ([view.debugDescription containsString:_selectedAction.debugDescription]) { + UIImageView *checkImage = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"checkmark"]]; + CGRect bounds = view.bounds; + CGRect frame = checkImage.frame; + frame.origin.x = bounds.size.width - frame.size.width - 12; + frame.origin.y = round((bounds.size.height - frame.size.height) / 2); + checkImage.frame = frame; + [view addSubview:checkImage]; + break; + } + [set addObjectsFromArray:view.subviews]; + } + } + } + else { + [_selectedAction _setChecked:true]; + } + [super viewWillAppear:animated]; +} + +@end diff --git a/bsnes/gb/iOS/GBColorWell.h b/bsnes/gb/iOS/GBColorWell.h new file mode 100644 index 00000000..0ba6a49c --- /dev/null +++ b/bsnes/gb/iOS/GBColorWell.h @@ -0,0 +1,7 @@ +#import + +API_AVAILABLE(ios(14.0)) +@interface GBColorWell : UIColorWell + +@end + diff --git a/bsnes/gb/iOS/GBColorWell.m b/bsnes/gb/iOS/GBColorWell.m new file mode 100644 index 00000000..7c8a33c2 --- /dev/null +++ b/bsnes/gb/iOS/GBColorWell.m @@ -0,0 +1,75 @@ +#import "GBColorWell.h" + +@implementation GBColorWell +{ + UIView *_proxyView; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + self.opaque = false; + [self addTarget:self action:@selector(setNeedsDisplay) forControlEvents:UIControlEventValueChanged]; + return self; +} + +- (void)drawRect:(CGRect)rect +{ + if (self.enabled) { + [[UIColor systemFillColor] set]; + } + else { + [[UIColor quaternarySystemFillColor] set]; + } + rect = self.bounds; + [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:6] fill]; + + rect.size.width -= 6; + rect.size.height -= 6; + rect.origin.x += 3; + rect.origin.y += 3; + + if (!self.enabled) { + rect.size.width -= 2; + rect.size.height -= 2; + rect.origin.x += 1; + rect.origin.y += 1; + } + + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSaveGState(context); + CGContextSetShadowWithColor(context, (CGSize){0, 0}, 2, [UIColor colorWithWhite:0 alpha:self.enabled? 0.25 : 0.125].CGColor); + [self.selectedColor set]; + [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:3] fill]; + CGContextRestoreGState(context); +} + +- (void)addSubview:(UIView *)view +{ + if (!_proxyView) { + _proxyView = [[UIView alloc] initWithFrame:self.bounds]; + [super addSubview:_proxyView]; + _proxyView.layer.mask = [CALayer layer]; + } + [_proxyView addSubview:view]; +} + +- (void)setSelectedColor:(UIColor *)selectedColor +{ + [super setSelectedColor:selectedColor]; + [self setNeedsDisplay]; +} + +- (UIColor *)selectedColor +{ + UIColor *orig = [super selectedColor]; + CGFloat red, green, blue; + [orig getRed:&red green:&green blue:&blue alpha:nil]; + red = MIN(MAX(red, 0), 1); + green = MIN(MAX(green, 0), 1); + blue = MIN(MAX(blue, 0), 1); + return [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; +} + +@end diff --git a/bsnes/gb/iOS/GBHapticManager.h b/bsnes/gb/iOS/GBHapticManager.h new file mode 100644 index 00000000..7f2e2d94 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBHapticManager.m b/bsnes/gb/iOS/GBHapticManager.m new file mode 100644 index 00000000..7c85d6e5 --- /dev/null +++ b/bsnes/gb/iOS/GBHapticManager.m @@ -0,0 +1,148 @@ +#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) { + @try { + /* Why must every method from this framework randomly throw exceptions whenever + anything remotely unusual happens? CoreHaptic sucks.*/ + [_rumblePlayer stopAtTime:0 error:nil]; + } + @catch (NSException *exception) {} + _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/bsnes/gb/iOS/GBHapticManagerLegacy.h b/bsnes/gb/iOS/GBHapticManagerLegacy.h new file mode 100644 index 00000000..1322d925 --- /dev/null +++ b/bsnes/gb/iOS/GBHapticManagerLegacy.h @@ -0,0 +1,5 @@ +#import "GBHapticManager.h" + +@interface GBHapticManagerLegacy : GBHapticManager + +@end diff --git a/bsnes/gb/iOS/GBHapticManagerLegacy.m b/bsnes/gb/iOS/GBHapticManagerLegacy.m new file mode 100644 index 00000000..43ce1601 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBHorizontalLayout.h b/bsnes/gb/iOS/GBHorizontalLayout.h new file mode 100644 index 00000000..d9d270eb --- /dev/null +++ b/bsnes/gb/iOS/GBHorizontalLayout.h @@ -0,0 +1,5 @@ +#import "GBLayout.h" + +@interface GBHorizontalLayout : GBLayout +- (instancetype)initWithTheme:(GBTheme *)theme cutoutOnRight:(bool)cutoutOnRight; +@end diff --git a/bsnes/gb/iOS/GBHorizontalLayout.m b/bsnes/gb/iOS/GBHorizontalLayout.m new file mode 100644 index 00000000..9da46851 --- /dev/null +++ b/bsnes/gb/iOS/GBHorizontalLayout.m @@ -0,0 +1,131 @@ +#define GBLayoutInternal +#import "GBHorizontalLayout.h" + +@implementation GBHorizontalLayout + +- (instancetype)initWithTheme:(GBTheme *)theme cutoutOnRight:(bool)cutoutOnRight +{ + self = [super initWithTheme:theme]; + if (!self) return nil; + + CGSize resolution = {self.resolution.height, 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; + + screenRect.origin.x = (resolution.width - screenRect.size.width) / 2; + screenRect.origin.y = (resolution.height - self.homeBar - screenRect.size.height) / 2; + self.fullScreenRect = screenRect; + + 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 = MIN(screenRect.size.width / 40, 16 * self.factor); + + 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) + (cutoutOnRight? 0 : self.cutout / 2), + round(resolution.height * 3 / 8) + }; + + double longWing = (resolution.width - screenRect.size.width) / 2 - screenBorderWidth * 5; + double shortWing = longWing - self.cutout; + double buttonRadius = 36 * self.factor; + CGSize buttonsDelta = [self buttonDeltaForMaxHorizontalDistance:(cutoutOnRight? shortWing : longWing) - buttonRadius * 2]; + CGPoint buttonsCenter = { + (resolution.width + screenRect.size.width + screenRect.origin.x) / 2 - (cutoutOnRight? self.cutout / 2 : 0), + self.dpadLocation.y, + }; + + self.abComboLocation = buttonsCenter; + + 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 + (cutoutOnRight? self.cutout / 2 : 0), + MIN(round(resolution.height * 3 / 4), self.dpadLocation.y + 180 * self.factor) + }; + + self.startLocation = (CGPoint){ + buttonsCenter.x - (cutoutOnRight? 0 : self.cutout / 2 ), + self.selectLocation.y + }; + + + 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} + controlPadding:0]; + } + + [self drawLabels]; + }]; + + self.background = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return self; +} + +- (instancetype)initWithTheme:(GBTheme *)theme +{ + assert(false); + __builtin_unreachable(); +} + +- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation +{ + 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/bsnes/gb/iOS/GBLayout.h b/bsnes/gb/iOS/GBLayout.h new file mode 100644 index 00000000..b2fbee98 --- /dev/null +++ b/bsnes/gb/iOS/GBLayout.h @@ -0,0 +1,51 @@ +#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 fullScreenRect; +@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; +@property (readonly) unsigned cutout; + +- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation; +@end + +#ifdef GBLayoutInternal + +@interface GBLayout() +@property UIImage *background; +@property CGRect screenRect; +@property CGRect fullScreenRect; +@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) bool hasFractionalPixels; + +- (void)drawBackground; +- (void)drawScreenBezels; +- (void)drawLogoInVerticalRange:(NSRange)range controlPadding:(double)padding; +- (void)drawLabels; +- (void)drawThemedLabelsWithBlock:(void (^)(void))block; + +- (CGSize)buttonDeltaForMaxHorizontalDistance:(double)distance; +@end + +#endif diff --git a/bsnes/gb/iOS/GBLayout.m b/bsnes/gb/iOS/GBLayout.m new file mode 100644 index 00000000..4ba974ef --- /dev/null +++ b/bsnes/gb/iOS/GBLayout.m @@ -0,0 +1,222 @@ +#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]; + if (!ret && [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { + ret = 32; // iPadOS is buggy af + } + } + }); + 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 || [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad)? 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 = MIN(self.screenRect.size.width / 40, 16 * _factor); + CGRect bezelRect = self.screenRect; + bezelRect.origin.x -= borderWidth; + bezelRect.origin.y -= borderWidth; + bezelRect.size.width += borderWidth * 2; + bezelRect.size.height += borderWidth * 2; + + if (bezelRect.origin.y + bezelRect.size.height >= self.size.height - _homeBar) { + bezelRect.origin.y = -32; + bezelRect.size.height = self.size.height + 32; + } + 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 controlPadding:(double)padding +{ + 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); + if (self.size.width > self.size.height) { + rect.origin.x += _cutout / 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 + padding, rect.origin.y}, + {_screenRect.size.width - padding * 2, 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/bsnes/gb/iOS/GBLibraryViewController.h b/bsnes/gb/iOS/GBLibraryViewController.h new file mode 100644 index 00000000..743df56f --- /dev/null +++ b/bsnes/gb/iOS/GBLibraryViewController.h @@ -0,0 +1,6 @@ +#import + +@interface GBLibraryViewController : UITabBarController +@end + + diff --git a/bsnes/gb/iOS/GBLibraryViewController.m b/bsnes/gb/iOS/GBLibraryViewController.m new file mode 100644 index 00000000..04ad3add --- /dev/null +++ b/bsnes/gb/iOS/GBLibraryViewController.m @@ -0,0 +1,26 @@ +#import "GBLibraryViewController.h" +#import "GBROMViewController.h" +#import "GBViewController.h" +#import "GBROMManager.h" + + +@implementation GBLibraryViewController + ++ (UIViewController *)wrapViewController:(UIViewController *)controller +{ + UINavigationController *ret = [[UINavigationController alloc] initWithRootViewController:controller]; + UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStylePlain + target:[UIApplication sharedApplication].delegate + action:@selector(dismissViewController)]; + [ret.visibleViewController.navigationItem setLeftBarButtonItem:close]; + return ret; +} + + +- (instancetype)init +{ + return (GBLibraryViewController *)[self.class wrapViewController:[[GBROMViewController alloc] init]]; +} + +@end diff --git a/bsnes/gb/iOS/GBMenuButton.h b/bsnes/gb/iOS/GBMenuButton.h new file mode 100644 index 00000000..846f050e --- /dev/null +++ b/bsnes/gb/iOS/GBMenuButton.h @@ -0,0 +1,5 @@ +#import + +@interface GBMenuButton : UIButton + +@end diff --git a/bsnes/gb/iOS/GBMenuButton.m b/bsnes/gb/iOS/GBMenuButton.m new file mode 100644 index 00000000..6c4fb521 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBMenuViewController.h b/bsnes/gb/iOS/GBMenuViewController.h new file mode 100644 index 00000000..d66a21c5 --- /dev/null +++ b/bsnes/gb/iOS/GBMenuViewController.h @@ -0,0 +1,7 @@ +#import + +@interface GBMenuViewController : UIAlertController ++ (instancetype)menu; +@property (nonatomic) NSInteger selectedButtonIndex; +@property (nonatomic, strong) NSArray *menuButtons; +@end diff --git a/bsnes/gb/iOS/GBMenuViewController.m b/bsnes/gb/iOS/GBMenuViewController.m new file mode 100644 index 00000000..802de1b5 --- /dev/null +++ b/bsnes/gb/iOS/GBMenuViewController.m @@ -0,0 +1,332 @@ +#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.", + @"Tip: Use an external game conrtoller and Screen Mirroring for a big screen experience!", + @"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; + NSMutableArray *_buttons; +} + ++ (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]]; + ret.selectedButtonIndex = -1; + ret->_buttons = [[NSMutableArray alloc] init]; + 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]; + if (_effectView) return; + static const struct { + NSString *label; + NSString *image; + NSString *selector; + bool requireRunning; + } buttons[] = { + {@"Reset", @"arrow.2.circlepath", SelectorString(reset), true}, + {@"Library", @"bookmark", SelectorString(openLibrary)}, + {@"Connect", @"LinkCableTemplate", SelectorString(openConnectMenu), true}, + {@"Model", @"ModelTemplate", SelectorString(changeModel)}, + {@"States", @"square.stack", SelectorString(openStates), true}, + {@"Cheats", @"CheatsTemplate", SelectorString(openCheats), true}, + {@"Settings", @"gear", SelectorString(openSettings)}, + {@"About", @"info.circle", SelectorString(showAbout)}, + }; + + double width = self.view.frame.size.width / 4; + double height = 88; + for (unsigned i = 0; i < 8; i++) { + unsigned x = i % 4; + unsigned y = i / 4; + 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(round(width * x), height * y, round(width), height); + button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [self.view addSubview:button]; + [_buttons addObject:button]; + + if (!buttons[i].selector) { + button.enabled = false; + continue; + } + SEL selector = NSSelectorFromString(buttons[i].selector); + if (buttons[i].requireRunning && ![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]; + } + + self.menuButtons = [_buttons copy]; + [self updateSelectedButton]; + + UIVisualEffect *effect = nil; + /* + // Unfortunately, UIGlassEffect is still very buggy. + if (@available(iOS 19.0, *)) { + effect = [[objc_getClass("UIGlassEffect") alloc] init]; + } + else */ { + effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent]; + } + + _effectView = [[UIVisualEffectView alloc] initWithEffect:nil]; + _effectView.layer.cornerRadius = 8; + _effectView.layer.masksToBounds = true; + [self.view.window 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; + [[NSUserDefaults standardUserDefaults] setInteger:tipIndex + 1 forKey:@"GBTipIndex"]; + _tipLabel.lineBreakMode = NSLineBreakByWordWrapping; + _tipLabel.numberOfLines = 3; + [_effectView.contentView addSubview:_tipLabel]; + [self layoutTip]; + + [UIView animateWithDuration:0.25 animations:^{ + _effectView.effect = effect; + _tipLabel.alpha = 0.8; + }]; + +} + +- (void)layoutTip +{ + [_effectView.superview addSubview:_effectView]; + 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}; + unsigned topInset = view.window.safeAreaInsets.top; + if (!topInset && [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { + topInset = 32; // iPadOS is buggy af + } + _effectView.frame = (CGRect) { + {round((outerSize.width - size.width - 16) / 2), topInset + 12}, + {size.width + 16, size.height + 16} + }; +} + + +- (void)viewWillDisappear:(BOOL)animated +{ + [UIView animateWithDuration:0.25 animations:^{ + _effectView.effect = nil; + _tipLabel.alpha = 0; + } completion:^(BOOL finished) { + [_effectView removeFromSuperview]; + }]; + [super viewWillDisappear:animated]; +} + +- (void)viewDidLayoutSubviews +{ + CGRect frame = self.view.frame; + if (frame.size.height < 88 * 2) { + [self.view.heightAnchor constraintEqualToConstant:frame.size.height + 88 * 2].active = true; + } + double width = MIN(MIN(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height) - 16, 400); + /* Damn I hate NSLayoutConstraints */ + if (frame.size.width != width) { + for (UIView *subview in self.view.subviews) { + if (![subview isKindOfClass:[GBMenuButton class]]) { + for (NSLayoutConstraint *constraint in subview.constraints) { + if (constraint.constant == frame.size.width) { + constraint.active = false; + } + } + [subview.widthAnchor constraintEqualToConstant:width].active = true; + for (UIView *subsubview in subview.subviews) { + for (NSLayoutConstraint *constraint in subsubview.constraints) { + if (constraint.constant == frame.size.width) { + constraint.active = false; + } + } + [subsubview.widthAnchor constraintEqualToConstant:width].active = true; + } + } + } + [self.view.widthAnchor constraintEqualToConstant:width].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]; +} + +#pragma mark - Vim Navigation + +- (BOOL)canBecomeFirstResponder +{ + return YES; +} + +- (NSArray *)keyCommands +{ + return @[ + [UIKeyCommand keyCommandWithInput:@"h" modifierFlags:0 action:@selector(moveLeft)], + [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:0 action:@selector(moveLeft)], + [UIKeyCommand keyCommandWithInput:@"j" modifierFlags:0 action:@selector(moveDown)], + [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:0 action:@selector(moveDown)], + [UIKeyCommand keyCommandWithInput:@"k" modifierFlags:0 action:@selector(moveUp)], + [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(moveUp)], + [UIKeyCommand keyCommandWithInput:@"l" modifierFlags:0 action:@selector(moveRight)], + [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:0 action:@selector(moveRight)], + [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(activateSelected)], + [UIKeyCommand keyCommandWithInput:@" " modifierFlags:0 action:@selector(activateSelected)], + [UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:0 action:@selector(dismissSelf)], + ]; +} + +- (void)moveLeft +{ + if (self.selectedButtonIndex == -1) { + self.selectedButtonIndex = 0; + } + else if (self.selectedButtonIndex % 4 > 0) { + self.selectedButtonIndex--; + } + [self updateSelectedButton]; +} + +- (void)moveRight +{ + if (self.selectedButtonIndex == -1) { + self.selectedButtonIndex = 0; + } + else if (self.selectedButtonIndex % 4 < 3 && self.selectedButtonIndex + 1 < self.menuButtons.count) { + self.selectedButtonIndex++; + } + [self updateSelectedButton]; + +} + +- (void)moveUp +{ + if (self.selectedButtonIndex == -1) { + self.selectedButtonIndex = 0; + } + else if (self.selectedButtonIndex >= 4) { + self.selectedButtonIndex -= 4; + } + [self updateSelectedButton]; +} + +- (void)moveDown +{ + if (self.selectedButtonIndex == -1) { + self.selectedButtonIndex = 0; + } + else if (self.selectedButtonIndex + 4 < self.menuButtons.count) { + self.selectedButtonIndex += 4; + } + [self updateSelectedButton]; +} + +- (void)activateSelected +{ + if (self.selectedButtonIndex >= 0 && self.selectedButtonIndex < self.menuButtons.count) { + UIButton *button = self.menuButtons[self.selectedButtonIndex]; + if (button.enabled) { + [button sendActionsForControlEvents:UIControlEventTouchUpInside]; + } + } +} + +- (void)updateSelectedButton +{ + for (NSInteger i = 0; i < self.menuButtons.count; i++) { + UIButton *button = self.menuButtons[i]; + if (i == self.selectedButtonIndex) { + button.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.3]; + button.layer.borderWidth = 2.0; + button.layer.borderColor = [UIColor systemBlueColor].CGColor; + if (@available(iOS 19.0, *)) { + button.layer.cornerRadius = 32.0; + } + else { + button.layer.cornerRadius = 8.0; + } + } + else { + button.backgroundColor = [UIColor clearColor]; + button.layer.borderWidth = 0.0; + button.layer.borderColor = [UIColor clearColor].CGColor; + } + } +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self becomeFirstResponder]; +} + +- (void)dismissSelf +{ + [self.presentingViewController dismissViewControllerAnimated:true completion:nil]; +} +@end diff --git a/bsnes/gb/iOS/GBOptionViewController.h b/bsnes/gb/iOS/GBOptionViewController.h new file mode 100644 index 00000000..283ae9cd --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBOptionViewController.m b/bsnes/gb/iOS/GBOptionViewController.m new file mode 100644 index 00000000..85f3910b --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBPaletteEditor.h b/bsnes/gb/iOS/GBPaletteEditor.h new file mode 100644 index 00000000..90cfb325 --- /dev/null +++ b/bsnes/gb/iOS/GBPaletteEditor.h @@ -0,0 +1,7 @@ +#import + +API_AVAILABLE(ios(14.0)) +@interface GBPaletteEditor : UITableViewController +- (instancetype)initForPalette:(NSString *)name; +@end + diff --git a/bsnes/gb/iOS/GBPaletteEditor.m b/bsnes/gb/iOS/GBPaletteEditor.m new file mode 100644 index 00000000..e254fb5b --- /dev/null +++ b/bsnes/gb/iOS/GBPaletteEditor.m @@ -0,0 +1,348 @@ +#import "GBPaletteEditor.h" +#import "GBColorWell.h" +#import "GBSlider.h" +#import "GBPalettePicker.h" + +static double blend(double from, double to, double position) +{ + return from * (1 - position) + to * position; +} + +@implementation GBPaletteEditor +{ + bool _displayingManual; + NSString *_paletteName; + bool _isCurrent; + + UITableViewCell *_nameCell; + UITextField *_nameField; + + UITableViewCell *_colorsCell; + UIColorWell *_colorWells[5]; + + UITableViewCell *_disabledLCDCell; + UISwitch *_disabledLCDSwitch; + + UITableViewCell *_manualCell; + UISwitch *_manualSwitch; + + UITableViewCell *_brightnessCell; + GBSlider *_brightnessSlider; + + UITableViewCell *_hueCell; + GBSlider *_hueSlider; + + UITableViewCell *_hueStrengthCell; + UISlider *_hueStrengthSlider; +} + + +- (instancetype)initForPalette:(NSString *)name +{ + self = [self initWithStyle:UITableViewStyleInsetGrouped]; + _paletteName = name; + _isCurrent = [[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] isEqual:name]; + return self; +} + +- (UITableViewCell *)sliderCellWithSlider:(UISlider *)slider +{ + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + CGRect rect = cell.contentView.bounds; + rect.size.width -= 24; + rect.size.height -= 24; + rect.origin.x += 12; + rect.origin.y += 12; + slider.frame = rect; + slider.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [cell.contentView addSubview:slider]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + [slider addTarget:self action:@selector(updateAutoColors) forControlEvents:UIControlEventValueChanged]; + return cell; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *theme = [defaults dictionaryForKey:@"GBThemes"][_paletteName]; + + { + _nameCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + CGRect frame = _nameCell.contentView.bounds; + frame.size.width -= - 32; + frame.origin.x += 16; + _nameField = [[UITextField alloc] initWithFrame:frame]; + _nameField.font = _nameCell.textLabel.font; + _nameField.text = _paletteName; + _nameField.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [_nameCell.contentView addSubview:_nameField]; + _nameCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + + { + static const unsigned wellSize = 36; + _colorsCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + CGRect frame = _nameCell.contentView.bounds; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake((frame.size.width - wellSize * 5) / 2, + (frame.size.height - wellSize) / 2, + wellSize * 5, + wellSize)]; + NSArray *titles = @[ + @"Darkest Color", + @"Dark Midtone", + @"Light Midtone", + @"Lightest Color", + @"Display Off Color", + ]; + for (unsigned i = 0; i < 5; i++) { + _colorWells[i] = [[GBColorWell alloc] initWithFrame:CGRectMake(i * wellSize, 0, wellSize, wellSize)]; + _colorWells[i].supportsAlpha = false; + _colorWells[i].title = titles[i]; + _colorWells[i].selectedColor = [UIColor colorWithRed:(([theme[@"Colors"][i] unsignedIntValue] >> 0) & 0xFF) / 255.0 + green:(([theme[@"Colors"][i] unsignedIntValue] >> 8) & 0xFF) / 255.0 + blue:(([theme[@"Colors"][i] unsignedIntValue] >> 16) & 0xFF) / 255.0 + alpha:1.0]; + [_colorWells[i] addTarget:self action:@selector(updateToggles) forControlEvents:UIControlEventValueChanged]; + + [view addSubview:_colorWells[i]]; + } + view.autoresizingMask = + UIViewAutoresizingFlexibleLeftMargin | + UIViewAutoresizingFlexibleRightMargin | + UIViewAutoresizingFlexibleTopMargin | + UIViewAutoresizingFlexibleBottomMargin; + [_colorsCell.contentView addSubview:view]; + _colorsCell.selectionStyle = UITableViewCellSelectionStyleNone; + } + + { + _disabledLCDCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + _disabledLCDSwitch = [[UISwitch alloc] init]; + _disabledLCDCell.accessoryView = _disabledLCDSwitch; + if ([theme[@"DisabledLCDColor"] boolValue]) { + _disabledLCDSwitch.on = true; + } + + [_disabledLCDSwitch addTarget:self action:@selector(updateToggles) forControlEvents:UIControlEventValueChanged]; + _disabledLCDCell.selectionStyle = UITableViewCellSelectionStyleNone; + _disabledLCDCell.textLabel.text = @"Distinct Disabled LCD Color"; + } + + { + _manualCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + _manualSwitch = [[UISwitch alloc] init]; + _manualCell.accessoryView = _manualSwitch; + if ([theme[@"Manual"] boolValue]) { + _manualSwitch.on = true; + } + + [_manualSwitch addTarget:self action:@selector(updateToggles) forControlEvents:UIControlEventValueChanged]; + _manualCell.selectionStyle = UITableViewCellSelectionStyleNone; + _manualCell.textLabel.text = @"Manual Mode"; + } + + { + _brightnessSlider = [[GBSlider alloc] init]; + _brightnessSlider.minimumValue = -1; + _brightnessSlider.maximumValue = 1; + _brightnessSlider.continuous = true; + _brightnessSlider.style = GBSliderStyleTicks; + _brightnessSlider.value = [theme[@"BrightnessBias"] doubleValue]; + _brightnessCell = [self sliderCellWithSlider:_brightnessSlider]; + } + + { + _hueSlider = [[GBSlider alloc] init]; + _hueSlider.minimumValue = 0; + _hueSlider.maximumValue = 360; + _hueSlider.continuous = true; + _hueSlider.style = GBSliderStyleHue; + _hueSlider.value = [theme[@"HueBias"] doubleValue] * 360; + _hueCell = [self sliderCellWithSlider:_hueSlider]; + } + + { + _hueStrengthSlider = [[UISlider alloc] init]; + _hueStrengthSlider.minimumValue = 0; + _hueStrengthSlider.maximumValue = 1; + _hueStrengthSlider.continuous = true; + _hueStrengthSlider.value = [theme[@"HueBiasStrength"] doubleValue]; + _hueStrengthCell = [self sliderCellWithSlider:_hueStrengthSlider]; + } + + [self updateToggles]; + self.title = [NSString stringWithFormat:@"Editing %@", _paletteName]; +} + +- (UIColor *)autoColorAtPositon:(double)position +{ + + UIColor *first = _colorWells[0].selectedColor; + UIColor *second = _colorWells[4].selectedColor; + + CGFloat firstRed, firstGreen, firstBlue; + CGFloat secondRed, secondGreen, secondBlue; + [first getRed:&firstRed green:&firstGreen blue:&firstBlue alpha:NULL]; + [second getRed:&secondRed green:&secondGreen blue:&secondBlue alpha:NULL]; + + + double brightness = 1 / pow(4, _brightnessSlider.value); + position = pow(position, brightness); + UIColor *hue = _hueSlider.thumbTintColor; + double bias = _hueStrengthSlider.value; + + CGFloat red, green, blue; + [hue getRed:&red green:&green blue:&blue alpha:NULL]; + red = 1 / pow(4, (red * 2 - 1) * bias); + green = 1 / pow(4, (green * 2 - 1) * bias); + blue = 1 / pow(4, (blue * 2 - 1) * bias); + UIColor *ret = [UIColor colorWithRed:blend(firstRed, secondRed, pow(position, red)) + green:blend(firstGreen, secondGreen, pow(position, green)) + blue:blend(firstBlue, secondBlue, pow(position, blue)) + alpha:1.0]; + return ret; +} + +- (void)updateAutoColors +{ + if (_disabledLCDSwitch.on) { + _colorWells[1].selectedColor = [self autoColorAtPositon:8 / 25.0]; + _colorWells[2].selectedColor = [self autoColorAtPositon:16 / 25.0]; + _colorWells[3].selectedColor = [self autoColorAtPositon:24 / 25.0]; + } + else { + _colorWells[1].selectedColor = [self autoColorAtPositon:1 / 3.0]; + _colorWells[2].selectedColor = [self autoColorAtPositon:2 / 3.0]; + _colorWells[3].selectedColor = _colorWells[4].selectedColor; + } + [self save]; +} +- (void)updateToggles +{ + if (_manualSwitch.on) { + _colorWells[1].enabled = true; + _colorWells[2].enabled = true; + _colorWells[3].enabled = true; + if (!(_colorWells[4].enabled = _disabledLCDSwitch.on)) { + _colorWells[4].selectedColor = _colorWells[3].selectedColor; + } + if (!_displayingManual) { + [self.tableView deleteSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(2, 3)] + withRowAnimation:UITableViewRowAnimationFade]; + } + [self save]; + } + else { + _colorWells[1].enabled = false; + _colorWells[2].enabled = false; + _colorWells[3].enabled = false; + _colorWells[4].enabled = true; + if (_displayingManual) { + [self.tableView insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(2, 3)] + withRowAnimation:UITableViewRowAnimationFade]; + } + [self updateAutoColors]; + } +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + _displayingManual = _manualSwitch.on; + return _displayingManual? 2 : 5; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + switch (section) { + case 0: return 1; // Name + case 1: return 3; // Colors + case 2: return 1; // Brightness Bias + case 3: return 2; // Hue Bias + default: return 0; + }; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section +{ + switch (section) { + case 0: return @"Palette Name"; + case 2: return @"Brightness Bias"; + case 3: return @"Hue Bias"; + } + return nil; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + switch (indexPath.section) { + case 0: return _nameCell; + case 1: { + switch (indexPath.row) { + case 0: return _colorsCell; + case 1: return _disabledLCDCell; + case 2: return _manualCell; + } + return nil; + } + case 2: return _brightnessCell; + case 3: return indexPath.row == 0? _hueCell : _hueStrengthCell; + } + return [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 2) return 63; + return [super tableView:tableView heightForRowAtIndexPath:indexPath]; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + return nil; +} + +- (NSNumber *)encodeWell:(unsigned)index +{ + CGFloat r, g, b; + [_colorWells[index].selectedColor getRed:&r green:&g blue:&b alpha:NULL]; + return @((((unsigned)round(r * 255) << 0) | + ((unsigned)round(g * 255) << 8) | + ((unsigned)round(b * 255) << 16) | + 0xFF000000)); +} + +- (void)save +{ + NSMutableDictionary *themes = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].mutableCopy; + [themes removeObjectForKey:_paletteName]; + if (![_paletteName isEqual:_nameField.text]) { + _paletteName = [GBPalettePicker makeUnique:_nameField.text]; + } + + themes[_paletteName] = @{ + @"BrightnessBias": @(_brightnessSlider.value), + @"Colors": @[[self encodeWell:0], + [self encodeWell:1], + [self encodeWell:2], + [self encodeWell:3], + [self encodeWell:4]], + @"DisabledLCDColor": _disabledLCDSwitch.on? @YES : @NO, + @"HueBias": @(_hueSlider.value / 360.0), + @"HueBiasStrength": @(_hueStrengthSlider.value), + @"Manual": _manualSwitch.on? @YES : @NO, + }; + + [[NSUserDefaults standardUserDefaults] setObject:themes forKey:@"GBThemes"]; + if (_isCurrent) { + [[NSUserDefaults standardUserDefaults] setObject:@"" forKey:@"GBCurrentTheme"]; // Force a reload + [[NSUserDefaults standardUserDefaults] setObject:_paletteName forKey:@"GBCurrentTheme"]; + } +} +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + [self save]; +} + +@end diff --git a/bsnes/gb/iOS/GBPalettePicker.h b/bsnes/gb/iOS/GBPalettePicker.h new file mode 100644 index 00000000..2edbc791 --- /dev/null +++ b/bsnes/gb/iOS/GBPalettePicker.h @@ -0,0 +1,8 @@ +#import +#import + +@interface GBPalettePicker : UITableViewController ++ (const GB_palette_t *)paletteForTheme:(NSString *)theme; ++ (bool)importPalette:(NSString *)path; ++ (NSString *)makeUnique:(NSString *)name; +@end diff --git a/bsnes/gb/iOS/GBPalettePicker.m b/bsnes/gb/iOS/GBPalettePicker.m new file mode 100644 index 00000000..93bbc8b8 --- /dev/null +++ b/bsnes/gb/iOS/GBPalettePicker.m @@ -0,0 +1,515 @@ +#import "GBPalettePicker.h" +#import "GBPaletteEditor.h" +#import + +/* TODO: Unify with Cocoa? */ +#define MAGIC 'SBPL' + +typedef struct __attribute__ ((packed)) { + uint32_t magic; + bool manual:1; + bool disabled_lcd_color:1; + unsigned padding:6; + struct GB_color_s colors[5]; + int32_t brightness_bias; + uint32_t hue_bias; + uint32_t hue_bias_strength; +} theme_t; + + +@interface GBPalettePicker () +@end + +@implementation GBPalettePicker +{ + NSArray * _cacheNames; + NSIndexPath *_renamingPath; + NSMutableSet *_tempFiles; +} + ++ (NSString *)makeUnique:(NSString *)name +{ + NSArray *builtins = @[ + @"Greyscale", + @"Lime (Game Boy)", + @"Olive (Pocket)", + @"Teal (Light)", + ]; + + NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"]; + if (dict[name] || [builtins containsObject:name]) { + unsigned i = 2; + while (true) { + NSString *attempt = [NSString stringWithFormat:@"%@ %u", name, i]; + if (!dict[attempt] && ![builtins containsObject:attempt]) { + return attempt; + } + i++; + } + } + return name; +} + ++ (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; +} + +- (instancetype)initWithStyle:(UITableViewStyle)style +{ + self = [super initWithStyle:style]; + self.navigationItem.rightBarButtonItem = self.editButtonItem; + _tempFiles = [NSMutableSet set]; + self.tableView.allowsSelectionDuringEditing = true; + return self; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 3; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + if (section == 0) return 4; + if (section == 2) return 3; + _cacheNames = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; + return _cacheNames.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil]; + + NSString *name = nil; + if (indexPath.section == 2) { + switch (indexPath.row) { + case 0: + cell.textLabel.text = @"New Palette"; + break; + case 1: + cell.textLabel.text = @"Import Palette"; + break; + case 2: + cell.textLabel.text = @"Restore Defaults"; + cell.textLabel.textColor = [UIColor systemRedColor]; + break; + } + return cell; + } + else if (indexPath.section == 0) { + name = @[ + @"Greyscale", + @"Lime (Game Boy)", + @"Olive (Pocket)", + @"Teal (Light)", + ][indexPath.row]; + } + else { + name = _cacheNames[indexPath.row]; + } + + cell.textLabel.text = name; + if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] isEqual:name]) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } + + cell.imageView.image = [self.class previewImageForTheme:name]; + return cell; + +} + + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url +{ + [url startAccessingSecurityScopedResource]; + if ([self.class importPalette:url.path]) { + [self.tableView reloadData]; + } + else { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Import Failed" + message:@"The imported palette file is invalid." + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alertController animated:true completion:nil]; + return; + } + [url stopAccessingSecurityScopedResource]; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (self.editing) { + if (indexPath.section != 1) return nil; + if (@available(iOS 14.0, *)) { + [self.navigationController pushViewController:[[GBPaletteEditor alloc] initForPalette:[self.tableView cellForRowAtIndexPath:indexPath].textLabel.text] + animated:true]; + } + return nil; + } + if (indexPath.section == 2) { + switch (indexPath.row) { + case 0: { + if (@available(iOS 14.0, *)) { + NSString *name = [self.class makeUnique:@"Untitled Palette"]; + NSMutableDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].mutableCopy; + srandom(time(NULL)); + dict[name] = @{ + @"BrightnessBias": @((random() & 0xFFF) / (double)0xFFF * 2 - 1), + @"Colors": @[@((random() & 0x3f3f3f) | 0xFF000000), @0, @0, @0, @((random() | 0xffc0c0c0))], + @"DisabledLCDColor": @YES, + @"HueBias": @((random() & 0xFFF) / (double)0xFFF), + @"HueBiasStrength": @((random() & 0xFFF) / (double)0xFFF), + @"Manual": @NO, + }; + [[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"GBThemes"]; + [self.navigationController pushViewController:[[GBPaletteEditor alloc] initForPalette:name] + animated:true]; + } + else { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Palette Editor Unavailable" + message:@"The palette editor requires iOS 14 or newer." + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alertController animated:true completion:nil]; + } + break; + } + case 1: { + [self setEditing:false animated:true]; + NSString *sbpUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"sbp", NULL); + + + UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[sbpUTI] + inMode:UIDocumentPickerModeImport]; + if (@available(iOS 13.0, *)) { + picker.shouldShowFileExtensions = true; + } + picker.delegate = self; + [self presentViewController:picker animated:true completion:nil]; + break; + } + case 2: { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Restore default palettes?" + message: @"The defaults palettes will be restored, changes will be reverted, and created or imported palettes will be deleted. This change cannot be undone." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Restore" + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *action) { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBCurrentTheme"]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBThemes"]; + [self.tableView reloadData]; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; + break; + } + } + return nil; + } + [[NSUserDefaults standardUserDefaults] setObject:[self.tableView cellForRowAtIndexPath:indexPath].textLabel.text + forKey:@"GBCurrentTheme"]; + [self.tableView reloadData]; + return nil; +} + + +- (NSString *)title +{ + return @"Monochrome Palette"; +} + +- (void)renameRow:(NSIndexPath *)indexPath +{ + if (indexPath.section != 1) return; + + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; + CGRect frame = cell.textLabel.frame; + frame.size.width = cell.textLabel.superview.frame.size.width - 8 - frame.origin.x; + UITextField *field = [[UITextField alloc] initWithFrame:frame]; + field.font = cell.textLabel.font; + field.text = cell.textLabel.text; + cell.textLabel.textColor = [UIColor clearColor]; + [[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 = [self.tableView cellForRowAtIndexPath:_renamingPath].textLabel.text; + + _renamingPath = nil; + if ([newName isEqualToString:oldName]) { + [self.tableView reloadData]; + return; + } + + NSMutableDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].mutableCopy; + newName = [self.class makeUnique:newName]; + + dict[newName] = dict[oldName]; + [dict removeObjectForKey:oldName]; + [[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"GBThemes"]; + if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] isEqual:oldName]) { + [[NSUserDefaults standardUserDefaults] setObject:newName forKey:@"GBCurrentTheme"]; + } + [self.tableView reloadData]; + _renamingPath = nil; +} + + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section != 1) return; + + if (editingStyle != UITableViewCellEditingStyleDelete) return; + NSString *rom = [self.tableView cellForRowAtIndexPath:indexPath].textLabel.text; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Delete “%@”?", rom] + message: @"This change cannot be undone." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Delete" + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *action) { + NSMutableDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].mutableCopy; + NSString *name = _cacheNames[indexPath.row]; + [dict removeObjectForKey:name]; + [[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"GBThemes"]; + [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + + if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] isEqual:name]) { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBCurrentTheme"]; + [self.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationFade]; + } + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + return indexPath.section == 1; +} + +- (NSString *)exportTheme:(NSString *)name +{ + theme_t theme = {0,}; + theme.magic = MAGIC; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *themeDict = [defaults dictionaryForKey:@"GBThemes"][name]; + NSArray *colors = themeDict[@"Colors"]; + if (colors.count <= 5) { + unsigned i = 0; + for (NSNumber *color in colors) { + uint32_t c = [color unsignedIntValue]; + theme.colors[i++] = (struct GB_color_s){ + (c & 0xFF), + ((c >> 8) & 0xFF), + ((c >> 16) & 0xFF)}; + } + } + + theme.manual = [themeDict[@"Manual"] boolValue]; + theme.disabled_lcd_color = [themeDict[@"DisabledLCDColor"] boolValue]; + + theme.brightness_bias = ([themeDict[@"BrightnessBias"] doubleValue] * 0x40000000); + theme.hue_bias = round([themeDict[@"HueBias"] doubleValue] * 0x80000000); + theme.hue_bias_strength = round([themeDict[@"HueBiasStrength"] doubleValue] * 0x80000000); + + size_t size = sizeof(theme); + if (theme.manual) { + size = theme.disabled_lcd_color? 5 + 5 * sizeof(theme.colors[0]) : 5 + 4 * sizeof(theme.colors[0]); + } + + NSString *path = [[NSTemporaryDirectory() stringByAppendingPathComponent:name] stringByAppendingPathExtension:@"sbp"]; + [[NSData dataWithBytes:&theme length:size] writeToFile:path atomically:false]; + [_tempFiles addObject:path]; + return path; +} + ++ (bool)importPalette:(NSString *)path +{ + NSData *data = [NSData dataWithContentsOfFile:path]; + theme_t theme = {0,}; + memcpy(&theme, data.bytes, MIN(sizeof(theme), data.length)); + if (theme.magic != MAGIC) { + return false; + } + + NSMutableDictionary *themeDict = [NSMutableDictionary dictionary]; + themeDict[@"Manual"] = theme.manual? @YES : @NO; + themeDict[@"DisabledLCDColor"] = theme.disabled_lcd_color? @YES : @NO; + +#define COLOR_TO_INT(color) ((color.r << 0) | (color.g << 8) | (color.b << 16) | 0xFF000000) + themeDict[@"Colors"] = @[ + @(COLOR_TO_INT(theme.colors[0])), + @(COLOR_TO_INT(theme.colors[1])), + @(COLOR_TO_INT(theme.colors[2])), + @(COLOR_TO_INT(theme.colors[3])), + @(COLOR_TO_INT(theme.colors[theme.disabled_lcd_color? 4 : 3])), + ]; + + + themeDict[@"BrightnessBias"] = @(theme.brightness_bias / (double)0x40000000); + themeDict[@"HueBias"] = @(theme.hue_bias / (double)0x80000000); + themeDict[@"HueBiasStrength"] = @(theme.hue_bias_strength / (double)0x80000000); + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSMutableDictionary *themes = [defaults dictionaryForKey:@"GBThemes"].mutableCopy; + NSString *baseName = path.lastPathComponent.stringByDeletingPathExtension; + NSString *newName = [self.class makeUnique:baseName]; + themes[newName] = themeDict; + [defaults setObject:themes forKey:@"GBThemes"]; + [defaults setObject:newName forKey:@"GBCurrentTheme"]; + return true; +} + +- (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) { + UIAction *deleteAction = [UIAction actionWithTitle:@"Delete" + image:[UIImage systemImageNamed:@"trash"] + identifier:nil + handler:^(UIAction *action) { + [self tableView:tableView + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:indexPath]; + }]; + + deleteAction.attributes = UIMenuElementAttributesDestructive; + return [UIMenu menuWithTitle:nil children:@[ + [UIAction actionWithTitle:@"Edit" + image:[UIImage systemImageNamed:@"paintbrush"] + identifier:nil + handler:^(__kindof UIAction *action) { + if (@available(iOS 14.0, *)) { + [self.navigationController pushViewController:[[GBPaletteEditor alloc] initForPalette:[self.tableView cellForRowAtIndexPath:indexPath].textLabel.text] + animated:true]; + } + else { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Palette Editor Unavailable" + message:@"The palette editor requires iOS 14 or newer." + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alertController animated:true completion:nil]; + return; + } + }], + [UIAction actionWithTitle:@"Share" + image:[UIImage systemImageNamed:@"square.and.arrow.up"] + identifier:nil + handler:^(__kindof UIAction *action) { + [self setEditing:false animated:true]; + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; + NSString *file = [self exportTheme:cell.textLabel.text]; + NSURL *url = [NSURL fileURLWithPath:file]; + UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:@[url] + applicationActivities:nil]; + + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { + controller.popoverPresentationController.sourceView = cell.contentView; + } + + [self presentViewController:controller + animated:true + completion:nil]; + }], + [UIAction actionWithTitle:@"Rename" + image:[UIImage systemImageNamed:@"pencil"] + identifier:nil + handler:^(__kindof UIAction *action) { + [self renameRow:indexPath]; + }], + deleteAction, + ]]; + }]; +} + +- (void)dealloc +{ + for (NSString *file in _tempFiles) { + [[NSFileManager defaultManager] removeItemAtPath:file error:nil]; + } +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self.tableView reloadData]; + [super viewWillAppear:animated]; +} + +@end diff --git a/bsnes/gb/iOS/GBPrinterFeedController.h b/bsnes/gb/iOS/GBPrinterFeedController.h new file mode 100644 index 00000000..ca4ac2e7 --- /dev/null +++ b/bsnes/gb/iOS/GBPrinterFeedController.h @@ -0,0 +1,6 @@ +#import + +@interface GBPrinterFeedController : UINavigationController +- (instancetype)initWithImage:(UIImage *)image; +@end + diff --git a/bsnes/gb/iOS/GBPrinterFeedController.m b/bsnes/gb/iOS/GBPrinterFeedController.m new file mode 100644 index 00000000..603cfd5e --- /dev/null +++ b/bsnes/gb/iOS/GBPrinterFeedController.m @@ -0,0 +1,127 @@ +#import "GBPrinterFeedController.h" +#import "GBViewController.h" +#import "GBActivityViewController.h" + +@implementation GBPrinterFeedController +{ + UIImage *_image; +} + +- (instancetype)initWithImage:(UIImage *)image +{ + _image = image; + UIViewController *scrollViewController = [[UIViewController alloc] init]; + scrollViewController.title = @"Printer Feed"; + if (@available(iOS 13.0, *)) { + scrollViewController.view.backgroundColor = [UIColor systemBackgroundColor]; + } + else { + scrollViewController.view.backgroundColor = [UIColor whiteColor]; + } + + UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:scrollViewController.view.bounds]; + + scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + scrollView.scrollEnabled = true; + scrollView.pagingEnabled = false; + scrollView.showsVerticalScrollIndicator = true; + scrollView.showsHorizontalScrollIndicator = false; + [scrollViewController.view addSubview:scrollView]; + + CGSize size = image.size; + while (size.width < 320) { + size.width *= 2; + size.height *= 2; + } + UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; + imageView.contentMode = UIViewContentModeScaleToFill; + imageView.frame = (CGRect){{0, 0}, size}; + imageView.layer.magnificationFilter = kCAFilterNearest; + + scrollView.contentSize = size; + [scrollView addSubview:imageView]; + + CGSize contentSize = size; + self.preferredContentSize = contentSize; + + self = [self initWithRootViewController:scrollViewController]; + UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStylePlain + target:self + action:@selector(dismissFromParent)]; + [self.visibleViewController.navigationItem setLeftBarButtonItem:close]; + [scrollViewController setToolbarItems:@[ + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction + target:self + action:@selector(presentShareSheet)], + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace + target:nil + action:nil], + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash + target:self + action:@selector(emptyFeed)], + + ] animated:false]; + [self setToolbarHidden:false animated:false]; + return self; +} + +- (UIView *)viewToMask +{ + UIView *targetView = self.view; + while (targetView.superview != targetView.window && + targetView.superview.superview != targetView.window){ + targetView = targetView.superview; + } + return targetView; +} + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) { + UIView *targetView = self.view; + CGRect frame = targetView.frame; + frame.origin.x = ([UIScreen mainScreen].bounds.size.width - 320) / 2; + frame.size.width = 320; + targetView.frame = frame; + + UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(frame.origin.x, 0, + 320, 4096) + byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight + cornerRadii:CGSizeMake(10, 10)]; + + CAShapeLayer *maskLayer = [CAShapeLayer layer]; + maskLayer.frame = self.view.bounds; + maskLayer.path = maskPath.CGPath; + + self.viewToMask.layer.mask = maskLayer; + + } +} + +- (void)presentShareSheet +{ + NSURL *url = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"Game Boy Printer Image.png"]]; + [UIImagePNGRepresentation(_image) writeToURL:url atomically:false]; + [self presentViewController:[[GBActivityViewController alloc] initWithActivityItems:@[url] + applicationActivities:nil] + animated:true + completion:nil]; +} + +- (void)emptyFeed +{ + [(GBViewController *)UIApplication.sharedApplication.delegate emptyPrinterFeed]; +} + +- (void)dismissFromParent +{ + [self.presentingViewController dismissViewControllerAnimated:true completion:nil]; +} + +- (UIModalPresentationStyle)modalPresentationStyle +{ + return UIModalPresentationFormSheet; +} +@end diff --git a/bsnes/gb/iOS/GBROMManager.h b/bsnes/gb/iOS/GBROMManager.h new file mode 100644 index 00000000..44e88184 --- /dev/null +++ b/bsnes/gb/iOS/GBROMManager.h @@ -0,0 +1,28 @@ +#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; +@property (readonly) NSString *cheatsFile; +@property (readonly) NSArray *forbiddenNames; + +@property (readonly) NSString *localRoot; +- (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/bsnes/gb/iOS/GBROMManager.m b/bsnes/gb/iOS/GBROMManager.m new file mode 100644 index 00000000..8d8c7087 --- /dev/null +++ b/bsnes/gb/iOS/GBROMManager.m @@ -0,0 +1,260 @@ +#import "GBROMManager.h" +#import +#import + +@implementation GBROMManager +{ + NSString *_romFile; + NSMutableDictionary *_cloudNameToFile; + bool _doneInitializing; +} + ++ (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"]; + _doneInitializing = true; + + // Pre 1.0.2 versions might have kept temp files in there incorrectly + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBDeletedInbox"]) { + [[NSFileManager defaultManager] removeItemAtPath:[self.localRoot stringByAppendingPathComponent:@"Inbox"] error:nil]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBDeletedInbox"]; + } + return self; +} + +- (NSArray *)forbiddenNames +{ + return @[@"Inbox", @"Boot ROMs"]; +} + +- (void)setCurrentROM:(NSString *)currentROM +{ + _romFile = nil; + if ([self.forbiddenNames containsObject:currentROM]) { + currentROM = nil; + } + _currentROM = currentROM; + bool foundROM = self.romFile; + + if (currentROM && !foundROM) { + _currentROM = nil; + } + + [[NSUserDefaults standardUserDefaults] setObject:_currentROM forKey:@"GBLastROM"]; + if (_doneInitializing) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil]; + } +} + +- (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 *)romDirectoryForROM:(NSString *)romFile +{ + if ([self.forbiddenNames containsObject:romFile]) { + return nil; + } + + return [self.localRoot stringByAppendingPathComponent:romFile]; +} + +- (NSString *)romFile +{ + if (_romFile) return _romFile; + if (!_currentROM) return nil; + return _romFile = [self romFileForDirectory:[self romDirectoryForROM:_currentROM]]; +} + +- (NSString *)romFileForROM:(NSString *)rom +{ + if (rom == _currentROM) { + return self.romFile; + } + + if ([self.forbiddenNames containsObject:rom]) { + return nil; + } + + return [self romFileForDirectory:[self romDirectoryForROM:rom]]; +} + +- (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]; +} + + +- (NSString *)cheatsFileForROM:(NSString *)rom +{ + return [self auxilaryFileForROM:rom withExtension:@"cht"]; +} + +- (NSString *)cheatsFile +{ + return [self cheatsFileForROM:_currentROM]; +} + +- (NSArray *)allROMs +{ + NSMutableArray *ret = [NSMutableArray array]; + NSString *root = self.localRoot; + 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 +{ + if ([self.forbiddenNames containsObject:name]) { + name = @"Imported ROM"; + } + NSString *root = self.localRoot; + 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]]; + friendlyName = [self makeNameUnique:friendlyName]; + + return [self importROM:romFile withName:friendlyName keepOriginal:keep]; +} + +- (NSString *)importROM:(NSString *)romFile withName:(NSString *)friendlyName keepOriginal:(bool)keep +{ + NSString *root = self.localRoot; + 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; + } + } + + // Remove the Inbox directory if empty after import + rmdir([self.localRoot stringByAppendingPathComponent:@"Inbox"].UTF8String); + + return friendlyName; +} + +- (NSString *)renameROM:(NSString *)rom toName:(NSString *)newName +{ + newName = [self makeNameUnique:newName]; + if ([rom isEqualToString:_currentROM]) { + self.currentROM = newName; + } + NSString *root = self.localRoot; + + [[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 = self.localRoot; + NSString *romDirectory = [root stringByAppendingPathComponent:rom]; + [[NSFileManager defaultManager] removeItemAtPath:romDirectory error:nil]; +} + + +- (NSString *)localRoot +{ + return NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true).firstObject; +} +@end diff --git a/bsnes/gb/iOS/GBROMViewController.h b/bsnes/gb/iOS/GBROMViewController.h new file mode 100644 index 00000000..77a710c8 --- /dev/null +++ b/bsnes/gb/iOS/GBROMViewController.h @@ -0,0 +1,14 @@ +#import + +@interface GBROMViewController : UITableViewController + +/* For inheritance */ +- (void)romSelectedAtIndex:(unsigned)index; +- (void)deleteROMAtIndex:(unsigned)index; +- (void)renameROM:(NSString *)oldName toName:(NSString *)newName; +- (void)duplicateROMAtIndex:(unsigned)index; +- (NSString *)rootPath; + +/* To be used by subclasses */ +- (UITableViewCell *)cellForROM:(NSString *)rom; +@end diff --git a/bsnes/gb/iOS/GBROMViewController.m b/bsnes/gb/iOS/GBROMViewController.m new file mode 100644 index 00000000..ee15e6e0 --- /dev/null +++ b/bsnes/gb/iOS/GBROMViewController.m @@ -0,0 +1,354 @@ +#import "GBROMViewController.h" +#import "GBROMManager.h" +#import "GBViewController.h" +#import "GBLibraryViewController.h" +#import +#import + +@implementation GBROMViewController +{ + NSIndexPath *_renamingPath; + NSArray *_roms; + __weak UIAlertController *_alertToRemove; +} + +- (instancetype)init +{ + self = [super initWithStyle:UITableViewStyleGrouped]; + self.navigationItem.rightBarButtonItem = self.editButtonItem; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reactivate) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + return self; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 2; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + if (section == 1) return 2; + return (_roms = [GBROMManager sharedManager].allROMs).count; +} + +- (UITableViewCell *)cellForROM:(NSString *)rom +{ + UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; + cell.textLabel.text = rom.lastPathComponent; + bool isCurrentROM = [rom isEqualToString:[GBROMManager sharedManager].currentROM]; + bool checkmark = isCurrentROM; + cell.accessoryType = checkmark? 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; + +} + +- (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 = @"Show Library in Files"; break; + } + return cell; + } + return [self cellForROM:_roms[[indexPath indexAtPosition:1]]]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) return [super tableView:tableView heightForRowAtIndexPath:indexPath]; + + return 60; +} + +- (NSString *)title +{ + return @"Local 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)romSelectedAtIndex:(unsigned)index +{ + NSString *rom = _roms[index]; + [GBROMManager sharedManager].currentROM = rom; + [self.presentingViewController dismissViewControllerAnimated:true completion:nil]; +} + +- (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); + NSString *zipUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"zip", NULL); + + NSMutableSet *extensions = [NSMutableSet set]; + [extensions addObjectsFromArray:(__bridge_transfer NSArray *)UTTypeCopyAllTagsWithClass((__bridge CFStringRef)gbUTI, kUTTagClassFilenameExtension)]; + [extensions addObjectsFromArray:(__bridge_transfer NSArray *)UTTypeCopyAllTagsWithClass((__bridge CFStringRef)gbcUTI, kUTTagClassFilenameExtension)]; + [extensions addObjectsFromArray:(__bridge_transfer 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 ?: @"", + zipUTI ?: @""] + 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: { + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"shareddocuments://%@", + [self.rootPath stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]]]]; + [[UIApplication sharedApplication] openURL:url + options:nil + completionHandler:nil]; + return; + } + } + } + [self romSelectedAtIndex:indexPath.row]; +} + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls +{ + [(GBViewController *)[UIApplication sharedApplication].delegate handleOpenURLs:urls + openInPlace:false]; +} + +- (UIModalPresentationStyle)modalPresentationStyle +{ + return UIModalPresentationOverFullScreen; +} + +- (void)deleteROMAtIndex:(unsigned)index +{ + NSString *rom = _roms[index]; + + [[GBROMManager sharedManager] deleteROM:rom]; + [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic]; + if ([[GBROMManager sharedManager].currentROM isEqualToString:rom]) { + [GBROMManager sharedManager].currentROM = nil; + } +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) return; + + if (editingStyle != UITableViewCellEditingStyleDelete) return; + NSString *rom = [self.tableView cellForRowAtIndexPath:indexPath].textLabel.text; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Delete “%@”?", rom] + message: @"Save data for this ROM will also be deleted." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Delete" + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *action) { + [self deleteROMAtIndex:indexPath.row]; + }]]; + [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:nil]]; + _alertToRemove = alert; // indexPath becoomes invalid if we reload, dismiss the alert if it happens + [self presentViewController:alert animated:true completion:nil]; +} + +- (void)renameRow:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) return; + + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; + CGRect frame = cell.textLabel.frame; + frame.size.width = cell.textLabel.superview.frame.size.width - 8 - frame.origin.x; + UITextField *field = [[UITextField alloc] initWithFrame:frame]; + field.font = cell.textLabel.font; + field.text = cell.textLabel.text; + cell.textLabel.textColor = [UIColor clearColor]; + [[cell.textLabel superview] addSubview:field]; + [field becomeFirstResponder]; + [field selectAll:nil]; + _renamingPath = indexPath; + [field addTarget:self action:@selector(doneRename:) forControlEvents:UIControlEventEditingDidEnd | UIControlEventEditingDidEndOnExit]; +} + +- (void)renameROM:(NSString *)oldName toName:(NSString *)newName +{ + [[GBROMManager sharedManager] renameROM:oldName toName:newName]; + [self.tableView reloadData]; +} + +- (void)doneRename:(UITextField *)sender +{ + if (!_renamingPath) return; + NSString *newName = sender.text; + NSString *oldName = [self.tableView cellForRowAtIndexPath:_renamingPath].textLabel.text; + + _renamingPath = nil; + if ([newName isEqualToString:oldName]) { + [self.tableView reloadData]; + return; + } + + if ([newName containsString:@"/"]) { + [self.tableView reloadData]; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Invalid Name" + message:@"You can't use a name that contains “/”. Please choose another name." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; + return; + } + if ([[GBROMManager sharedManager].forbiddenNames containsObject:newName]) { + [self.tableView reloadData]; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Invalid Name" + message:@"This name is reserved by SameBoy or iOS. Please choose another name." + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; + return; + } + [self renameROM:oldName toName:newName]; + _renamingPath = nil; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + return indexPath.section == 0; +} + +- (void)duplicateROMAtIndex:(unsigned)index +{ + [[GBROMManager sharedManager] duplicateROM:_roms[index]]; + [self.tableView reloadData]; +} + + +// 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) { + UIAction *deleteAction = [UIAction actionWithTitle:@"Delete" + image:[UIImage systemImageNamed:@"trash"] + identifier:nil + handler:^(UIAction *action) { + [self tableView:tableView + commitEditingStyle:UITableViewCellEditingStyleDelete + forRowAtIndexPath:indexPath]; + }]; + deleteAction.attributes = UIMenuElementAttributesDestructive; + NSMutableArray *items = @[ + [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) { + [self duplicateROMAtIndex:indexPath.row]; + }], + ].mutableCopy; + [items addObject:deleteAction]; + return [UIMenu menuWithTitle:nil children:items]; + }]; +} + +- (void)deselectRow +{ + if (self.tableView.indexPathForSelectedRow) { + [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:true]; + } +} + +- (void)reactivate +{ + [self deselectRow]; + // Do not auto-reload if busy + if (self.view.window.userInteractionEnabled) { + [self.tableView reloadData]; + if (self.presentedViewController == _alertToRemove) { + [self dismissViewControllerAnimated:true completion:nil]; + } + } +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self.tableView reloadData]; + [super viewWillAppear:animated]; +} + +- (NSString *)rootPath +{ + return [GBROMManager sharedManager].localRoot; +} + +@end diff --git a/bsnes/gb/iOS/GBSettingsViewController.h b/bsnes/gb/iOS/GBSettingsViewController.h new file mode 100644 index 00000000..25b18fae --- /dev/null +++ b/bsnes/gb/iOS/GBSettingsViewController.h @@ -0,0 +1,29 @@ +#import +#import +#import "GCExtendedGamepad+AllElements.h" +#import "GBTheme.h" + +typedef enum { + GBRight, + GBLeft, + GBUp, + GBDown, + GBA, + GBB, + GBSelect, + GBStart, + GBTurbo, + GBRewind, + GBUnderclock, + GBRapidA, + GBRapidB, + // GBHotkey1, // Todo + // GBHotkey2, // Todo + GBUnusedButton = 0xFF, +} GBButton; + +@interface GBSettingsViewController : UITableViewController ++ (UIViewController *)settingsViewControllerWithLeftButton:(UIBarButtonItem *)button; ++ (GBButton)controller:(GCController *)controller convertUsageToButton:(GBControllerUsage)usage; ++ (GBTheme *)themeNamed:(NSString *)name; +@end diff --git a/bsnes/gb/iOS/GBSettingsViewController.m b/bsnes/gb/iOS/GBSettingsViewController.m new file mode 100644 index 00000000..6cbfab2d --- /dev/null +++ b/bsnes/gb/iOS/GBSettingsViewController.m @@ -0,0 +1,995 @@ +#import "GBSettingsViewController.h" +#import "GBSlider.h" +#import "GBViewBase.h" +#import "GBThemesViewController.h" +#import "GBPalettePicker.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 = @"lightTemp"; +static NSString const *typeTurboSlider = @"turboSlider"; + +@implementation GBSettingsViewController +{ + NSArray *_structure; + UINavigationController *_detailsNavigation; + NSArray *> *_themes; // For prewarming + bool _iPadRoot; + __weak UISlider *_turboSlider; +} + ++ (UIImage *)settingsImageNamed:(NSString *)name +{ + UIImage *base = [UIImage imageNamed:name]; + UIGraphicsBeginImageContextWithOptions(base.size, false, base.scale); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:(CGRect){{0, 0}, base.size} cornerRadius:8]; + CGContextSaveGState(UIGraphicsGetCurrentContext()); + [path addClip]; + [base drawInRect:path.bounds]; + if (@available(iOS 19.0, *)) { + CGContextRestoreGState(UIGraphicsGetCurrentContext()); + UIImage *overlay = [UIImage imageNamed:@"settingsOverlay"]; + [overlay drawInRect:path.bounds]; + } + UIImage *ret = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return ret; +} + ++ (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 Cap", + @"items": @[ + @{@"type": typeCheck, @"pref": @"GBTurboCap", @"title": @"Cap Turbo Speed", @"block": ^void(GBSettingsViewController *controller) { + if ([[NSUserDefaults standardUserDefaults] floatForKey:@"GBTurboCap"] == 1.0) { + if (controller->_turboSlider) { + [[NSUserDefaults standardUserDefaults] setFloat:controller->_turboSlider.value forKey:@"GBTurboCap"]; + controller->_turboSlider.enabled = true; + } + else { + [[NSUserDefaults standardUserDefaults] setFloat:2.0 forKey:@"GBTurboCap"]; + } + } + else { + controller->_turboSlider.enabled = false; + } + }}, + @{@"type": typeTurboSlider, @"pref": @"GBTurboCap", @"min": @1.5, @"max": @4, @"minImage": @"tortoise.fill", @"maxImage": @"hare.fill"}, + ], + @"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."; + } + if ([[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]) { + return [NSString stringWithFormat:@"Turbo speed is capped to %u%%", (unsigned)round([[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"] * 100)]; + } + return @"Turbo speed is not capped"; + }, + }, + @{ + @"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", @"value": @(GB_MODEL_CGB_0),}, + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB A", @"value": @(GB_MODEL_CGB_A),}, + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB B", @"value": @(GB_MODEL_CGB_B),}, + @{@"type": typeRadio, @"pref": @"GBCGBModel", @"title": @"CPU CGB C", @"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",}, + @{@"type": typeRadio, @"pref": @"GBFilter", @"title": @"Flat CRT Display", @"value": @"FlatCRT",}, + ] + }, + @{ + @"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": @"Anti-aliased 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 contrast 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": typeBlock, + @"block": ^bool(GBSettingsViewController *controller) { + UITableViewStyle style = UITableViewStyleGrouped; + if (@available(iOS 13.0, *)) { + style = UITableViewStyleInsetGrouped; + } + [controller.navigationController pushViewController:[[GBPalettePicker alloc] initWithStyle:style] animated:true]; + return true; + }, + @"pref": @"GBCurrentTheme", + }], + @"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": @"GBControllersHideInterface", @"title": @"Hide UI While Using a Controller"}, + ], + @"footer": @"When enabled, the on-screen user interface will be hidden while a game controller is being used." + }, + @{ + @"header": @"Controller Joystick Behavior", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBFauxAnalogInputs", @"title": @"Digital", @"value": @NO}, + @{@"type": typeRadio, @"pref": @"GBFauxAnalogInputs", @"title": @"Faux Analog", @"value": @YES}, + ], + }, + @{ + @"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]; + } + } + ], + }, + + ]; + + + NSArray *rootItems = @[ + @{ + @"title": @"Emulation", + @"type": typeSubmenu, + @"submenu": emulationMenu, + @"image": [self settingsImageNamed:@"emulationSettings"], + }, + @{ + @"title": @"Video", + @"type": typeSubmenu, + @"submenu": videoMenu, + @"image": [self settingsImageNamed:@"videoSettings"], + }, + @{ + @"title": @"Audio", + @"type": typeSubmenu, + @"submenu": audioMenu, + @"image": [self settingsImageNamed:@"audioSettings"], + }, + @{ + @"title": @"Controls", + @"type": typeSubmenu, + @"submenu": controlsMenu, + @"image": [self settingsImageNamed:@"controlsSettings"], + }, + @{ + @"title": @"Themes", + @"type": typeSubmenu, + @"class": [GBThemesViewController class], + @"image": [self settingsImageNamed:@"themeSettings"], + }, + ]; + + + return @[ + @{ + @"items": rootItems, + } + ]; +} + + ++ (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 = nil; + if (@available(iOS 14.5, *)) { + split = [[UISplitViewController alloc] initWithStyle:UISplitViewControllerStyleDoubleColumn]; + split.displayModeButtonVisibility = UISplitViewControllerDisplayModeButtonVisibilityNever; + } + else { + 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; + root->_iPadRoot = true; + 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, *)) { + NSString *ret = element.localizedName; + if (ret) { + 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": @"Rapid A", @"value": @(GBRapidA)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rapid B", @"value": @(GBRapidB)}, + @{@"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"]; + NSString *type = item[@"type"]; + if (type == typeSubmenu || type == typeOptionSubmenu || type == typeBlock) { + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + cell.selectionStyle = UITableViewCellSelectionStyleBlue; + if (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[@"pref"]) { + cell.detailTextLabel.text = [[NSUserDefaults standardUserDefaults] stringForKey:item[@"pref"]]; + } + } + else if (type == typeRadio) { + id settingValue = ValueForItem(item); + id itemValue = item[@"value"]; + if (settingValue == itemValue || [settingValue isEqual:itemValue]) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } + } + else if (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"]]; + if (item[@"block"]) { + ((void(^)(GBSettingsViewController *))item[@"block"])(self); + } + 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 (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 (type == typeSeparator) { + cell.backgroundColor = [UIColor clearColor]; + cell.separatorInset = UIEdgeInsetsZero; + } + else if (type == typeSlider || + type == typeLightTemp || + type == typeTurboSlider) { + CGRect rect = cell.contentView.bounds; + rect.size.width -= 24; + rect.size.height -= 24; + rect.origin.x += 12; + rect.origin.y += 12; + UISlider *slider = [type == typeLightTemp? [GBSlider 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]; + if (type == typeTurboSlider) { + slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]] ?: 2.0; + _turboSlider = slider; + if (![[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]]) { + slider.enabled = false; + } + } + else { + 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]; + } + } + + __weak typeof(self) weakSelf = self; + id block = ^(){ + [[NSUserDefaults standardUserDefaults] setDouble:slider.value forKey:item[@"pref"]]; + if (type == typeTurboSlider) { + 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); + + [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"]; + if (@available(iOS 19.0, *)) { + if (_iPadRoot) { + cell.textLabel.textColor = [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) { + return cell.isSelected? [UIColor whiteColor] : [UIColor labelColor]; + }]; + } + } + return cell; +} + +- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSDictionary *item = [self itemForIndexPath:indexPath]; + NSString *type = item[@"type"]; + if (type == typeSubmenu || 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 (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 (type == typeBlock) { + if (((bool(^)(GBSettingsViewController *))item[@"block"])(self)) { + return indexPath; + } + } + return nil; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + NSDictionary *item = [self itemForIndexPath:indexPath]; + NSString *type = item[@"type"]; + if (type == typeSeparator) { + return 8; + } + if (type == typeSlider || + type == typeLightTemp || + type == typeTurboSlider) { + 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/bsnes/gb/iOS/GBSlider.h b/bsnes/gb/iOS/GBSlider.h new file mode 100644 index 00000000..e94b9aac --- /dev/null +++ b/bsnes/gb/iOS/GBSlider.h @@ -0,0 +1,11 @@ +#import + +typedef enum { + GBSliderStyleTemperature, + GBSliderStyleHue, + GBSliderStyleTicks, +} GBSliderStyle; + +@interface GBSlider : UISlider +@property GBSliderStyle style; +@end diff --git a/bsnes/gb/iOS/GBSlider.m b/bsnes/gb/iOS/GBSlider.m new file mode 100644 index 00000000..7ccfc63e --- /dev/null +++ b/bsnes/gb/iOS/GBSlider.m @@ -0,0 +1,210 @@ +#import "GBSlider.h" +#import + +#if !__has_include() +/* Building with older SDKs */ +API_AVAILABLE(ios(19.0)) +@interface UISliderTrackConfiguration : NSObject +@property (nonatomic, readwrite) bool allowsTickValuesOnly; +@property (nonatomic, readwrite) float neutralValue; ++ (instancetype)configurationWithNumberOfTicks:(NSInteger)ticks; +@end + +@interface UISlider (configuration) +@property(nonatomic, copy, nullable) UISliderTrackConfiguration *trackConfiguration API_AVAILABLE(ios(19.0)); +@end +#endif + +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 GBSlider +{ + GBSliderStyle _style; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + [self addTarget:self action:@selector(valueChanged) forControlEvents:UIControlEventValueChanged]; + self.style = GBSliderStyleTemperature; + return self; +} + +- (void)updateTint +{ + if (_style == GBSliderStyleTemperature) { + double r, g, b; + temperature_tint(self.value, &r, &g, &b); + self.thumbTintColor = [UIColor colorWithRed:r green:g blue:b alpha:1.0]; + } + if (_style == GBSliderStyleHue) { + double hue = (self.value - self.minimumValue) / (self.maximumValue - self.minimumValue); + double r = 0, g = 0, b =0 ; + double t = fmod(hue * 6, 1); + switch ((int)(hue * 6) % 6) { + case 0: + r = 1; + g = t; + break; + case 1: + r = 1 - t; + g = 1; + break; + case 2: + g = 1; + b = t; + break; + case 3: + g = 1 - t; + b = 1; + break; + case 4: + b = 1; + r = t; + break; + case 5: + b = 1 - t; + r = 1; + break; + } + 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 && _style != GBSliderStyleHue) { + self.value = 0; + } + [self updateTint]; +} + +-(UIImage *)maximumTrackImageForState:(UIControlState)state +{ + return [[UIImage alloc] init]; +} + + +-(UIImage *)minimumTrackImageForState:(UIControlState)state +{ + return [[UIImage alloc] init]; +} + +- (void)drawRect:(CGRect)rect +{ + bool solarium = false; + if (@available(iOS 19.0, *)) { + solarium = true; + } + CGSize size = self.bounds.size; + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(2, round(size.height / 2 - 1.5), size.width - 4, 3) cornerRadius:4]; + if (_style != GBSliderStyleHue) { + [path appendPath:[UIBezierPath bezierPathWithRoundedRect:CGRectMake(round(size.width / 2 - 1.5), 12, 3, size.height - 24) cornerRadius:4]]; + } + if (_style == GBSliderStyleTicks) { + [path appendPath:[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 12, 3, size.height - 24) cornerRadius:4]]; + [path appendPath:[UIBezierPath bezierPathWithRoundedRect:CGRectMake(size.width - 3, 12, 3, size.height - 24) cornerRadius:4]]; + } + if (_style == GBSliderStyleHue) { + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(3, round(size.height / 2 - 1.5) + 1 - solarium, size.width - 6, solarium? 2 : 1) cornerRadius:8]; + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSaveGState(context); + [path addClip]; + + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + CGColorRef colors[] = { + UIColor.redColor.CGColor, + UIColor.yellowColor.CGColor, + UIColor.greenColor.CGColor, + UIColor.cyanColor.CGColor, + UIColor.blueColor.CGColor, + UIColor.magentaColor.CGColor, + UIColor.redColor.CGColor, + }; + CFArrayRef colorsArray = CFArrayCreate(NULL, (const void **)colors, 7, &kCFTypeArrayCallBacks); + CGGradientRef gradient = CGGradientCreateWithColors(colorspace, colorsArray, NULL); + unsigned spacing = solarium? 16 : 3; + CGContextDrawLinearGradient(context, + gradient, + (CGPoint){spacing, 0}, + (CGPoint){size.width - spacing, 0}, + kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + CFRelease(gradient); + CFRelease(colorsArray); + CFRelease(colorspace); + CGContextRestoreGState(context); + + } + [[UIColor colorWithRed:120 / 255.0 + green:120 / 255.0 + blue:130 / 255.0 + alpha:70 / 255.0] set]; + if (!solarium) { + [path fill]; + } + + [super drawRect:rect]; +} + +- (void)setStyle:(GBSliderStyle)style +{ + _style = style; + if (@available(iOS 19.0, *)) { + switch (_style) { + case GBSliderStyleTemperature: + case GBSliderStyleTicks: { + UISliderTrackConfiguration *conf = [objc_getClass("UISliderTrackConfiguration") configurationWithNumberOfTicks:3]; + conf.allowsTickValuesOnly = false; + conf.neutralValue = 0.5; + self.trackConfiguration = conf; + self.maximumTrackTintColor = nil; + self.minimumTrackTintColor = nil; + break; + } + case GBSliderStyleHue: { + UISliderTrackConfiguration *conf = [objc_getClass("UISliderTrackConfiguration") configurationWithNumberOfTicks:0]; + conf.allowsTickValuesOnly = false; + self.trackConfiguration = conf; + self.minimumTrackTintColor = [UIColor clearColor]; + break; + } + } + } +} + +- (GBSliderStyle)style +{ + return _style; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + [self setNeedsDisplay]; +} + +@end diff --git a/bsnes/gb/iOS/GBSlotButton.h b/bsnes/gb/iOS/GBSlotButton.h new file mode 100644 index 00000000..f3e90f0b --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBSlotButton.m b/bsnes/gb/iOS/GBSlotButton.m new file mode 100644 index 00000000..dc0ba944 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBStatesViewController.h b/bsnes/gb/iOS/GBStatesViewController.h new file mode 100644 index 00000000..c32d9141 --- /dev/null +++ b/bsnes/gb/iOS/GBStatesViewController.h @@ -0,0 +1,5 @@ +#import + +@interface GBStatesViewController : UIViewController + +@end diff --git a/bsnes/gb/iOS/GBStatesViewController.m b/bsnes/gb/iOS/GBStatesViewController.m new file mode 100644 index 00000000..a6d8efa6 --- /dev/null +++ b/bsnes/gb/iOS/GBStatesViewController.m @@ -0,0 +1,127 @@ +#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]; + UIView *root = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0x300, 0x300)]; + [self.view addSubview:root]; + 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]; +} + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + self.view.subviews.firstObject.frame = [self.view.safeAreaLayoutGuide layoutFrame]; +} + +- (NSString *)title +{ + return @"Save States"; +} +@end diff --git a/bsnes/gb/iOS/GBTheme.h b/bsnes/gb/iOS/GBTheme.h new file mode 100644 index 00000000..884cd79b --- /dev/null +++ b/bsnes/gb/iOS/GBTheme.h @@ -0,0 +1,27 @@ +#import + +@interface GBTheme : NSObject +@property (readonly, direct) UIColor *brandColor; +@property (readonly, direct) UIColor *buttonColor; +@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/bsnes/gb/iOS/GBTheme.m b/bsnes/gb/iOS/GBTheme.m new file mode 100644 index 00000000..448b058a --- /dev/null +++ b/bsnes/gb/iOS/GBTheme.m @@ -0,0 +1,236 @@ +#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; +} + + +- (UIColor *)buttonColor +{ + double r, g, b; + [_brandColor getRed:&r green:&g blue:&b alpha:NULL]; + if (r == 1.0 && g == 1.0 && b == 1.0) { + return _backgroundGradientTop; + } + return [UIColor colorWithRed:r green:g blue:b alpha:1.0]; +} + +- (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 cutoutOnRight:false]; + _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/bsnes/gb/iOS/GBThemePreviewController.h b/bsnes/gb/iOS/GBThemePreviewController.h new file mode 100644 index 00000000..bac864e9 --- /dev/null +++ b/bsnes/gb/iOS/GBThemePreviewController.h @@ -0,0 +1,7 @@ +#import +#import "GBTheme.h" + +@interface GBThemePreviewController : UIViewController +- (instancetype)initWithTheme:(GBTheme *)theme; +@end + diff --git a/bsnes/gb/iOS/GBThemePreviewController.m b/bsnes/gb/iOS/GBThemePreviewController.m new file mode 100644 index 00000000..efde9e6d --- /dev/null +++ b/bsnes/gb/iOS/GBThemePreviewController.m @@ -0,0 +1,132 @@ +#import "GBThemePreviewController.h" +#import "GBVerticalLayout.h" +#import "GBHorizontalLayout.h" +#import "GBBackgroundView.h" + +@implementation GBThemePreviewController +{ + GBHorizontalLayout *_horizontalLayoutLeft; + GBHorizontalLayout *_horizontalLayoutRight; + GBVerticalLayout *_verticalLayout; + GBBackgroundView *_backgroundView; +} + +- (instancetype)initWithTheme:(GBTheme *)theme +{ + self = [super init]; + _horizontalLayoutLeft = [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:false]; + _horizontalLayoutRight = _horizontalLayoutLeft.cutout? + [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:true] : + _horizontalLayoutLeft; + _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 = nil; + switch (orientation) { + default: + case UIInterfaceOrientationUnknown: + case UIInterfaceOrientationPortrait: + case UIInterfaceOrientationPortraitUpsideDown: + layout = _verticalLayout; + break; + case UIInterfaceOrientationLandscapeRight: + layout = _horizontalLayoutLeft; + break; + case UIInterfaceOrientationLandscapeLeft: + layout = _horizontalLayoutRight; + break; + } + + _backgroundView.frame = [layout viewRectForOrientation:orientation]; + _backgroundView.layout = layout; +} + +- (void)showPopup +{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Apply Theme" + message:[NSString stringWithFormat:@"Apply “%@” as the current theme?", _verticalLayout.theme.name] + 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/bsnes/gb/iOS/GBThemesViewController.h b/bsnes/gb/iOS/GBThemesViewController.h new file mode 100644 index 00000000..348597e4 --- /dev/null +++ b/bsnes/gb/iOS/GBThemesViewController.h @@ -0,0 +1,7 @@ +#import +#import "GBTheme.h" + +@interface GBThemesViewController : UITableViewController ++ (NSArray *> *)themes; +@end + diff --git a/bsnes/gb/iOS/GBThemesViewController.m b/bsnes/gb/iOS/GBThemesViewController.m new file mode 100644 index 00000000..a6642bd2 --- /dev/null +++ b/bsnes/gb/iOS/GBThemesViewController.m @@ -0,0 +1,105 @@ +#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]; + [super viewWillAppear:animated]; +} + +@end diff --git a/bsnes/gb/iOS/GBVerticalLayout.h b/bsnes/gb/iOS/GBVerticalLayout.h new file mode 100644 index 00000000..a5720eaa --- /dev/null +++ b/bsnes/gb/iOS/GBVerticalLayout.h @@ -0,0 +1,5 @@ +#import "GBLayout.h" + +@interface GBVerticalLayout : GBLayout + +@end diff --git a/bsnes/gb/iOS/GBVerticalLayout.m b/bsnes/gb/iOS/GBVerticalLayout.m new file mode 100644 index 00000000..a42d97f9 --- /dev/null +++ b/bsnes/gb/iOS/GBVerticalLayout.m @@ -0,0 +1,93 @@ +#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; + + screenRect.origin.x = (resolution.width - screenRect.size.width) / 2; + screenRect.origin.y = (resolution.height - screenRect.size.height) / 2; + self.fullScreenRect = screenRect; + + double screenBorderWidth = MIN(screenRect.size.width / 40, 16 * self.factor); + screenRect.origin.y = self.minY + MIN(screenBorderWidth * 2, 20 * self.factor); + self.screenRect = screenRect; + + double controlAreaStart = screenRect.origin.y + screenRect.size.height + MIN(screenBorderWidth * 2, 20 * self.factor); + + 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) { + [self drawLogoInVerticalRange:(NSRange){controlAreaStart + screenBorderWidth, 24 * self.factor} + controlPadding:0]; + } + else if (middleSpace > 160 * self.factor) { + [self drawLogoInVerticalRange:(NSRange){controlAreaStart + screenBorderWidth, 24 * self.factor} + controlPadding:self.dpadLocation.x * 2]; + } + + [self drawLabels]; + }]; + + self.background = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return self; +} + +@end diff --git a/bsnes/gb/iOS/GBView.h b/bsnes/gb/iOS/GBView.h new file mode 100644 index 00000000..f30cf5a1 --- /dev/null +++ b/bsnes/gb/iOS/GBView.h @@ -0,0 +1,5 @@ +#import "GBViewBase.h" + +@interface GBView : GBViewBase + +@end diff --git a/bsnes/gb/iOS/GBView.m b/bsnes/gb/iOS/GBView.m new file mode 100644 index 00000000..40e08b38 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GBViewController.h b/bsnes/gb/iOS/GBViewController.h new file mode 100644 index 00000000..e27714c6 --- /dev/null +++ b/bsnes/gb/iOS/GBViewController.h @@ -0,0 +1,34 @@ +#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)openConnectMenu; +- (void)openCheats; +- (void)emptyPrinterFeed; +- (void)saveStateToFile:(NSString *)file; +- (bool)loadStateFromFile:(NSString *)file; +- (bool)handleOpenURLs:(NSArray *)urls + openInPlace:(bool)inPlace; +- (void)dismissViewController; +@property (nonatomic) GBRunMode runMode; +@end diff --git a/bsnes/gb/iOS/GBViewController.m b/bsnes/gb/iOS/GBViewController.m new file mode 100644 index 00000000..a66023e6 --- /dev/null +++ b/bsnes/gb/iOS/GBViewController.m @@ -0,0 +1,2211 @@ +#import "GBViewController.h" +#import "GBHorizontalLayout.h" +#import "GBVerticalLayout.h" +#import "GBViewMetal.h" +#import "GBAudioClient.h" +#import "GBROMManager.h" +#import "GBLibraryViewController.h" +#import "GBBackgroundView.h" +#import "GBHapticManager.h" +#import "GBMenuViewController.h" +#import "GBOptionViewController.h" +#import "GBAboutController.h" +#import "GBSettingsViewController.h" +#import "GBPalettePicker.h" +#import "GBStatesViewController.h" +#import "GBCheckableAlertController.h" +#import "GBPrinterFeedController.h" +#import "GBCheatsController.h" +#import "GCExtendedGamepad+AllElements.h" +#import "GBZipReader.h" +#import +#import +#import +#import + +#if !__has_include() +/* Building with older SDKs */ + +typedef NS_ENUM(NSInteger, UIMenuSystemElementGroupPreference) { + UIMenuSystemElementGroupPreferenceAutomatic = 0, + UIMenuSystemElementGroupPreferenceRemoved, + UIMenuSystemElementGroupPreferenceIncluded, +}; + +API_AVAILABLE(ios(19.0)) +@interface UIMainMenuSystemConfiguration : NSObject +@property (nonatomic, assign) UIMenuSystemElementGroupPreference newScenePreference; +@property (nonatomic, assign) UIMenuSystemElementGroupPreference documentPreference; +@property (nonatomic, assign) UIMenuSystemElementGroupPreference printingPreference; +@property (nonatomic, assign) UIMenuSystemElementGroupPreference findingPreference; +@property (nonatomic, assign) UIMenuSystemElementGroupPreference toolbarPreference; +@property (nonatomic, assign) UIMenuSystemElementGroupPreference sidebarPreference; +@property (nonatomic, assign) UIMenuSystemElementGroupPreference inspectorPreference; +@property (nonatomic, assign) UIMenuSystemElementGroupPreference textFormattingPreference; +@end + +API_AVAILABLE(ios(19.0)) +@interface UIMainMenuSystem : UIMenuSystem +@property (class, nonatomic, readonly) UIMainMenuSystem *sharedSystem; +- (void)setBuildConfiguration:(UIMainMenuSystemConfiguration *)configuration buildHandler:(void(^)(NSObject *builder))buildHandler; +@end + +API_AVAILABLE(ios(19.0)) +@interface NSObject(UIMenuBuilder) +- (void)insertElements:(NSArray *)childElements atStartOfMenuForIdentifier:(UIMenuIdentifier)parentIdentifier; +@end + +#endif + + +static UIImage *CreateMenuImage(NSString *name) +{ + static const unsigned size = 20; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(size, size)]; + CGRect destRect = {0,}; + UIImage *source = [UIImage imageNamed:name]; + CGSize sourceSize = source.size; + if (sourceSize.width > sourceSize.height) { + destRect.size.width = size; + destRect.size.height = sourceSize.height * size / sourceSize.width; + destRect.origin.y = (size - destRect.size.height) / 2; + } + else { + destRect.size.height = size; + destRect.size.width = sourceSize.width * size / sourceSize.height; + destRect.origin.x = (size - destRect.size.width) / 2; + } + UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *myContext) { + [source drawInRect:destRect]; + }]; + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + +} + +API_AVAILABLE(ios(13.0)) +@implementation UIKeyCommand (KeyCommandWithImage) + ++ (instancetype)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action title:(NSString *)title image:(UIImage *)image +{ + UIKeyCommand *ret = [self keyCommandWithInput:input modifierFlags:modifierFlags action:action]; + ret.title = title; + ret.image = image; + return ret; +} + +@end + +@implementation GBViewController +{ + GB_gameboy_t _gb; + GBView *_gbView; + dispatch_queue_t _runQueue; + + volatile bool _running; + volatile bool _stopping; + bool _rewind; + bool _rewindOver; + bool _romLoaded; + bool _swappingROM; + bool _skipAutoLoad; + + bool _rapidA, _rapidB; + uint8_t _rapidACount, _rapidBCount; + + UIInterfaceOrientation _orientation; + GBHorizontalLayout *_horizontalLayoutLeft; + GBHorizontalLayout *_horizontalLayoutRight; + 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; + AVCaptureDevice *_frontCaptureDevice; + AVCaptureDevice *_backCaptureDevice; + NSMutableArray *_zoomLevels; + unsigned _currentZoomIndex; + + __weak GCController *_lastController; + + dispatch_queue_t _cameraQueue; + + bool _runModeFromController; + + UIWindow *_mirrorWindow; + GBView *_mirrorView; + + bool _printerConnected; + UIButton *_printerButton; + UIActivityIndicatorView *_printerSpinner; + NSMutableData *_currentPrinterImageData; + + NSString *_lastSavedROM; + NSDate *_saveDate; +} + +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 printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, + uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure]; +} + +static void printDone(GB_gameboy_t *gb) +{ + GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb); + [self printDone]; +} + + +static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes_t 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) { + GB_set_turbo_cap(gb, [newValue doubleValue]); + } forKey:@"GBTurboCap"]; + [self addDefaultObserver:^(id newValue) { + [[AVAudioSession sharedInstance] setCategory:[newValue isEqual:@"on"]? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient + mode:AVAudioSessionModeDefault + routeSharingPolicy:AVAudioSessionRouteSharingPolicyDefault + options:0 + 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]); +} + +- (NSArray *)zoomFactorsForDevice:(AVCaptureDevice *)device +{ + if (@available(iOS 13.0, *)) { + return device.virtualDeviceSwitchOverVideoZoomFactors; + } + double factor = device.dualCameraSwitchOverVideoZoomFactor; + if (factor == 1.0) { + return @[]; + } + return @[@(factor)]; +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + _window = [[UIWindow alloc] init]; + _window.rootViewController = self; + [_window makeKeyAndVisible]; + + _runQueue = dispatch_queue_create("SameBoy Emulation Queue", NULL); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + [self addDefaultObserver:^(id newValue) { + GBTheme *theme = [GBSettingsViewController themeNamed:newValue]; + _horizontalLayoutLeft = [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:false]; + _horizontalLayoutRight = _horizontalLayoutLeft.cutout? + [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:true] : + _horizontalLayoutLeft; + _verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme]; + _printerSpinner.color = theme.buttonColor; + + [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]; + + [[NSNotificationCenter defaultCenter] addObserverForName:@"GBROMChanged" + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + _swappingROM = true; + [self stop]; + [self start]; + }]; + + _motionManager = [[CMMotionManager alloc] init]; + _cameraPosition = AVCaptureDevicePositionBack; + + // Back camera setup + NSArray *deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera, + AVCaptureDeviceTypeBuiltInTelephotoCamera, + AVCaptureDeviceTypeBuiltInDualCamera]; + if (@available(iOS 13.0, *)) { + // AVCaptureDeviceTypeBuiltInUltraWideCamera is only available in iOS 13+ + deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera, + AVCaptureDeviceTypeBuiltInUltraWideCamera, + AVCaptureDeviceTypeBuiltInTelephotoCamera, + AVCaptureDeviceTypeBuiltInTripleCamera, + AVCaptureDeviceTypeBuiltInDualWideCamera, + AVCaptureDeviceTypeBuiltInDualCamera]; + } + + // 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]; + for (AVCaptureDevice *device in cameraDiscoverySession.devices) { + if ([device position] == AVCaptureDevicePositionBack) { + if (!_backCaptureDevice || + [self zoomFactorsForDevice:_backCaptureDevice].count < [self zoomFactorsForDevice:device].count) { + _backCaptureDevice = device; + } + } + else if ([device position] == AVCaptureDevicePositionFront) { + _frontCaptureDevice = device; + } + } + + _zoomLevels = [self zoomFactorsForDevice:_backCaptureDevice].mutableCopy; + [_zoomLevels insertObject:@1 atIndex:0]; + if (_zoomLevels.count == 3 && _zoomLevels[2].doubleValue > 5.5 && _zoomLevels[1].doubleValue < 3.5) { + [_zoomLevels insertObject:@4 atIndex:2]; + } + + _cameraPositionButton = [[UIButton alloc] init]; + [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] init]; + [_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 (_zoomLevels.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]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(controllerDisconnected:) + name:GCControllerDidDisconnectNotification + object:nil]; + + for (NSString *name in @[UIScreenDidConnectNotification, + UIScreenDidDisconnectNotification, + UIScreenModeDidChangeNotification]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateMirrorWindow) + name:name + object:nil]; + } + + _printerButton = [[UIButton alloc] init]; + _printerSpinner = [[UIActivityIndicatorView alloc] init]; + _printerSpinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite; + _printerSpinner.color = _verticalLayout.theme.buttonColor; + [self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation]; + + if (@available(iOS 13.0, *)) { + [_printerButton setImage:[UIImage systemImageNamed:@"printer" + withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]] + forState:UIControlStateNormal]; + _printerButton.backgroundColor = [UIColor systemBackgroundColor]; + } + else { + UIImage *rotateImage = [[UIImage imageNamed:@"PrinterTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [_printerButton setImage:rotateImage + forState:UIControlStateNormal]; + _printerButton.backgroundColor = [UIColor whiteColor]; + } + + _printerButton.layer.cornerRadius = 6; + _printerButton.alpha = 0; + [_printerButton addTarget:self + action:@selector(showPrinterFeed) + forControlEvents:UIControlEventTouchUpInside]; + + + [_backgroundView addSubview:_printerButton]; + [_backgroundView addSubview:_printerSpinner]; + + + [self updateMirrorWindow]; + + if (@available(iOS 26.0, *)) { + UIMainMenuSystemConfiguration *conf = [[objc_getClass("UIMainMenuSystemConfiguration") alloc] init]; + conf.newScenePreference = UIMenuSystemElementGroupPreferenceRemoved; + conf.documentPreference = UIMenuSystemElementGroupPreferenceRemoved; + conf.printingPreference = UIMenuSystemElementGroupPreferenceRemoved; + conf.findingPreference = UIMenuSystemElementGroupPreferenceRemoved; + conf.toolbarPreference = UIMenuSystemElementGroupPreferenceRemoved; + conf.sidebarPreference = UIMenuSystemElementGroupPreferenceRemoved; + conf.inspectorPreference = UIMenuSystemElementGroupPreferenceRemoved; + conf.textFormattingPreference = UIMenuSystemElementGroupPreferenceRemoved; + + UIMainMenuSystem *system = (id)[objc_getClass("UIMainMenuSystem") sharedSystem]; + [system setBuildConfiguration:conf + buildHandler:^(id builder) { + [builder removeMenuForIdentifier:UIMenuView]; // This menu's always empty + [builder removeMenuForIdentifier:UIMenuOpenRecent]; // This will list files to re-import, bad + + [(id)builder insertElements:@[[UICommand commandWithTitle:@"About SameBoy" + image:nil + action:@selector(showAbout) + propertyList:nil]] + atStartOfMenuForIdentifier:UIMenuApplication]; + + UIMenu *emulationMenu = [UIMenu menuWithTitle:@"Emulation" children:@[ + [UIKeyCommand keyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand action:@selector(reset) title:@"Reset" image:[UIImage systemImageNamed:@"arrow.2.circlepath"]], + [UICommand commandWithTitle:@"Change Model…" image:CreateMenuImage(@"ModelTemplate") action:@selector(changeModel) propertyList:nil], + [UICommand commandWithTitle:@"Save States…" image:[UIImage systemImageNamed:@"square.stack"] action:@selector(openStates) propertyList:nil], + [UIMenu menuWithTitle:@"Cheats" image:CreateMenuImage(@"CheatsTemplate") identifier:nil options:0 children:@[ + [UIKeyCommand keyCommandWithInput:@"c" modifierFlags:UIKeyModifierCommand | UIKeyModifierShift action:@selector(toggleCheats) title:@"Enable Cheats" image:nil], + [UICommand commandWithTitle:@"Show Cheats…" image:nil action:@selector(openCheats) propertyList:nil], + ]], + [UIMenu menuWithTitle:@"Connect" image:CreateMenuImage(@"LinkCableTemplate") identifier:nil options:0 children:@[ + [UICommand commandWithTitle:@"None" image:nil action:@selector(disconnectCable) propertyList:nil], + [UICommand commandWithTitle:@"Printer" image:[UIImage systemImageNamed:@"printer"] action:@selector(connectPrinter) propertyList:nil], + ]], + ]]; + [builder insertSiblingMenu:emulationMenu beforeMenuForIdentifier:UIMenuWindow]; + }]; + } + + return true; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + if (action == @selector(reset)) { + if (self.presentedViewController || ![GBROMManager sharedManager].currentROM) return false; + } + if (action == @selector(openStates) || action == @selector(changeModel) || action == @selector(openCheats) || + action == @selector(toggleCheats) || action == @selector(disconnectCable) || action == @selector(connectPrinter)) { + if (![GBROMManager sharedManager].currentROM) return false; + } + return [super canPerformAction:action withSender:sender]; +} + +- (void)validateCommand:(UICommand *)command +{ + if (command.action == @selector(toggleCheats)) { + command.state = GB_is_inited(&_gb) && GB_cheats_enabled(&_gb); + } + + if (command.action == @selector(connectPrinter)) { + command.state = _printerConnected; + } + + if (command.action == @selector(disconnectCable)) { + command.state = !_printerConnected; + } + + [super validateCommand:command]; +} + +- (void)toggleCheats +{ + GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb)); +} + +- (void)orderFrontPreferencesPanel:(id)sender +{ + [self openSettings]; +} + +- (void)open:(id)sender +{ + [self openLibrary]; +} + +- (void)updateMirrorWindow +{ + if ([UIScreen screens].count == 1) { + _mirrorWindow = nil; + _mirrorView = nil; + return; + } + if (_mirrorWindow && ![[UIScreen screens] containsObject:_mirrorWindow.screen]) { + _mirrorWindow = nil; + _mirrorView = nil; + } + for (UIScreen *screen in [UIScreen screens]) { + if (screen == UIScreen.mainScreen) continue; + CGRect rect = screen.bounds; + rect.size.height = floor(rect.size.height / 144) * 144; + rect.size.width = rect.size.height / 144 * 160; + rect.origin.x = (screen.bounds.size.width - rect.size.width) / 2; + rect.origin.y = (screen.bounds.size.height - rect.size.height) / 2; + _mirrorWindow = [[UIWindow alloc] initWithFrame:screen.bounds]; + _mirrorWindow.screen = screen; + _mirrorView = [_gbView mirroredView]; + _mirrorView.frame = rect; + _mirrorWindow.backgroundColor = [UIColor blackColor]; + [_mirrorWindow addSubview:_mirrorView]; + [_mirrorWindow setHidden:false]; + break; + } +} + +- (void)controllerDisconnected:(NSNotification *)notification +{ + if (notification.object == _lastController) { + _backgroundView.fullScreenMode = false; + } +} + +- (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]; + if (_running && button.value > 0.25 && + [[NSUserDefaults standardUserDefaults] boolForKey:@"GBControllersHideInterface"]) { + _backgroundView.fullScreenMode = true; + } + + GBButton gbButton = [GBSettingsViewController controller:controller convertUsageToButton:usage]; + static const double analogThreshold = 0.0625; + switch (gbButton) { + case GBRight: + case GBLeft: + case GBUp: + case GBDown: + GB_set_use_faux_analog_inputs(&_gb, 0, false); + case GBA: + case GBB: + case GBSelect: + case GBStart: + GB_set_key_state(&_gb, (GB_key_t)gbButton, button.value > 0.25); + break; + case GBRapidA: + _rapidA = button.value > 0.25; + _rapidACount = 0; + break; + case GBRapidB: + _rapidB = button.value > 0.25; + _rapidBCount = 0; + 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 || self.runMode == GBRunModePaused) && _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]; + bool left = axis.left.value > 0.5; + bool right = axis.right.value > 0.5; + bool up = axis.up.value > 0.5; + bool down = axis.down.value > 0.5; + + if (_running && (left || right || up || down ) && + [[NSUserDefaults standardUserDefaults] boolForKey:@"GBControllersHideInterface"]) { + _backgroundView.fullScreenMode = true; + } + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFauxAnalogInputs"]) { + GB_set_use_faux_analog_inputs(&_gb, 0, true); + GB_set_faux_analog_inputs(&_gb, 0, axis.right.value - axis.left.value, axis.down.value - axis.up.value); + } + + GB_set_key_state(&_gb, GB_KEY_LEFT, left); + GB_set_key_state(&_gb, GB_KEY_RIGHT, right); + GB_set_key_state(&_gb, GB_KEY_UP, up); + GB_set_key_state(&_gb, GB_KEY_DOWN, down); +} + +- (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; + + 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 +{ + NSString *tempPath = [file stringByAppendingPathExtension:@"tmp"]; + int error = GB_save_state(&_gb, tempPath.UTF8String); + if (!error) { + rename(tempPath.UTF8String, file.UTF8String); + 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]; + } + else { + dispatch_async(dispatch_get_main_queue(), ^{ + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Could not Save State" + message:[NSString stringWithFormat:@"An error occured while attempting to save: %s", strerror(error)] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:nil]]; + UIViewController *top = self; + while (top.presentedViewController) { + top = top.presentedViewController; + } + [top presentViewController:alert animated:true completion:nil]; + }); + } +} + +- (bool)loadStateFromFile:(NSString *)file +{ + _skipAutoLoad = true; + 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 +{ + GBROMManager *romManager = [GBROMManager sharedManager]; + if (romManager.romFile) { + if (!_skipAutoLoad) { + // Todo: display errors and warnings + bool needsStateLoad = false; + if (![_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) { + 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; + } + needsStateLoad = true; + if (@available(iOS 16.0, *)) { + dispatch_async(dispatch_get_main_queue(), ^{ + [super setNeedsUpdateOfSupportedInterfaceOrientations]; + }); + } + } + else if (access(romManager.romFile.fileSystemRepresentation, R_OK)) { + _romLoaded = false; + } + if (!_romLoaded) { + dispatch_async(dispatch_get_main_queue(), ^{ + romManager.currentROM = nil; + }); + } + + if (!needsStateLoad) { + NSDate *date = nil; + [[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date + forKey:NSURLContentModificationDateKey + error:nil]; + if (![_saveDate isEqual:date]) { + needsStateLoad = true; + } + } + + if (_romLoaded && needsStateLoad) { + GB_reset(&_gb); + GB_load_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation); + GB_remove_all_cheats(&_gb); + GB_load_cheats(&_gb, [GBROMManager sharedManager].cheatsFile.UTF8String, false); + if (![self loadStateFromFile:[GBROMManager sharedManager].autosaveStateFile]) { + if ([_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) { + /* Something weird just happened: we didn't change a ROM, but we failed to load the + latest save state. Save over the existing file, it's probably corrupt in some + way. */ + [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile]; + } + else { + // 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; + } + dispatch_async(dispatch_get_main_queue(), ^{ + _gbView.hidden = !_romLoaded; + }); + _swappingROM = false; + _skipAutoLoad = false; +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + [self start]; +} + +-(void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion +{ + [self stop]; + [super presentViewController:viewControllerToPresent + animated:flag + completion:completion]; +} + +- (void)reset +{ + UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad? + UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet; + UIAlertController *menu = [UIAlertController alertControllerWithTitle:@"Reset Emulation?" + message:@"Unsaved progress will be lost." + preferredStyle:style]; + [menu addAction:[UIAlertAction actionWithTitle:@"Reset" + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *action) { + [self stop]; + _skipAutoLoad = true; + GB_reset(&_gb); + [self start]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:menu animated:true completion:nil]; +} + +- (void)openLibrary +{ + static __weak UIViewController *presentedController; + if (presentedController && self.presentedViewController == presentedController) return; + if (![self dismissViewControllerIfSafe]) return; + + UIViewController *controller = [[GBLibraryViewController alloc] init]; + presentedController = controller; + + [self presentViewController:controller + animated:true + completion:nil]; +} + +- (void)changeModel +{ + static __weak UIViewController *presentedController; + if (presentedController && self.presentedViewController == presentedController) return; + if (![self dismissViewControllerIfSafe]) return; + + GBOptionViewController *controller = [[GBOptionViewController alloc] initWithHeader:@"Select a model to emulate"]; + controller.footer = @"Changing the emulated model will reset the emulator"; + presentedController = controller; + + 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]; + _skipAutoLoad = true; + 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 +{ + static __weak UIViewController *presentedController; + if (presentedController && self.presentedViewController == presentedController) return; + if (![self dismissViewControllerIfSafe]) return; + + UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBStatesViewController alloc] init]]; + presentedController = controller; + 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 +{ + static __weak UIViewController *presentedController; + if (presentedController && self.presentedViewController == presentedController) return; + if (![self dismissViewControllerIfSafe]) return; + + UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close" + style:UIBarButtonItemStylePlain + target:self + action:@selector(dismissViewController)]; + UIViewController *controller = [GBSettingsViewController settingsViewControllerWithLeftButton:close]; + presentedController = controller; + [self presentViewController:controller + animated:true + completion:nil]; +} + +- (void)showAbout +{ + static __weak UIViewController *presentedController; + if (presentedController && self.presentedViewController == presentedController) return; + if (![self dismissViewControllerIfSafe]) return; + + UIViewController *controller = [[GBAboutController alloc] init]; + presentedController = controller; + + [self presentViewController:controller animated:true completion:nil]; +} + +- (void)openCheats +{ + static __weak UIViewController *presentedController; + if (presentedController && self.presentedViewController == presentedController) return; + if (![self dismissViewControllerIfSafe]) return; + + UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBCheatsController alloc] initWithGameBoy:&_gb]]; + presentedController = controller; + 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)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion +{ + [super dismissViewControllerAnimated:flag completion:^() { + if (completion) { + completion(); + } + dispatch_async(dispatch_get_main_queue(), ^{ + [self start]; + }); + }]; +} + +- (void)setNeedsUpdateOfSupportedInterfaceOrientations +{ + /* Hack. Some view controllers dismiss without calling the method above. */ + dispatch_async(dispatch_get_main_queue(), ^{ + [self start]; + [super setNeedsUpdateOfSupportedInterfaceOrientations]; + }); +} + +- (void)dismissViewController +{ + [self dismissViewControllerAnimated:true completion:nil]; +} + +- (bool)dismissViewControllerIfSafe +{ + if (!self.presentedViewController) return true; + + if (![self.presentedViewController isKindOfClass:[UIAlertController class]]) { + [self dismissViewController]; + return true; + } + + if ([self.presentedViewController isKindOfClass:[GBMenuViewController class]]) { + [self dismissViewController]; + return true; + } + return false; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + [self stop]; +} + +- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration +{ + if (_orientation != UIInterfaceOrientationUnknown && !((1 << orientation) & self.supportedInterfaceOrientations)) return; + GBLayout *layout = nil; + _orientation = orientation; + switch (orientation) { + default: + case UIInterfaceOrientationUnknown: + case UIInterfaceOrientationPortrait: + case UIInterfaceOrientationPortraitUpsideDown: + layout = _verticalLayout; + break; + case UIInterfaceOrientationLandscapeRight: + layout = _horizontalLayoutLeft; + break; + case UIInterfaceOrientationLandscapeLeft: + layout = _horizontalLayoutRight; + break; + } + + _backgroundView.frame = [layout viewRectForOrientation:orientation]; + _backgroundView.layout = layout; + if (!self.presentedViewController) { + _window.backgroundColor = _backgroundView.fullScreenMode? [UIColor blackColor] : + layout.theme.backgroundGradientBottom; + } +} + +- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation +{ + UIEdgeInsets insets = self.window.safeAreaInsets; + bool landscape = true; + if (_orientation == UIInterfaceOrientationPortrait || _orientation == UIInterfaceOrientationPortraitUpsideDown) { + landscape = false; + } + + + _cameraPositionButton.frame = CGRectMake(insets.left + 8, + _backgroundView.bounds.size.height - 8 - insets.bottom - 32, + 32, + 32); + if (_changeCameraButton) { + _changeCameraButton.frame = CGRectMake(insets.left + 8 + (landscape? (32 + 8) : 0 ), + _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - (landscape? 0 : (32 + 8)), + 32, + 32); + } + _printerButton.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32, + _backgroundView.bounds.size.height - 8 - insets.bottom - 32, + 32, + 32); + + _printerSpinner.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32 - (landscape? (32 + 4) : 0), + _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - (landscape? 0 : (32 + 4)), + 32, + 32); + + [super didRotateFromInterfaceOrientation:fromInterfaceOrientation]; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + if (!self.shouldAutorotate && _orientation != UIInterfaceOrientationUnknown) { + return 1 << _orientation; + } + 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 || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent; + } + return (_verticalLayout.theme.isDark || _backgroundView.fullScreenMode)? 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 loadROM]; + if (!_romLoaded) { + _running = false; + _stopping = false; + return; + } + [self preRun]; + while (_running) { + if (_rewind) { + _rewind = false; + GB_rewind_pop(&_gb); + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { + if (!GB_rewind_pop(&_gb)) { + dispatch_sync(dispatch_get_main_queue(), ^{ + if (_runMode == GBRunModeRewind) { + self.runMode = GBRunModePaused; + } + }); + _rewindOver = true; + } + } + else { + for (unsigned i = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindSpeed"]; i--;) { + if (!GB_rewind_pop(&_gb)) { + dispatch_sync(dispatch_get_main_queue(), ^{ + if (_runMode == GBRunModeRewind) { + 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 = 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); + + 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]; + + NSDate *date; + [[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date + forKey:NSURLContentModificationDateKey + error:nil]; + _saveDate = date; + _lastSavedROM = [GBROMManager sharedManager].currentROM; + + } + [[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 (_running) return; + if (self.presentedViewController) return; + _running = true; + dispatch_async(_runQueue, ^{ + [self run]; + }); +} + +- (void)stop +{ + if (!_running) return; + [_audioLock lock]; + _stopping = true; + [_audioLock signal]; + [_audioLock unlock]; + _running = false; + while (_stopping) { + [_audioLock lock]; + [_audioLock signal]; + [_audioLock unlock]; + } + dispatch_sync(_runQueue, ^{}); + 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); + } + if (_rapidA) { + _rapidACount++; + GB_set_key_state(&_gb, GB_KEY_A, !(_rapidACount & 2)); + } + if (_rapidB) { + _rapidBCount++; + GB_set_key_state(&_gb, GB_KEY_B, !(_rapidBCount & 2)); + } + _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, + [GBPalettePicker paletteForTheme:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]], + sizeof(_palette)); + GB_set_palette(&_gb, &_palette); +} + +- (bool)handleOpenURLs:(NSArray *)urls + openInPlace:(bool)inPlace +{ + NSMutableArray *validURLs = [NSMutableArray array]; + NSMutableArray *skippedBasenames = [NSMutableArray array]; + NSMutableArray *unusedZips = [NSMutableArray array]; + NSString *tempDir = NSTemporaryDirectory(); + + __block unsigned tempIndex = 0; + for (NSURL *url in urls) { + if ([url.pathExtension.lowercaseString isEqualToString:@"zip"]) { + GBZipReader *reader = [[GBZipReader alloc] initWithPath:url.path]; + if (!reader) { + [skippedBasenames addObject:url.lastPathComponent]; + continue; + } + __block bool used = false; + [reader iterateFiles:^bool(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)) { + if ([@[@"gb", @"gbc", @"isx"] containsObject:filename.pathExtension.lowercaseString] && uncompressedSize <= 32 * 1024 * 1024) { + tempIndex++; + NSString *subDir = [tempDir stringByAppendingFormat:@"/%u", tempIndex]; + mkdir(subDir.UTF8String, 0755); + NSString *dest = [subDir stringByAppendingPathComponent:filename.lastPathComponent]; + if (writeToPath(dest)) { + [validURLs addObject:[NSURL fileURLWithPath:dest]]; + used = true; + } + } + return true; + }]; + if (!used) { + [unusedZips addObject:url.lastPathComponent]; + } + } + else if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) { + [validURLs addObject:url]; + } + else { + [skippedBasenames addObject:url.lastPathComponent]; + } + } + + if (unusedZips.count) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No ROMs in archive" + message:[NSString stringWithFormat:@"Could not find any Game Boy ROM files in the following archives:\n%@", + [unusedZips componentsJoinedByString:@"\n"]] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:alert animated:true completion:nil]; + } + + 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 + }]]; + [self presentViewController:alert animated:true completion:nil]; + } + + + if (validURLs.count == 1 && urls.count == 1) { + NSURL *url = validURLs.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:![url.path hasPrefix:tempDir] && inPlace]; + [url stopAccessingSecurityScopedResource]; + } + return true; + } + for (NSURL *url in validURLs) { + 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:![url.path hasPrefix:tempDir] && inPlace]; + [url stopAccessingSecurityScopedResource]; + } + [self openLibrary]; + + return validURLs.count; +} + +- (void)doImportedPaletteNotification +{ + UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent]]; + effectView.layer.cornerRadius = 8; + effectView.layer.masksToBounds = true; + [self.view addSubview:effectView]; + UILabel *tipLabel = [[UILabel alloc] init]; + tipLabel.text = [NSString stringWithFormat:@"Imported palette “%@”", [[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]]; + if (@available(iOS 13.0, *)) { + tipLabel.textColor = [UIColor labelColor]; + } + tipLabel.font = [UIFont systemFontOfSize:16]; + tipLabel.alpha = 0.8; + [effectView.contentView addSubview:tipLabel]; + + UIView *view = self.view; + CGSize outerSize = view.frame.size; + CGSize size = [tipLabel textRectForBounds:(CGRect){{0, 0}, + {outerSize.width - 32, + outerSize.height - 32}} + limitedToNumberOfLines:1].size; + size.width = ceil(size.width); + tipLabel.frame = (CGRect){{8, 8}, size}; + CGRect finalFrame = (CGRect) { + {round((outerSize.width - size.width - 16) / 2), view.window.safeAreaInsets.top + 12}, + {size.width + 16, size.height + 16} + }; + + CGRect initFrame = finalFrame; + initFrame.origin.y = -initFrame.size.height; + effectView.frame = initFrame; + + effectView.alpha = 0; + [UIView animateWithDuration:0.5 animations:^{ + effectView.alpha = 1.0; + effectView.frame = finalFrame; + }]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * 1.5)), dispatch_get_main_queue(), ^{ + [UIView animateWithDuration:0.5 animations:^{ + effectView.alpha = 0.0; + effectView.frame = initFrame; + } completion:^(BOOL finished) { + if (finished) { + [effectView removeFromSuperview]; + } + }]; + }); + +} + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options +{ + if (self.presentedViewController && ![self.presentedViewController isKindOfClass:[UIAlertController class]]) { + [self dismissViewController]; + } + if ([url.pathExtension.lowercaseString isEqual:@"sbp"]) { + [url startAccessingSecurityScopedResource]; + bool success = [GBPalettePicker importPalette:url.path]; + [url stopAccessingSecurityScopedResource]; + if (!success) { + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Palette Import Failed" + message:@"The imported palette file is invalid." + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]]; + [self presentViewController:alertController animated:true completion:nil]; + } + else { + [self doImportedPaletteNotification]; + } + return success; + } + NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; + if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { + [self stop]; + [GBROMManager sharedManager].currentROM = potentialROM; + [self start]; + return [GBROMManager sharedManager].currentROM != nil; + } + return [self handleOpenURLs:@[url] + openInPlace:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]]; +} + +- (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) { + GB_set_turbo_mode(&_gb, true, false); + } + 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 +{ + if (_cameraPosition == AVCaptureDevicePositionFront) { + return _frontCaptureDevice ?: [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo]; + } + return _backCaptureDevice ?: [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, ^{ + if (![_backCaptureDevice lockForConfiguration:nil]) return; + _currentZoomIndex++; + if (_currentZoomIndex == _zoomLevels.count) { + _currentZoomIndex = 0; + } + [_backCaptureDevice rampToVideoZoomFactor:_zoomLevels[_currentZoomIndex].doubleValue withRate:2]; + [_backCaptureDevice unlockForConfiguration]; + }); +} + +- (void)disconnectCable +{ + if (!_printerConnected) return; + _printerConnected = false; + _currentPrinterImageData = nil; + [UIView animateWithDuration:0.25 animations:^{ + _printerButton.alpha = 0; + }]; + [_printerSpinner stopAnimating]; + GB_disconnect_serial(&_gb); +} + +- (void)connectPrinter +{ + if (_printerConnected) return; + _printerConnected = true; + GB_connect_printer(&_gb, printImage, printDone); +} + +- (void)openConnectMenu +{ + UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad? + UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet; + GBCheckableAlertController *menu = [GBCheckableAlertController alertControllerWithTitle:@"Connect Accessory" + message:@"Choose an accessory to connect." + preferredStyle:style]; + [menu addAction:[UIAlertAction actionWithTitle:@"None" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self disconnectCable]; + }]]; + [menu addAction:[UIAlertAction actionWithTitle:@"Game Boy Printer" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self connectPrinter]; + }]]; + menu.selectedAction = menu.actions[_printerConnected]; + [menu addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:menu animated:true completion:nil]; +} + +- (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 (!_currentPrinterImageData) { + _currentPrinterImageData = [[NSMutableData alloc] init]; + } + [_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [UIView animateWithDuration:0.25 animations:^{ + _printerButton.alpha = 1; + }]; + [_printerSpinner startAnimating]; + }); + +} + +- (void)printDone +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [_printerSpinner stopAnimating]; + }); +} + +- (void)showPrinterFeed +{ + UIImage *image = [self imageFromData:_currentPrinterImageData + width:160 + height:_currentPrinterImageData.length / 160 / sizeof(uint32_t)]; + + _window.backgroundColor = [UIColor blackColor]; + [self presentViewController:[[GBPrinterFeedController alloc] initWithImage:image] + animated:true + completion:nil]; + +} + +- (void)emptyPrinterFeed +{ + _currentPrinterImageData = nil; + [UIView animateWithDuration:0.25 animations:^{ + _printerButton.alpha = 0; + }]; + [_printerSpinner stopAnimating]; + [self dismissViewController]; +} + +@end + +/* +[UIColor labelColor] is broken in some contexts in iOS 26 and despite being such a critical method + Apple isn't going to fix this in time. */ +API_AVAILABLE(ios(19.0)) +@implementation UIColor(SolariumBugs) ++ (UIColor *)_labelColor +{ + return [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) { + switch (traitCollection.userInterfaceStyle) { + + case UIUserInterfaceStyleUnspecified: + case UIUserInterfaceStyleLight: + return [UIColor blackColor]; + case UIUserInterfaceStyleDark: + return [UIColor whiteColor]; + } + }]; +} + ++ (void)load +{ + if (@available(iOS 19.0, *)) { + method_setImplementation(class_getClassMethod(self, @selector(labelColor)), + [self methodForSelector:@selector(_labelColor)]); + } +} + +@end diff --git a/bsnes/gb/iOS/GBZipReader.h b/bsnes/gb/iOS/GBZipReader.h new file mode 100644 index 00000000..3b7eb0af --- /dev/null +++ b/bsnes/gb/iOS/GBZipReader.h @@ -0,0 +1,7 @@ +#import + +@interface GBZipReader : NSObject +- (instancetype)initWithBuffer:(const void *)buffer size:(size_t)size free:(void (^)(void))callback; +- (instancetype)initWithPath:(NSString *)path; +- (void)iterateFiles:(bool (^)(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)))callback; +@end diff --git a/bsnes/gb/iOS/GBZipReader.m b/bsnes/gb/iOS/GBZipReader.m new file mode 100644 index 00000000..88f86bc1 --- /dev/null +++ b/bsnes/gb/iOS/GBZipReader.m @@ -0,0 +1,179 @@ +#import "GBZipReader.h" +#import +#import +#import +#pragma clang diagnostic ignored "-Wimplicit-retain-self" + +@implementation GBZipReader +{ + void (^_freeCallback)(void); + const void *_buffer; + size_t _size; + + const struct __attribute__((packed)) { + uint32_t magic; + uint8_t skip[6]; + uint16_t fileCount; + uint32_t cdSize; + uint32_t cdOffset; + uint16_t commentSize; + } *_eocd; +} + +- (instancetype)initWithBuffer:(const void *)buffer size:(size_t)size free:(void (^)(void))callback +{ + self = [super init]; + if (!self) return nil; + + _buffer = buffer; + _size = size; + _freeCallback = callback; + + if (_size < sizeof(*_eocd)) return nil; + + for (unsigned i = 0; i < 0x10000; i++) { + _eocd = (void *)((uint8_t *)buffer + size - sizeof(*_eocd) - i); + if ((void *)_eocd < buffer) return nil; + if (_eocd->magic == htonl('PK\05\06')) { + break; + } + } + + return self; +} + +- (instancetype)initWithPath:(NSString *)path +{ + int fd = open(path.UTF8String, O_RDONLY); + if (fd < 0) return nil; + size_t size = lseek(fd, 0, SEEK_END); + size_t alignedSize = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + void *mapping = mmap(NULL, alignedSize, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0); + close(fd); + if (!mapping) return nil; + + return [self initWithBuffer:mapping size:size free:^{ + munmap(mapping, alignedSize); + }]; +} + +- (void)iterateFiles:(bool (^)(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)))callback { + const struct __attribute__((packed)) { + uint32_t magic; + uint8_t skip[6]; + uint16_t compressionMethod; + uint8_t skip2[8]; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t nameLength; + uint16_t extraLength; + uint16_t commentLength; + uint8_t skip3[8]; + uint32_t localHeaderOffset; + char name[0]; + } *entry = (void *)((uint8_t *)_buffer + _eocd->cdOffset); + for (unsigned i = _eocd->fileCount; i--;) { + if ((uint8_t *)entry + sizeof(*entry) - (uint8_t *)_buffer >= _size) return; + if (entry->magic != htonl('PK\01\02')) return; + + typeof(entry) next = (void *)((uint8_t *)entry + sizeof(*entry) + + entry->nameLength + entry->extraLength + entry->commentLength); + if ((uint8_t *)next - (uint8_t *)_buffer >= _size) return; + + + bool (^getData)(void *) = ^bool(void *output) { + // Directory, no data + if (entry->name[entry->nameLength - 1] == '/') return false; + + if (entry->uncompressedSize == 0xffffffff || entry->compressedSize == 0xffffffff) { + // ZIP64 + return false; + } + + const struct __attribute__((packed)) { + uint32_t magic; + uint8_t skip[4]; + uint16_t compressionMethod; + uint8_t skip2[8]; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t nameLength; + uint16_t extraLength; + char name[0]; + } *localEntry = (void *)((uint8_t *)_buffer + entry->localHeaderOffset); + + if ((uint8_t *)localEntry + sizeof(*localEntry) - (uint8_t *)_buffer >= _size) return nil; + if ((uint8_t *)localEntry + sizeof(*localEntry) + + localEntry->nameLength + localEntry->extraLength + + entry->compressedSize - (uint8_t *)_buffer >= _size) { + return false; + } + + if (localEntry->magic != htonl('PK\03\04')) return nil; + if (entry->uncompressedSize != localEntry->uncompressedSize) return nil; + + const void *dataStart = &localEntry->name[localEntry->nameLength + localEntry->extraLength]; + if (localEntry->compressionMethod == 0) { + if (localEntry->uncompressedSize != entry->compressedSize) return false; + memcpy(output, dataStart, localEntry->uncompressedSize); + return true; + } + else if (localEntry->compressionMethod != 8) { + // Unsupported compression + return false; + } + if (compression_decode_buffer(output, localEntry->uncompressedSize, + dataStart, entry->compressedSize, + NULL, COMPRESSION_ZLIB) != localEntry->uncompressedSize) { + return false; + } + return true; + }; + + bool (^writeToPath)(NSString *) = ^bool(NSString *path) { + int fd = open(path.UTF8String, O_CREAT | O_RDWR, 0644); + if (!fd) return false; + if (ftruncate(fd, entry->uncompressedSize) != 0) { + close(fd); + unlink(path.UTF8String); + return false; + } + size_t alignedSize = (entry->uncompressedSize + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + void *mapping = mmap(NULL, alignedSize, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0); + close(fd); + if (!mapping) { + unlink(path.UTF8String); + return false; + } + + bool ret = getData(mapping); + if (!ret) { + unlink(path.UTF8String); + } + munmap(mapping, alignedSize); + + return ret; + }; + + + if (!callback([[NSString alloc] initWithBytes:entry->name + length:entry->nameLength + encoding:NSUTF8StringEncoding], + entry->uncompressedSize, + getData, + writeToPath)) { + return; + } + + entry = next; + } +} + +- (void)dealloc +{ + if (_freeCallback) { + _freeCallback(); + } +} + +@end diff --git a/bsnes/gb/iOS/GCExtendedGamepad+AllElements.h b/bsnes/gb/iOS/GCExtendedGamepad+AllElements.h new file mode 100644 index 00000000..427d5eb4 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/GCExtendedGamepad+AllElements.m b/bsnes/gb/iOS/GCExtendedGamepad+AllElements.m new file mode 100644 index 00000000..006ea3a8 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/Info.plist b/bsnes/gb/iOS/Info.plist new file mode 100644 index 00000000..0250a908 --- /dev/null +++ b/bsnes/gb/iOS/Info.plist @@ -0,0 +1,276 @@ + + + + + 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 + AppIcon76x76 + + CFBundleIconName + AppIcon + + + LSApplicationCategoryType + public.app-category.games + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + + UISupportsDocumentBrowser + + NSCameraUsageDescription + SameBoy needs to access your device's camera to emulate the Game Boy Camera + NSPhotoLibraryAddUsageDescription + SameBoy needs access to save Game Boy Printer images to your photo library + CFBundleDocumentTypes + + + CFBundleTypeIconFiles + + Cartridge64.png + Cartridge.png + + CFBundleTypeExtensions + + gb + + CFBundleTypeName + Game Boy Game + LSItemContentTypes + + com.github.liji32.sameboy.gb + + LSHandlerRank + Owner + + + CFBundleTypeIconFiles + + ColorCartridge64.png + ColorCartridge.png + + CFBundleTypeExtensions + + gbc + + CFBundleTypeName + Game Boy Color Game + LSItemContentTypes + + com.github.liji32.sameboy.gbc + + LSHandlerRank + Owner + + + CFBundleTypeIconFiles + + ColorCartridge64.png + ColorCartridge.png + + CFBundleTypeExtensions + + isx + + CFBundleTypeName + Game Boy ISX File + LSItemContentTypes + + com.github.liji32.sameboy.isx + + LSHandlerRank + Owner + + + CFBundleTypeName + zip + LSItemContentTypes + + com.pkware.zip-archive + + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + sbp + + CFBundleTypeName + SameBoy Palette + LSItemContentTypes + + com.github.liji32.sameboy.sbp + + LSHandlerRank + Owner + + + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy Game + UTTypeIdentifier + com.github.liji32.sameboy.gb + UTTypeTagSpecification + + public.filename-extension + + gb + + + UTTypeIconFiles + + Cartridge64 + Cartridge + + + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy Color Game + UTTypeIdentifier + com.github.liji32.sameboy.gbc + UTTypeTagSpecification + + public.filename-extension + + gbc + + + UTTypeIconFiles + + ColorCartridge64 + ColorCartridge + + + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy ISX File + UTTypeIdentifier + com.github.liji32.sameboy.isx + UTTypeTagSpecification + + public.filename-extension + + isx + + + UTTypeIconFiles + + ColorCartridge64 + ColorCartridge + + + + UTTypeConformsTo + + public.data + + UTTypeDescription + SameBoy Cheats Database + UTTypeIdentifier + com.github.liji32.sameboy.cheats + UTTypeTagSpecification + + public.filename-extension + + cht + + + + + UTTypeConformsTo + + public.data + + UTTypeDescription + SameBoy Palette + UTTypeIdentifier + com.github.liji32.sameboy.sbp + UTTypeTagSpecification + + public.filename-extension + + sbp + + + + + UIDeviceFamily + + 1 + 2 + + CFBundleSupportedPlatforms + + iPhoneOS + + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + GCSupportedGameControllers + + + ProfileName + ExtendedGamepad + + + GCSupportsControllerUserInteraction + + + diff --git a/bsnes/gb/iOS/LaunchScreen.storyboard b/bsnes/gb/iOS/LaunchScreen.storyboard new file mode 100644 index 00000000..605cefa0 --- /dev/null +++ b/bsnes/gb/iOS/LaunchScreen.storyboard @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/iOS/License.html b/bsnes/gb/iOS/License.html new file mode 100644 index 00000000..488c8ca5 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/LinkCableTemplate@2x.png b/bsnes/gb/iOS/LinkCableTemplate@2x.png new file mode 100644 index 00000000..266474db Binary files /dev/null and b/bsnes/gb/iOS/LinkCableTemplate@2x.png differ diff --git a/bsnes/gb/iOS/LinkCableTemplate@3x.png b/bsnes/gb/iOS/LinkCableTemplate@3x.png new file mode 100644 index 00000000..64b6c637 Binary files /dev/null and b/bsnes/gb/iOS/LinkCableTemplate@3x.png differ diff --git a/bsnes/gb/iOS/ModelTemplate@2x.png b/bsnes/gb/iOS/ModelTemplate@2x.png new file mode 100644 index 00000000..c08c9a33 Binary files /dev/null and b/bsnes/gb/iOS/ModelTemplate@2x.png differ diff --git a/bsnes/gb/iOS/ModelTemplate@3x.png b/bsnes/gb/iOS/ModelTemplate@3x.png new file mode 100644 index 00000000..2bd77a56 Binary files /dev/null and b/bsnes/gb/iOS/ModelTemplate@3x.png differ diff --git a/bsnes/gb/iOS/PrinterTemplate@2x.png b/bsnes/gb/iOS/PrinterTemplate@2x.png new file mode 100644 index 00000000..fd1e75ac Binary files /dev/null and b/bsnes/gb/iOS/PrinterTemplate@2x.png differ diff --git a/bsnes/gb/iOS/PrinterTemplate@3x.png b/bsnes/gb/iOS/PrinterTemplate@3x.png new file mode 100644 index 00000000..3d8c6c7a Binary files /dev/null and b/bsnes/gb/iOS/PrinterTemplate@3x.png differ diff --git a/bsnes/gb/iOS/UILabel+TapLocation.h b/bsnes/gb/iOS/UILabel+TapLocation.h new file mode 100644 index 00000000..168bdeb6 --- /dev/null +++ b/bsnes/gb/iOS/UILabel+TapLocation.h @@ -0,0 +1,5 @@ +#import + +@interface UILabel (TapLocation) +- (unsigned)characterAtTap:(UITapGestureRecognizer *)tap; +@end diff --git a/bsnes/gb/iOS/UILabel+TapLocation.m b/bsnes/gb/iOS/UILabel+TapLocation.m new file mode 100644 index 00000000..d797568e --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/UIToolbar+disableCompact.h b/bsnes/gb/iOS/UIToolbar+disableCompact.h new file mode 100644 index 00000000..2691c5cd --- /dev/null +++ b/bsnes/gb/iOS/UIToolbar+disableCompact.h @@ -0,0 +1,5 @@ +#import + +@interface UIToolbar (disableCompact) +@property bool disableCompactLayout; +@end diff --git a/bsnes/gb/iOS/UIToolbar+disableCompact.m b/bsnes/gb/iOS/UIToolbar+disableCompact.m new file mode 100644 index 00000000..bd810037 --- /dev/null +++ b/bsnes/gb/iOS/UIToolbar+disableCompact.m @@ -0,0 +1,33 @@ +#import "UIToolbar+disableCompact.h" +#import + +@implementation UIToolbar (disableCompact) + +- (void)setDisableCompactLayout:(bool)disableCompactLayout +{ + objc_setAssociatedObject(self, @selector(disableCompactLayout), @(disableCompactLayout), OBJC_ASSOCIATION_RETAIN); +} + +- (bool)disableCompactLayout +{ + return [objc_getAssociatedObject(self, _cmd) boolValue]; +} + +- (CGSize)sizeThatFitsHook:(CGSize)size +{ + CGSize ret = [self sizeThatFitsHook:size]; + if (!self.disableCompactLayout) return ret; + if (ret.height < 44) { + ret.height = 44; + } + return ret; +} + ++ (void)load +{ + method_exchangeImplementations(class_getInstanceMethod(self, @selector(sizeThatFitsHook:)), + class_getInstanceMethod(self, @selector(sizeThatFits:))); + +} + +@end diff --git a/bsnes/gb/iOS/audioSettings@2x.png b/bsnes/gb/iOS/audioSettings@2x.png new file mode 100644 index 00000000..e1654dad Binary files /dev/null and b/bsnes/gb/iOS/audioSettings@2x.png differ diff --git a/bsnes/gb/iOS/audioSettings@3x.png b/bsnes/gb/iOS/audioSettings@3x.png new file mode 100644 index 00000000..41ebed83 Binary files /dev/null and b/bsnes/gb/iOS/audioSettings@3x.png differ diff --git a/bsnes/gb/iOS/button2-tint@2x.png b/bsnes/gb/iOS/button2-tint@2x.png new file mode 100644 index 00000000..88469b21 Binary files /dev/null and b/bsnes/gb/iOS/button2-tint@2x.png differ diff --git a/bsnes/gb/iOS/button2-tint@3x.png b/bsnes/gb/iOS/button2-tint@3x.png new file mode 100644 index 00000000..8d8fbfe0 Binary files /dev/null and b/bsnes/gb/iOS/button2-tint@3x.png differ diff --git a/bsnes/gb/iOS/button2@2x.png b/bsnes/gb/iOS/button2@2x.png new file mode 100644 index 00000000..ccb6a4e7 Binary files /dev/null and b/bsnes/gb/iOS/button2@2x.png differ diff --git a/bsnes/gb/iOS/button2@3x.png b/bsnes/gb/iOS/button2@3x.png new file mode 100644 index 00000000..8feb4682 Binary files /dev/null and b/bsnes/gb/iOS/button2@3x.png differ diff --git a/bsnes/gb/iOS/button2Pressed-tint@2x.png b/bsnes/gb/iOS/button2Pressed-tint@2x.png new file mode 100644 index 00000000..81e336ac Binary files /dev/null and b/bsnes/gb/iOS/button2Pressed-tint@2x.png differ diff --git a/bsnes/gb/iOS/button2Pressed-tint@3x.png b/bsnes/gb/iOS/button2Pressed-tint@3x.png new file mode 100644 index 00000000..893b50ed Binary files /dev/null and b/bsnes/gb/iOS/button2Pressed-tint@3x.png differ diff --git a/bsnes/gb/iOS/button2Pressed@2x.png b/bsnes/gb/iOS/button2Pressed@2x.png new file mode 100644 index 00000000..45b19ab8 Binary files /dev/null and b/bsnes/gb/iOS/button2Pressed@2x.png differ diff --git a/bsnes/gb/iOS/button2Pressed@3x.png b/bsnes/gb/iOS/button2Pressed@3x.png new file mode 100644 index 00000000..f86440dd Binary files /dev/null and b/bsnes/gb/iOS/button2Pressed@3x.png differ diff --git a/bsnes/gb/iOS/button@2x.png b/bsnes/gb/iOS/button@2x.png new file mode 100644 index 00000000..16a9c280 Binary files /dev/null and b/bsnes/gb/iOS/button@2x.png differ diff --git a/bsnes/gb/iOS/button@3x.png b/bsnes/gb/iOS/button@3x.png new file mode 100644 index 00000000..1a63ce32 Binary files /dev/null and b/bsnes/gb/iOS/button@3x.png differ diff --git a/bsnes/gb/iOS/buttonPressed@2x.png b/bsnes/gb/iOS/buttonPressed@2x.png new file mode 100644 index 00000000..b36d64b1 Binary files /dev/null and b/bsnes/gb/iOS/buttonPressed@2x.png differ diff --git a/bsnes/gb/iOS/buttonPressed@3x.png b/bsnes/gb/iOS/buttonPressed@3x.png new file mode 100644 index 00000000..2ce934f3 Binary files /dev/null and b/bsnes/gb/iOS/buttonPressed@3x.png differ diff --git a/bsnes/gb/iOS/controlsSettings@2x.png b/bsnes/gb/iOS/controlsSettings@2x.png new file mode 100644 index 00000000..bcd06439 Binary files /dev/null and b/bsnes/gb/iOS/controlsSettings@2x.png differ diff --git a/bsnes/gb/iOS/controlsSettings@3x.png b/bsnes/gb/iOS/controlsSettings@3x.png new file mode 100644 index 00000000..0b0b3283 Binary files /dev/null and b/bsnes/gb/iOS/controlsSettings@3x.png differ diff --git a/bsnes/gb/iOS/deb-control b/bsnes/gb/iOS/deb-control new file mode 100644 index 00000000..4c92a801 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/deb-postinst b/bsnes/gb/iOS/deb-postinst new file mode 100755 index 00000000..13dc0e2f --- /dev/null +++ b/bsnes/gb/iOS/deb-postinst @@ -0,0 +1,2 @@ +#!/bin/bash +/private/var/containers/SameBoy-iOS.app/installer install diff --git a/bsnes/gb/iOS/deb-prerm b/bsnes/gb/iOS/deb-prerm new file mode 100755 index 00000000..849f266d --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/dpad-tint@2x.png b/bsnes/gb/iOS/dpad-tint@2x.png new file mode 100644 index 00000000..125b0b9a Binary files /dev/null and b/bsnes/gb/iOS/dpad-tint@2x.png differ diff --git a/bsnes/gb/iOS/dpad-tint@3x.png b/bsnes/gb/iOS/dpad-tint@3x.png new file mode 100644 index 00000000..c434b01a Binary files /dev/null and b/bsnes/gb/iOS/dpad-tint@3x.png differ diff --git a/bsnes/gb/iOS/dpad@2x.png b/bsnes/gb/iOS/dpad@2x.png new file mode 100644 index 00000000..3ead7c5a Binary files /dev/null and b/bsnes/gb/iOS/dpad@2x.png differ diff --git a/bsnes/gb/iOS/dpad@3x.png b/bsnes/gb/iOS/dpad@3x.png new file mode 100644 index 00000000..401c4ccf Binary files /dev/null and b/bsnes/gb/iOS/dpad@3x.png differ diff --git a/bsnes/gb/iOS/dpadShadow@2x.png b/bsnes/gb/iOS/dpadShadow@2x.png new file mode 100644 index 00000000..cdbf9c36 Binary files /dev/null and b/bsnes/gb/iOS/dpadShadow@2x.png differ diff --git a/bsnes/gb/iOS/dpadShadow@3x.png b/bsnes/gb/iOS/dpadShadow@3x.png new file mode 100644 index 00000000..a37d7fc8 Binary files /dev/null and b/bsnes/gb/iOS/dpadShadow@3x.png differ diff --git a/bsnes/gb/iOS/dpadShadowDiagonal@2x.png b/bsnes/gb/iOS/dpadShadowDiagonal@2x.png new file mode 100644 index 00000000..3e9d6e6f Binary files /dev/null and b/bsnes/gb/iOS/dpadShadowDiagonal@2x.png differ diff --git a/bsnes/gb/iOS/dpadShadowDiagonal@3x.png b/bsnes/gb/iOS/dpadShadowDiagonal@3x.png new file mode 100644 index 00000000..6fe0c1c2 Binary files /dev/null and b/bsnes/gb/iOS/dpadShadowDiagonal@3x.png differ diff --git a/bsnes/gb/iOS/emulationSettings@2x.png b/bsnes/gb/iOS/emulationSettings@2x.png new file mode 100644 index 00000000..17d66f99 Binary files /dev/null and b/bsnes/gb/iOS/emulationSettings@2x.png differ diff --git a/bsnes/gb/iOS/emulationSettings@3x.png b/bsnes/gb/iOS/emulationSettings@3x.png new file mode 100644 index 00000000..6a391695 Binary files /dev/null and b/bsnes/gb/iOS/emulationSettings@3x.png differ diff --git a/bsnes/gb/iOS/installer.entitlements b/bsnes/gb/iOS/installer.entitlements new file mode 100644 index 00000000..5dd0f8a8 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/installer.m b/bsnes/gb/iOS/installer.m new file mode 100644 index 00000000..2fa2ff6e --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/jailbreak.entitlements b/bsnes/gb/iOS/jailbreak.entitlements new file mode 100644 index 00000000..ce0a6623 --- /dev/null +++ b/bsnes/gb/iOS/jailbreak.entitlements @@ -0,0 +1,8 @@ + + + + +com.apple.private.security.container-required + + + \ No newline at end of file diff --git a/bsnes/gb/iOS/logo@2x.png b/bsnes/gb/iOS/logo@2x.png new file mode 100644 index 00000000..60b7e74a Binary files /dev/null and b/bsnes/gb/iOS/logo@2x.png differ diff --git a/bsnes/gb/iOS/logo@3x.png b/bsnes/gb/iOS/logo@3x.png new file mode 100644 index 00000000..a3f354c3 Binary files /dev/null and b/bsnes/gb/iOS/logo@3x.png differ diff --git a/bsnes/gb/iOS/main.m b/bsnes/gb/iOS/main.m new file mode 100644 index 00000000..1ce112e5 --- /dev/null +++ b/bsnes/gb/iOS/main.m @@ -0,0 +1,175 @@ +#import +#import +#import "GBViewController.h" +#import "GBView.h" + +static double MigrateTurboSpeed(void) +{ + unsigned old = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBTurboSpeed"]; + if (old == 1) return 0; + return old; +} + +int main(int argc, char * argv[]) +{ + @autoreleasepool { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults 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, + @"GBTurboCap": @(MigrateTurboSpeed()), + @"GBRewindSpeed": @1, + @"GBDynamicSpeed": @NO, + @"GBFauxAnalogInputs": @NO, + + @"GBInterfaceTheme": @"SameBoy", + @"GBControllersHideInterface": @YES, + + @"GBCurrentTheme": @"Lime (Game Boy)", + // Default themes + @"GBThemes": @{ + @"Canyon": @{ + @"BrightnessBias": @0.1227009965823247, + @"Colors": @[@0xff0c1e20, @0xff122b91, @0xff466aa2, @0xfff1efae, @0xfff1efae], + @"DisabledLCDColor": @NO, + @"HueBias": @0.01782661816105247, + @"HueBiasStrength": @1, + @"Manual": @NO, + }, + @"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, + }, + @"Green Slate": @{ + @"BrightnessBias": @0.2210012227296829, + @"Colors": @[@0xff343117, @0xff6a876f, @0xff98b4a1, @0xffc3daca, @0xffc8decf], + @"DisabledLCDColor": @YES, + @"HueBias": @0.1887667975388467, + @"HueBiasStrength": @0.1272283345460892, + @"Manual": @NO, + }, + @"Green Tea": @{ + @"BrightnessBias": @-0.4946326622596153, + @"Colors": @[@0xff1a1d08, @0xff1d5231, @0xff3b9774, @0xff97e4c6, @0xffa9eed1], + @"DisabledLCDColor": @YES, + @"HueBias": @0.1912955007245464, + @"HueBiasStrength": @0.3621708039314516, + @"Manual": @NO, + }, + @"Lavender": @{ + @"BrightnessBias": @0.10072476038566, + @"Colors": @[@0xff2b2a3a, @0xff8c507c, @0xffbf82a8, @0xffe9bcce, @0xffeec3d3], + @"DisabledLCDColor": @YES, + @"HueBias": @0.7914529587142169, + @"HueBiasStrength": @0.2498168498277664, + @"Manual": @NO, + }, + @"Magic Eggplant": @{ + @"BrightnessBias": @0.0, + @"Colors": @[@0xff3c2136, @0xff942e84, @0xffc7699d, @0xfff1e4b0, @0xfff6f9b2], + @"DisabledLCDColor": @YES, + @"HueBias": @0.87717878486055778, + @"HueBiasStrength": @0.65018052788844627, + @"Manual": @NO, + }, + @"Mystic Blue": @{ + @"BrightnessBias": @-0.3291049897670746, + @"Colors": @[@0xff3b2306, @0xffa27807, @0xffd1b523, @0xfff6ebbe, @0xfffaf1e4], + @"DisabledLCDColor": @YES, + @"HueBias": @0.5282051088288426, + @"HueBiasStrength": @0.7699633836746216, + @"Manual": @NO, + }, + @"Pink Pop": @{ + @"BrightnessBias": @0.624908447265625, + @"Colors": @[@0xff28140a, @0xff7c42cb, @0xffaa83de, @0xffd1ceeb, @0xffd5d8ec], + @"DisabledLCDColor": @YES, + @"HueBias": @0.9477411056868732, + @"HueBiasStrength": @0.80024421215057373, + @"Manual": @NO, + }, + @"Radioactive Pea": @{ + @"BrightnessBias": @-0.48079556772908372, + @"Colors": @[@0xff215200, @0xff1f7306, @0xff169e34, @0xff03ceb8, @0xff00d4d1], + @"DisabledLCDColor": @YES, + @"HueBias": @0.3795131972111554, + @"HueBiasStrength": @0.34337649402390436, + @"Manual": @NO, + }, + @"Rose": @{ + @"BrightnessBias": @0.2727272808551788, + @"Colors": @[@0xff001500, @0xff4e1fae, @0xff865ac4, @0xffb7e6d3, @0xffbdffd4], + @"DisabledLCDColor": @YES, + @"HueBias": @0.9238900924101472, + @"HueBiasStrength": @0.9957716464996338, + @"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, + }, + }, + + // Forces iOS to use Solarium even when linking against older SDKs + @"com.apple.SwiftUI.IgnoreSolariumLinkedOnCheck": @YES, + }]; + + if (![[defaults stringForKey:@"GBThemesVersion"] isEqualToString:@(GB_VERSION)]) { + NSMutableDictionary *currentThemes = [defaults dictionaryForKey:@"GBThemes"].mutableCopy; + [defaults removeObjectForKey:@"GBThemes"]; + NSMutableDictionary *defaultThemes = [defaults dictionaryForKey:@"GBThemes"].mutableCopy; + if (![[NSUserDefaults standardUserDefaults] stringForKey:@"GBThemesVersion"]) { + // Force update the Pink Pop theme, it was glitchy in 1.0 + [currentThemes removeObjectForKey:@"Pink Pop"]; + } + [defaultThemes addEntriesFromDictionary:currentThemes]; + [defaults setObject:defaultThemes forKey:@"GBThemes"]; + [[NSUserDefaults standardUserDefaults] setObject:@(GB_VERSION) forKey:@"GBThemesVersion"]; + } + } + return UIApplicationMain(argc, argv, nil, NSStringFromClass([GBViewController class])); +} diff --git a/bsnes/gb/iOS/settingsOverlay@2x.png b/bsnes/gb/iOS/settingsOverlay@2x.png new file mode 100644 index 00000000..26ba1752 Binary files /dev/null and b/bsnes/gb/iOS/settingsOverlay@2x.png differ diff --git a/bsnes/gb/iOS/settingsOverlay@3x.png b/bsnes/gb/iOS/settingsOverlay@3x.png new file mode 100644 index 00000000..19013906 Binary files /dev/null and b/bsnes/gb/iOS/settingsOverlay@3x.png differ diff --git a/bsnes/gb/iOS/sideload.entitlements b/bsnes/gb/iOS/sideload.entitlements new file mode 100644 index 00000000..51178940 --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/iOS/swipepad-tint@2x.png b/bsnes/gb/iOS/swipepad-tint@2x.png new file mode 100644 index 00000000..08df0ea3 Binary files /dev/null and b/bsnes/gb/iOS/swipepad-tint@2x.png differ diff --git a/bsnes/gb/iOS/swipepad-tint@3x.png b/bsnes/gb/iOS/swipepad-tint@3x.png new file mode 100644 index 00000000..e011c001 Binary files /dev/null and b/bsnes/gb/iOS/swipepad-tint@3x.png differ diff --git a/bsnes/gb/iOS/swipepad@2x.png b/bsnes/gb/iOS/swipepad@2x.png new file mode 100644 index 00000000..7ebeda32 Binary files /dev/null and b/bsnes/gb/iOS/swipepad@2x.png differ diff --git a/bsnes/gb/iOS/swipepad@3x.png b/bsnes/gb/iOS/swipepad@3x.png new file mode 100644 index 00000000..65d3b46a Binary files /dev/null and b/bsnes/gb/iOS/swipepad@3x.png differ diff --git a/bsnes/gb/iOS/swipepadShadow@2x.png b/bsnes/gb/iOS/swipepadShadow@2x.png new file mode 100644 index 00000000..8840d240 Binary files /dev/null and b/bsnes/gb/iOS/swipepadShadow@2x.png differ diff --git a/bsnes/gb/iOS/swipepadShadow@3x.png b/bsnes/gb/iOS/swipepadShadow@3x.png new file mode 100644 index 00000000..115a58a2 Binary files /dev/null and b/bsnes/gb/iOS/swipepadShadow@3x.png differ diff --git a/bsnes/gb/iOS/swipepadShadowDiagonal@2x.png b/bsnes/gb/iOS/swipepadShadowDiagonal@2x.png new file mode 100644 index 00000000..7afe1956 Binary files /dev/null and b/bsnes/gb/iOS/swipepadShadowDiagonal@2x.png differ diff --git a/bsnes/gb/iOS/swipepadShadowDiagonal@3x.png b/bsnes/gb/iOS/swipepadShadowDiagonal@3x.png new file mode 100644 index 00000000..fa8d576e Binary files /dev/null and b/bsnes/gb/iOS/swipepadShadowDiagonal@3x.png differ diff --git a/bsnes/gb/iOS/themeSettings@2x.png b/bsnes/gb/iOS/themeSettings@2x.png new file mode 100644 index 00000000..ea048ca9 Binary files /dev/null and b/bsnes/gb/iOS/themeSettings@2x.png differ diff --git a/bsnes/gb/iOS/themeSettings@3x.png b/bsnes/gb/iOS/themeSettings@3x.png new file mode 100644 index 00000000..67cd924b Binary files /dev/null and b/bsnes/gb/iOS/themeSettings@3x.png differ diff --git a/bsnes/gb/iOS/videoSettings@2x.png b/bsnes/gb/iOS/videoSettings@2x.png new file mode 100644 index 00000000..387f09b4 Binary files /dev/null and b/bsnes/gb/iOS/videoSettings@2x.png differ diff --git a/bsnes/gb/iOS/videoSettings@3x.png b/bsnes/gb/iOS/videoSettings@3x.png new file mode 100644 index 00000000..4e1fdd45 Binary files /dev/null and b/bsnes/gb/iOS/videoSettings@3x.png differ diff --git a/bsnes/gb/iOS/waveform.weak@2x.png b/bsnes/gb/iOS/waveform.weak@2x.png new file mode 100644 index 00000000..e206b664 Binary files /dev/null and b/bsnes/gb/iOS/waveform.weak@2x.png differ diff --git a/bsnes/gb/iOS/waveform.weak@3x.png b/bsnes/gb/iOS/waveform.weak@3x.png new file mode 100644 index 00000000..be46c966 Binary files /dev/null and b/bsnes/gb/iOS/waveform.weak@3x.png differ diff --git a/bsnes/gb/libretro/Makefile b/bsnes/gb/libretro/Makefile index ada200df..3d69d740 100644 --- a/bsnes/gb/libretro/Makefile +++ b/bsnes/gb/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,19 +138,49 @@ 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) - 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 + CC = $(DEVKITPPC)/bin/powerpc-eabi-gcc$(EXE_EXT) + AR = $(DEVKITPPC)/bin/powerpc-eabi-ar$(EXE_EXT) + CFLAGS += -DGEKKO -DHW_RVL -DWIIU -mcpu=750 -meabi -mhard-float -D__ppc__ -DMSB_FIRST -I$(DEVKITPRO)/libogc/include + CFLAGS += -ffunction-sections -fdata-sections -D__wiiu__ -D__wut__ STATIC_LINKING = 1 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) $@ $(TARGET): $(OBJECTS) -@$(MKDIR) -p $(dir $@) diff --git a/bsnes/gb/libretro/Makefile.common b/bsnes/gb/libretro/Makefile.common index fabe3ad4..72fcd38c 100644 --- a/bsnes/gb/libretro/Makefile.common +++ b/bsnes/gb/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/bsnes/gb/libretro/gitlab-ci.yml b/bsnes/gb/libretro/gitlab-ci.yml new file mode 100644 index 00000000..9cb683bc --- /dev/null +++ b/bsnes/gb/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/bsnes/gb/libretro/jni/Android.mk b/bsnes/gb/libretro/jni/Android.mk index 8ac1b3ba..d1b7f67a 100644 --- a/bsnes/gb/libretro/jni/Android.mk +++ b/bsnes/gb/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/bsnes/gb/libretro/libretro.c b/bsnes/gb/libretro/libretro.c index 5a559c4c..1f9ca479 100644 --- a/bsnes/gb/libretro/libretro.c +++ b/bsnes/gb/libretro/libretro.c @@ -7,12 +7,8 @@ #include #include #include -#ifndef WIIU -#define AUDIO_FREQUENCY 384000 -#else -/* Use the internal sample rate for the Wii U */ -#define AUDIO_FREQUENCY 48000 -#endif + +#define WIIU_SAMPLE_RATE 48000 #ifdef _WIN32 #include @@ -34,7 +30,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 +37,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, - [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, @@ -73,20 +52,37 @@ enum screen_layout { }; enum audio_out { - GB_1, - GB_2 + AUDIO_OUT_GB_1, + AUDIO_OUT_GB_2, + AUDIO_OUT_BOTH, }; -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 +97,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; + uint32_t sizes[2]; + uint32_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 +172,131 @@ 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 init_output_audio_buffer(int32_t capacity) +{ + output_audio_buffer.data = NULL; + output_audio_buffer.sizes[0] = output_audio_buffer.sizes[1] = 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.sizes[0] = output_audio_buffer.sizes[1] = 0; + output_audio_buffer.capacity = 0; +} + +static void upload_output_audio_buffer() +{ + + uint32_t remaining_frames; + if (emulated_devices == 2) { + remaining_frames = MIN(output_audio_buffer.sizes[0], output_audio_buffer.sizes[1]) / 2; + output_audio_buffer.sizes[1] -= remaining_frames * 2; + } + else { + remaining_frames = output_audio_buffer.sizes[0] / 2; + } + output_audio_buffer.sizes[0] -= remaining_frames * 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; + } + if (emulated_devices == 2) { + memcpy(output_audio_buffer.data, buf_pos, MAX(output_audio_buffer.sizes[0], output_audio_buffer.sizes[1])); } } -static void vblank1(GB_gameboy_t *gb) +static void audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) { + unsigned index = 0; + if (gb == &gameboy[1]) { + index = 1; + } + + if (output_audio_buffer.capacity - MAX(output_audio_buffer.sizes[0], output_audio_buffer.sizes[1]) < 2) { + ensure_output_audio_buffer_capacity(output_audio_buffer.capacity * 1.5); + } + + if ((index == 0 && audio_out == AUDIO_OUT_GB_1) || + (index == 1 && audio_out == AUDIO_OUT_GB_2)) { + output_audio_buffer.data[output_audio_buffer.sizes[0]++] = sample->left; + output_audio_buffer.data[output_audio_buffer.sizes[0]++] = sample->right; + output_audio_buffer.sizes[1] = output_audio_buffer.sizes[0]; + } + else if (audio_out == AUDIO_OUT_BOTH) { + if (output_audio_buffer.sizes[index] < output_audio_buffer.sizes[!index]) { + // We're the second instance to reach this sample, add and divide (To prevent overflow) + output_audio_buffer.data[output_audio_buffer.sizes[index]] = + (output_audio_buffer.data[output_audio_buffer.sizes[index]] + (signed)sample->left) / 2; + output_audio_buffer.sizes[index]++; + + output_audio_buffer.data[output_audio_buffer.sizes[index]] = + (output_audio_buffer.data[output_audio_buffer.sizes[index]] + (signed)sample->right) / 2; + output_audio_buffer.sizes[index]++; + } + else { + // We're the first instance, set its contents + output_audio_buffer.data[output_audio_buffer.sizes[index]++] = sample->left; + output_audio_buffer.data[output_audio_buffer.sizes[index]++] = 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 +368,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 +381,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 +417,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[] = { @@ -418,44 +526,54 @@ static void set_link_cable_state(bool state) static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) { const char *model_name = (char *[]) { - [GB_BOOT_ROM_DMG0] = "dmg0", + [GB_BOOT_ROM_DMG_0] = "dmg0", [GB_BOOT_ROM_DMG] = "dmg", [GB_BOOT_ROM_MGB] = "mgb", [GB_BOOT_ROM_SGB] = "sgb", [GB_BOOT_ROM_SGB2] = "sgb2", - [GB_BOOT_ROM_CGB0] = "cgb0", + [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_DMG0] = dmg_boot, // dmg0 not implemented yet + [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_CGB0] = cgb_boot, // cgb0 not implemented yet + [GB_BOOT_ROM_CGB_0] = cgb0_boot, [GB_BOOT_ROM_CGB] = cgb_boot, [GB_BOOT_ROM_AGB] = agb_boot, }[type]; unsigned boot_length = (unsigned []) { - [GB_BOOT_ROM_DMG0] = dmg_boot_length, // dmg0 not implemented yet + [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_CGB0] = cgb_boot_length, // cgb0 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 +648,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); @@ -552,21 +671,27 @@ static void init_for_current_model(unsigned id) GB_set_pixels_output(&gameboy[i], (uint32_t *)(frame_buf + GB_get_screen_width(&gameboy[0]) * GB_get_screen_height(&gameboy[0]) * i)); GB_set_rgb_encode_callback(&gameboy[i], rgb_encode); - GB_set_sample_rate(&gameboy[i], AUDIO_FREQUENCY); +#ifdef WIIU + GB_set_sample_rate(&gameboy[i], WIIU_SAMPLE_RATE); +#else + GB_set_sample_rate(&gameboy[i], GB_get_clock_rate(&gameboy[i]) / 2); +#endif GB_apu_set_sample_callback(&gameboy[i], audio_callback); GB_set_rumble_callback(&gameboy[i], rumble_callback); /* 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 +721,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 +756,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 +831,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 +842,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"; @@ -781,75 +944,93 @@ static void check_variables() var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (strcmp(var.value, "Game Boy #1") == 0) { - audio_out = GB_1; + audio_out = AUDIO_OUT_GB_1; + } + else if (strcmp(var.value, "Game Boy #2") == 0) { + audio_out = AUDIO_OUT_GB_2; } else { - audio_out = GB_2; + audio_out = AUDIO_OUT_BOTH; } } 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 +1077,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 +1088,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 +1103,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 +1114,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 +1214,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 +1224,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 +1235,8 @@ void retro_deinit(void) frame_buf = NULL; frame_buf_copy = NULL; + free_output_audio_buffer(); + libretro_supports_bitmasks = false; } @@ -1075,14 +1259,14 @@ 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"; } void retro_get_system_av_info(struct retro_system_av_info *info) { struct retro_game_geometry geom; - struct retro_system_timing timing = { GB_get_usual_frame_rate(&gameboy[0]), AUDIO_FREQUENCY }; + struct retro_system_timing timing = { GB_get_usual_frame_rate(&gameboy[0]), GB_get_sample_rate(&gameboy[audio_out == AUDIO_OUT_BOTH? 0 : audio_out])}; if (emulated_devices == 2) { if (screen_layout == LAYOUT_TOP_DOWN) { @@ -1109,23 +1293,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) @@ -1151,6 +1337,15 @@ void retro_reset(void) init_for_current_model(i); GB_reset(&gameboy[i]); } + + if (emulated_devices == 2) { + if (GB_get_unmultiplied_clock_rate(&gameboy[0]) != GB_get_unmultiplied_clock_rate(&gameboy[1])) { + audio_out = AUDIO_OUT_GB_1; + } + } + else { + audio_out = AUDIO_OUT_GB_1; + } geometry_updated = true; } @@ -1183,7 +1378,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 +1431,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 +1539,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 +1556,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 +1817,3 @@ void retro_cheat_set(unsigned index, bool enabled, const char *code) (void)enabled; (void)code; } - diff --git a/bsnes/gb/libretro/libretro.h b/bsnes/gb/libretro/libretro.h index e6ee6269..4f4db1cf 100644 --- a/bsnes/gb/libretro/libretro.h +++ b/bsnes/gb/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/bsnes/gb/libretro/libretro_core_options.inc b/bsnes/gb/libretro/libretro_core_options.inc index 11857632..c372b7d3 100644 --- a/bsnes/gb/libretro/libretro_core_options.inc +++ b/bsnes/gb/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,31 +45,83 @@ 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" }, - { "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 }, - { "Super Game Boy", "Super Game Boy NTSC" }, + { "Auto", "Auto Detect DMG/CGB" }, + { "Auto (SGB)", "Auto Detect DMG/SGB/CGB" }, + { "Game Boy", "Game Boy (DMG-CPU B)" }, + { "Game Boy Pocket", "Game Boy Pocket/Light" }, + { "Game Boy Color 0", "Game Boy Color (CPU CGB 0)" }, + { "Game Boy Color A", "Game Boy Color (CPU CGB A)" }, + { "Game Boy Color B", "Game Boy Color (CPU CGB B)" }, + { "Game Boy Color C", "Game Boy Color (CPU CGB C)" }, + { "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 }, { NULL, NULL }, }, "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,10 +309,14 @@ struct retro_core_option_definition option_defs_us[] = { { "sameboy_audio_output", "System - Audio Output", - "Selects which of the two emulated Game Boy systems should output audio.", + "Audio Output", + "Selects which of the two emulated Game Boy systems should output audio. If Mix Both is chosen, both Game Boys must have the same clock speed.", + NULL, + "system", { { "Game Boy #1", NULL }, { "Game Boy #2", NULL }, + { "Mix Both", NULL }, { NULL, NULL }, }, "Game Boy #1" @@ -240,41 +324,94 @@ 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" }, - { "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 }, - { "Super Game Boy", "Super Game Boy NTSC" }, + { "Auto", "Auto Detect DMG/CGB" }, + { "Auto (SGB)", "Auto Detect DMG/SGB/CGB" }, + { "Game Boy", "Game Boy (DMG-CPU B)" }, + { "Game Boy Pocket", "Game Boy Pocket/Light" }, + { "Game Boy Color 0", "Game Boy Color (CPU CGB 0)" }, + { "Game Boy Color A", "Game Boy Color (CPU CGB A)" }, + { "Game Boy Color B", "Game Boy Color (CPU CGB B)" }, + { "Game Boy Color C", "Game Boy Color (CPU CGB C)" }, + { "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 }, { NULL, NULL }, }, "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" }, - { "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 }, - { "Super Game Boy", "Super Game Boy NTSC" }, + { "Auto", "Auto Detect DMG/CGB" }, + { "Auto (SGB)", "Auto Detect DMG/SGB/CGB" }, + { "Game Boy", "Game Boy (DMG-CPU B)" }, + { "Game Boy Pocket", "Game Boy Pocket/Light" }, + { "Game Boy Color 0", "Game Boy Color (CPU CGB 0)" }, + { "Game Boy Color A", "Game Boy Color (CPU CGB A)" }, + { "Game Boy Color B", "Game Boy Color (CPU CGB B)" }, + { "Game Boy Color C", "Game Boy Color (CPU CGB C)" }, + { "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 }, { NULL, NULL }, }, "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 +424,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 +440,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 +459,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 +478,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 +503,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 +511,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 +536,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 +544,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 +559,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 +574,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 +607,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 +640,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 +652,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 +683,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 +708,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 +725,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 +946,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/bsnes/gb/sameboy.pc.in b/bsnes/gb/sameboy.pc.in new file mode 100644 index 00000000..17c1b723 --- /dev/null +++ b/bsnes/gb/sameboy.pc.in @@ -0,0 +1,11 @@ +prefix=@prefix@ +exec_prefix=${prefix} +includedir=${prefix}/include +libdir=${prefix}/lib + +Name: sameboy +Description: SameBoy's emulation core as a library +Version: @version@ +Cflags: -I${includedir} +Libs: -L${libdir} -lsameboy +Libs.private: -lm -lc \ No newline at end of file diff --git a/bsnes/gb/version.mk b/bsnes/gb/version.mk index 89642709..9a3b395a 100644 --- a/bsnes/gb/version.mk +++ b/bsnes/gb/version.mk @@ -1 +1 @@ -VERSION := 0.14.7 \ No newline at end of file +VERSION := 1.0.2 \ No newline at end of file diff --git a/bsnes/sfc/coprocessor/icd/icd.cpp b/bsnes/sfc/coprocessor/icd/icd.cpp index ae56d819..9a39f90b 100644 --- a/bsnes/sfc/coprocessor/icd/icd.cpp +++ b/bsnes/sfc/coprocessor/icd/icd.cpp @@ -42,10 +42,10 @@ namespace SameBoy { icd.apuWrite(left, right); } - static auto vblank(GB_gameboy_t*) -> void { + static auto vblank(GB_gameboy_t*, GB_vblank_type_t) -> void { } - static auto log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) -> void { + static auto log(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes) -> void { } } diff --git a/bsnes/sfc/sfc.hpp b/bsnes/sfc/sfc.hpp index 9ecf56de..8a0c9dc1 100644 --- a/bsnes/sfc/sfc.hpp +++ b/bsnes/sfc/sfc.hpp @@ -3,6 +3,13 @@ //license: GPLv3 //started: 2004-10-14 +extern "C" { + #include + #include + // Avoid conflict because unreachable() in SameBoy and unreachable in nall + #undef unreachable +} + #include #include #include @@ -14,11 +21,6 @@ #include #include -extern "C" { - #include - #include -} - namespace SuperFamicom { #define platform Emulator::platform namespace File = Emulator::File; diff --git a/bsnes/target-bsnes/presentation/presentation.cpp b/bsnes/target-bsnes/presentation/presentation.cpp index a872c2ff..13440579 100644 --- a/bsnes/target-bsnes/presentation/presentation.cpp +++ b/bsnes/target-bsnes/presentation/presentation.cpp @@ -183,9 +183,9 @@ auto Presentation::create() -> void { .setName("SameBoy") .setLogo(Resource::SameBoy) .setDescription("Super Game Boy emulator") - .setVersion("0.14.7") + .setVersion("1.0.2") .setCopyright("Lior Halphon") - .setLicense("MIT") + .setLicense("Expat") .setWebsite("https://sameboy.github.io") .setAlignment(*this) .show();