Accurately emulate CGB first frame behavior. Fixes #432, fixes #482

This commit is contained in:
Lior Halphon 2022-08-09 00:54:31 +03:00
parent f0959d4e70
commit 52ab200544
8 changed files with 132 additions and 74 deletions

View File

@ -56,7 +56,16 @@ enum model {
};
@interface Document ()
@property GBAudioClient *audioClient;
@end
@implementation Document
{
GB_gameboy_t gb;
volatile bool running;
volatile bool stopping;
NSConditionLock *has_debugger_input;
NSMutableArray *debugger_input_queue;
NSMutableAttributedString *pending_console_output;
NSRecursiveLock *console_output_lock;
@ -66,10 +75,10 @@ enum model {
bool fullScreen;
bool in_sync_input;
HFController *hex_controller;
NSString *lastConsoleInput;
HFLineCountingRepresenter *lineRep;
CVImageBufferRef cameraImage;
AVCaptureSession *cameraSession;
AVCaptureConnection *cameraConnection;
@ -111,25 +120,6 @@ enum model {
void (^ volatile _pendingAtomicBlock)();
}
@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);
@ -139,7 +129,7 @@ static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type)
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)
@ -227,15 +217,6 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on)
}
@implementation Document
{
GB_gameboy_t gb;
volatile bool running;
volatile bool stopping;
NSConditionLock *has_debugger_input;
NSMutableArray *debugger_input_queue;
}
- (instancetype)init
{
self = [super init];
@ -341,26 +322,29 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on)
self.osdView.usesSGBScale = GB_get_screen_width(&gb) == 256;
}
- (void) vblank
- (void) vblankWithType:(GB_vblank_type_t)type
{
if (_gbsVisualizer) {
dispatch_async(dispatch_get_main_queue(), ^{
[_gbsVisualizer setNeedsDisplay:true];
});
}
[self.view flip];
if (borderModeChanged) {
dispatch_sync(dispatch_get_main_queue(), ^{
size_t previous_width = GB_get_screen_width(&gb);
GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
if (GB_get_screen_width(&gb) != previous_width) {
[self.view screenSizeChanged];
[self updateMinSize];
}
});
borderModeChanged = false;
if (type != GB_VBLANK_TYPE_REPEAT) {
[self.view flip];
if (borderModeChanged) {
dispatch_sync(dispatch_get_main_queue(), ^{
size_t previous_width = GB_get_screen_width(&gb);
GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
if (GB_get_screen_width(&gb) != previous_width) {
[self.view screenSizeChanged];
[self updateMinSize];
}
});
borderModeChanged = false;
}
GB_set_pixels_output(&gb, self.view.pixels);
}
GB_set_pixels_output(&gb, self.view.pixels);
if (self.vramWindow.isVisible) {
dispatch_async(dispatch_get_main_queue(), ^{
self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0;

View File

@ -107,7 +107,7 @@ typedef struct __attribute__((packed)) {
} object_t;
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;
@ -123,9 +123,19 @@ void GB_display_vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
}
}
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;
}
bool is_ppu_stopped = !GB_is_cgb(gb) && gb->stopped && gb->io_registers[GB_IO_LCDC] & 0x80;
if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & 0x80) || is_ppu_stopped) || gb->cgb_repeated_a_frame || gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON)) {
if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & 0x80) || 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;
@ -313,9 +323,9 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border)
new_b = new_b * 15 / 16 + (r + g ) / 32;
if (agb) {
new_r = new_r * (224 - 40) / 255 + 20;
new_g = new_g * (220 - 36) / 255 + 18;
new_b = new_b * (216 - 32) / 255 + 16;
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;
@ -1398,6 +1408,13 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force)
gb->mode_for_interrupt = 3;
}
gb->cycles_since_vblank_callback += cycles / 2;
if (cycles < gb->frame_repeat_countdown) {
gb->frame_repeat_countdown -= cycles;
}
else {
gb->frame_repeat_countdown = 0;
}
/* The PPU does not advance while in STOP mode on the DMG */
if (gb->stopped && !GB_is_cgb(gb)) {
@ -1457,7 +1474,6 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force)
GB_SLEEP(gb, display, 1, LCDC_PERIOD - gb->cycles_since_vblank_callback);
}
GB_display_vblank(gb, GB_VBLANK_TYPE_LCD_OFF);
gb->cgb_repeated_a_frame = true;
}
return;
}
@ -1987,14 +2003,14 @@ skip_slow_mode_3:
if (gb->frame_skip_state == GB_FRAMESKIP_LCD_TURNED_ON) {
if (GB_is_cgb(gb)) {
GB_display_vblank(gb, GB_VBLANK_TYPE_NORMAL_FRAME);
gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_SKIPPED;
gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_RENDERED;
}
else {
if (!GB_is_sgb(gb) || gb->current_lcd_line < LINES) {
gb->is_odd_frame ^= true;
GB_display_vblank(gb, GB_VBLANK_TYPE_NORMAL_FRAME);
}
gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED;
gb->frame_skip_state = GB_FRAMESKIP_FIRST_FRAME_RENDERED;
}
}
else {
@ -2002,16 +2018,19 @@ skip_slow_mode_3:
gb->is_odd_frame ^= true;
GB_display_vblank(gb, GB_VBLANK_TYPE_NORMAL_FRAME);
}
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;
}
}
}
/* 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);
}

View File

@ -9,6 +9,7 @@ typedef enum {
GB_VBLANK_TYPE_NORMAL_FRAME, // An actual Vblank-triggered frame
GB_VBLANK_TYPE_LCD_OFF, // An artificial frame pushed while the LCD was off
GB_VBLANK_TYPE_ARTIFICIAL, // An artificial frame pushed for some other reason
GB_VBLANK_TYPE_REPEAT, // A frame that would not render on actual hardware, but the screen should retain the previous frame
} GB_vblank_type_t;
#ifdef GB_INTERNAL

View File

@ -1183,6 +1183,11 @@ void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output)
gb->screen = output;
}
uint32_t *GB_get_pixels_output(GB_gameboy_t *gb)
{
return gb->screen;
}
void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback)
{
gb->vblank_callback = callback;
@ -1220,6 +1225,11 @@ void GB_set_lcd_line_callback(GB_gameboy_t *gb, GB_lcd_line_callback_t callback)
gb->lcd_line_callback = callback;
}
void GB_set_lcd_status_callback(GB_gameboy_t *gb, GB_lcd_status_callback_t callback)
{
gb->lcd_status_callback = callback;
}
const GB_palette_t GB_PALETTE_GREY = {{{0x00, 0x00, 0x00}, {0x55, 0x55, 0x55}, {0xAA, 0xAA, 0xAA}, {0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF}}};
const GB_palette_t GB_PALETTE_DMG = {{{0x08, 0x18, 0x10}, {0x39, 0x61, 0x39}, {0x84, 0xA5, 0x63}, {0xC6, 0xDE, 0x8C}, {0xD2, 0xE6, 0xA6}}};
const GB_palette_t GB_PALETTE_MGB = {{{0x07, 0x10, 0x0E}, {0x3A, 0x4C, 0x3A}, {0x81, 0x8D, 0x66}, {0xC2, 0xCE, 0x93}, {0xCF, 0xDA, 0xAC}}};

View File

@ -304,6 +304,7 @@ typedef void (*GB_boot_rom_load_callback_t)(GB_gameboy_t *gb, GB_boot_rom_t type
typedef void (*GB_execution_callback_t)(GB_gameboy_t *gb, uint16_t address, uint8_t opcode);
typedef void (*GB_lcd_line_callback_t)(GB_gameboy_t *gb, uint8_t line);
typedef void (*GB_lcd_status_callback_t)(GB_gameboy_t *gb, bool on);
struct GB_breakpoint_s;
struct GB_watchpoint_s;
@ -600,8 +601,8 @@ struct GB_gameboy_internal_s {
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,
GB_FRAMESKIP_FIRST_FRAME_SKIPPED__DEPRECATED,
GB_FRAMESKIP_FIRST_FRAME_RENDERED,
} frame_skip_state;
bool oam_read_blocked;
bool vram_read_blocked;
@ -650,9 +651,10 @@ 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;
GB_PADDING(bool, cgb_repeated_a_frame);
uint8_t data_for_sel_glitch;
bool delayed_glitch_hblank_interrupt;
uint32_t frame_repeat_countdown;
)
/* Unsaved data. This includes all pointers, as well as everything that shouldn't be on a save state */
@ -732,7 +734,7 @@ struct GB_gameboy_internal_s {
GB_workboy_get_time_callback workboy_get_time_callback;
GB_execution_callback_t execution_callback;
GB_lcd_line_callback_t lcd_line_callback;
GB_lcd_status_callback_t lcd_status_callback;
/*** Debugger ***/
volatile bool debug_stopped, debug_disable;
bool debug_fin_command, debug_next_command;
@ -894,6 +896,7 @@ void GB_log(GB_gameboy_t *gb, const char *fmt, ...) __printflike(2, 3);
void GB_attributed_log(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, ...) __printflike(3, 4);
void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output);
uint32_t *GB_get_pixels_output(GB_gameboy_t *gb);
void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode);
void GB_set_infrared_input(GB_gameboy_t *gb, bool state);
@ -911,6 +914,7 @@ void GB_set_boot_rom_load_callback(GB_gameboy_t *gb, GB_boot_rom_load_callback_t
void GB_set_execution_callback(GB_gameboy_t *gb, GB_execution_callback_t callback);
void GB_set_lcd_line_callback(GB_gameboy_t *gb, GB_lcd_line_callback_t callback);
void GB_set_lcd_status_callback(GB_gameboy_t *gb, GB_lcd_status_callback_t callback);
void GB_set_palette(GB_gameboy_t *gb, const GB_palette_t *palette);
const GB_palette_t *GB_get_palette(GB_gameboy_t *gb);

View File

@ -1440,6 +1440,9 @@ 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)) {
// 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
@ -1451,15 +1454,18 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
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)) {
/* Sync after turning off LCD */
if (gb->lcd_status_callback) {
gb->lcd_status_callback(gb, false);
}
gb->double_speed_alignment = 0;
GB_timing_sync(gb);
GB_lcd_off(gb);

View File

@ -509,15 +509,17 @@ static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
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);

View File

@ -99,6 +99,8 @@ static bool auto_sgb_enabled[2] = {
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;
@ -250,14 +252,42 @@ static void audio_callback(GB_gameboy_t *gb, GB_sample_t *sample)
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, 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)
@ -627,8 +657,10 @@ static void init_for_current_model(unsigned id)
/* todo: attempt to make these more generic */
GB_set_vblank_callback(&gameboy[0], (GB_vblank_callback_t) vblank1);
GB_set_lcd_status_callback(&gameboy[0], lcd_status_change_1);
if (emulated_devices == 2) {
GB_set_vblank_callback(&gameboy[1], (GB_vblank_callback_t) vblank2);
GB_set_lcd_status_callback(&gameboy[2], lcd_status_change_2);
if (link_cable_emulation) {
set_link_cable_state(true);
}