More debug tools and finally fix rendering!

This commit is contained in:
william 2024-08-20 20:43:42 -04:00
parent 1add13dd20
commit 32c9cebd19
14 changed files with 251 additions and 70 deletions

1
.gitignore vendored
View File

@ -155,3 +155,4 @@ environment_run.sh.env
# End of https://www.toptal.com/developers/gitignore/api/cmake,clion,conan
/test_roms/
/logs/

View File

@ -9,9 +9,11 @@ only tested on Linux. Here is how to run the project:
- Run the emulator: ```./nes_emulator```
## Controls
- `p`: Pauses the emulation
- `o`: Go to the next palette in the pattern viewer
- `t`: Show tile IDs
- `P`: Pauses the emulation (CPU)
- `Ctrl+P`: Pauses the emulation and the PPU rendering
- `O`: Go to the next palette in the pattern viewer
- `T`: Show tile IDs
- `N`: Switch low/high pattern data
## Dependencies
- GCC compiler

View File

@ -41,7 +41,7 @@ void cpu_init() {
}
void print_registers(byte op, unsigned long cycle_count) {
log_debug("%#02x %#02x %s \t A:%#02x X:%#02x Y:%#02x F:%#02x SP:%#02x \t [%d]",
log_trace("%#02x %#02x %s \t A:%#02x X:%#02x Y:%#02x F:%#02x SP:%#02x \t [%d]",
cpu_state.program_counter - 1, // The PC as been incremented when printing
op,
get_op_code_name(op),

View File

@ -25,17 +25,17 @@ typedef struct dbg_nametable {
} DebugNameTable;
/**
* Initializes the debug nametable.
* Initializes the debug tile_id.
*/
void dbg_nametable_init();
/**
* Updates the debug nametable. Updates the tiles from the PPU memory.
* Updates the debug tile_id. Updates the tiles from the PPU memory.
*/
void dbg_nametable_update();
/**
* Renders a nametable bank to a buffer.
* Renders a tile_id bank to a buffer.
* @param bank
* @param buffer
*/

View File

@ -76,25 +76,45 @@ void gui_post_sysinit() {
#endif
}
bool lctrl = false;
int gui_input() {
SDL_Event event;
#if DEBUG
PPUDebugFlags *ppu_debug = &ppu_get_state()->debug;
#endif
while (SDL_PollEvent(&event)) {
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE) {
return -1;
}
if (event.type == SDL_KEYUP) {
if (event.key.keysym.sym == SDLK_p) {
system_toggle_pause();
} else {
#if DEBUG
if (event.key.keysym.sym == SDLK_t) {
PPUDebugFlags *ppu_debug = &ppu_get_state()->debug;
ppu_debug->flags.tile_debugger = !ppu_debug->flags.tile_debugger;
} else {
pattern_window_key_up(&gui.pattern_window, event.key.keysym.sym);
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_LCTRL) {
lctrl = true;
}
if (event.type == SDL_KEYUP) {
switch (event.key.keysym.sym) {
case SDLK_LCTRL:
lctrl = false;
break;
case SDLK_p:
system_toggle_pause(lctrl);
break;
#if DEBUG
case SDLK_t:
ppu_debug->flags.tile_debugger = !ppu_debug->flags.tile_debugger;
break;
case SDLK_n:
ppu_debug->flags.tile_debugger_pattern_half = (ppu_debug->flags.tile_debugger_pattern_half + 1) % 3;
break;
default:
pattern_window_key_up(&gui.pattern_window, event.key.keysym.sym);
break;
#else
default:
break;
#endif
}
}
@ -104,6 +124,8 @@ int gui_input() {
}
void gui_render() {
main_window_render(&gui.main_window, ppu_get_state()->pixels);
#if DEBUG
dbg_palette_init();
pattern_window_render(&gui.pattern_window);
@ -113,8 +135,6 @@ void gui_render() {
gui.tick++;
#endif
main_window_render(&gui.main_window, ppu_get_state()->pixels);
}
void gui_present() {

View File

@ -36,7 +36,7 @@ void main_window_render_delay(SDL_Renderer *renderer) {
void main_window_render(NesMainWindow *window, pixel *pixels) {
SDL_RenderClear(window->sdl_context.renderer);
SDL_UpdateTexture(window->texture, NULL, pixels, 240 * sizeof(unsigned int));
SDL_UpdateTexture(window->texture, NULL, pixels, 256 * sizeof(pixel));
SDL_RenderCopy(window->sdl_context.renderer, window->texture, NULL, NULL);
#if DEBUG

View File

@ -25,4 +25,8 @@ void main_window_uninit(NesMainWindow *window);
void main_window_render(NesMainWindow *window, pixel* pixels);
void main_window_present(NesMainWindow *window);
#if DEBUG
void main_window_render_delay(SDL_Renderer *renderer);
#endif
#endif //NES_EMULATOR_MAIN_WINDOW_H

View File

@ -63,7 +63,7 @@ typedef struct ppu_memory {
} PPUMemory;
typedef struct ppu_tile_fetch {
byte nametable;
byte tile_id;
byte attribute_table;
byte pattern_table_tile_low;
byte pattern_table_tile_high;
@ -73,6 +73,7 @@ typedef struct ppu_tile_fetch {
typedef union {
struct {
byte tile_debugger: 1;
byte tile_debugger_pattern_half: 2;
} flags;
byte flags_byte;
} PPUDebugFlags;

View File

@ -26,6 +26,7 @@ typedef struct system {
Mapper mapper;
unsigned long cycle_count;
bool paused;
bool ppu_paused;
} System;
/**
@ -40,7 +41,7 @@ void system_next_frame();
/**
* Toggle pause for the system. If not paused, CPU and PPU cycles will be stopped until this method is called again.
*/
void system_toggle_pause();
void system_toggle_pause(bool pause_ppu);
/**
* De-initialize the components of a system.

67
main.c
View File

@ -15,17 +15,80 @@
*
* =====================================================================================
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include "log.h"
#include "include/rom.h"
#include "include/system.h"
#include "gui.h"
struct log_files {
FILE *info;
FILE *debug;
};
struct log_files log_files;
FILE *add_log_file(char *name, int level) {
char *full_name = malloc((5 + strlen(name)) * sizeof(char));
strcpy(full_name, "logs/");
strcat(full_name, name);
FILE *log_file = fopen(full_name, "w");
if (!log_file) {
perror("fopen");
int fopen_err = errno;
log_error("Failed to open log file '%s': %s", full_name, strerror(fopen_err));
return NULL;
}
log_add_fp(log_file, level);
return log_file;
}
void init_logging() {
log_set_level(LOG_DEBUG);
// Print current working directory
char cwd[256];
if (getcwd(cwd, 256) == NULL) {
int getcwd_error = errno;
log_error("Failed to read current working directory: %s", strerror(getcwd_error));
return;
}
log_info("Using working directory '%s'", cwd);
// Make sure log directory exists
char *folder_name = "logs/";
struct stat sb;
if (stat(folder_name, &sb) != 0 || !S_ISDIR(sb.st_mode)) {
if (mkdir(folder_name, 0755) == 0) {
log_debug("Created logs folder");
} else {
log_error("Failed to create logs folder: %s", strerror(errno));
return;
}
}
log_files.info = add_log_file("nes.log", LOG_INFO);
#if DEBUG
log_files.debug = add_log_file("nes_debug.log", LOG_DEBUG);
#endif
}
void close_logging() {
fclose(log_files.info);
fclose(log_files.debug);
}
int main() {
char *rom_path = "./test_roms/dk_japan.nes";
// char *rom_path = "./test_roms/smb.nes";
log_set_level(LOG_INFO);
init_logging();
if (!gui_init()) {
return EXIT_FAILURE;
@ -60,5 +123,7 @@ int main() {
rom_unload();
gui_uninit();
close_logging();
return EXIT_SUCCESS;
}

View File

@ -96,4 +96,6 @@
COLOR_LIST_(3) \
}
typedef pixel color_list[0x40];
#endif //NES_EMULATOR_COLORS_H

159
ppu/ppu.c
View File

@ -7,7 +7,7 @@
// 3. Implement PPUADDR/PPUDATA so that the nametables are filled out
// 4. Now you have some data your PPU can actually read for rendering background. Render it scanline by scanline - just follow the wiki on this. Maybe the timing will be bad, it doesn't matter for this game. Start off with rendering tiles based on the pattern table ID, don't try and fetch patterns.
// 5. Fix the inevitable bugs with your PPUDATA implementation until you see a blocky version of the Donkey Kong screen.
// 6. Now fetch pattern table data using the nametable data. If it looks "wrong" make sure you are consuming the background address flag. Start off with black and white, then pick two colors to mix for the two bits. Now you should have something like https://i.imgur.com/7OIpHgd.png
// 6. Now fetch pattern table data using the tile_id data. If it looks "wrong" make sure you are consuming the background address flag. Start off with black and white, then pick two colors to mix for the two bits. Now you should have something like https://i.imgur.com/7OIpHgd.png
// 7. (Optional) implement palette reads (I'm skipping this for now).
// 8. Implement OAMDMA (and OAMDATA I guess, I implemented one on top of the other)
// 9. Now you should have sprite data to render. Implement the logic for copying from primary OAM to scanline OAM. I'm doing it all as one step (not smearing it over up to 256 cycles like the actual hardware). Skip the confusing sprite overflow junk.
@ -21,18 +21,20 @@
#include "../include/rom.h"
#include "colors.h"
#include "tile_debugger.h"
#include "log.h"
#define PPU_VISIBLE_FRAME_END 240
#define PPU_POST_RENDER_LINE_START PPU_VISIBLE_FRAME_END
#define PPU_POST_RENDER_LINE_END 242
#define PPU_PRE_RENDER_LINE 261
#define PPU_LINE_END PPU_PRE_RENDER_LINE
#define PPU_LINE_WIDTH 340
#define PPU_SCANLINE_VISIBLE_MAX 240
#define PPU_SCANLINE_POST_RENDER_MIN PPU_SCANLINE_VISIBLE_MAX
#define PPU_SCANLINE_POST_RENDER_MAX 242
#define PPU_SCANLINE_PRE_RENDER 261
#define PPU_SCANLINE_MAX PPU_SCANLINE_PRE_RENDER
#define PPU_CYCLE_MAX 340
#define PPU_CYCLE_VISIBLE_MAX 256
#define NAMETABLE_TILE_SIZE 8
PPU ppu_state;
pixel color_list[0x40] = COLOR_LIST;
color_list colors = COLOR_LIST;
void ppu_init() {
memset(&ppu_state, 0, sizeof(PPU));
@ -50,12 +52,14 @@ void ppu_status_set(byte mask, bool enabled) {
}
}
int hits = 0;
void ppu_trigger_vbl_nmi() {
if (!ppu_read_flag(PPU_REGISTER_CTRL, PPU_CTRL_GEN_VBLANK_NMI)) {
// VBlank NMI generation is disabled
return;
}
hits++;
cpu_trigger_nmi();
}
@ -110,7 +114,7 @@ static inline void ppu_pixel_set_color(pixel *pixel, byte pt_low, byte pt_high,
}
byte color = ppu_read(color_addr);
*(pixel + i) = color_list[color];
*(pixel + i) = colors[color];
}
}
@ -120,16 +124,9 @@ void ppu_draw_tile() {
unsigned int y = ppu_state.scanline;
unsigned int x = ppu_state.cycle;
// if (x > PPU_LINE_WIDTH) {
// x -= PPU_LINE_WIDTH;
// y++;
// }
//
// if (y > PPU_PRE_RENDER_LINE) {
// y -= PPU_PRE_RENDER_LINE;
// }
unsigned int pixel_index = (y * PPU_CYCLE_VISIBLE_MAX + x) % (240 * 256);
assert(pixel_index < 240 * 256); // If this goes over, the PPU registers will be overridden
unsigned int pixel_index = y * PPU_VISIBLE_FRAME_END + x;
pixel *pixel = &ppu_state.pixels[pixel_index];
ppu_pixel_set_color(pixel, fetch.pattern_table_tile_low, fetch.pattern_table_tile_high, fetch.attribute_table);
}
@ -137,6 +134,11 @@ void ppu_draw_tile() {
byte ppu_get_pattern(byte tile_index, byte high) {
#if DEBUG
if (ppu_state.debug.flags.tile_debugger) {
if ((ppu_state.debug.flags.tile_debugger_pattern_half == 1 && high) ||
(ppu_state.debug.flags.tile_debugger_pattern_half == 2 && !high)) {
return 0;
}
return tile_debugger_encode_number_as_pattern(tile_index, ppu_state.scanline % 8);
}
#endif
@ -151,16 +153,16 @@ void ppu_fetch_tile(bool render) {
if (fetch_cycle == 1) {
address nametable_addr = (ppu_state.ppu_address & 0xfff) | 0x2000;
ppu_state.fetch.nametable = ppu_read(nametable_addr);
ppu_state.fetch.tile_id = ppu_read(nametable_addr);
} else if (fetch_cycle == 3) {
// PPU address:
// yyy NN YYYYY XXXXX
// ||| || ||||| +++++-- coarse X scroll
// ||| || +++++-------- coarse Y scroll
// ||| ++-------------- nametable select
// ||| ++-------------- tile_id select
// +++----------------- fine Y scroll
//
// The attribute table is at the end of the nametable and contains 64 bytes
// The attribute table is at the end of the tile_id and contains 64 bytes
// It controls the palette assignation of a 4x4 tiles area
byte tile_col = ppu_state.ppu_address & 0x1f;
byte tile_attr_col = (tile_col >> 2) & 0x7;
@ -172,9 +174,9 @@ void ppu_fetch_tile(bool render) {
address attr_addr = 0x23c0 | (ppu_state.ppu_address & 0x0c00) | (tile_attr_row << 3) | tile_attr_col;
ppu_state.fetch.attribute_table = ppu_read(attr_addr);
} else if (fetch_cycle == 5) {
ppu_state.fetch.pattern_table_tile_low = ppu_get_pattern(ppu_state.fetch.nametable, 0);
ppu_state.fetch.pattern_table_tile_low = ppu_get_pattern(ppu_state.fetch.tile_id, 0);
} else if (fetch_cycle == 7) {
ppu_state.fetch.pattern_table_tile_high = ppu_get_pattern(ppu_state.fetch.nametable, 1);
ppu_state.fetch.pattern_table_tile_high = ppu_get_pattern(ppu_state.fetch.tile_id, 1);
if (render) {
ppu_draw_tile();
@ -197,7 +199,7 @@ void ppu_visible_frame(unsigned int cycle) {
if (cycle == 0) {
// Idle...
} else if (cycle >= 8 && cycle <= 256) {
} else if (cycle <= 256) {
ppu_fetch_tile(true);
if (cycle == 256) {
@ -247,38 +249,30 @@ void ppu_post_render(unsigned int x, unsigned int y) {
}
}
int cycles = 0;
void ppu_cycle() {
if (ppu_state.scanline < PPU_VISIBLE_FRAME_END) {
if (ppu_state.scanline < PPU_SCANLINE_VISIBLE_MAX) {
ppu_visible_frame(ppu_state.cycle);
} else if (ppu_state.scanline >= PPU_POST_RENDER_LINE_START && ppu_state.scanline <= PPU_POST_RENDER_LINE_END) {
} else if (ppu_state.scanline >= PPU_SCANLINE_POST_RENDER_MIN && ppu_state.scanline <= PPU_SCANLINE_POST_RENDER_MAX) {
ppu_post_render(ppu_state.cycle, ppu_state.scanline);
} else if (ppu_state.scanline == PPU_PRE_RENDER_LINE) {
} else if (ppu_state.scanline == PPU_SCANLINE_PRE_RENDER) {
ppu_pre_render(ppu_state.cycle);
// ppu_visible_frame(ppu_state.cycle);
ppu_state.ppu_address = ppu_state.temp_ppu_addr;
}
int frame_width = PPU_LINE_WIDTH;
int frame_height = PPU_LINE_END;
bool rendering_enabled = ppu_read_flag(PPU_REGISTER_MASK, PPU_MASK_SHOW_BG | PPU_MASK_SHOW_SP);
if (rendering_enabled && ppu_state.odd_frame) {
// With rendering enabled, the odd frames are shorter
// TODO: and doing the last cycle of the last dummy nametable fetch there instead
// frame_width = PPU_LINE_WIDTH - 2;
// frame_height = PPU_LINE_END - 1;
}
ppu_state.cycle++;
if (ppu_state.cycle >= frame_width) {
if (ppu_state.cycle >= PPU_CYCLE_MAX) {
ppu_state.cycle = 0;
ppu_state.scanline++;
}
if (ppu_state.scanline > frame_height) {
if (ppu_state.scanline > PPU_SCANLINE_MAX) {
ppu_state.scanline = 0;
ppu_state.frame++;
ppu_state.odd_frame = !ppu_state.odd_frame;
}
cycles++;
}
void ppu_write(address addr, byte data) {
@ -379,7 +373,50 @@ bool ppu_read_flag(size_t reg, byte mask) {
* 88 YD Y88888P Y888P Y888888P `8888Y' YP Y88888P 88 YD `8888Y'
*/
static inline bool ppu_reg_bit_changed(byte reg, byte data, byte bit, byte *new_val) {
byte old_val = (ppu_state.registers[reg] >> bit) & 1;
*new_val = (data >> bit) & 1;
return old_val != *new_val;
}
void ppu_write_ctrl(byte data) {
// Logging
log_debug("PPU Ctrl - %#02x", data);
byte new_ctrl;
for (int i = 0; i < 8; i++) {
if (!ppu_reg_bit_changed(PPU_REGISTER_CTRL, data, i, &new_ctrl)) {
continue;
}
switch (i) {
case 0:
case 1:
log_debug("PPU Ctrl - Base tile_id address = %#04x", 0x2000 + (0x400 * data & 3));
break;
case 2:
log_debug("PPU Ctrl - VRAM address increment = %d", new_ctrl ? 32 : 1);
break;
case 3:
log_debug("PPU Ctrl - Sprite pattern table address = %#04x", new_ctrl ? 0x1000 : 0);
break;
case 4:
log_debug("PPU Ctrl - Background pattern table address = %#04x", new_ctrl ? 0x1000 : 0);
break;
case 5:
log_debug("PPU Ctrl - Sprite size = %s", new_ctrl ? "8x16" : "8x8");
break;
case 6:
log_debug("PPU Ctrl - PPU master/slave select = %s", new_ctrl ? "output color" : "backdrop");
break;
case 7:
log_debug("PPU Ctrl - Generate NMI at VBlanks = %s", new_ctrl ? "yes" : "no");
break;
default:
assert(false);
}
}
ppu_state.temp_ppu_addr = (ppu_state.temp_ppu_addr & 0xf3ff) | ((data & PPU_CTRL_BASE_NAMETABLE_ADDR) << 10);
ppu_state.bg_pattern_table_addr = (data & PPU_CTRL_BG_PATTERN_TABLE_ADDR) << 0x8; // 0x0000 or 0x1000
@ -393,6 +430,45 @@ void ppu_write_ctrl(byte data) {
}
}
void ppu_write_mask(byte data) {
// Logging
byte new_mask;
for (int i = 0; i < 8; i++) {
if (!ppu_reg_bit_changed(PPU_REGISTER_MASK, data, i, &new_mask)) {
continue;
}
switch (i) {
case 0:
log_debug("PPU Mask - Greyscale = %d", new_mask ? "normal" : "greyscale");
break;
case 1:
log_debug("PPU Mask - Render background in first vertical tile = %s", new_mask ? "yes" : "no");
break;
case 2:
log_debug("PPU Mask - Render sprites in first vertical tile = %s", new_mask ? "yes" : "no");
break;
case 3:
log_debug("PPU Mask - Render background = %s", new_mask ? "yes" : "no");
break;
case 4:
log_debug("PPU Mask - Render sprites = %s", new_mask ? "yes" : "no");
break;
case 5:
log_debug("PPU Mask - Emphasize red = %s", new_mask ? "yes" : "no");
break;
case 6:
log_debug("PPU Mask - Emphasize green = %s", new_mask ? "yes" : "no");
break;
case 7:
log_debug("PPU Mask - Emphasize blue = %s", new_mask ? "yes" : "no");
break;
default:
assert(false);
}
}
}
void ppu_write_scroll(byte data) {
ppu_state.w = !ppu_state.w;
@ -443,6 +519,9 @@ void ppu_write_reg(byte reg, byte data) {
case PPU_REGISTER_CTRL:
ppu_write_ctrl(data);
break;
case PPU_REGISTER_MASK:
ppu_write_mask(data);
break;
case PPU_REGISTER_SCROLL:
ppu_write_scroll(data);
break;

View File

@ -7,11 +7,11 @@
// Contains the patterns of every hexadecimal digit encoded as pattern data.
// The first dimension of the table represents a row in a tile.
byte hex_pattern_table[5][0x10] = {
{0b111, 0b001, 0b111, 0b111, 0b101, 0b111, 0b111, 0b111, 0b111, 0b111, 0b010, 0b111, 0b111, 0b110, 0b111, 0b111},
{0b111, 0b001, 0b111, 0b111, 0b101, 0b111, 0b111, 0b111, 0b111, 0b111, 0b010, 0b110, 0b111, 0b110, 0b111, 0b111},
{0b101, 0b001, 0b001, 0b001, 0b101, 0b100, 0b100, 0b001, 0b101, 0b101, 0b101, 0b101, 0b100, 0b101, 0b100, 0b100},
{0b101, 0b001, 0b111, 0b111, 0b111, 0b111, 0b111, 0b010, 0b111, 0b111, 0b111, 0b110, 0b100, 0b101, 0b111, 0b110},
{0b101, 0b001, 0b100, 0b001, 0b001, 0b001, 0b101, 0b010, 0b101, 0b001, 0b101, 0b101, 0b100, 0b101, 0b100, 0b100},
{0b111, 0b001, 0b111, 0b111, 0b001, 0b111, 0b111, 0b010, 0b111, 0b001, 0b101, 0b111, 0b111, 0b110, 0b111, 0b100},
{0b111, 0b001, 0b111, 0b111, 0b001, 0b111, 0b111, 0b010, 0b111, 0b001, 0b101, 0b110, 0b111, 0b110, 0b111, 0b100},
};
byte tile_debugger_encode_number_as_pattern(byte num, byte tile_fine_y) {

View File

@ -23,21 +23,27 @@ void system_start() {
}
void system_next_frame() {
if (current_sys.paused) {
return;
for (int cpu_c = 0; cpu_c < CPU_CYCLE_PER_FRAME; cpu_c++) {
if (!current_sys.paused) {
cpu_cycle();
}
for (int cpu_c = 0; cpu_c < CPU_CYCLE_PER_FRAME; cpu_c++) {
cpu_cycle();
if (!current_sys.ppu_paused) {
for (int ppu_c = 0; ppu_c < PPU_CYCLE_PER_CPU_CYCLE; ppu_c++) {
ppu_cycle();
}
}
}
}
void system_toggle_pause() {
void system_toggle_pause(bool pause_ppu) {
current_sys.paused = !current_sys.paused;
if (!current_sys.paused) {
current_sys.ppu_paused = false;
} else if (pause_ppu) {
current_sys.ppu_paused = true;
}
}
void system_uninit() {