#include #include "render.h" #include "ppu.h" static SDL_Color nes_palette[64] = { {0x80,0x80,0x80}, {0x00,0x00,0xBB}, {0x37,0x00,0xBF}, {0x84,0x00,0xA6}, {0xBB,0x00,0x6A}, {0xB7,0x00,0x1E}, {0xB3,0x00,0x00}, {0x91,0x26,0x00}, {0x7B,0x2B,0x00}, {0x00,0x3E,0x00}, {0x00,0x48,0x0D}, {0x00,0x3C,0x22}, {0x00,0x2F,0x66}, {0x00,0x00,0x00}, {0x05,0x05,0x05}, {0x05,0x05,0x05}, {0xC8,0xC8,0xC8}, {0x00,0x59,0xFF}, {0x44,0x3C,0xFF}, {0xB7,0x33,0xCC}, {0xFF,0x33,0xAA}, {0xFF,0x37,0x5E}, {0xFF,0x37,0x1A}, {0xD5,0x4B,0x00}, {0xC4,0x62,0x00}, {0x3C,0x7B,0x00}, {0x1E,0x84,0x15}, {0x00,0x95,0x66}, {0x00,0x84,0xC4}, {0x11,0x11,0x11}, {0x09,0x09,0x09}, {0x09,0x09,0x09}, {0xFF,0xFF,0xFF}, {0x00,0x95,0xFF}, {0x6F,0x84,0xFF}, {0xD5,0x6F,0xFF}, {0xFF,0x77,0xCC}, {0xFF,0x6F,0x99}, {0xFF,0x7B,0x59}, {0xFF,0x91,0x5F}, {0xFF,0xA2,0x33}, {0xA6,0xBF,0x00}, {0x51,0xD9,0x6A}, {0x4D,0xD5,0xAE}, {0x00,0xD9,0xFF}, {0x66,0x66,0x66}, {0x0D,0x0D,0x0D}, {0x0D,0x0D,0x0D}, {0xFF,0xFF,0xFF}, {0x84,0xBF,0xFF}, {0xBB,0xBB,0xFF}, {0xD0,0xBB,0xFF}, {0xFF,0xBF,0xEA}, {0xFF,0xBF,0xCC}, {0xFF,0xC4,0xB7}, {0xFF,0xCC,0xAE}, {0xFF,0xD9,0xA2}, {0xCC,0xE1,0x99}, {0xAE,0xEE,0xB7}, {0xAA,0xF7,0xEE}, {0xB3,0xEE,0xFF}, {0xDD,0xDD,0xDD}, {0x11,0x11,0x11}, {0x11,0x11,0x11} }; typedef struct { SDL_Window* window; SDL_Renderer* renderer; SDL_Surface* background; SDL_Surface* sprite8; SDL_Surface* target; } sdl_render_data; static sdl_render_data the_render_data = {0}; static int sdl_render_init(nes_Renderer* rend) { sdl_render_data* data = &the_render_data; int status = SDL_Init(SDL_INIT_VIDEO); if (0 != status) { fprintf(stderr, "SDL: Failed to initialize\n"); } else { data->window = SDL_CreateWindow( "NESe", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, nes_ppu_scan_w * 4, nes_ppu_scan_h * 4, 0 ); if (NULL == data->window) { fprintf(stderr, "SDL: Failed to create window\n"); SDL_Quit(); status = -1; } } if (0 == status) { data->renderer = SDL_CreateRenderer(data->window, -1, 0); if (NULL == data->renderer) { fprintf(stderr, "SDL: Failed to create renderer\n"); SDL_DestroyWindow(data->window); SDL_Quit(); status = -1; } } if (0 == status) { data->background = SDL_CreateRGBSurfaceWithFormat( 0, nes_ppu_render_w + 8, nes_ppu_render_h + 8, 8, SDL_PIXELFORMAT_INDEX8 ); if (NULL == data->background) { fprintf(stderr, "SDL: Failed to create background 0\n"); SDL_DestroyRenderer(data->renderer); SDL_DestroyWindow(data->window); SDL_Quit(); status = -1; } } if (0 == status) { data->sprite8 = SDL_CreateRGBSurfaceWithFormat( 0, 8, 8, 8, SDL_PIXELFORMAT_INDEX8 ); if (NULL == data->sprite8) { fprintf(stderr, "SDL: Failed to create sprite\n"); SDL_FreeSurface(data->background); SDL_DestroyRenderer(data->renderer); SDL_DestroyWindow(data->window); SDL_Quit(); status = -1; } } if (0 == status) { data->target = SDL_CreateRGBSurfaceWithFormat( 0U, nes_ppu_render_w, nes_ppu_render_h, 24U, SDL_PIXELFORMAT_RGB888 ); if (NULL == data->target) { fprintf(stderr, "SDL: Failed to create target\n"); SDL_FreeSurface(data->sprite8); SDL_FreeSurface(data->background); SDL_DestroyRenderer(data->renderer); SDL_DestroyWindow(data->window); SDL_Quit(); status = -1; } } if (0 == status) { SDL_SetPaletteColors(data->background->format->palette, nes_palette, 0U, 64U); SDL_SetColorKey(data->background, SDL_TRUE, 0xFFU); SDL_SetPaletteColors(data->sprite8->format->palette, nes_palette, 0U, 64U); SDL_SetColorKey(data->sprite8, SDL_TRUE, 0xFFU); rend->data = &the_render_data; } return status; } static void sdl_render_done(nes_Renderer* rend) { sdl_render_data* data = (sdl_render_data*)rend->data; SDL_FreeSurface(data->target); SDL_FreeSurface(data->sprite8); SDL_FreeSurface(data->background); SDL_DestroyRenderer(data->renderer); SDL_DestroyWindow(data->window); SDL_Quit(); } typedef enum { Render_Mode_Sprite = 0b00000, Render_Mode_Background = 0b00001, Render_Mode_Behind = 0b00010, // Render_Mode_Collide = 0b00100, Render_Mode_Flip_X = 0b01000, Render_Mode_Flip_Y = 0b10000, } Render_Mode; 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]; uint8_t* dst_line = (uint8_t*)loc; for (int y = 8; y > 0; --y) { uint8_t lo = sprite[0U]; uint8_t hi = sprite[8U]; uint8_t* dst = dst_line; for (int x = 8; x > 0; --x) { int pal_idx = ( ((hi & 0x80) >> 6) | ((lo & 0x80) >> 7)); *dst++ = (pal_idx ? pal[pal_idx] : 0xFFU); hi <<= 1; lo <<= 1; } dst_line += pitch; ++sprite; } } 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; 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 = ys; y < h + ys; ++y) { uint8_t* dst = dst_line; 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) { // TODO: Handle vertical scrolling // TODO: Handle column 0 flag buffer += line * pitch * 8U; int page = (ppu->control & ppu_Control_Nametable_Mask); int x = ppu->scroll_x / 8; // Left 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, page ^ 1, buffer, pitch, 0, line, 1U + x, 1 ); } static void render_sprite(nes_ppu* ppu, int index, const uint8_t* pal, Render_Mode mode, void* loc, int pitch, const void* back_loc, int back_pitch) { uint8_t* sprite = &ppu->chr_mem[index * 16U]; uint8_t* dst_line = (uint8_t*)loc; const uint8_t* back_line = (uint8_t*)back_loc; int dx = 1; if (mode & Render_Mode_Flip_X) { dst_line += 7; back_line += 7; dx = -dx; } if (mode & Render_Mode_Flip_Y) { dst_line += (7 * pitch); back_line += (7 * back_pitch); pitch = -pitch; back_pitch = -back_pitch; } for (int y = 8; y > 0; --y) { uint8_t lo = sprite[0U]; uint8_t hi = sprite[8U]; uint8_t* dst = dst_line; const uint8_t* back = back_line; for (int x = 8; x > 0; --x) { int pal_idx = ( ((hi & 0x80) >> 6) | ((lo & 0x80) >> 7)); int nes_pal_idx = (pal_idx ? pal[pal_idx] : 0xFFU); if ((mode & Render_Mode_Behind) && *back != 0xFFU) { nes_pal_idx = 0xFFU; } *dst = nes_pal_idx; dst += dx; back += dx; hi <<= 1; lo <<= 1; } dst_line += pitch; back_line += back_pitch; ++sprite; } } typedef struct { uint8_t y; uint8_t index; uint8_t attr; uint8_t x; } oam_sprite; typedef enum { oam_Attr_Pal_Mask = 0b00000011, oam_Attr_Background = 0b00100000, oam_Attr_Flip_X = 0b01000000, oam_Attr_Flip_Y = 0b10000000, } oam_Attribute; typedef enum { oam_Index_Bank = 0b00000001, 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. // Scanline is 0-239 from inside the rendering window // (though we should never see this called with 0). 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]; back += sprite->x; for (int x = sprite->x; x < nes_ppu_render_w; ++x) { int pal_idx = (sprite->attr & oam_Attr_Flip_X) ? (((hi & 1) << 1) | (lo & 1)) : ( ((hi & 0x80) >> 6) | ((lo & 0x80) >> 7)); if (pal_idx && *back != 0xFFU) { hit_pos = 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, .w = 8, .h = 8, }; static void render_sprites(nes_ppu* ppu, SDL_Surface* buffer, SDL_Surface* target, const void* back, int back_pitch) { int bank = (ppu->control & ppu_Control_Sprite_Bank) ? 0x100 : 0; const oam_sprite* sprites = (const oam_sprite*)ppu->oam; uint8_t* dst_origin = (uint8_t*)buffer->pixels; int pitch = buffer->pitch; const uint8_t* back_origin = (uint8_t*)back; for ( int i_sprite = nes_ppu_oam_sprite_count - 1; i_sprite >= 0; --i_sprite) { const oam_sprite* sprite = &sprites[i_sprite]; if ( !(ppu->mask & ppu_Mask_Left_Sprite) && sprite->x < 8) { continue; } int y = (sprite->y + 1); if (y >= nes_ppu_render_h) continue; uint8_t* dst = dst_origin; int back_offset = sprite->x + (y * back_pitch); const uint8_t* dst_back = back_offset + back_origin; // TODO: Support 8x16 sprites int index = bank + sprite->index; int pal_idx = (sprite->attr & oam_Attr_Pal_Mask); const uint8_t* pal = &ppu->palette[16 + (pal_idx * 4)]; Render_Mode mode = (sprite->attr & oam_Attr_Background) ? Render_Mode_Behind : Render_Mode_Sprite; // if (i_sprite == 0) mode |= Render_Mode_Collide; if (sprite->attr & oam_Attr_Flip_X) { mode |= Render_Mode_Flip_X; } if (sprite->attr & oam_Attr_Flip_Y) { mode |= Render_Mode_Flip_Y; } render_sprite(ppu, index, pal, mode, dst, pitch, dst_back, back_pitch); SDL_Rect target_rect = { .x = sprite->x, .y = y, .w = 8, .h = 8, }; SDL_BlitSurface(buffer, &sprite_rect, target, &target_rect); } } static void update_sprite_hit(nes_ppu* ppu, const void* back_line, int back_pitch) { const oam_sprite* sprite = (oam_sprite*)ppu->oam; int block_line = (ppu->scanline - nes_ppu_prerender) / 8U; int x_fine = ppu->scroll_x % 8; int index = sprite->index; if (ppu->control & ppu_Control_Sprite_Bank) { index += 0x100U; } const uint8_t* chr = &ppu->chr_mem[index * 16U]; int render_line = block_line * 8U; int start_y = (sprite->y + 1) - render_line; int end_y = start_y + 8; if (start_y < 8 && end_y > 0) { if (start_y < 0) start_y = 0; if (end_y > 8) end_y = 8; int hit = -1; const uint8_t* back = (uint8_t*)back_line + x_fine; back += (render_line + start_y) * back_pitch; for (int y = start_y; y < end_y; ++y) { hit = eval_sprite_line( ppu, render_line + y, sprite, chr, back ); if (hit >= 0) { ppu->hit_line = y + render_line; ppu->hit_dot = hit; break; } back += back_pitch; } } } static int sdl_render(nes_Renderer* rend, nes_ppu* ppu) { int status = 0; sdl_render_data* data = (sdl_render_data*)rend->data; if (ppu->scanline < nes_ppu_prerender) { // printf("Scanline %3d -> Prerender\n", ppu->scanline); // 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) { int line = (ppu->scanline - (int)nes_ppu_prerender) / 8; // printf("Scanline %3d -> Line %2d @ X %d\n", ppu->scanline, line, ppu->scroll_x); if (ppu->mask & ppu_Mask_Back) { // TODO: Only re-render if VRAM/scroll changes render_background_line(ppu, line, data->background->pixels, data->background->pitch); // TODO: Y scroll support int x_fine = ppu->scroll_x % 8; // Check for Sprite 0 Hit if ( 0 >= ppu->hit_line && (ppu->mask & ppu_Mask_Sprite)) { update_sprite_hit(ppu, data->background->pixels, data->background->pitch); } // Gotta render it now while scroll is set SDL_Rect back_rect = { .x = x_fine, .y = line * 8U, .w = nes_ppu_render_w, .h = 8U, }; SDL_Rect render_rect = { .x = 0, .y = line * 8U, .w = nes_ppu_render_w, .h = 8U, }; SDL_BlitSurface(data->background, &back_rect, data->target, &render_rect); } } else { // printf("Scanline %3d -> Postrender\n", ppu->scanline); 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); status = 1; } return status; } nes_Renderer sdl_renderer = { .init = sdl_render_init, .done = sdl_render_done, .render = sdl_render, };