STAT timing and LCD interrupts rewritten, should be more accurate.

This commit is contained in:
Lior Halphon 2017-02-19 02:22:50 +02:00
parent 74d00b84b7
commit 399e88d5fe
4 changed files with 267 additions and 125 deletions

View File

@ -11,6 +11,25 @@
#include <Windows.h> #include <Windows.h>
#endif #endif
/*
Each line is 456 cycles, approximately:
Mode 2 - 80 cycles / OAM Transfer
Mode 3 - 172 cycles / Rendering
Mode 0 - 204 cycles / HBlank
Mode 1 is VBlank
Todo: Mode lengths are not constants, see http://blog.kevtris.org/blogfiles/Nitty%20Gritty%20Gameboy%20VRAM%20Timing.txt
*/
#define MODE2_LENGTH (80)
#define MODE3_LENGTH (172)
#define MODE0_LENGTH (204)
#define LINE_LENGTH (MODE2_LENGTH + MODE3_LENGTH + MODE0_LENGTH) // = 456
#define LINES (144)
#define WIDTH (160)
#define VIRTUAL_LINES (LCDC_PERIOD / LINE_LENGTH) // = 154
typedef struct __attribute__((packed)) { typedef struct __attribute__((packed)) {
uint8_t y; uint8_t y;
uint8_t x; uint8_t x;
@ -244,7 +263,7 @@ void display_vblank(GB_gameboy_t *gb)
if (!(gb->io_registers[GB_IO_LCDC] & 0x80) || gb->stopped) { if (!(gb->io_registers[GB_IO_LCDC] & 0x80) || gb->stopped) {
/* LCD is off, memset screen to white */ /* LCD is off, memset screen to white */
memset(gb->screen, 0xFF, 160 * 144 * 4); memset(gb->screen, 0xFF, WIDTH * LINES * 4);
} }
gb->vblank_callback(gb); gb->vblank_callback(gb);
@ -281,25 +300,232 @@ void GB_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index
(background_palette? gb->background_palletes_rgb : gb->sprite_palletes_rgb)[index / 2] = gb->rgb_encode_callback(gb, r, g, b); (background_palette? gb->background_palletes_rgb : gb->sprite_palletes_rgb)[index / 2] = gb->rgb_encode_callback(gb, r, g, b);
} }
/* /*
Each line is 456 cycles, approximately: STAT interrupt is implemented based on this finding:
Mode 2 - 80 cycles / OAM Transfer http://board.byuu.org/phpbb3/viewtopic.php?p=25527#p25531
Mode 3 - 172 cycles / Rendering
Mode 0 - 204 cycles / HBlank
Mode 1 is VBlank General timing is based on GiiBiiAdvance's documents:
https://github.com/AntonioND/giibiiadvance
Todo: Mode lengths are not constants, see http://blog.kevtris.org/blogfiles/Nitty%20Gritty%20Gameboy%20VRAM%20Timing.txt
*/ */
#define MODE2_LENGTH (80) static void update_display_state(GB_gameboy_t *gb, uint8_t cycles)
#define MODE3_LENGTH (172)
#define MODE0_LENGTH (204)
#define LINE_LENGTH (MODE2_LENGTH + MODE3_LENGTH + MODE0_LENGTH) // = 456
void GB_display_run(GB_gameboy_t *gb)
{ {
uint8_t previous_stat_interrupt_line = gb->stat_interrupt_line;
gb->stat_interrupt_line = false;
if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) {
/* LCD is disabled, do nothing */
/* When the LCD is off, LY is 0 and STAT mode is 0.
Todo: how is the LY=LYC flag handled? */
gb->io_registers[GB_IO_LY] = 0;
gb->io_registers[GB_IO_STAT] &= ~7;
/* Keep sending vblanks to user even if the screen is off */
gb->display_cycles += cycles;
if (gb->display_cycles >= LCDC_PERIOD) {
/* VBlank! */
gb->display_cycles -= LCDC_PERIOD;
display_vblank(gb);
}
/* Reset window rendering state */
gb->effective_window_enabled = false;
gb->effective_window_y = 0xFF;
return;
}
uint8_t atomic_increase = gb->cgb_double_speed? 2 : 4;
uint8_t stat_delay = gb->cgb_double_speed? 2 : (gb->cgb_mode? 0 : 4);
for (; cycles; cycles -= atomic_increase) {
gb->display_cycles += atomic_increase;
bool should_compare_ly = true;
uint8_t ly_for_comparison = gb->io_registers[GB_IO_LY] = gb->display_cycles / LINE_LENGTH;
/* Handle cycle completion. STAT's initial value depends on model and mode */
if (gb->display_cycles == LCDC_PERIOD) {
/* VBlank! */
gb->display_cycles = 0;
gb->io_registers[GB_IO_STAT] &= ~3;
if (gb->is_cgb) {
if (stat_delay) {
gb->io_registers[GB_IO_STAT] |= 1;
}
else {
gb->io_registers[GB_IO_STAT] |= 2;
}
}
ly_for_comparison = gb->io_registers[GB_IO_LY] = 0;
display_vblank(gb);
}
/* Entered VBlank state, update STAT and IF */
else if (gb->display_cycles == LINES * LINE_LENGTH + stat_delay) {
gb->io_registers[GB_IO_STAT] &= ~3;
gb->io_registers[GB_IO_STAT] |= 1;
gb->io_registers[GB_IO_IF] |= 1;
/* Entering VBlank state triggers the OAM interrupt */
if (gb->io_registers[GB_IO_STAT] & 0x20) {
gb->stat_interrupt_line = true;
}
}
/* Handle STAT changes for lines 0-143 */
else if (gb->display_cycles < LINES * LINE_LENGTH ) {
unsigned position_in_line = gb->display_cycles % LINE_LENGTH;
if (position_in_line == stat_delay) {
gb->io_registers[GB_IO_STAT] &= ~3;
gb->io_registers[GB_IO_STAT] |= 2;
}
else if (position_in_line == 0) {
should_compare_ly = gb->is_cgb;
ly_for_comparison--;
}
else if (position_in_line == MODE2_LENGTH + stat_delay) {
gb->io_registers[GB_IO_STAT] &= ~3;
gb->io_registers[GB_IO_STAT] |= 3;
}
else if (position_in_line == MODE2_LENGTH + MODE3_LENGTH + stat_delay) {
gb->io_registers[GB_IO_STAT] &= ~3;
if (gb->hdma_on_hblank) {
gb->hdma_on = true;
gb->hdma_cycles = 0;
}
}
}
/* Line 153 is special */
else if (gb->display_cycles >= (VIRTUAL_LINES - 1) * LINE_LENGTH) {
/* DMG */
if (!gb->is_cgb) {
switch (gb->display_cycles - (VIRTUAL_LINES - 1) * LINE_LENGTH) {
case 0:
should_compare_ly = false;
break;
case 4:
gb->io_registers[GB_IO_LY] = 0;
ly_for_comparison = VIRTUAL_LINES - 1;
break;
case 8:
gb->io_registers[GB_IO_LY] = 0;
should_compare_ly = false;
break;
default:
gb->io_registers[GB_IO_LY] = 0;
ly_for_comparison = 0;
}
}
/* CGB in DMG mode */
else if (!gb->cgb_mode) {
switch (gb->display_cycles - (VIRTUAL_LINES - 1) * LINE_LENGTH) {
case 0:
ly_for_comparison = VIRTUAL_LINES - 2;
break;
case 4:
break;
case 8:
gb->io_registers[GB_IO_LY] = 0;
break;
default:
gb->io_registers[GB_IO_LY] = 0;
ly_for_comparison = 0;
}
}
/* Single speed CGB */
else if (!gb->cgb_double_speed) {
switch (gb->display_cycles - (VIRTUAL_LINES - 1) * LINE_LENGTH) {
case 0:
break;
case 4:
gb->io_registers[GB_IO_LY] = 0;
break;
default:
gb->io_registers[GB_IO_LY] = 0;
ly_for_comparison = 0;
}
}
/* Double speed CGB */
else {
switch (gb->display_cycles - (VIRTUAL_LINES - 1) * LINE_LENGTH) {
case 0:
ly_for_comparison = VIRTUAL_LINES - 2;
break;
case 2:
case 4:
break;
case 6:
case 8:
gb->io_registers[GB_IO_LY] = 0;
break;
default:
gb->io_registers[GB_IO_LY] = 0;
ly_for_comparison = 0;
}
}
}
/* Lines 144 - 152 */
else {
if (stat_delay && gb->display_cycles % LINE_LENGTH == 0) {
should_compare_ly = gb->is_cgb;
ly_for_comparison--;
}
}
/* Set LY=LYC bit */
if (should_compare_ly && (ly_for_comparison == gb->io_registers[GB_IO_LYC])) {
gb->io_registers[GB_IO_STAT] |= 4;
}
else {
gb->io_registers[GB_IO_STAT] &= ~4;
}
if (!gb->stat_interrupt_line) {
switch (gb->io_registers[GB_IO_STAT] & 3) {
case 0: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 8; break;
case 1: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 0x10; break;
case 2: gb->stat_interrupt_line = gb->io_registers[GB_IO_STAT] & 0x20; break;
}
/* Use requested a LY=LYC interrupt and the LY=LYC bit is on */
if ((gb->io_registers[GB_IO_STAT] & 0x44) == 0x44) {
gb->stat_interrupt_line = true;
}
}
if (!gb->stat_interrupt_line) {
}
}
if (gb->stat_interrupt_line && !previous_stat_interrupt_line) {
gb->io_registers[GB_IO_IF] |= 2;
}
/* The value of LY is glitched in the last cycle of every line in CGB mode CGB in single speed
This is based on GiiBiiAdvance's docs */
if (gb->cgb_mode && !gb->cgb_double_speed &&
gb->display_cycles % LINE_LENGTH == LINE_LENGTH - 4) {
uint8_t glitch_pattern[] = {0, 0, 2, 0, 4, 4, 6, 0, 8};
if ((gb->io_registers[GB_IO_LY] & 0xF) == 0xF) {
gb->io_registers[GB_IO_LY] = glitch_pattern[gb->io_registers[GB_IO_LY] >> 4] << 4;
}
else {
gb->io_registers[GB_IO_LY] = glitch_pattern[gb->io_registers[GB_IO_LY] & 7] | (gb->io_registers[GB_IO_LY] & 0xF8);
}
}
}
void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
{
update_display_state(gb, cycles);
if (gb->disable_rendering) {
return;
}
/* /*
Display controller bug: For some reason, the OAM STAT interrupt is called, as expected, for LY = 0..143. Display controller bug: For some reason, the OAM STAT interrupt is called, as expected, for LY = 0..143.
However, it is also called from LY = 144. However, it is also called from LY = 144.
@ -307,134 +533,46 @@ void GB_display_run(GB_gameboy_t *gb)
See http://forums.nesdev.com/viewtopic.php?f=20&t=13727 See http://forums.nesdev.com/viewtopic.php?f=20&t=13727
*/ */
/*
STAT interrupt is implemented based on this finding:
http://board.byuu.org/phpbb3/viewtopic.php?p=25527#p25531
*/
uint8_t previous_stat_interrupt_line = gb->stat_interrupt_line;
gb->stat_interrupt_line = false;
uint8_t last_mode = gb->io_registers[GB_IO_STAT] & 3;
gb->io_registers[GB_IO_STAT] &= ~3;
if (gb->display_cycles >= LCDC_PERIOD) {
/* VBlank! */
gb->display_cycles -= LCDC_PERIOD;
display_vblank(gb);
}
if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) { if (!(gb->io_registers[GB_IO_LCDC] & 0x80)) {
/* LCD is disabled, do nothing */ /* LCD is disabled, do nothing */
return;
/* Some games expect LY to be zero when the LCD is off. }
Todo: Verify this behavior. if (gb->display_cycles >= LINE_LENGTH * 144) { /* VBlank */
Keep in mind that this only affects the value being read from the Gameboy, not the actualy display state.
This also explains why the coincidence interrupt triggers when LYC = 0 and LY = 153. */
gb->io_registers[GB_IO_LY] = 0;
return; return;
} }
uint8_t effective_ly = gb->display_cycles / LINE_LENGTH;
gb->io_registers[GB_IO_LY] = gb->display_cycles / LINE_LENGTH;
/* Todo: This behavior is seen in BGB and it fixes some ROMs with delicate timing, such as Hitman's 8bit.
This should be verified to be correct on a real gameboy. */
if (gb->io_registers[GB_IO_LY] == 153 && gb->display_cycles % LINE_LENGTH > 8) {
gb->io_registers[GB_IO_LY] = 0;
}
gb->io_registers[GB_IO_STAT] &= ~4;
if (gb->io_registers[GB_IO_LY] == gb->io_registers[GB_IO_LYC]) {
gb->io_registers[GB_IO_STAT] |= 4;
if (gb->io_registers[GB_IO_STAT] & 0x40) {
/* User requests LYC interrupt. */
gb->stat_interrupt_line = true;
}
}
if (gb->display_cycles >= LINE_LENGTH * 144) { /* VBlank */
gb->io_registers[GB_IO_STAT] |= 1; /* Set mode to 1 */
gb->effective_window_enabled = false;
gb->effective_window_y = 0xFF;
if (gb->io_registers[GB_IO_STAT] & 16) { /* User requests an interrupt on VBlank*/
gb->stat_interrupt_line = true;
}
if (last_mode != 1) {
gb->io_registers[GB_IO_IF] |= 1;
}
// LY = 144 interrupt bug
if (gb->io_registers[GB_IO_LY] == 144 && !gb->is_cgb) {
/* User requests an interrupt on Mode 2 */
if (gb->display_cycles % LINE_LENGTH < MODE2_LENGTH && gb->io_registers[GB_IO_STAT] & 0x20) { // Mode 2
gb->stat_interrupt_line = true;
}
}
goto updateSTAT;
}
// Todo: verify this window behavior. It is assumed from the expected behavior of 007 - The World Is Not Enough. // Todo: verify this window behavior. It is assumed from the expected behavior of 007 - The World Is Not Enough.
if ((gb->io_registers[GB_IO_LCDC] & 0x20) && gb->io_registers[GB_IO_LY] == gb->io_registers[GB_IO_WY]) { if ((gb->io_registers[GB_IO_LCDC] & 0x20) && effective_ly == gb->io_registers[GB_IO_WY]) {
gb->effective_window_enabled = true; gb->effective_window_enabled = true;
} }
if (gb->display_cycles % LINE_LENGTH < MODE2_LENGTH) { /* Mode 2 */ if (gb->display_cycles % LINE_LENGTH < MODE2_LENGTH) { /* Mode 2 */
gb->io_registers[GB_IO_STAT] |= 2; /* Set mode to 2 */
if (gb->io_registers[GB_IO_STAT] & 0x20) { /* User requests an interrupt on Mode 2 */
gb->stat_interrupt_line = true;
}
/* See above comment about window behavior. */ /* See above comment about window behavior. */
if (gb->effective_window_enabled && gb->effective_window_y == 0xFF) { if (gb->effective_window_enabled && gb->effective_window_y == 0xFF) {
gb->effective_window_y = gb->io_registers[GB_IO_LY]; gb->effective_window_y = effective_ly;
} }
gb->effective_scx = gb->io_registers[GB_IO_SCX]; gb->effective_scx = gb->io_registers[GB_IO_SCX];
gb->previous_lcdc_x = - (gb->effective_scx & 0x7); gb->previous_lcdc_x = - (gb->effective_scx & 0x7);
goto updateSTAT; return;
} }
/* Render. This chunk is outside the Mode 3 if, because otherwise we might not render some pixels, since this /* Render */
function only runs between atomic CPU changes, and not every clock. */ int16_t current_lcdc_x = ((gb->display_cycles % LINE_LENGTH - MODE2_LENGTH) & ~7) - (gb->effective_scx & 0x7);
if (!gb->disable_rendering) {
int16_t current_lcdc_x = ((gb->display_cycles % LINE_LENGTH - MODE2_LENGTH) & ~7) - (gb->effective_scx & 0x7); for (;gb->previous_lcdc_x < current_lcdc_x; gb->previous_lcdc_x++) {
for (;gb->previous_lcdc_x < current_lcdc_x; gb->previous_lcdc_x++) { if (gb->previous_lcdc_x >= WIDTH) {
if (gb->previous_lcdc_x >= 160) { continue;
continue;
}
if (gb->previous_lcdc_x < 0) {
continue;
}
gb->screen[gb->io_registers[GB_IO_LY] * 160 + gb->previous_lcdc_x] =
get_pixel(gb, gb->previous_lcdc_x, gb->io_registers[GB_IO_LY]);
} }
} if (gb->previous_lcdc_x < 0) {
continue;
if (gb->display_cycles % LINE_LENGTH < MODE2_LENGTH + MODE3_LENGTH) { /* Mode 3 */
gb->io_registers[GB_IO_STAT] |= 3; /* Set mode to 3 */
goto updateSTAT;
}
/* Mode 0*/
if (gb->io_registers[GB_IO_STAT] & 8) { /* User requests an interrupt on Mode 0 */
gb->stat_interrupt_line = true;
}
if (last_mode != 0) {
if (gb->hdma_on_hblank) {
gb->hdma_on = true;
gb->hdma_cycles = 0;
} }
} gb->screen[effective_ly * WIDTH + gb->previous_lcdc_x] =
get_pixel(gb, gb->previous_lcdc_x, effective_ly);
updateSTAT:
if (gb->stat_interrupt_line && !previous_stat_interrupt_line) {
gb->io_registers[GB_IO_IF] |= 2;
} }
} }
@ -563,7 +701,7 @@ uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest)
uint8_t count = 0; uint8_t count = 0;
unsigned sprite_height = (gb->io_registers[GB_IO_LCDC] & 4) ? 16:8; unsigned sprite_height = (gb->io_registers[GB_IO_LCDC] & 4) ? 16:8;
uint8_t oam_to_dest_index[40] = {0,}; uint8_t oam_to_dest_index[40] = {0,};
for (unsigned y = 0; y < 144; y++) { for (unsigned y = 0; y < LINES; y++) {
GB_sprite_t *sprite = (GB_sprite_t *) &gb->oam; GB_sprite_t *sprite = (GB_sprite_t *) &gb->oam;
uint8_t sprites_in_line = 0; uint8_t sprites_in_line = 0;
for (uint8_t i = 0; i < 40; i++, sprite++) { for (uint8_t i = 0; i < 40; i++, sprite++) {

View File

@ -2,7 +2,7 @@
#define display_h #define display_h
#include "gb.h" #include "gb.h"
void GB_display_run(GB_gameboy_t *gb); 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_palette_changed(GB_gameboy_t *gb, bool background_palette, uint8_t index);
typedef enum { typedef enum {

View File

@ -68,7 +68,6 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles)
// Not affected by speed boost // Not affected by speed boost
gb->hdma_cycles += cycles; gb->hdma_cycles += cycles;
gb->display_cycles += cycles;
gb->apu_sample_cycles += cycles; gb->apu_sample_cycles += cycles;
gb->apu.apu_cycles += cycles; gb->apu.apu_cycles += cycles;
gb->cycles_since_ir_change += cycles; gb->cycles_since_ir_change += cycles;
@ -76,7 +75,7 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles)
GB_dma_run(gb); GB_dma_run(gb);
GB_hdma_run(gb); GB_hdma_run(gb);
GB_apu_run(gb); GB_apu_run(gb);
GB_display_run(gb); GB_display_run(gb, cycles);
GB_ir_run(gb); GB_ir_run(gb);
} }

View File

@ -25,6 +25,11 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode)
{ {
GB_advance_cycles(gb, 4); GB_advance_cycles(gb, 4);
if (gb->io_registers[GB_IO_KEY1] & 0x1) { if (gb->io_registers[GB_IO_KEY1] & 0x1) {
/* Make sure we don't leave display_cycles not divisble by 4 in single speed mode */
if (gb->display_cycles % 4 == 2) {
GB_advance_cycles(gb, 4);
}
/* Todo: the switch is not instant. We should emulate this. */ /* Todo: the switch is not instant. We should emulate this. */
gb->cgb_double_speed ^= true; gb->cgb_double_speed ^= true;
gb->io_registers[GB_IO_KEY1] = 0; gb->io_registers[GB_IO_KEY1] = 0;