From 4f46423e6d4b47432db5e11ff393a655c0513767 Mon Sep 17 00:00:00 2001 From: Nathaniel Walizer Date: Sat, 30 Nov 2024 23:11:49 -0800 Subject: [PATCH] Flesh out PPU rendering by using LUTs --- src/nese.c | 3 +- src/ppu.c | 26 ++--- src/ppu.h | 39 +++++++- src/sdl_render.c | 256 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 271 insertions(+), 53 deletions(-) diff --git a/src/nese.c b/src/nese.c index d9825f8..6cf6ea9 100644 --- a/src/nese.c +++ b/src/nese.c @@ -58,7 +58,7 @@ int main(int argc, char* argv[]) { int last_frame_rendered = -1; for (int i = 0; i < n_loops && status == 0; ++i) { int run = 0; - status = nes_run(&sys, 1000, &run); + status = nes_run(&sys, 12, &run); total_cycles += run; /* float us_run = ( run * 1000. * 1000. * @@ -68,6 +68,7 @@ int main(int argc, char* argv[]) { us_run, run, status == 0 ? "OK" : "Halted"); */ + // TODO: Check VBlank or scanline? if ( status == 0 && sys.ppu.frame != last_frame_rendered) { status = nes_render(rend, &sys.ppu); diff --git a/src/ppu.c b/src/ppu.c index 4a19bde..10a3686 100644 --- a/src/ppu.c +++ b/src/ppu.c @@ -84,7 +84,7 @@ void nes_ppu_write(nes_ppu* ppu, uint16_t addr, uint8_t val) { if (ppu->latch) { ppu->addr &= 0x3F00U; ppu->addr |= val; - printf("PPU: VRAM ADDR %04x\n", ppu->addr); +// printf("PPU: VRAM ADDR %04x\n", ppu->addr); } else { ppu->addr &= 0x00FFU; @@ -100,6 +100,7 @@ void nes_ppu_write(nes_ppu* ppu, uint16_t addr, uint8_t val) { uint8_t pal_addr = (ppu->addr - nes_ppu_mem_pal_start) & (nes_ppu_mem_pal_size - 1); +// fprintf(stderr, "PPU PAL %02x < %02x\n", pal_addr, val); ppu->palette[pal_addr] = val; } else if (ppu->addr >= nes_ppu_mem_vram_start) { @@ -140,6 +141,12 @@ int nes_ppu_run(nes_ppu* ppu, int cycles) { ppu->cycle += cycles; + if ( 0 != ppu->hit_line && + ppu->scanline > ppu->hit_line && + ppu->cycle > ppu->hit_dot) { + ppu->status |= ppu_Status_Hit; + } + while (ppu->cycle >= nes_ppu_dots) { ppu->cycle -= nes_ppu_dots; if ( ppu->scanline <= nes_ppu_prerender && @@ -153,13 +160,15 @@ int nes_ppu_run(nes_ppu* ppu, int cycles) { nes_ppu_height + nes_ppu_postrender + nes_ppu_vblank) { - ppu->status &= ~ppu_Status_VBlank; + ppu->status &= ~(ppu_Status_VBlank | ppu_Status_Hit); + ppu->hit_line = 0; + ppu->hit_dot = 0; ppu->scanline = 0; ppu->frame++; // TODO: Render callback if vblank was previously set } else if (ppu->scanline >= nes_ppu_prerender + - nes_ppu_height + - nes_ppu_postrender) { + nes_ppu_height + + nes_ppu_postrender) { ppu->status |= ppu_Status_VBlank; if (ppu->control & ppu_Control_VBlank) { vblank = 1; @@ -170,15 +179,6 @@ int nes_ppu_run(nes_ppu* ppu, int cycles) { return vblank; } -#define nes_ppu_active_cycles \ - (nes_ppu_dots * (nes_ppu_prerender + \ - nes_ppu_height + \ - nes_ppu_postrender)) - -#define nes_ppu_vblank_cycles (nes_ppu_dots * nes_ppu_vblank) - -#define nes_frame_cycles (nes_ppu_active_cycles + \ - nes_ppu_vblank_cycles) int nes_ppu_cycles_til_vblank(nes_ppu* ppu) { int cycles_til_vblank = nes_ppu_active_cycles - ( diff --git a/src/ppu.h b/src/ppu.h index 2cfbe82..44c594c 100644 --- a/src/ppu.h +++ b/src/ppu.h @@ -10,12 +10,25 @@ #define nes_ppu_postrender (1U) #define nes_ppu_vblank (20U) -#define nes_ppu_render_w (320U) // Includes full overscan -#define nes_ppu_render_h nes_ppu_height +#define nes_ppu_active_cycles \ + (nes_ppu_dots * (nes_ppu_prerender + \ + nes_ppu_height + \ + nes_ppu_postrender)) + +#define nes_ppu_vblank_cycles (nes_ppu_dots * nes_ppu_vblank) + +#define nes_frame_cycles (nes_ppu_active_cycles + \ + nes_ppu_vblank_cycles) + +#define nes_ppu_scan_w (320U) // Includes full overscan +#define nes_ppu_scan_h nes_ppu_height #define nes_ppu_blocks_w (32U) #define nes_ppu_blocks_h (30U) +#define nes_ppu_render_w (nes_ppu_blocks_w * 8U) +#define nes_ppu_render_h (nes_ppu_blocks_h * 8U) + #define nes_ppu_mem_size (0x4000U) #define nes_ppu_mem_pal_start (0x3F00U) #define nes_ppu_mem_pal_size (0x0020U) @@ -24,6 +37,10 @@ #define nes_ppu_oam_size (256U) +#define nes_ppu_oam_sprite_size (4U) +#define nes_ppu_oam_sprite_count (nes_ppu_oam_size / \ + nes_ppu_oam_sprite_size) + typedef enum { ppu_Control_Nametable_Mask = 0b00000011, ppu_Control_VRAM_Inc = 0b00000100, @@ -41,16 +58,33 @@ typedef enum { ppu_Status_VBlank = 0b10000000, } nes_ppu_Status; +typedef enum { + ppu_Mask_Greyscale = 0b00000001, + ppu_Mask_Left_Back = 0b00000010, + ppu_Mask_Left_Sprite = 0b00000100, + ppu_Mask_Back = 0b00001000, + ppu_Mask_Sprite = 0b00010000, + ppu_Mask_More_Red = 0b00100000, + ppu_Mask_More_Green = 0b01000000, + ppu_Mask_More_Blue = 0b10000000, +} nes_ppu_Mask; + typedef struct { + // Memory uint8_t* chr_mem; uint8_t oam[nes_ppu_oam_size]; uint8_t vram[nes_ppu_mem_vram_size]; uint8_t palette[nes_ppu_mem_pal_size]; + // Timing int frame; int scanline; int cycle; + int hit_line; + int hit_dot; + + // External registers uint8_t control; uint8_t mask; uint8_t status; @@ -59,6 +93,7 @@ typedef struct { uint8_t data; uint8_t oam_addr; + // Internal Registers uint8_t latch; } nes_ppu; diff --git a/src/sdl_render.c b/src/sdl_render.c index 3c41cf0..d76a3c1 100644 --- a/src/sdl_render.c +++ b/src/sdl_render.c @@ -3,14 +3,15 @@ #include "render.h" #include "ppu.h" - +/* typedef struct { uint8_t r; uint8_t g; uint8_t b; } __attribute__ (( packed )) sPal; +*/ -static sPal nes_palette[64] = { +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}, @@ -34,10 +35,11 @@ static sPal nes_palette[64] = { typedef struct { - SDL_Surface* surface; SDL_Window* window; SDL_Renderer* renderer; - SDL_Texture* texture; + SDL_Surface* background; + SDL_Surface* sprite8; + SDL_Surface* target; } sdl_render_data; static sdl_render_data the_render_data = {0}; @@ -59,8 +61,8 @@ static int sdl_render_init(nes_Renderer* rend) { "NESe", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - nes_ppu_render_w * 4, - nes_ppu_render_h * 4, + nes_ppu_scan_w * 4, + nes_ppu_scan_h * 4, 0 ); if (NULL == data->window) { @@ -81,6 +83,11 @@ static int sdl_render_init(nes_Renderer* rend) { } if (0 == status) { + data->background = SDL_CreateRGBSurfaceWithFormat( + 0, nes_ppu_render_w, nes_ppu_render_h, + 8, SDL_PIXELFORMAT_INDEX8 + ); +/* data->texture = SDL_CreateTexture( the_render_data.renderer, SDL_PIXELFORMAT_RGB24, @@ -88,8 +95,33 @@ static int sdl_render_init(nes_Renderer* rend) { nes_ppu_render_w, nes_ppu_render_h ); - if (NULL == data->texture) { - fprintf(stderr, "SDL: Failed to create texture\n"); +*/ + if (NULL == data->background) { + fprintf(stderr, "SDL: Failed to create background\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 + ); + SDL_SetPaletteColors(data->sprite8->format->palette, + nes_palette, 0U, 64U); + SDL_SetColorKey(data->sprite8, SDL_TRUE, 0xFFU); + } + + if (0 == status) { + data->target = SDL_CreateRGBSurfaceWithFormat( + 0U, nes_ppu_scan_w, nes_ppu_scan_h, 24U, + SDL_PIXELFORMAT_RGB888 + ); + if (NULL == data->target) { + fprintf(stderr, "SDL: Failed to create target\n"); + SDL_FreeSurface(data->background); SDL_DestroyRenderer(data->renderer); SDL_DestroyWindow(data->window); SDL_Quit(); @@ -98,6 +130,9 @@ static int sdl_render_init(nes_Renderer* rend) { } if (0 == status) { + SDL_SetPaletteColors(data->background->format->palette, + nes_palette, 0U, 64U); + SDL_SetColorKey(data->background, SDL_TRUE, 0xFFU); SDL_SetEventFilter(filter, NULL); rend->data = &the_render_data; } @@ -107,66 +142,213 @@ static int sdl_render_init(nes_Renderer* rend) { static void sdl_render_done(nes_Renderer* rend) { sdl_render_data* data = (sdl_render_data*)rend->data; - SDL_DestroyTexture(data->texture); + SDL_FreeSurface(data->target); + SDL_FreeSurface(data->background); SDL_DestroyRenderer(data->renderer); SDL_DestroyWindow(data->window); SDL_Quit(); } -typedef struct { +typedef enum { + Render_Mode_Sprite = 0b000, + Render_Mode_Background = 0b001, + Render_Mode_Behind = 0b010, + Render_Mode_Collide = 0b100, +} Render_Mode; -} oam_sprite; -static void render_sprite(const nes_ppu* ppu, int index, - void* loc, int pitch) { +static int 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) { + int hit_pos = -1; uint8_t* sprite = &ppu->chr_mem[index * 16U]; uint8_t* dst_line = (uint8_t*)loc; + const uint8_t* back_line = (uint8_t*)back_loc; for (int y = 8; y > 0; --y) { - uint8_t hi = sprite[0U]; - uint8_t lo = sprite[8U]; - sPal* dst = (sPal*)dst_line; + 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) << 1) | !!(lo & 0x80); - *dst = nes_palette[16 + (pal_idx * 4)]; - ++dst; + 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) && + nes_pal_idx != 0xFFU && + *back != 0xFFU ) { + hit_pos = (8 - x) + (8 * (8 - y)); + } + if ((mode & Render_Mode_Behind) && *back != 0xFFU) { + nes_pal_idx = 0xFFU; + } + *dst++ = nes_pal_idx; + ++back; hi <<= 1; lo <<= 1; } dst_line += pitch; + back_line += back_pitch; ++sprite; } + return hit_pos; } // TODO: Don't re-render background unless VRAM has changed -static int sdl_render(nes_Renderer* rend, nes_ppu* ppu) { - sdl_render_data* data = (sdl_render_data*)rend->data; - - // TODO - void* buffer = NULL; - int pitch = 0; - SDL_LockTexture(data->texture, NULL, &buffer, &pitch); +static void render_background(nes_ppu* ppu, + void* buffer, int pitch) { int bank = (ppu->control & ppu_Control_Back_Bank) ? 0x100 : 0; - uint8_t* index = &ppu->vram[0]; - uint8_t* dst_line = - (uint8_t*)buffer + - ((nes_ppu_render_w - (8U * nes_ppu_blocks_w)) / 2); + // TODO: Support beyond nametable 0 + const uint8_t* index = &ppu->vram[0U]; + const uint8_t* attrs = &ppu->vram[960U]; + uint8_t* dst_line = (uint8_t*)buffer; for (int y = 0; y < nes_ppu_blocks_h; ++y) { uint8_t* dst = dst_line; for (int x = 0; x < nes_ppu_blocks_w; ++x) { - render_sprite( - ppu, bank + *index, dst, pitch - ); + 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_sprite(ppu, bank + *index, pal, + Render_Mode_Background, + dst, pitch, NULL, 0); + } ++index; - dst += 3 * 8; + dst += 8; } dst_line += pitch * 8; } - SDL_UnlockTexture(data->texture); +} +typedef struct { + uint8_t y; + uint8_t index; + uint8_t attr; + uint8_t x; +} oam_sprite; - SDL_RenderCopy(data->renderer, data->texture, NULL, NULL); - SDL_RenderPresent(data->renderer); +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; + +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; + // TODO: Support mirroring + 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; + int hit_pos = render_sprite(ppu, index, pal, mode, + dst, pitch, + dst_back, back_pitch); + if (hit_pos >= 0) { + ppu->hit_line = y + (hit_pos / 8); + ppu->hit_dot = sprite->x + (hit_pos % 8); + } + + SDL_Rect target_rect = { + .x = sprite->x + + ((nes_ppu_scan_w - nes_ppu_render_w) / 2), + .y = y, + .w = 8, + .h = 8, + }; + SDL_BlitSurface(buffer, &sprite_rect, + target, &target_rect); + } +} + +static SDL_Rect render_rect = { + .x = (nes_ppu_scan_w - nes_ppu_render_w) / 2, + .y = 0, + .w = nes_ppu_render_w, + .h = nes_ppu_render_h, +}; + +static SDL_Rect back_rect = { + .x = 0, + .y = 0, + .w = nes_ppu_render_w, + .h = nes_ppu_render_h, +}; + +static int sdl_render(nes_Renderer* rend, nes_ppu* ppu) { + sdl_render_data* data = (sdl_render_data*)rend->data; + + if (ppu->mask & (ppu_Mask_Back | ppu_Mask_Sprite)) { + SDL_FillRect(data->target, NULL, 0x808080/*nes_palette[0]*/); + if (ppu->mask & ppu_Mask_Back) { + render_background(ppu, data->background->pixels, + data->background->pitch); + 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); + } + +/* + // HACK: THIS IS TERRIBLE + static uint32_t last_ms = 0; + uint32_t now_ms = SDL_GetTicks(); + if (last_ms != 0) { + int delay = (last_ms + (1000 / 35)) - now_ms; + if (delay > 0) SDL_Delay(delay); +// else fprintf(stderr, "Delta %d\n", now_ms - last_ms); + } + last_ms = now_ms; +*/ SDL_Event event = {0}; return (1 == SDL_PollEvent(&event) && event.type == SDL_QUIT) ?