diff --git a/Cocoa/AppDelegate.m b/Cocoa/AppDelegate.m index dce9a70..d9cfdef 100644 --- a/Cocoa/AppDelegate.m +++ b/Cocoa/AppDelegate.m @@ -28,10 +28,12 @@ @"GBStart": @(kVK_Return), @"GBTurbo": @(kVK_Space), + @"GBRewind": @(kVK_Tab), @"GBFilter": @"NearestNeighbor", @"GBColorCorrection": @(GB_COLOR_CORRECTION_EMULATE_HARDWARE), - @"GBHighpassFilter": @(GB_HIGHPASS_REMOVE_DC_OFFSET) + @"GBHighpassFilter": @(GB_HIGHPASS_REMOVE_DC_OFFSET), + @"GBRewindLength": @(10) }]; } diff --git a/Cocoa/Document.m b/Cocoa/Document.m index 6ec45c8..d4a4288 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -51,6 +51,8 @@ enum model { bool logToSideView; bool shouldClearSideView; enum model current_model; + + bool rewind; } @property GBAudioClient *audioClient; @@ -166,6 +168,7 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, 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"]); [self loadROM]; } @@ -179,6 +182,9 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, [self reloadVRAMData: nil]; }); } + if (self.view.isRewinding) { + rewind = true; + } } - (void) run @@ -197,7 +203,16 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, NSTimer *hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode]; while (running) { - GB_run(&gb); + if (rewind) { + rewind = false; + GB_rewind_pop(&gb); + if (!GB_rewind_pop(&gb)) { + rewind = self.view.isRewinding; + } + } + else { + GB_run(&gb); + } } [hex_timer invalidate]; [self.audioClient stop]; @@ -327,6 +342,11 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, name:@"GBColorCorrectionChanged" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateRewindLength) + name:@"GBRewindLengthChanged" + object:nil]; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) { [self initDMG]; } @@ -1324,4 +1344,11 @@ static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height, } } +- (void) updateRewindLength +{ + if (GB_is_inited(&gb)) { + GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]); + } +} + @end diff --git a/Cocoa/GBButtons.h b/Cocoa/GBButtons.h index e02440e..0bd9fdc 100644 --- a/Cocoa/GBButtons.h +++ b/Cocoa/GBButtons.h @@ -11,6 +11,7 @@ typedef enum : NSUInteger { GBSelect, GBStart, GBTurbo, + GBRewind, GBButtonCount } GBButton; diff --git a/Cocoa/GBButtons.m b/Cocoa/GBButtons.m index 9784eef..db0f364 100644 --- a/Cocoa/GBButtons.m +++ b/Cocoa/GBButtons.m @@ -1,4 +1,4 @@ #import #import "GBButtons.h" -NSString const *GBButtonNames[] = {@"Right", @"Left", @"Up", @"Down", @"A", @"B", @"Select", @"Start", @"Turbo"}; \ No newline at end of file +NSString const *GBButtonNames[] = {@"Right", @"Left", @"Up", @"Down", @"A", @"B", @"Select", @"Start", @"Turbo", @"Rewind"}; diff --git a/Cocoa/GBPreferencesWindow.h b/Cocoa/GBPreferencesWindow.h index 88cd926..1334d48 100644 --- a/Cocoa/GBPreferencesWindow.h +++ b/Cocoa/GBPreferencesWindow.h @@ -7,6 +7,7 @@ @property (strong) IBOutlet NSButton *aspectRatioCheckbox; @property (strong) IBOutlet NSPopUpButton *highpassFilterPopupButton; @property (strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton; +@property (strong) IBOutlet NSPopUpButton *rewindPopupButton; @property (strong) IBOutlet NSButton *configureJoypadButton; @property (strong) IBOutlet NSButton *skipButton; @end diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m index e85d2e1..c2dfe2d 100644 --- a/Cocoa/GBPreferencesWindow.m +++ b/Cocoa/GBPreferencesWindow.m @@ -14,6 +14,7 @@ NSPopUpButton *_graphicsFilterPopupButton; NSPopUpButton *_highpassFilterPopupButton; NSPopUpButton *_colorCorrectionPopupButton; + NSPopUpButton *_rewindPopupButton; NSButton *_aspectRatioCheckbox; } @@ -77,6 +78,18 @@ return _colorCorrectionPopupButton; } +- (void)setRewindPopupButton:(NSPopUpButton *)rewindPopupButton +{ + _rewindPopupButton = rewindPopupButton; + NSInteger length = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]; + [_rewindPopupButton selectItemWithTag:length]; +} + +- (NSPopUpButton *)rewindPopupButton +{ + return _rewindPopupButton; +} + - (void)setHighpassFilterPopupButton:(NSPopUpButton *)highpassFilterPopupButton { _highpassFilterPopupButton = highpassFilterPopupButton; @@ -161,6 +174,13 @@ } +- (IBAction)rewindLengthChanged:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) + forKey:@"GBRewindLength"]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBRewindLengthChanged" object:nil]; +} + - (IBAction) configureJoypad:(id)sender { [self.configureJoypadButton setEnabled:NO]; diff --git a/Cocoa/GBView.h b/Cocoa/GBView.h index 813f8c0..ffd6dc0 100644 --- a/Cocoa/GBView.h +++ b/Cocoa/GBView.h @@ -10,4 +10,5 @@ @property (nonatomic) BOOL shouldBlendFrameWithPrevious; @property GBShader *shader; @property (getter=isMouseHidingEnabled) BOOL mouseHidingEnabled; +@property bool isRewinding; @end diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m index 3afc247..71aee65 100644 --- a/Cocoa/GBView.m +++ b/Cocoa/GBView.m @@ -173,7 +173,12 @@ handled = true; switch (i) { case GBTurbo: - GB_set_turbo_mode(_gb, true, false); + GB_set_turbo_mode(_gb, true, self.isRewinding); + break; + + case GBRewind: + self.isRewinding = true; + GB_set_turbo_mode(_gb, false, false); break; default: @@ -201,6 +206,10 @@ case GBTurbo: GB_set_turbo_mode(_gb, false, false); break; + + case GBRewind: + self.isRewinding = false; + break; default: GB_set_key_state(_gb, (GB_key_t)i, false); @@ -224,7 +233,14 @@ if (mapped_button && [mapped_button integerValue] == button) { switch (i) { case GBTurbo: - GB_set_turbo_mode(_gb, state, false); + GB_set_turbo_mode(_gb, state, state && self.isRewinding); + break; + + case GBRewind: + self.isRewinding = state; + if (state) { + GB_set_turbo_mode(_gb, false, false); + } break; default: diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib index 57c42db..c599225 100644 --- a/Cocoa/Preferences.xib +++ b/Cocoa/Preferences.xib @@ -1,8 +1,8 @@ - + - + @@ -17,14 +17,14 @@ - + - + - + @@ -33,7 +33,7 @@ - + @@ -67,7 +67,7 @@ - + @@ -76,7 +76,7 @@ - + @@ -97,7 +97,7 @@ - + @@ -116,8 +116,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -137,7 +161,7 @@ - + @@ -145,15 +169,46 @@ + + + + + + + + + + + - + - + - + @@ -203,28 +258,6 @@ - - @@ -235,9 +268,10 @@ + - + diff --git a/Core/display.c b/Core/display.c index f9533ba..41f85bf 100755 --- a/Core/display.c +++ b/Core/display.c @@ -198,6 +198,8 @@ static uint32_t get_pixel(GB_gameboy_t *gb, uint8_t x, uint8_t y) static void display_vblank(GB_gameboy_t *gb) { + gb->vblank_just_occured = true; + if (gb->turbo) { if (GB_timing_sync_turbo(gb)) { return; @@ -216,8 +218,6 @@ static void display_vblank(GB_gameboy_t *gb) gb->vblank_callback(gb); GB_timing_sync(gb); - - gb->vblank_just_occured = true; } static inline uint8_t scale_channel(uint8_t x) diff --git a/Core/gb.c b/Core/gb.c index cc86fef..c2b6390 100755 --- a/Core/gb.c +++ b/Core/gb.c @@ -11,6 +11,10 @@ #endif #include "gb.h" +/* The libretro frontend does not link against rewind.c, so we provide empty weak alternatives to its functions */ +void __attribute__((weak)) GB_rewind_free(GB_gameboy_t *gb) { } +void __attribute__((weak)) GB_rewind_push(GB_gameboy_t *gb) { } + void GB_attributed_logv(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, va_list args) { char *string = NULL; @@ -149,6 +153,7 @@ void GB_free(GB_gameboy_t *gb) gb->reversed_symbol_map.buckets[i] = next; } } + GB_rewind_free(gb); memset(gb, 0, sizeof(*gb)); } @@ -280,6 +285,7 @@ uint8_t GB_run(GB_gameboy_t *gb) GB_update_joyp(gb); GB_rtc_run(gb); GB_debugger_handle_async_commands(gb); + GB_rewind_push(gb); } return gb->cycles_since_run; } @@ -505,6 +511,7 @@ void GB_switch_model_and_reset(GB_gameboy_t *gb, bool is_cgb) gb->vram = realloc(gb->vram, gb->vram_size = 0x2000); } gb->is_cgb = is_cgb; + GB_rewind_free(gb); GB_reset(gb); } diff --git a/Core/gb.h b/Core/gb.h index ca373f5..8f41e78 100644 --- a/Core/gb.h +++ b/Core/gb.h @@ -17,6 +17,7 @@ #include "memory.h" #include "printer.h" #include "timing.h" +#include "rewind.h" #include "z80_cpu.h" #include "symbol_hash.h" @@ -161,7 +162,7 @@ typedef enum { #define CPU_FREQUENCY 0x400000 #define DIV_CYCLES (0x100) #define INTERNAL_DIV_CYCLES (0x40000) -#define FRAME_LENGTH 16742706 // in nanoseconds +#define FRAME_LENGTH (1000000000LL * LCDC_PERIOD / CPU_FREQUENCY) // in nanoseconds #if !defined(MIN) #define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) @@ -475,6 +476,16 @@ struct GB_gameboy_internal_s { /* Ticks command */ unsigned long debugger_ticks; + + /* Rewind */ +#define GB_REWIND_FRAMES_PER_KEY 255 + size_t rewind_buffer_length; + struct { + uint8_t *key_state; + uint8_t *compressed_states[GB_REWIND_FRAMES_PER_KEY]; + unsigned pos; + } *rewind_sequences; // lasts about 4 seconds + size_t rewind_pos; /* Misc */ bool turbo; diff --git a/Core/rewind.c b/Core/rewind.c new file mode 100644 index 0000000..ae711a2 --- /dev/null +++ b/Core/rewind.c @@ -0,0 +1,205 @@ +#include "rewind.h" +#include + +static uint8_t *state_compress(const uint8_t *prev, const uint8_t *data, size_t uncompressed_size) +{ + size_t malloc_size = 0x1000; + uint8_t *compressed = malloc(malloc_size); + size_t counter_pos = 0; + size_t data_pos = sizeof(uint16_t); + bool prev_mode = true; + *(uint16_t *)compressed = 0; +#define COUNTER (*(uint16_t *)&compressed[counter_pos]) +#define DATA (compressed[data_pos]) + + while (uncompressed_size) { + if (prev_mode) { + if (*data == *prev && COUNTER != 0xffff) { + COUNTER++; + data++; + prev++; + uncompressed_size--; + } + else { + prev_mode = false; + counter_pos += sizeof(uint16_t); + data_pos = counter_pos + sizeof(uint16_t); + if (data_pos >= malloc_size) { + malloc_size *= 2; + compressed = realloc(compressed, malloc_size); + } + COUNTER = 0; + } + } + else { + if (*data != *prev && COUNTER != 0xffff) { + COUNTER++; + DATA = *data; + data_pos++; + data++; + prev++; + uncompressed_size--; + if (data_pos >= malloc_size) { + malloc_size *= 2; + compressed = realloc(compressed, malloc_size); + } + } + else { + prev_mode = true; + counter_pos = data_pos; + data_pos = counter_pos + sizeof(uint16_t); + if (counter_pos >= malloc_size - 1) { + malloc_size *= 2; + compressed = realloc(compressed, malloc_size); + } + COUNTER = 0; + } + } + } + + return realloc(compressed, data_pos); +#undef DATA +#undef COUNTER +} + + +static void state_decompress(const uint8_t *prev, uint8_t *data, uint8_t *dest, size_t uncompressed_size) +{ + size_t counter_pos = 0; + size_t data_pos = sizeof(uint16_t); + bool prev_mode = true; +#define COUNTER (*(uint16_t *)&data[counter_pos]) +#define DATA (data[data_pos]) + + while (uncompressed_size) { + if (prev_mode) { + if (COUNTER) { + COUNTER--; + *(dest++) = *(prev++); + uncompressed_size--; + } + else { + prev_mode = false; + counter_pos += sizeof(uint16_t); + data_pos = counter_pos + sizeof(uint16_t); + } + } + else { + if (COUNTER) { + COUNTER--; + *(dest++) = DATA; + data_pos++; + prev++; + uncompressed_size--; + } + else { + prev_mode = true; + counter_pos = data_pos; + data_pos += sizeof(uint16_t); + } + } + } +#undef DATA +#undef COUNTER +} + +void GB_rewind_push(GB_gameboy_t *gb) +{ + const size_t save_size = GB_get_save_state_size(gb); + if (!gb->rewind_sequences) { + if (gb->rewind_buffer_length) { + gb->rewind_sequences = malloc(sizeof(*gb->rewind_sequences) * gb->rewind_buffer_length); + memset(gb->rewind_sequences, 0, sizeof(*gb->rewind_sequences) * gb->rewind_buffer_length); + gb->rewind_pos = 0; + } + else { + return; + } + } + + if (gb->rewind_sequences[gb->rewind_pos].pos == GB_REWIND_FRAMES_PER_KEY) { + gb->rewind_pos++; + if (gb->rewind_pos == gb->rewind_buffer_length) { + gb->rewind_pos = 0; + } + if (gb->rewind_sequences[gb->rewind_pos].key_state) { + free(gb->rewind_sequences[gb->rewind_pos].key_state); + gb->rewind_sequences[gb->rewind_pos].key_state = NULL; + } + 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].pos = 0; + } + + if (!gb->rewind_sequences[gb->rewind_pos].key_state) { + gb->rewind_sequences[gb->rewind_pos].key_state = malloc(save_size); + GB_save_state_to_buffer(gb, gb->rewind_sequences[gb->rewind_pos].key_state); + } + else { + uint8_t *save_state = malloc(save_size); + GB_save_state_to_buffer(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); + free(save_state); + } + +} + +bool GB_rewind_pop(GB_gameboy_t *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(gb); + if (gb->rewind_sequences[gb->rewind_pos].pos == 0) { + GB_load_state_from_buffer(gb, gb->rewind_sequences[gb->rewind_pos].key_state, save_size); + 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; + return true; + } + + uint8_t *save_state = malloc(save_size); + state_decompress(gb->rewind_sequences[gb->rewind_pos].key_state, + gb->rewind_sequences[gb->rewind_pos].compressed_states[--gb->rewind_sequences[gb->rewind_pos].pos], + save_state, + 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_load_state_from_buffer(gb, save_state, save_size); + free(save_state); + return true; +} + +void GB_rewind_free(GB_gameboy_t *gb) +{ + if (!gb->rewind_sequences) return; + for (unsigned i = 0; i < gb->rewind_buffer_length; i++) { + if (gb->rewind_sequences[i].key_state) { + free(gb->rewind_sequences[i].key_state); + } + for (unsigned j = 0; j < GB_REWIND_FRAMES_PER_KEY; j++) { + if (gb->rewind_sequences[i].compressed_states[j]) { + free(gb->rewind_sequences[i].compressed_states[j]); + } + } + } + free(gb->rewind_sequences); + gb->rewind_sequences = NULL; +} + +void GB_set_rewind_length(GB_gameboy_t *gb, double seconds) +{ + GB_rewind_free(gb); + if (seconds == 0) { + gb->rewind_buffer_length = 0; + } + else { + gb->rewind_buffer_length = (size_t) ceil(seconds * CPU_FREQUENCY / LCDC_PERIOD / GB_REWIND_FRAMES_PER_KEY); + } +} diff --git a/Core/rewind.h b/Core/rewind.h new file mode 100644 index 0000000..30d795c --- /dev/null +++ b/Core/rewind.h @@ -0,0 +1,13 @@ +#ifndef rewind_h +#define rewind_h + +#include "gb.h" + +#ifdef GB_INTERNAL +void GB_rewind_push(GB_gameboy_t *gb); +void GB_rewind_free(GB_gameboy_t *gb); +#endif +bool GB_rewind_pop(GB_gameboy_t *gb); +void GB_set_rewind_length(GB_gameboy_t *gb, double seconds); + +#endif