diff --git a/examples/sokol/CMakeLists.txt b/examples/sokol/CMakeLists.txt index 31238e7..5a99811 100644 --- a/examples/sokol/CMakeLists.txt +++ b/examples/sokol/CMakeLists.txt @@ -231,3 +231,21 @@ fips_begin_app(lc80 windowed) endif() fips_deps(roms ui) fips_end_app() + +fips_ide_group(examples/nes) +fips_begin_app(nes windowed) + fips_files(nes.c) + if (FIPS_IOS) + fips_files(ios-info.plist) + endif() + fips_deps(roms common) +fips_end_app() + +fips_begin_app(nes-ui windowed) + fips_files(nes.c nes-ui-impl.cc) + if (FIPS_IOS) + fips_files(ios-info.plist) + endif() + fips_deps(roms common ui) +fips_end_app() +target_compile_definitions(nes-ui PRIVATE CHIPS_USE_UI) diff --git a/examples/sokol/nes-ui-impl.cc b/examples/sokol/nes-ui-impl.cc new file mode 100644 index 0000000..173f534 --- /dev/null +++ b/examples/sokol/nes-ui-impl.cc @@ -0,0 +1,25 @@ +/* + UI implementation for nes.c, this must live in a .cc file. +*/ +#include "chips/chips_common.h" +#include "chips/m6502.h" +#include "chips/r2c02.h" +#include "chips/clk.h" +#include "chips/mem.h" +#include "systems/nes.h" +#define UI_DASM_USE_M6502 +#define UI_DBG_USE_M6502 +#define CHIPS_UTIL_IMPL +#include "util/m6502dasm.h" +#define CHIPS_UI_IMPL +#include "imgui.h" +#include "ui/ui_audio.h" +#include "ui/ui_util.h" +#include "ui/ui_chip.h" +#include "ui/ui_memedit.h" +#include "ui/ui_memmap.h" +#include "ui/ui_dasm.h" +#include "ui/ui_dbg.h" +#include "ui/ui_m6502.h" +#include "ui/ui_snapshot.h" +#include "ui/ui_nes.h" diff --git a/examples/sokol/nes.c b/examples/sokol/nes.c new file mode 100644 index 0000000..37ea140 --- /dev/null +++ b/examples/sokol/nes.c @@ -0,0 +1,293 @@ +/* + nes.c + + NES. +*/ +#include +#include +#define CHIPS_IMPL +#include "chips/chips_common.h" +#include "common.h" +#include "chips/clk.h" +#include "chips/m6502.h" +#include "chips/r2c02.h" +#include "systems/nes.h" +#if defined(CHIPS_USE_UI) + #define UI_DBG_USE_M6502 + #include "ui.h" + #include "ui/ui_audio.h" + #include "ui/ui_chip.h" + #include "ui/ui_memedit.h" + #include "ui/ui_memmap.h" + #include "ui/ui_dasm.h" + #include "ui/ui_dbg.h" + #include "ui/ui_m6502.h" + #include "ui/ui_snapshot.h" + #include "ui/ui_nes.h" +#endif + +typedef struct { + uint32_t version; + nes_t nes; +} nes_snapshot_t; + +static struct { + nes_t nes; + uint32_t frame_time_us; + uint32_t ticks; + double emu_time_ms; + #if defined(CHIPS_USE_UI) + ui_nes_t ui; + nes_snapshot_t snapshots[UI_SNAPSHOT_MAX_SLOTS]; + #endif +} state; + +#ifdef CHIPS_USE_UI +static void ui_draw_cb(void); +static bool ui_load_snapshot(size_t slot_index); +static void ui_save_snapshot(size_t slot_index); +#endif + +static void draw_status_bar(void); + +// audio-streaming callback +static void push_audio(const float* samples, int num_samples, void* user_data) { + (void)user_data; + saudio_push(samples, num_samples); +} + +static void app_init(void) { + nes_init(&state.nes, &(nes_desc_t) { + .audio = { + .callback = { .func = push_audio }, + .sample_rate = saudio_sample_rate(), + }, + #if defined(CHIPS_USE_UI) + .debug = ui_nes_get_debug(&state.ui) + #endif + }); + gfx_init(&(gfx_desc_t){ + #ifdef CHIPS_USE_UI + .draw_extra_cb = ui_draw, + #endif + .display_info = nes_display_info(&state.nes), + }); + clock_init(); + prof_init(); + saudio_setup(&(saudio_desc){ + .logger.func = slog_func, + }); + fs_init(); + +#ifdef CHIPS_USE_UI + ui_init(ui_draw_cb); + ui_nes_init(&state.ui, &(ui_nes_desc_t){ + .nes = &state.nes, + .dbg_texture = { + .create_cb = ui_create_texture, + .update_cb = ui_update_texture, + .destroy_cb = ui_destroy_texture, + }, + .snapshot = { + .load_cb = ui_load_snapshot, + .save_cb = ui_save_snapshot, + .empty_slot_screenshot = { + .texture = ui_shared_empty_snapshot_texture(), + } + }, + .dbg_keys = { + .cont = { .keycode = simgui_map_keycode(SAPP_KEYCODE_F5), .name = "F5" }, + .stop = { .keycode = simgui_map_keycode(SAPP_KEYCODE_F5), .name = "F5" }, + .step_over = { .keycode = simgui_map_keycode(SAPP_KEYCODE_F6), .name = "F6" }, + .step_into = { .keycode = simgui_map_keycode(SAPP_KEYCODE_F7), .name = "F7" }, + .step_tick = { .keycode = simgui_map_keycode(SAPP_KEYCODE_F8), .name = "F8" }, + .toggle_breakpoint = { .keycode = simgui_map_keycode(SAPP_KEYCODE_F9), .name = "F9" } + } + }); +#endif + + if (sargs_exists("file")) { + fs_start_load_file(FS_SLOT_IMAGE, sargs_value("file")); + } +} + +static void handle_file_loading(void); + +static void app_frame(void) { + state.frame_time_us = clock_frame_time(); + const uint64_t emu_start_time = stm_now(); + state.ticks = nes_exec(&state.nes, state.frame_time_us); + state.emu_time_ms = stm_ms(stm_since(emu_start_time)); + draw_status_bar(); + gfx_draw(nes_display_info(&state.nes)); + handle_file_loading(); +} + +static void app_cleanup(void) { + nes_discard(&state.nes); + #ifdef CHIPS_USE_UI + ui_nes_discard(&state.ui); + ui_discard(); + #endif + saudio_shutdown(); + gfx_shutdown(); + sargs_shutdown(); +} + +static void draw_status_bar(void) { + prof_push(PROF_EMU, (float)state.emu_time_ms); + prof_stats_t emu_stats = prof_stats(PROF_EMU); + + const uint32_t text_color = 0xFFFFFFFF; + const uint32_t cart_active = 0xFF00EE00; + const uint32_t cart_inactive = 0xFF006600; + const uint32_t pad_active = 0xFFFFEE00; + const uint32_t pad_inactive = 0xFF886600; + + const float w = sapp_widthf(); + const float h = sapp_heightf(); + sdtx_canvas(w, h); + sdtx_origin(1.0f, (h / 8.0f) - 3.5f); + + sdtx_puts("PAD: "); + sdtx_font(1); + const uint8_t padmask = nes_pad_mask(&state.nes); + sdtx_color1i((padmask & NES_PAD_LEFT) ? pad_active : pad_inactive); + sdtx_putc(0x88); // arrow left + sdtx_color1i((padmask & NES_PAD_RIGHT) ? pad_active : pad_inactive); + sdtx_putc(0x89); // arrow right + sdtx_color1i((padmask & NES_PAD_UP) ? pad_active : pad_inactive); + sdtx_putc(0x8B); // arrow up + sdtx_color1i((padmask & NES_PAD_DOWN) ? pad_active : pad_inactive); + sdtx_putc(0x8A); // arrow down + sdtx_color1i((padmask & NES_PAD_START) ? pad_active : pad_inactive); + sdtx_putc(0x87); // btn + sdtx_color1i((padmask & NES_PAD_SEL) ? pad_active : pad_inactive); + sdtx_putc(0x87); // btn + sdtx_color1i((padmask & NES_PAD_B) ? pad_active : pad_inactive); + sdtx_putc(0x87); // btn + sdtx_color1i((padmask & NES_PAD_A) ? pad_active : pad_inactive); + sdtx_putc(0x87); // btn + sdtx_font(0); + + // cartridge inserted LED + sdtx_color1i(text_color); + sdtx_puts(" CART: "); + sdtx_color1i(nes_cartridge_inserted(&state.nes) ? cart_active : cart_inactive); + sdtx_putc(0xCF); // filled circle + + sdtx_font(0); + sdtx_color1i(text_color); + sdtx_pos(0.0f, 1.5f); + sdtx_printf("frame:%.2fms emu:%.2fms (min:%.2fms max:%.2fms) ticks:%d", (float)state.frame_time_us * 0.001f, emu_stats.avg_val, emu_stats.min_val, emu_stats.max_val, state.ticks); +} + +static void handle_file_loading(void) { + fs_dowork(); + const uint32_t load_delay_frames = 120; + if (fs_success(FS_SLOT_IMAGE) && clock_frame_count_60hz() > load_delay_frames) { + + bool load_success = false; + if (fs_ext(FS_SLOT_IMAGE, "nes")) { + load_success = nes_insert_cart(&state.nes, fs_data(FS_SLOT_IMAGE)); + } + if (load_success) { + if (clock_frame_count_60hz() > (load_delay_frames + 10)) { + gfx_flash_success(); + } + } + else { + gfx_flash_error(); + } + fs_reset(FS_SLOT_IMAGE); + } +} + +void app_input(const sapp_event* event) { + // accept dropped files also when ImGui grabs input + if (event->type == SAPP_EVENTTYPE_FILES_DROPPED) { + fs_start_load_dropped_file(FS_SLOT_IMAGE); + } +#ifdef CHIPS_USE_UI + if (ui_input(event)) { + // input was handled by UI + return; + } +#endif + switch (event->type) { + case SAPP_EVENTTYPE_KEY_DOWN: + case SAPP_EVENTTYPE_KEY_UP: { + int c; + switch (event->key_code) { + case SAPP_KEYCODE_LEFT: c = 0x01; break; + case SAPP_KEYCODE_RIGHT: c = 0x02; break; + case SAPP_KEYCODE_DOWN: c = 0x03; break; + case SAPP_KEYCODE_UP: c = 0x04; break; + case SAPP_KEYCODE_ENTER: c = 0x05; break; + case SAPP_KEYCODE_F: c = 0x06; break; + case SAPP_KEYCODE_D: c = 0x07; break; + case SAPP_KEYCODE_S: c = 0x08; break; + default: c = 0x00; break; + } + if (c) { + if (event->type == SAPP_EVENTTYPE_KEY_DOWN) { + nes_key_down(&state.nes, c); + } + else { + nes_key_up(&state.nes, c); + } + } + break; + default: + break; + } + } +} + +#if defined(CHIPS_USE_UI) +static void ui_draw_cb(void) { + ui_nes_draw(&state.ui); +} + +static void ui_update_snapshot_screenshot(size_t slot) { + ui_snapshot_screenshot_t screenshot = { + .texture = ui_create_screenshot_texture(nes_display_info(&state.snapshots[slot].nes)) + }; + ui_snapshot_screenshot_t prev_screenshot = ui_snapshot_set_screenshot(&state.ui.snapshot, slot, screenshot); + if (prev_screenshot.texture) { + ui_destroy_texture(prev_screenshot.texture); + } +} + +static bool ui_load_snapshot(size_t slot) { + bool success = false; + if ((slot < UI_SNAPSHOT_MAX_SLOTS) && (state.ui.snapshot.slots[slot].valid)) { + success = nes_load_snapshot(&state.nes, state.snapshots[slot].version, &state.snapshots[slot].nes); + } + return success; +} + +static void ui_save_snapshot(size_t slot) { + if (slot < UI_SNAPSHOT_MAX_SLOTS) { + state.snapshots[slot].version = nes_save_snapshot(&state.nes, &state.snapshots[slot].nes); + ui_update_snapshot_screenshot(slot); + fs_save_snapshot("nes", slot, (chips_range_t){ .ptr = &state.snapshots[slot], sizeof(nes_snapshot_t) }); + } +} +#endif + +sapp_desc sokol_main(int argc, char* argv[]) { + sargs_setup(&(sargs_desc){ .argc=argc, .argv=argv }); + return (sapp_desc) { + .init_cb = app_init, + .event_cb = app_input, + .frame_cb = app_frame, + .cleanup_cb = app_cleanup, + .width = 800, + .height = 600, + .window_title = "NES", + .icon.sokol_default = true, + .enable_dragndrop = true, + .logger.func = slog_func, + }; +} diff --git a/fips-files/verbs/webpage.py b/fips-files/verbs/webpage.py index 4a4dcec..99c9983 100644 --- a/fips-files/verbs/webpage.py +++ b/fips-files/verbs/webpage.py @@ -25,6 +25,7 @@ 'bombjack', 'bombjack-ui', 'pacman', 'pacman-ui', 'pengo', 'pengo-ui', + 'nes', 'nes-ui', 'lc80', 'ext' ] @@ -50,6 +51,7 @@ { 'type':'emu', 'title':'Robotron Z1013', 'system':'z1013', 'url':'z1013.html', 'img':'z1013/z1013.jpg', 'note':'' }, { 'type':'emu', 'title':'Robotron Z9001', 'system':'z9001', 'url':'z9001.html', 'img':'z9001/z9001.jpg', 'note':'(with BASIC and RAM modules)' }, { 'type':'emu', 'title':'Robotron KC87', 'system':'z9001', 'url':'z9001.html?type=kc87', 'img':'z9001/kc87.jpg', 'note':'BASIC[Enter]' }, + { 'type':'emu', 'title':'NES', 'system':'nes', 'url':'nes.html', 'img':'nes/nes.jpg', 'note':'' }, { 'type':'demo', 'title':'FORTH (KC85/4)', 'system':'kc854', 'url':'kc854.html?mod=m026&mod_image=kc85/forth.853&input=SWITCH%208%20C1%0AFORTH%0A', 'img':'kc85/forth.jpg', 'note':'' }, { 'type':'demo', 'title':'FORTH (Z1013)', 'system':'z1013', 'url':'z1013.html?type=z1013_64&file=z1013/z1013_forth.z80', 'img':'z1013/z1013_forth.jpg', 'note':''}, { 'type':'demo', 'title':'BASIC (Z1013)', 'system':'z1013', 'url':'z1013.html?type=z1013_64&file=z1013/kc_basic.z80', 'img':'z1013/z1013_basic.jpg', 'note':''}, diff --git a/fips.yml b/fips.yml index b3e2006..de262ee 100644 --- a/fips.yml +++ b/fips.yml @@ -8,7 +8,7 @@ imports: fips-imgui: git: https://github.com/fips-libs/fips-imgui.git chips: - git: https://github.com/floooh/chips + git: https://github.com/scemino/chips sokol: git: https://github.com/floooh/sokol sokol-tools-bin: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9bc0889..daa60a4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -65,6 +65,13 @@ fips_begin_app(m6502-perfect cmdline) ) fips_end_app() +fips_begin_app(nes-test1 cmdline) + fips_files(nes-test1.c) + fips_dir(nestest) + fips_generate(FROM nestest.log.txt TYPE nestestlog HEADER nestestlog.h) + fipsutil_embed(dump.yml dump.h) +fips_end_app() + fips_begin_app(z80-fuse cmdline) fips_files(z80-fuse.c) fips_dir(fuse) diff --git a/tests/nes-test1.c b/tests/nes-test1.c new file mode 100644 index 0000000..fe58b97 --- /dev/null +++ b/tests/nes-test1.c @@ -0,0 +1,58 @@ +//------------------------------------------------------------------------------ +// nes-test1.c +// +// Tests NES state after documented instructions. +//------------------------------------------------------------------------------ +#define CHIPS_IMPL +#include "chips/chips_common.h" +#include "chips/clk.h" +#include "chips/m6502.h" +#include "chips/r2c02.h" +#include "systems/nes.h" +#include +#include "nestest/dump.h" +#include "nestest/nestestlog.h" +#include "test.h" + +int main() { + test_begin("NES TEST (NES)"); + test_no_verbose(); + + /* initialize the NES */ + nes_t nes; + nes_init(&nes, &(nes_desc_t){}); + nes_insert_cart(&nes, (chips_range_t){ + .ptr = dump_nestest_nes, + .size = sizeof(dump_nestest_nes), + }); + /* patch the test start address into the RESET vector */ + nes.cart.rom[0x3FFC] = 0x00; + dump_nestest_nes[0x3FFD] = 0xc0; + nes.cart.rom[0x3FFD] = 0xC0; + /* set RESET vector and run through RESET sequence */ + uint64_t pins = nes.pins; + for (int i = 0; i < 7; i++) { + pins = _nes_tick(&nes, pins); + } + nes.cpu.P &= ~M6502_ZF; + + /* run the test */ + int num_tests = sizeof(state_table) / sizeof(cpu_state); + for (int i = 0; i < num_tests; i++) { + cpu_state* state = &state_table[i]; + test(state->desc); + T(nes.cpu.PC == state->PC); + T(nes.cpu.A == state->A); + T(nes.cpu.X == state->X); + T(nes.cpu.Y == state->Y); + T((nes.cpu.P & ~(M6502_XF|M6502_BF)) == (state->P & ~(M6502_XF|M6502_BF))); + T(nes.cpu.S == state->S); + if (test_failed()) { + printf("### NESTEST failed at pos %d, PC=0x%04X: %s\n", i, nes.cpu.PC, state->desc); + } + do { + pins = _nes_tick(&nes, pins); + } while (0 == (pins & M6502_SYNC)); + } + return test_end(); +}