diff --git a/src/nes.c b/src/nes.c index 09ec21d..d8a5c52 100644 --- a/src/nes.c +++ b/src/nes.c @@ -71,7 +71,7 @@ void nes_reset(nes* sys) { nes_apu_reset(&sys->apu); } -int nes_run(nes* sys, int master_cycles, int* run) { +nes_ppu_Result nes_run(nes* sys, int master_cycles, int* run) { int cpu_run = 0; int cpu_cycles = (master_cycles + (nes_clock_cpu_div - 1)) / nes_clock_cpu_div; @@ -79,11 +79,19 @@ int nes_run(nes* sys, int master_cycles, int* run) { master_cycles = cpu_run * nes_clock_cpu_div; int ppu_cycles = master_cycles / nes_clock_ppu_div; - int vblank = nes_ppu_run(&sys->ppu, ppu_cycles); + nes_ppu_Result result = nes_ppu_run(&sys->ppu, ppu_cycles); - e6502_set_nmi(&sys->cpu, vblank); + if (result == ppu_Result_VBlank_On) { + e6502_set_nmi(&sys->cpu, 1); + } else if (result == ppu_Result_VBlank_Off) { + e6502_set_nmi(&sys->cpu, 0); + } if (run) *run = master_cycles; - return status; + if (status < 0) { + result = ppu_Result_Halt; + } + + return result; } diff --git a/src/nes.h b/src/nes.h index 68e8ca4..b6cef28 100644 --- a/src/nes.h +++ b/src/nes.h @@ -59,7 +59,7 @@ void nes_init(nes*); void nes_reset(nes*); -int nes_run(nes*, int cycles, int* run); +nes_ppu_Result nes_run(nes*, int cycles, int* run); #endif // NES_H_ diff --git a/src/nese.c b/src/nese.c index c8fe125..44da303 100644 --- a/src/nese.c +++ b/src/nese.c @@ -72,15 +72,17 @@ int main(int argc, char* argv[]) { nes_init(&sys); nes_reset(&sys); + nes_render(rend, &sys.ppu); + struct timespec t_target = {0}; clock_gettime(CLOCK_MONOTONIC, &t_target); uint64_t cycle_last_frame = 0; uint64_t total_cycles = 0; - int last_frame_rendered = -1; +// int last_frame_rendered = -1; for (int i = 0; i < n_loops && status == 0; ++i) { int run = 0; - status = nes_run(&sys, nes_clock_cpu_div, &run); + nes_ppu_Result result = nes_run(&sys, 1U, &run); total_cycles += run; /* float us_run = ( run * 1000. * 1000. * @@ -90,26 +92,32 @@ int main(int argc, char* argv[]) { us_run, run, status == 0 ? "OK" : "Halted"); */ - if ( status == 0 && - sys.ppu.frame != last_frame_rendered) { - // TODO: Check VBlank or scanline? + if ( result == ppu_Result_Ready || + result == ppu_Result_VBlank_Off) { status = nes_render(rend, &sys.ppu); - last_frame_rendered = sys.ppu.frame; - // Sleep to catch up to master clock - uint64_t elapsed_cycles = total_cycles - - cycle_last_frame; - int elapsed_ns = ( elapsed_cycles * - nes_clock_master_den * - NS_PER_S ) / - nes_clock_master_num; + if (status > 0) { +// last_frame_rendered = sys.ppu.frame; + + // Sleep to catch up to master clock + uint64_t elapsed_cycles = total_cycles - + cycle_last_frame; + int elapsed_ns = ( elapsed_cycles * + nes_clock_master_den * + NS_PER_S ) / + nes_clock_master_num; + + t_add_ns(&t_target, &t_target, elapsed_ns); - t_add_ns(&t_target, &t_target, elapsed_ns); + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, + &t_target, NULL); - clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, - &t_target, NULL); + cycle_last_frame = total_cycles; - cycle_last_frame = total_cycles; + status = 0; + } + } else if (result == ppu_Result_Halt) { + status = -1; } } diff --git a/src/ppu.c b/src/ppu.c index 8417375..02e96a9 100644 --- a/src/ppu.c +++ b/src/ppu.c @@ -147,7 +147,7 @@ void nes_ppu_init(nes_ppu* ppu, uint8_t* chr_mem) { } int nes_ppu_run(nes_ppu* ppu, int cycles) { - int vblank = 0; + nes_ppu_Result result = ppu_Result_Running; ppu->cycle += cycles; @@ -167,15 +167,18 @@ int nes_ppu_run(nes_ppu* ppu, int cycles) { ppu->status &= ~(ppu_Status_VBlank | ppu_Status_Hit); ppu->hit_line = 0; ppu->hit_dot = 0; - ppu->scanline -= nes_ppu_frame; + ppu->scanline = 0; ppu->frame++; - // TODO: Render callback if vblank was previously set? + result = ppu_Result_VBlank_Off; } else if (ppu->scanline >= nes_ppu_active) { ppu->status |= ppu_Status_VBlank; if (ppu->control & ppu_Control_VBlank) { - vblank = 1; + result = ppu_Result_VBlank_On; } + + } else if (ppu->scanline % 8 == 1) { + result = ppu_Result_Ready; } } @@ -185,13 +188,14 @@ int nes_ppu_run(nes_ppu* ppu, int cycles) { ppu->status |= ppu_Status_Hit; } - return vblank; + return result; } - +/* int nes_ppu_cycles_til_vblank(nes_ppu* ppu) { int cycles_til_vblank = nes_ppu_active_cycles - ( ppu->cycle + (ppu->scanline * nes_ppu_dots)); return (cycles_til_vblank > 0 ? cycles_til_vblank : nes_ppu_vblank_cycles + cycles_til_vblank); } +*/ diff --git a/src/ppu.h b/src/ppu.h index b2362d6..fc18574 100644 --- a/src/ppu.h +++ b/src/ppu.h @@ -47,6 +47,8 @@ typedef enum { ppu_Control_Nametable_Mask = 0b00000011, + ppu_Control_Scroll_Page_X = 0b00000001, + ppu_Control_Scroll_Page_Y = 0b00000010, ppu_Control_VRAM_Inc = 0b00000100, ppu_Control_Sprite_Bank = 0b00001000, ppu_Control_Back_Bank = 0b00010000, @@ -108,7 +110,15 @@ void nes_ppu_write(nes_ppu* ppu, uint16_t addr, uint8_t val); void nes_ppu_reset(nes_ppu* ppu); void nes_ppu_init(nes_ppu* ppu, uint8_t* chr_mem); -int nes_ppu_run(nes_ppu* ppu, int cycles); -int nes_ppu_cycles_til_vblank(nes_ppu* ppu); +typedef enum { + ppu_Result_Halt = -1, + ppu_Result_Running = 0U, + ppu_Result_VBlank_On, + ppu_Result_VBlank_Off, + ppu_Result_Ready, +} nes_ppu_Result; + +nes_ppu_Result nes_ppu_run(nes_ppu* ppu, int cycles); +//int nes_ppu_cycles_til_vblank(nes_ppu* ppu); #endif // ENES_PPU_H_ diff --git a/src/sdl_render.c b/src/sdl_render.c index 944805b..e3b5e7d 100644 --- a/src/sdl_render.c +++ b/src/sdl_render.c @@ -84,7 +84,7 @@ static int sdl_render_init(nes_Renderer* rend) { if (0 == status) { data->background = SDL_CreateRGBSurfaceWithFormat( - 0, nes_ppu_render_w, nes_ppu_render_h, + 0, nes_ppu_render_w + 8, nes_ppu_render_h + 8, 8, SDL_PIXELFORMAT_INDEX8 ); @@ -159,13 +159,13 @@ typedef enum { Render_Mode_Sprite = 0b00000, Render_Mode_Background = 0b00001, Render_Mode_Behind = 0b00010, - Render_Mode_Collide = 0b00100, +// Render_Mode_Collide = 0b00100, Render_Mode_Flip_X = 0b01000, Render_Mode_Flip_Y = 0b10000, } Render_Mode; -static void render_bg_sprite(nes_ppu* ppu, int index, +static void render_bg_sprite(const nes_ppu* ppu, int index, const uint8_t* pal, void* loc, int pitch) { uint8_t* sprite = &ppu->chr_mem[index * 16U]; @@ -187,31 +187,80 @@ static void render_bg_sprite(nes_ppu* ppu, int index, } } -// TODO: Don't re-render background unless VRAM/scroll changes - -static void render_background(nes_ppu* ppu, int nametable, - void* buffer, int pitch) { +static void render_background_area(const nes_ppu* ppu, int page, + void* buffer, int pitch, + int xs, int ys, int w, int h) { int bank = (ppu->control & ppu_Control_Back_Bank) ? 0x100 : 0; - // TODO: Support beyond nametable 0 - const uint8_t* index = &ppu->vram[nametable * 0x400U]; - const uint8_t* attrs = index + 960U; + const uint8_t* index_line = &ppu->vram[page * 0x400U]; + const uint8_t* attrs = index_line + 960U; + index_line += xs + (ys * nes_ppu_blocks_w); uint8_t* dst_line = (uint8_t*)buffer; - for (int y = 0; y < nes_ppu_blocks_h; ++y) { + for (int y = ys; y < h + ys; ++y) { uint8_t* dst = dst_line; - for (int x = 0; x < nes_ppu_blocks_w; ++x) { - if (x > 0 || (ppu->mask & ppu_Mask_Left_Back)) { - int attr_idx = ((y / 4) * 8) + (x / 4); - int shift = 2 * ((y & 0b10) | ((x & 0b10) >> 1)); - int pal_idx = (attrs[attr_idx] >> shift) & 3; - const uint8_t* pal = &ppu->palette[pal_idx * 4]; - render_bg_sprite(ppu, bank + *index, pal, - dst, pitch); - } + const uint8_t* index = index_line; + for (int x = xs; x < w + xs; ++x) { + int attr_idx = ((y / 4) * 8) + (x / 4); + int shift = 2 * ((y & 0b10) | ((x & 0b10) >> 1)); + int pal_idx = (attrs[attr_idx] >> shift) & 3; + const uint8_t* pal = &ppu->palette[pal_idx * 4]; + render_bg_sprite(ppu, bank + *index, pal, + dst, pitch); ++index; dst += 8; } dst_line += pitch * 8; + index_line += nes_ppu_blocks_w; + } +} + +static void render_background_line(const nes_ppu* ppu, int line, + void* buffer, int pitch) { + int scroll_x = ppu->scroll_x; + if (ppu->control & ppu_Control_Scroll_Page_X) { + // TODO: This looks like a kludge. Why check for 0? + if (scroll_x != 0) scroll_x += nes_ppu_render_w; + } + +/* + int scroll_y = ppu->scroll_y + ( + (ppu->control & ppu_Control_Scroll_Page_Y) ? + nes_ppu_render_h : 0); +*/ + int block_x = scroll_x / 8; +/* + int block_y = scroll_y / 8; + int fine_x = scroll_x % 8; + int fine_y = scroll_y % 8; +*/ + + // TODO: Handle vertical scrolling + // TODO: Handle column 0 flag + + buffer += line * pitch * 8U; + + // Left + + int page = 0; + int x = block_x; + if (x >= nes_ppu_blocks_w) { + x -= nes_ppu_blocks_w; + page += 1; } + + render_background_area( + ppu, page, buffer, pitch, + x, line, + nes_ppu_blocks_w - x, 1 + ); + + // Right + + buffer += (nes_ppu_blocks_w - x) * 8U; + render_background_area( + ppu, 1U - page, buffer, pitch, + 0, line, + 1U + x, 1 + ); } static int render_sprite(nes_ppu* ppu, int index, @@ -244,6 +293,7 @@ static int render_sprite(nes_ppu* ppu, int index, int pal_idx = ( ((hi & 0x80) >> 6) | ((lo & 0x80) >> 7)); int nes_pal_idx = (pal_idx ? pal[pal_idx] : 0xFFU); +/* if ( hit_pos < 0 && (ppu->mask & ppu_Mask_Back) && (mode & Render_Mode_Collide) && @@ -251,6 +301,7 @@ static int render_sprite(nes_ppu* ppu, int index, *back != 0xFFU ) { hit_pos = (8 - x) + (8 * (8 - y)); } +*/ if ((mode & Render_Mode_Behind) && *back != 0xFFU) { nes_pal_idx = 0xFFU; } @@ -286,6 +337,51 @@ typedef enum { oam_Index_Tile_Mask = 0b11111110, } oam_Index; +// Check sprite (0 only) collision on a scanline +// This assumes that we've verified that this sprite +// intersects with this scanline. +static int eval_sprite_line(const nes_ppu* ppu, int line, + const oam_sprite* sprite, + const uint8_t* chr, + const uint8_t* back) { + int hit_pos = -1; + + int y = line - (sprite->y + 1); + + if (sprite->attr & oam_Attr_Flip_Y) y = 7 - y; + uint8_t lo = chr[0U + y]; + uint8_t hi = chr[8U + y]; + + int w = nes_ppu_render_w - sprite->x; + if (w > 8) w = 8; + + back += sprite->x; + + for (int x = 0; x < w; ++x) { + int pal_idx = (sprite->attr & oam_Attr_Flip_X) ? + (((hi & 1) << 1) | (lo & 1)) : + ( ((hi & 0x80) >> 6) | + ((lo & 0x80) >> 7)); + if ( hit_pos < 0 && + pal_idx && + *back != 0xFFU ) { + hit_pos = x + sprite->x; + break; + } + ++back; + if (sprite->attr & oam_Attr_Flip_X) { + hi >>= 1; + lo >>= 1; + } else { + hi <<= 1; + lo <<= 1; + } + } + + return hit_pos; +} + + static const SDL_Rect sprite_rect = { .x = 0, .y = 0, @@ -322,7 +418,7 @@ static void render_sprites(nes_ppu* ppu, Render_Mode mode = (sprite->attr & oam_Attr_Background) ? Render_Mode_Behind : Render_Mode_Sprite; - if (i_sprite == 0) mode |= Render_Mode_Collide; +// if (i_sprite == 0) mode |= Render_Mode_Collide; if (sprite->attr & oam_Attr_Flip_X) { mode |= Render_Mode_Flip_X; } @@ -349,54 +445,102 @@ static void render_sprites(nes_ppu* ppu, } static int sdl_render(nes_Renderer* rend, nes_ppu* ppu) { + int status = 0; sdl_render_data* data = (sdl_render_data*)rend->data; - // Emulate the happy part of the backdrop override quirk - int pal_idx = (ppu->addr >= nes_ppu_mem_pal_start) ? - (ppu->addr & (nes_ppu_mem_pal_size - 1)) : 0; - SDL_Color ext = nes_palette[ppu->palette[pal_idx]]; - SDL_FillRect(data->target, NULL, - ((int)ext.r << 16) | ((int)ext.g << 8) | ext.b); - if (ppu->mask & ppu_Mask_Back) { - // TODO: Only render visible parts of background - render_background(ppu, 0, data->background->pixels, - data->background->pitch); - - int scroll_x = 0; //ppu->scroll_x; - - SDL_Rect back_rect = { - .x = scroll_x, - .y = 0, - .w = nes_ppu_render_w - scroll_x, - .h = nes_ppu_render_h, - }; + int line = (ppu->scanline - nes_ppu_prerender) / 8U; + + printf("Scanline %3d -> Line %2d @ X %d\n", + ppu->scanline, line, ppu->scroll_x); + + if (ppu->scanline < nes_ppu_prerender) { + // Emulate the happy part of the backdrop override quirk + int pal_idx = (ppu->addr >= nes_ppu_mem_pal_start) ? + (ppu->addr & (nes_ppu_mem_pal_size - 1)) : 0; + SDL_Color ext = nes_palette[ppu->palette[pal_idx]]; + SDL_FillRect(data->target, NULL, + ((int)ext.r << 16) | ((int)ext.g << 8) | ext.b); + + } else if (ppu->scanline < nes_ppu_prerender + + nes_ppu_height) { + if (ppu->mask & ppu_Mask_Back) { + // TODO: Only re-render background if VRAM/scroll changes + render_background_line(ppu, line, + data->background->pixels, + data->background->pitch); + + // Check for Sprite 0 Hit + // TODO: Account for fine X scroll + oam_sprite* sprite = (oam_sprite*)ppu->oam; + int bank = (ppu->control & ppu_Control_Sprite_Bank) ? + 0x100 : 0; + int index = bank + sprite->index; + uint8_t* chr = &ppu->chr_mem[index * 16U]; + int sprite_line = ppu->scanline - 2; + if ( ppu->hit_dot == 0 && + sprite->y + 7 >= sprite_line && + sprite->y - 7 <= sprite_line) { + int hit = -1; + uint8_t* back = data->background->pixels; + back += 8U * line * data->background->pitch; + for (int y = 0; y < 8; ++y) { + hit = eval_sprite_line( + ppu, ppu->scanline + y, + sprite, chr, back + ); + if (hit >= 0) { + ppu->hit_line = ppu->scanline + y; + ppu->hit_dot = hit; + break; + } + back += data->background->pitch; + } + } + } - SDL_Rect render_rect = { - .x = 0, - .y = 0, - .w = nes_ppu_render_w - scroll_x, - .h = nes_ppu_render_h, - }; + } else { + if (ppu->mask & ppu_Mask_Back) { + int scroll_x = 0; //ppu->scroll_x; + + SDL_Rect back_rect = { + .x = scroll_x, + .y = 0, + .w = nes_ppu_render_w - scroll_x, + .h = nes_ppu_render_h, + }; + + SDL_Rect render_rect = { + .x = 0, + .y = 0, + .w = nes_ppu_render_w - scroll_x, + .h = nes_ppu_render_h, + }; + + SDL_BlitSurface(data->background, &back_rect, + data->target, &render_rect); + } + if (ppu->mask & ppu_Mask_Sprite) { + render_sprites(ppu, data->sprite8, data->target, + data->background->pixels, + data->background->pitch); + } + SDL_Texture* texture = SDL_CreateTextureFromSurface( + data->renderer, data->target + ); + SDL_RenderCopy(data->renderer, texture, NULL, NULL); + SDL_RenderPresent(data->renderer); + SDL_DestroyTexture(texture); - SDL_BlitSurface(data->background, &back_rect, - data->target, &render_rect); - } - if (ppu->mask & ppu_Mask_Sprite) { - render_sprites(ppu, data->sprite8, data->target, - data->background->pixels, - data->background->pitch); + status = 1; } - SDL_Texture* texture = SDL_CreateTextureFromSurface( - data->renderer, data->target - ); - SDL_RenderCopy(data->renderer, texture, NULL, NULL); - SDL_RenderPresent(data->renderer); - SDL_DestroyTexture(texture); // TODO: Handle this in the input loop, or anywhere else SDL_Event event = {0}; - return (1 == SDL_PollEvent(&event) && event.type == SDL_QUIT) ? - -1 : 0; + if (1 == SDL_PollEvent(&event) && event.type == SDL_QUIT) { + status = -1; + } + + return status; }