diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 986a3ed..de5370e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -14,9 +14,11 @@ jobs: with: submodules: recursive - name: set up packages - run: sudo apt install gcc libsdl2-dev libxxhash-dev + run: sudo apt install meson ninja-build gcc libsdl2-dev libxxhash-dev - name: build release - run: ./build.sh + run: | + meson setup build + meson compile -C build - name: upload build uses: actions/upload-artifact@v3 with: @@ -37,14 +39,16 @@ jobs: - uses: msys2/setup-msys2@v2 with: release: false - install: git mingw-w64-x86_64-binutils mingw-w64-x86_64-gcc mingw-w64-x86_64-SDL2 zlib mingw-w64-x86_64-xxhash + install: mingw-w64-x86_64-meson mingw-w64-x86_64-pkgconf git mingw-w64-x86_64-binutils mingw-w64-x86_64-gcc mingw-w64-x86_64-SDL2 zlib mingw-w64-x86_64-xxhash mingw-w64-x86_64-freetype - run: git config --global core.autocrlf input shell: bash - uses: actions/checkout@v3 with: submodules: recursive - name: build release - run: ./build.sh -p win32 + run: | + meson setup build + meson compile -C build - name: upload build uses: actions/upload-artifact@v3 with: diff --git a/.gitmodules b/.gitmodules index 71ea916..ad16518 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "ayumi"] path = ayumi url = https://github.com/wermipls/ayumi +[submodule "microui"] + path = microui + url = https://github.com/wermipls/microui diff --git a/build.sh b/build.sh deleted file mode 100755 index 0ad12c1..0000000 --- a/build.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -set -e - -FILES="src/*.c ayumi/ayumi.c" -FLAGS="-std=gnu11 -Wall -Wextra -Wpedantic -lz -lm `sdl2-config --libs`" -FLAGS_DEBUG="-Og -ggdb" -FLAGS_RELEASE="-O3 -ffast-math -flto -fwhole-program" -RELEASE_FILES="sleepdart* rom/* palettes/*" - -while getopts ':p:dr' opt; do - case $opt in - (p) PLATFORM=$OPTARG;; - (d) DEBUG=1;; - (r) RELEASEPKG=1;; - (:) :;; - esac -done - -if [[ $PLATFORM == "WIN32" ]] || [[ $PLATFORM == "win32" ]]; then - WIN32_FILES="src/win32/*.c src/win32/*.o" - WIN32_FLAGS="-DPLATFORM_WIN32 -static `sdl2-config --static-libs`" - FILES="$FILES $WIN32_FILES" - FLAGS="$FLAGS $WIN32_FLAGS" - - windres src/win32/resource.rc src/win32/resource.o -fi - -if [[ $DEBUG == "1" ]]; then - FLAGS="$FLAGS $FLAGS_DEBUG" -else - FLAGS="$FLAGS $FLAGS_RELEASE" -fi - -set +e -DESCRIBE=`git describe --tags --dirty --always` -set -e -if [[ $? -eq 0 ]]; then - FLAGS="$FLAGS -DGIT_DESCRIBE=\"$DESCRIBE\"" -else - DESCRIBE=0 -fi - -echo "FILES: $FILES" -echo "FLAGS: $FLAGS" -gcc $FILES $FLAGS -o sleepdart - -if [[ $RELEASEPKG == "1" ]]; then - if [[ "$DESCRIBE" == "0" ]]; then - PKGNAME="sleepdart.zip" - else - PKGNAME="sleepdart-$DESCRIBE.zip" - fi - - zip "$PKGNAME" $RELEASE_FILES -fi diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..a95f7ef --- /dev/null +++ b/meson.build @@ -0,0 +1,96 @@ +project('sleepdart', 'c', default_options: ['buildtype=release']) + +release_args = ['-ffast-math', '-flto', '-fwhole-program'] +if get_option('buildtype') == 'release' + add_project_arguments(release_args, language: 'c') + add_project_link_arguments(release_args, language: 'c') +endif + +includes = include_directories('microui/src') + +extra_src = [] +extra_obj = [] +static_libs = false + +if target_machine.system() == 'windows' + windres = find_program('windres') + run_command(windres, 'src/win32/resource.rc', 'src/win32/resource.o') + extra_src = [ + 'src/win32/gui_windows.c', + ] + extra_obj = [ + 'src/win32/resource.o', + ] + add_project_arguments('-DPLATFORM_WIN32', language: 'c') + static_libs = true +endif + +git = find_program('git', required: false) +if git.found() + gitargs = ['describe', '--tags', '--dirty', '--always'] + describe = run_command(git, gitargs, check: false).stdout().strip() + if describe != '' + add_project_arguments('-DGIT_DESCRIBE="' + describe + '"', language: 'c') + endif +endif + +deps = [ + dependency('zlib', static: static_libs), + dependency('sdl2', static: static_libs), + dependency('libxxhash', static: static_libs), + dependency('freetype2', static: static_libs), +] + +add_project_link_arguments('-lstdc++', language: 'c') + +exe = executable( + 'sleepdart', + 'src/main.c', + 'src/argparser.c', + 'src/audio_sdl.c', + 'src/ay.c', + 'src/beeper.c', + 'src/config.c', + 'src/config_parser.c', + 'src/debugger.c', + 'src/disasm.c', + 'src/dsp.c', + 'src/file.c', + 'src/hotkeys.c', + 'src/input_sdl.c', + 'src/io.c', + 'src/keyboard.c', + 'src/keyboard_macro.c', + 'src/log.c', + 'src/machine.c', + 'src/machine_hooks.c', + 'src/machine_test.c', + 'src/memory.c', + 'src/microui_render.c', + 'src/palette.c', + 'src/parser_helpers.c', + 'src/szx_file.c', + 'src/szx_state.c', + 'src/tape.c', + 'src/ula.c', + 'src/video_sdl.c', + 'src/z80.c', + 'microui/src/microui.c', + 'ayumi/ayumi.c', + extra_src, + include_directories: includes, + dependencies: deps, + objects: extra_obj, + win_subsystem: 'windows', + link_args: '-lm', +) + +# a lil hacky... +dest = meson.project_source_root() +custom_target( + 'finalize', + depends: exe, + input: exe, + output: 'fake', + command: ['cp', '@INPUT@', dest], + build_by_default : true) diff --git a/microui b/microui new file mode 160000 index 0000000..de1f356 --- /dev/null +++ b/microui @@ -0,0 +1 @@ +Subproject commit de1f3567ebbdb9b6635f1ccc981779c409d6f052 diff --git a/src/config_parser.c b/src/config_parser.c index fa7470e..a6da810 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -231,7 +231,7 @@ int config_get_float(CfgData_t *cfg, const char *key, float *dest) return 0; } -void config_set_str(CfgData_t *cfg, const char *key, char *value) +void config_set_str(CfgData_t *cfg, const char *key, const char *value) { if (value == NULL) { return; diff --git a/src/config_parser.h b/src/config_parser.h index 8989a06..801c614 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -37,6 +37,6 @@ int config_get_int(CfgData_t *cfg, const char *key, int *dest); * config value gets copied to dest */ int config_get_float(CfgData_t *cfg, const char *key, float *dest); -void config_set_str(CfgData_t *cfg, const char *key, char *value); +void config_set_str(CfgData_t *cfg, const char *key, const char *value); void config_set_int(CfgData_t *cfg, const char *key, int value); void config_set_float(CfgData_t *cfg, const char *key, float value); diff --git a/src/debugger.c b/src/debugger.c new file mode 100644 index 0000000..371e308 --- /dev/null +++ b/src/debugger.c @@ -0,0 +1,507 @@ +#include "debugger.h" + +#include "microui.h" +#include "microui_render.h" +#include "machine.h" +#include "z80.h" +#include "disasm.h" +#include "vector.h" +#include "input_sdl.h" +#include +#include +#include +#include "log.h" + +static mu_Context context; +static Machine_t *machine; + +struct Disasm +{ + int breakpoint; + uint16_t pc; + int len; + int dirty; + int prev_offset; + char *pcstr; + char *bytestr; + char *instr; +}; + +struct Breakpoint +{ + int enabled; + uint16_t pc; +}; + +enum Condition +{ + DBG_NONE, + DBG_STEPINTO, + DBG_STEPOVER, + DBG_BREAKPOINT, +}; + +static struct Breakpoint breakpoints[256]; +static char registers[1024]; +static enum Condition break_on = DBG_STEPINTO; +static bool debugger_enabled = false; + +struct Disasm *disasm = NULL; + +int mu_button_ex_id(mu_Context *ctx, const char *label, int id_num, int icon, int opt) { + int res = 0; + mu_Id id = id_num ? mu_get_id(ctx, &id_num, sizeof(id_num)) + : mu_get_id(ctx, &icon, sizeof(icon)); + mu_Rect r = mu_layout_next(ctx); + mu_update_control(ctx, id, r, opt); + /* handle click */ + if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) { + res |= MU_RES_SUBMIT; + } + /* draw */ + mu_draw_control_frame(ctx, id, r, MU_COLOR_BUTTON, opt); + if (label) { mu_draw_control_text(ctx, label, r, MU_COLOR_TEXT, opt); } + if (icon) { mu_draw_icon(ctx, icon, r, ctx->style->colors[MU_COLOR_TEXT]); } + return res; +} + +static int is_breakpoint(uint16_t pc) +{ + for (int i = 0; i < 256; i++) { + if (breakpoints[i].enabled && pc == breakpoints[i].pc) { + return 1; + } + } + return 0; +} + +static void add_breakpoint(uint16_t pc) +{ + for (int i = 0; i < 256; i++) { + if (!breakpoints[i].enabled) { + breakpoints[i].enabled = 1; + breakpoints[i].pc = pc; + return; + } + } +} + +static void remove_breakpoint(uint16_t pc) +{ + for (int i = 0; i < 256; i++) { + if (breakpoints[i].enabled && pc == breakpoints[i].pc) { + breakpoints[i].enabled = 0; + } + } +} + +static int disasm_update(struct Disasm *d, uint16_t pc) +{ + d->prev_offset = 0; + d->dirty = 0; + d->pc = pc; + + char buf[256]; + + int index = 0; + int len = 1; + uint8_t opbuf[4]; + opbuf[0] = machine->memory.bus[(uint16_t)(pc+0)]; + opbuf[1] = machine->memory.bus[(uint16_t)(pc+1)]; + opbuf[2] = machine->memory.bus[(uint16_t)(pc+2)]; + opbuf[3] = machine->memory.bus[(uint16_t)(pc+3)]; + + char *op = disasm_opcode(opbuf, &len, pc); + d->len = len; + + snprintf( + buf, sizeof(buf), + "%04x", pc); + size_t size = strlen(buf)+1; + d->pcstr = malloc(size); + if (d->pcstr == NULL) { + free(op); + return -1; + } + memcpy(d->pcstr, buf, size-1); + d->pcstr[size-1] = 0; + + uint16_t i = pc; + while (len--) { + index += snprintf( + buf+index, sizeof(buf)-index, + "%02x ", machine->memory.bus[i]); + i++; + } + + size = strlen(buf)+1; + d->bytestr = malloc(size); + if (d->bytestr == NULL) { + free(d->pcstr); + free(op); + return -2; + } + memcpy(d->bytestr, buf, size-1); + d->bytestr[size-1] = 0; + + d->instr = op; + return 0; +} + +static void disasm_free(struct Disasm *d) +{ + if (d->bytestr) { free(d->bytestr); d->bytestr = NULL; } + if (d->instr) { free(d->instr); d->instr = NULL; } + if (d->pcstr) { free(d->pcstr); d->pcstr = NULL; } +} + +static void update_dirty(uint16_t pc) +{ + int org_pc = pc; + int offset = 0; + do { + struct Disasm *d = &disasm[(uint16_t)org_pc]; + offset = d->prev_offset; + disasm_free(d); + disasm_update(d, d->pc); + org_pc -= offset; + } while (offset && disasm[(uint16_t)org_pc].dirty); + + while (org_pc <= pc) { + int b = disasm[(uint16_t)org_pc].len; + org_pc += b; + disasm[(uint16_t)org_pc].prev_offset = b; + } +} + +void debugger_mark_dirty(uint16_t addr) +{ + if (!debugger_enabled) return; + + disasm[addr--].dirty = true; + disasm[addr--].dirty = true; + disasm[addr--].dirty = true; + disasm[addr--].dirty = true; +} + +static void update_regs() +{ + struct Z80Regs *r = &machine->cpu.regs; + snprintf(registers, sizeof(registers), + "PC %04x SP %04x\n\n" + "AF %04x AF` %04x\n" + "BC %04x BC` %04x\n" + "DE %04x DE` %04x\n" + "HL %04x HL` %04x\n" + "IX %04x IY %04x\n\n" + "cycles: %05lld/%05u", + r->pc, r->sp, + r->main.af, r->alt.af, + r->main.bc, r->alt.bc, + r->main.de, r->alt.de, + r->main.hl, r->alt.hl, + r->ix, r->iy, + machine->cpu.cycles, machine->timing.t_frame); +} + +static disasm_row(mu_Context *ctx, struct Disasm *d, int id, int line_h) +{ + if (d->dirty) { + update_dirty(d->pc); + } + mu_layout_row(ctx, 4, (int[]) { line_h, 32, 100, -1 }, line_h); + int icon = d->breakpoint ? DBGICON_BREAKPOINT : 0; + if (d->pc == machine->cpu.regs.pc) { + icon = DBGICON_CURRENT; + } + if (mu_button_ex_id(ctx, NULL, 1+id, icon, MU_OPT_NOFRAME)) { + d->breakpoint ^= 1; + if (d->breakpoint) add_breakpoint(d->pc); + else remove_breakpoint(d->pc); + } + mu_label(ctx, d->pcstr); + mu_label(ctx, d->bytestr); + mu_label(ctx, d->instr); +} + +static uint16_t find_prev_pc(uint16_t pc) +{ + if (disasm[pc].dirty) { + update_dirty(pc); + } + pc -= disasm[pc].prev_offset; + return pc; +} + +static disasm_panel(mu_Context *ctx) +{ + int line_h = (render_text_height(0) + 5) & ~1; + static int pc = -1; + static uint16_t abs_scroll_line = 0; + int pc_changed = (machine->cpu.regs.pc != pc); + pc = machine->cpu.regs.pc; + abs_scroll_line = pc_changed ? pc : abs_scroll_line; + + mu_begin_panel_ex(ctx, "disassembly", MU_OPT_INFSCROLLY); + mu_Container *panel = mu_get_current_container(ctx); + + const int range = 64; + int up_offset = range / 2; + uint16_t line_start = abs_scroll_line; + do { + uint16_t old = line_start; + line_start = find_prev_pc(line_start); + if (line_start == old) { + break; + } + } while (--up_offset); + + for (int i = range; i--; ) { + disasm_row(ctx, &disasm[line_start], i, line_h); + line_start += disasm[line_start].len; + } + + int line_h_actual = line_h + ctx->style->spacing; + int target_center_scroll = ((range / 2 - up_offset) * line_h_actual) - (panel->body.h / 2); + if (pc_changed) panel->scroll.y = target_center_scroll; + + int offset = target_center_scroll - panel->scroll.y; + int offset_line = offset / line_h_actual; + if (offset_line < -5) { + do { + abs_scroll_line += disasm[abs_scroll_line].len; + offset_line++; + panel->scroll.y -= line_h_actual; + } while (offset_line < -5); + } else if (offset_line > 5) { + do { + uint16_t prev = find_prev_pc(abs_scroll_line); + if (prev == abs_scroll_line) { + break; + } + abs_scroll_line = prev; + offset_line--; + panel->scroll.y += line_h_actual; + } while (offset_line > 5); + } + + mu_end_panel(ctx); +} + +static void disasm_window(mu_Context *ctx) { + if (mu_begin_window_ex(ctx, "Debugger", mu_rect(0, 0, 400, 600), MU_OPT_NOCLOSE)) { + mu_layout_row(ctx, 4, (int[]) { 70, -200, -100, -1 }, 25); + if (mu_button(ctx, "Pause") && break_on == DBG_BREAKPOINT) { + break_on = DBG_STEPINTO; + }; + if (mu_button(ctx, "Continue")) { + break_on = DBG_BREAKPOINT; + }; + mu_button(ctx, "Step over"); + if (mu_button(ctx, "Step into")) { + break_on = DBG_STEPINTO; + }; + + mu_layout_row(ctx, 1, (int[]) { -1 }, -1); + disasm_panel(ctx); + + mu_end_window(ctx); + } +} + +static void regs_window(mu_Context *ctx) +{ + if (mu_begin_window(ctx, "Registers", mu_rect(400, 0, 200, 400))) { + mu_layout_row(ctx, 1, (int[]) { -1 }, 220); + mu_text(ctx, registers); + + mu_layout_row(ctx, 8, (int[]) { 16, 16, 16, 16, 16, 16, 16, 16 }, 16); + static const char *flag_label[8] = { + "C", "N", "PV", "X", "H", "Y", "Z", "S" + }; + for (int i = 7; i >= 0; i--) { + mu_draw_control_text( + ctx, flag_label[i], mu_layout_next(ctx), + MU_COLOR_TEXT, MU_OPT_ALIGNCENTER); + } + int flags[8]; + int clicked = 0; + for (int i = 7; i >= 0; i--) { + flags[i] = machine->cpu.regs.main.f & (1<cpu.regs.main.f = f; + update_regs(); + } + mu_end_window(ctx); + } +} + +static void process_frame(mu_Context *ctx) { + mu_begin(ctx); + disasm_window(ctx); + regs_window(ctx); + mu_end(ctx); +} + +static int window_loop(mu_Context *ctx, int flush_events) +{ + SDL_Event e; + SDL_Event events[1024]; + if (flush_events) SDL_PumpEvents(); + int count = SDL_PeepEvents(events, 1024, SDL_PEEKEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT); + + for (int i = 0; i < count; i++) { + e = events[i]; + switch (e.type) + { + case SDL_MOUSEMOTION: + if (e.motion.windowID != render_get_window_id()) break; + mu_input_mousemove(ctx, e.motion.x, e.motion.y); + break; + case SDL_MOUSEBUTTONDOWN: + if (e.button.windowID != render_get_window_id()) break; + mu_input_mousedown(ctx, e.button.x, e.button.y, e.button.button); + break; + case SDL_MOUSEBUTTONUP: + if (e.button.windowID != render_get_window_id()) break; + mu_input_mouseup(ctx, e.button.x, e.button.y, e.button.button); + break; + case SDL_WINDOWEVENT: + if (e.window.windowID != render_get_window_id()) break; + if (e.window.event == SDL_WINDOWEVENT_CLOSE) { + debugger_close(); + break_on = DBG_NONE; + return 0; + } + break; + case SDL_MOUSEWHEEL: + if (e.wheel.windowID != render_get_window_id()) break; + mu_input_scroll(ctx, 0, e.wheel.y * -30); + break; + } + } + + if (flush_events) { + input_sdl_update(); + SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); + } + + static uint64_t ticks_old = 0; + uint64_t ticks = SDL_GetTicks64(); + ctx->delta_ms = ticks - ticks_old; + + process_frame(ctx); + process_frame(ctx); + + ticks_old = ticks; + + render_clear(mu_color(0, 0, 0, 255)); + mu_Command *cmd = NULL; + while (mu_next_command(ctx, &cmd)) { + switch(cmd->type) + { + case MU_COMMAND_TEXT: render_text(cmd->text.font, cmd->text.str, cmd->text.pos, cmd->text.color); break; + case MU_COMMAND_RECT: render_draw_rect(cmd->rect.rect, cmd->rect.color); break; + case MU_COMMAND_CLIP: render_clip_rect(cmd->rect.rect); break; + case MU_COMMAND_ICON: render_draw_icon(cmd->icon.id, cmd->icon.rect, cmd->icon.color); break; + } + } + render_present(); + + if (break_on != DBG_NONE) { + return 0; + } + + return 1; +} + +void debugger_handle() +{ + if (!debugger_enabled) return; + + bool is_break = false; + + switch (break_on) + { + case DBG_BREAKPOINT: + if (is_breakpoint(machine->cpu.regs.pc)) { + is_break = true; + } + break; + case DBG_STEPOVER: + case DBG_STEPINTO: + is_break = true; + break; + default: + break; + } + + if (is_break) { + break_on = DBG_NONE; + update_regs(); + while (window_loop(&context, true)); + } +} + +void debugger_update_window() +{ + if (!debugger_enabled) return; + + update_regs(); + window_loop(&context, false); +} + +void debugger_open(Machine_t *m) +{ + if (debugger_enabled) return; + + mu_init(&context); + context.text_height = render_text_height; + context.text_width = render_text_width; + + if (render_init()) { + return; + } + + disasm = vector_create(); + + memset(breakpoints, 0, sizeof(breakpoints)); + + machine = m; + + int pc = 0; + for (; pc < 0x10000;) { + struct Disasm d = { 0 }; + disasm_update(&d, pc); + vector_add(disasm, d); + + pc++; + } + + for (pc = 0; pc < 0x10000;) { + int b = disasm[(uint16_t)(pc)].len; + pc += b; + disasm[(uint16_t)(pc)].prev_offset = b; + } + + update_regs(); + + debugger_enabled = true; +} + +void debugger_close() +{ + if (!debugger_enabled) return; + + render_deinit(); + vector_free(disasm); + + debugger_enabled = false; +} + diff --git a/src/debugger.h b/src/debugger.h new file mode 100644 index 0000000..005581f --- /dev/null +++ b/src/debugger.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +struct Machine; + +void debugger_handle(); +void debugger_update_window(); +void debugger_mark_dirty(uint16_t addr); +void debugger_open(struct Machine *m); +void debugger_close(); diff --git a/src/disasm.c b/src/disasm.c new file mode 100644 index 0000000..f102873 --- /dev/null +++ b/src/disasm.c @@ -0,0 +1,384 @@ +#include "disasm.h" + +#include +#include +#include + +static const char *t_r[] = { + "b", "c", "d", "e", "h", "l", "(hl)", "a" +}; + +static const char *t_rp[] = { + "bc", "de", "hl", "sp" +}; + +static const char *t_rp2[] = { + "bc", "de", "hl", "af" +}; + +static const char *t_cc[] = { + "nz", "z", "nc", "c", "po", "pe", "p", "m" +}; + +static const char *t_alu[] = { + "add a,", "adc a,", "sub", "sbc a,", "and", "xor", "or", "cp" +}; + +static const char *t_sro[] = { + "rlc", "rrc", "rl", "rr", "sla", "sra", "sll", "srl" +}; + +static const char *t_blk[] = { + "ldi", "cpi", "ini", "outi", + "ldd", "cpd", "ind", "outd", + "ldir", "cpir", "inir", "otir", + "lddr", "cpdr", "indr", "otdr", +}; + +static const char *t_im[] = { + "0", "0*", "1", "2", "0*", "0*", "1*", "2*" +}; + +static const char *t_x1_z1_q[] = { + "ret", "exx", "jp %s", "ld sp, %s" +}; + +static const char *t_x3_z3[] = { + "ex (sp), hl", "ex de", "di", "ei" +}; + +static const char *t_x0_z7[] = { + "rlca", "rrca", "rla", "rra", "daa", "cpl", "scf", "ccf" +}; + +static const char *t_x0_z2[] = { + "ld (bc), a", "ld a, (bc)", + "ld (de), a", "ld a, (de)", + "ld (%s), %s", "ld %s, (%s)", + "ld (%s), a", "ld a, (%s)", +}; + +static const char *ed_x1_z7[] = { + "ld i, a", "ld r, a", + "ld a, i", "ld a, r", + "rrd", "rld", + "nop*", "nop*" +}; + +static const char *tt_r(uint8_t **data, int i, int prefix) +{ + const char *iid[] = { "(ix%+hhd)", "(iy%+hhd)" }; + const char *ih[] = { "ixh", "iyh" }; + const char *il[] = { "ixl", "iyl" }; + + static char buf[128]; + if (prefix) { + if (i == 6) { + int8_t offset = **data; + *data += 1; + snprintf(buf, sizeof(buf), iid[prefix-1], offset); + return buf; + } else if (i == 4) { + return ih[prefix-1]; + } else if (i == 5) { + return il[prefix-1]; + } + } + + return t_r[i]; +} + +static const char *tt_rp(int i, int prefix) +{ + if (i == 2 && prefix) { + return prefix == 1 ? "ix" : "iy"; + } + + return t_rp[i]; +} + +static const char *tt_rp2(int i, int prefix) +{ + if (i == 2 && prefix) { + return prefix == 1 ? "ix" : "iy"; + } + + return t_rp2[i]; +} + +/* should potentially handle labels and shite + * NON REENTRANT */ +static char *get_paddr_str(uint8_t *p_addr) +{ + uint16_t a = p_addr[1] << 8 | p_addr[0]; + static char buf[128]; + snprintf(buf, sizeof(buf), "$%04x", a); + return buf; +} + +static char *get_addr_str(uint16_t addr) +{ + static char buf[128]; + snprintf(buf, sizeof(buf), "$%04x", addr); + return buf; +} + +static char *get_byte_str(uint8_t b) +{ + static char buf[128]; + snprintf(buf, sizeof(buf), "$%02x", b); + return buf; +} + +static int prefix_cb(uint8_t *data, char *buf, size_t buflen) +{ + uint8_t op = *data; + + int x = op >> 6; + int y = (op >> 3) & 7; + int z = op & 7; + + switch (x) + { + case 0: snprintf(buf, buflen, "%s %s", t_sro[y], t_r[z]); break; + case 1: snprintf(buf, buflen, "bit %d, %s", y, t_r[z]); break; + case 2: snprintf(buf, buflen, "res %d, %s", y, t_r[z]); break; + case 3: snprintf(buf, buflen, "set %d, %s", y, t_r[z]); break; + } + + return 1; +} + +static int prefix_ddfd_cb(uint8_t *data, char *buf, size_t buflen, int prefix) +{ + const char *iid = tt_r(&data, 6, prefix); + uint8_t op = *data; + + int x = op >> 6; + int y = (op >> 3) & 7; + int z = op & 7; + + static const char *r[] = { + ", b", ", c", ", d", ", e", ", h", ", l", "", ", a" + }; + + switch (x) + { + case 0: snprintf(buf, buflen, "%s %s%s", t_sro[y], iid, r[z]); break; + case 1: snprintf(buf, buflen, "bit %d, %s", y, iid); break; + case 2: snprintf(buf, buflen, "res %d, %s%s", y, iid, r[z]); break; + case 3: snprintf(buf, buflen, "set %d, %s%s", y, iid, r[z]); break; + } + + return 2; +} + +static int prefix_ed(uint8_t *data, char *buf, size_t buflen) +{ + uint8_t op = *data; + data++; + + int x = op >> 6; + int y = (op >> 3) & 7; + int z = op & 7; + int q = op & (1<<3); + int p = (op >> 4) & 3; + + const char *fmt = "UNHANDLED"; + const char *s1 = NULL; + const char *s2 = NULL; + + int len = 1; + + switch (x) + { + case 0: + case 3: fmt = "nop*"; break; + case 2: fmt = (z<4 && y>=4) ? t_blk[(y-4)*4 + z] : "nop*"; break; + case 1: + switch (z) + { + case 0: fmt = (y != 6) ? "in %s, (c)" : "in (c)"; s1 = t_r[z]; break; + case 1: fmt = (y != 6) ? "out (c), %s" : "out (c), 0"; s1 = t_r[z]; break; + case 2: fmt = q ? "adc hl, %s" : "sbc hl, %s"; s1 = t_rp[p]; break; + case 3: + if (q) { fmt = "ld %s, (%s)"; s1 = t_rp[p]; s2 = get_paddr_str(data); } + else { fmt = "ld (%s), %s"; s1 = get_paddr_str(data); s2 = t_rp[p]; } + len += 2; + break; + case 4: fmt = (y == 0) ? "neg" : "neg*"; break; + case 5: fmt = (y == 1) ? "reti" : "retn"; break; + case 6: fmt = "im %s"; s1 = t_im[y]; break; + case 7: fmt = ed_x1_z7[y]; break; + } + } + + snprintf(buf, buflen, fmt, s1, s2); + return len; +} + +static char *opcode(uint8_t *data, int *len, uint16_t pc, int prefix) +{ + uint8_t *dorg = data; + uint8_t op = *data; + data++; + + int x = op >> 6; + int y = (op >> 3) & 7; + int z = op & 7; + int q = op & (1<<3); + int p = (op >> 4) & 3; + + char buf[128] = "UNHANDLED"; + const size_t buflen = sizeof(buf); + + const char *fmt = "UNHANDLED"; + const char *s1 = NULL; + const char *s2 = NULL; + int noprint = 0; + + switch (x) + { + case 0: + switch (z) + { + case 0: + switch (y) + { + case 0: fmt = "nop"; break; + case 1: fmt = "ex af"; break; + case 2: fmt = "djnz %s"; s1 = get_addr_str(pc+2 + (int8_t)*data); data++; break; + case 3: fmt = "jr %s"; s1 = get_addr_str(pc+2 + (int8_t)*data); data++; break; + default: + fmt = "jr %s, %s"; + s1 = t_cc[y-4]; s2 = get_addr_str(pc+2 + (int8_t)*data); + data++; + break; + } + break; + case 1: + if (q) { fmt = "add %s, %s"; s1 = tt_rp(2, prefix); s2 = tt_rp(p, prefix); } + else { fmt = "ld %s, %s", s1 = tt_rp(p, prefix); s2 = get_paddr_str(data); data += 2; } + break; + case 2: + fmt = t_x0_z2[y]; + if (y >= 4) { + if (y == 5) { + s1 = tt_rp(2, prefix); + s2 = get_paddr_str(data); + } else { + s1 = get_paddr_str(data); + s2 = tt_rp(2, prefix); + } + data += 2; + } + break; + case 3: fmt = q ? "dec %s" : "inc %s"; s1 = tt_rp(p, prefix);break; + case 4: fmt = "inc %s"; s1 = tt_r(&data, y, prefix); break; + case 5: fmt = "dec %s"; s1 = tt_r(&data, y, prefix); break; + case 6: fmt = "ld %s, %s", s1 = tt_r(&data, y, prefix); s2 = get_byte_str(*data); data++; break; + case 7: fmt = t_x0_z7[y]; break; + } + break; + case 1: + if (op == 0x76) { + fmt = "halt"; + } else { + fmt = "ld %s, %s"; + if (y == 6) { + s1 = tt_r(&data, y, prefix); + s2 = t_r[z]; + } else if (z == 6) { + s1 = t_r[y]; + s2 = tt_r(&data, z, prefix); + } else { + s1 = tt_r(&data, y, prefix); + s2 = tt_r(&data, z, prefix); + } + } + break; + case 2: fmt = "%s %s"; s1 = t_alu[y]; s2 = tt_r(&data, z, prefix); break; + case 3: + switch (z) + { + case 0: fmt = "ret %s"; s1 = t_cc[y]; break; + case 1: + if (q) { fmt = t_x1_z1_q[p]; s1 = tt_rp(2, prefix); } + else { fmt = "pop %s"; s1 = tt_rp2(p, prefix); } + break; + case 2: fmt = "jp %s, %s"; s1 = t_cc[y]; s2 = get_paddr_str(data); data += 2; break; + case 3: + switch (y) + { + case 0: fmt = "jp %s"; s1 = get_paddr_str(data); data += 2; break; + case 1: + if (prefix) { + data += prefix_ddfd_cb(data, buf, buflen,prefix); + } else { + data += prefix_cb(data, buf, buflen); + } + noprint = 1; + break; + case 2: fmt = "out (%s), a"; s1 = get_byte_str(*data); data++; break; + case 3: fmt = "in a, (%s)"; s1 = get_byte_str(*data); data++; break; + default: + fmt = t_x3_z3[y-4]; + } + break; + case 4: fmt = "call %s, %s"; s1 = t_cc[y]; s2 = get_paddr_str(data); data += 2; break; + case 5: + if (q) { + switch (p) + { + case 0: fmt = "call %s"; s1 = get_byte_str(*data); data += 2; break; + case 1: // dd + if (prefix) { + fmt = "nop*"; + } else { + return opcode(data, len, pc, 1); + } + break; + case 2: + if (prefix) { + fmt = "nop*"; + data--; + } else { + data += prefix_ed(data, buf, buflen); noprint = 1; + } + break; + case 3: // fd + if (prefix) { + fmt = "nop*"; + } else { + return opcode(data, len, pc, 2); + } + break; + } + } else { + fmt = "push %s"; s1 = tt_rp2(p, prefix); + } + break; + case 6: fmt = "%s %s"; s1 = t_alu[y]; s2 = get_byte_str(*data); data++; break; + case 7: fmt = "rst %s"; s1 = get_byte_str(y*8); break; + } + } + + if (!noprint) snprintf(buf, buflen, fmt, s1, s2); + + *len = data - dorg + !(!prefix); + + size_t slen = strlen(buf) + 1; + char *str = malloc(slen); + if (str == NULL) { + return NULL; + } + memcpy(str, buf, slen-1); + str[slen-1] = 0; + + return str; +} + +char *disasm_opcode(uint8_t *data, int *len, uint16_t pc) +{ + return opcode(data, len, pc, 0); +} diff --git a/src/disasm.h b/src/disasm.h new file mode 100644 index 0000000..a8b260a --- /dev/null +++ b/src/disasm.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +/* returns a disassembled instruction string or NULL on error. + * len gets set to instruction length in bytes. + * user needs to free the string after use */ +char *disasm_opcode(uint8_t *data, int *len, uint16_t pc); diff --git a/src/input_sdl.c b/src/input_sdl.c index b396cad..f4bd6e8 100644 --- a/src/input_sdl.c +++ b/src/input_sdl.c @@ -32,7 +32,11 @@ int input_sdl_update() int quit = 0; SDL_Event e; - while (SDL_PollEvent(&e)) { + SDL_Event events[1024]; + int count = SDL_PeepEvents(events, 1024, SDL_PEEKEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT); + + for (int i = 0; i < count; i++) { + e = events[i]; switch (e.type) { case SDL_QUIT: @@ -48,6 +52,16 @@ int input_sdl_update() return quit; } +void input_sdl_pump_events() +{ + SDL_PumpEvents(); +} + +void input_sdl_flush_events() +{ + SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT); +} + void input_sdl_copy_old_state() { if (keyboard_state_old != NULL) { diff --git a/src/input_sdl.h b/src/input_sdl.h index baca715..ff1e3fb 100644 --- a/src/input_sdl.h +++ b/src/input_sdl.h @@ -5,6 +5,8 @@ void input_sdl_init(); void input_sdl_deinit(); int input_sdl_update(); +void input_sdl_pump_events(); +void input_sdl_flush_events(); void input_sdl_copy_old_state(); uint8_t input_sdl_get_key(uint16_t scancode); uint8_t input_sdl_get_key_pressed(uint16_t scancode); diff --git a/src/machine.c b/src/machine.c index c82e0ab..9dc473d 100644 --- a/src/machine.c +++ b/src/machine.c @@ -11,6 +11,7 @@ #include "audio_sdl.h" #include "dsp.h" #include "hotkeys.h" +#include "debugger.h" static Machine_t *m_cur = NULL; static bool file_open; @@ -198,6 +199,7 @@ int machine_do_cycles() machine_process_hooks(m_cur); machine_test_iterate(m_cur); + debugger_handle(); cpu_do_cycles(&m_cur->cpu); @@ -218,9 +220,15 @@ int machine_do_cycles() keyboard_macro_process(); input_sdl_copy_old_state(); + input_sdl_pump_events(); + + debugger_update_window(); + int quit = input_sdl_update(); if (quit) return -2; + input_sdl_flush_events(); + hotkeys_process(); machine_process_events(); return 0; diff --git a/src/machine_hooks.c b/src/machine_hooks.c index 838ff57..6f59796 100644 --- a/src/machine_hooks.c +++ b/src/machine_hooks.c @@ -17,6 +17,8 @@ static FILE *out_stream = NULL; static void putc_zx(uint8_t ch, FILE *f) { + if (!f) return; + char *s = NULL; switch (ch) { diff --git a/src/machine_test.c b/src/machine_test.c index 065ee05..0aafeed 100644 --- a/src/machine_test.c +++ b/src/machine_test.c @@ -376,7 +376,7 @@ static void finish_print() remove(tmp); } -static void test_finish(struct Machine *m) { +static void test_finish() { if (test.docflags) { finish_hash(test.docflags, "docflags"); } @@ -437,8 +437,10 @@ int machine_test_iterate(struct Machine *m) } if (test_condition(m)) { - test_finish(m); + test_finish(); } + + return 0; } void machine_test_close() diff --git a/src/main.c b/src/main.c index f58fa48..37092c4 100644 --- a/src/main.c +++ b/src/main.c @@ -16,6 +16,8 @@ #include "sleepdart_info.h" #include "config.h" +#include "debugger.h" + int main(int argc, char *argv[]) { char *errsilent = getenv("SLEEPDART_ERRSILENT"); @@ -27,6 +29,7 @@ int main(int argc, char *argv[]) argparser_add_arg(parser, "file", 0, 0, true, "tape or snapshot file to be loaded"); argparser_add_arg(parser, "--scale", 's', ARG_INT, 0, "integer window scale"); argparser_add_arg(parser, "--fullscreen", 'f', ARG_STORE_TRUE, 0, "run in fullscreen mode"); + argparser_add_arg(parser, "--debugger", 'd', ARG_STORE_TRUE, 0, "open the debugger"); argparser_add_arg(parser, "--test", 0, ARG_STRING, 0, "perform an automated regression test"); dlog(LOG_INFO, @@ -108,6 +111,10 @@ int main(int argc, char *argv[]) machine_process_events(); + if (argparser_get(parser, "debugger")) { + debugger_open(&m); + } + for (;;) { int err = machine_do_cycles(); if (err) break; @@ -117,8 +124,7 @@ int main(int argc, char *argv[]) config_set_int(&g_config, "window-scale", video_sdl_get_scale()); config_set_int(&g_config, "limit-fps", video_sdl_get_fps_limit()); - char **palette_list = palette_list_get(); - config_set_str(&g_config, "palette", palette_list[palette_get_index()]); + config_set_str(&g_config, "palette", palette_get_name()); config_save(); diff --git a/src/memory.c b/src/memory.c index 2991b02..15040bc 100644 --- a/src/memory.c +++ b/src/memory.c @@ -6,6 +6,7 @@ #include "log.h" #include "ula.h" #include "machine.h" +#include "debugger.h" /* Initializes the DRAM to a pseudo-random state it would have on initial power-on. */ void memory_init(Memory_t *mem) @@ -59,6 +60,8 @@ uint8_t memory_write(struct Machine *ctx, uint16_t addr, uint8_t value) ctx->memory.bus[addr] = value; } + debugger_mark_dirty(addr); + return 0; } diff --git a/src/microui_render.c b/src/microui_render.c new file mode 100644 index 0000000..bdd7c04 --- /dev/null +++ b/src/microui_render.c @@ -0,0 +1,320 @@ +#include "microui_render.h" + +#include "microui.h" +#include "file.h" +#include "log.h" +#include +#include +#include +#include FT_FREETYPE_H + +static FT_Library ft; +static FT_Face face; +static SDL_Window *window = NULL; +static SDL_Renderer *renderer = NULL; +static int width = 600; +static int height = 600; + +struct Glyph +{ + SDL_Texture *tex; + int8_t adv_x; + int8_t adv_y; + int8_t offset_x; + int8_t offset_y; + int8_t w; + int8_t h; +}; + +static struct Glyph cache[256] = { 0 }; +static int glyph_height; + +static SDL_Texture *icons_atlas; + +struct Icon +{ + SDL_Rect src; + mu_Color color; +}; + +static struct Icon icons[16]; + +static int cache_glyph(FT_Face f, char c) +{ + struct Glyph cached = { 0 }; + FT_GlyphSlot slot = f->glyph; + uint8_t cache_i = c; + + uint32_t idx = FT_Get_Char_Index(f, c); + int err = FT_Load_Glyph(f, idx, FT_LOAD_NO_BITMAP); + if (err) { + goto notex; + } + + err = FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL); + if (err) { + goto notex; + } + + FT_Bitmap *bitmap = &slot->bitmap; + SDL_Texture *tex = SDL_CreateTexture( + renderer, + SDL_PIXELFORMAT_ARGB32, + SDL_TEXTUREACCESS_STREAMING, bitmap->width, bitmap->rows); + + void *buf; + int pitch; + + err = SDL_LockTexture(tex, NULL, &buf, &pitch); + if (err) { + goto notex; + } + + uint32_t *px = buf; + int size = bitmap->pitch*bitmap->rows; + + for (int i = 0; i < size; i++) { + px[i] = 0xFFFFFF00 | bitmap->buffer[i]; + } + + SDL_UnlockTexture(tex); + + SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); + + cached.tex = tex; + cached.w = bitmap->width; + cached.h = bitmap->rows; +notex: + cached.offset_x = slot->bitmap_left; + cached.offset_y = 0 - slot->bitmap_top; + cached.adv_x = slot->advance.x / 64; + cached.adv_y = slot->advance.y / 64; + + cache[cache_i] = cached; + + return 0; +} + +SDL_Texture *load_rgba32_zlib_texture(const char *path, int width, int height) +{ + int64_t fsize = file_get_size(path); + if (fsize <= 0) return NULL; + + uint8_t src[fsize]; + FILE *f = fopen(path, "rb"); + fread(src, 1, fsize, f); + fclose(f); + + SDL_Texture *tex = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, + width, height); + + if (!tex) return NULL; + + void *buf; + int pitch; + + SDL_LockTexture(tex, NULL, &buf, &pitch); + uLongf len = pitch * height; + + uncompress(buf, &len, src, fsize); + SDL_UnlockTexture(tex); + SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); + + return tex; +} + +int render_text_width(mu_Font font, const char *text, int len) +{ + if (!text) return 0; + + int x = 0; + + while (*text && len) { + uint8_t i = *text; + x += cache[i].adv_x; + + text++; + len--; + } + + return x; +} + +int render_text_height(mu_Font font) +{ + return glyph_height; +} + +void render_text(mu_Font font, const char *text, mu_Vec2 pos, mu_Color color) +{ + if (!text) return; + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + pos.y += face->size->metrics.ascender / 64; + + while (*text) { + struct Glyph *g = &cache[(uint8_t)*text]; + SDL_Rect r = { + .x = pos.x + g->offset_x, + .y = pos.y + g->offset_y, + .w = g->w, + .h = g->h, + }; + + SDL_SetTextureColorMod(g->tex, color.r, color.g, color.b); + SDL_SetTextureAlphaMod(g->tex, color.a); + SDL_RenderCopy(renderer, g->tex, NULL, &r); + + pos.x += g->adv_x; + pos.y += g->adv_y; + + text++; + } +} + +int render_init() +{ + if (renderer) return -1; + + int err = FT_Init_FreeType(&ft); + if (err) { + dlog(LOG_ERR, "%s: failed to initialize FreeType", __func__); + return -4; + } + + char buf[2048]; + file_path_append(buf, file_get_basedir(), "assets/font.ttf", sizeof(buf)); + err = FT_New_Face(ft, buf, 0, &face); + if (err) { + dlog(LOG_ERR, "%s: failed to load face \"%s\"", __func__, buf); + return -5; + } + + err = FT_Set_Char_Size(face, 0, 12*64, 72, 72); + if (err) { + dlog(LOG_ERR, "%s: failed to set character size", __func__); + return -6; + } + + window = SDL_CreateWindow( + "sleepdart dbg", + SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + width, height, 0); + + if (!window) { + dlog(LOG_ERR, "%s: failed to create window", __func__); + return -2; + } + + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); + + renderer = SDL_CreateRenderer(window, -1, 0); + if (!renderer) { + SDL_DestroyWindow(window); + dlog(LOG_ERR, "%s: failed to create renderer", __func__, buf); + return -3; + } + + for (int i = 0; i < 256; i++) { + cache_glyph(face, i); + } + + glyph_height = face->size->metrics.height / 64; + + file_path_append(buf, file_get_basedir(), "assets/icons.raw.zlib", sizeof(buf)); + icons_atlas = load_rgba32_zlib_texture(buf, 1024, 64); + if (!icons_atlas) { + dlog(LOG_WARN, "%s: failed to load \"%s\"", __func__, buf); + } + + for (int i = 1; i < 1024/64; i++) { + icons[i].src = (SDL_Rect) { (i-1)*64, 0, 64, 64 }; + icons[i].color = mu_color(0, 0, 0, 0); + } + + icons[DBGICON_CURRENT].color = (mu_Color) { 255, 192, 50, 255 }; + icons[DBGICON_BREAKPOINT].color = (mu_Color) { 238, 49, 116, 255 }; + + return 0; +} + +void render_deinit() +{ + if (renderer) { + SDL_DestroyRenderer(renderer); + } + if (window) { + SDL_DestroyWindow(window); + } + + return; +} + +void render_draw_rect(mu_Rect rect, mu_Color c) +{ + SDL_Rect r = { + .x = rect.x, + .y = rect.y, + .w = rect.w, + .h = rect.h, + }; + + SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); + SDL_RenderFillRect(renderer, &r); +} + +void render_draw_icon(int id, mu_Rect rect, mu_Color c) +{ + SDL_Rect r = { + .x = rect.x, + .y = rect.y, + .w = rect.w, + .h = rect.h, + }; + + SDL_Rect *src = &icons[id].src; + mu_Color colors[2] = { + c, + icons[id].color, + }; + + c = colors[!(!colors[1].a)]; + + if (!icons_atlas) { + render_draw_rect(rect, c); + return; + } + + SDL_SetTextureColorMod(icons_atlas, c.r, c.g, c.b); + SDL_SetTextureAlphaMod(icons_atlas, c.a); + SDL_RenderCopy(renderer, icons_atlas, src, &r); +} + +void render_clip_rect(mu_Rect rect) +{ + SDL_Rect r = { + .x = rect.x, + .y = rect.y, + .w = rect.w, + .h = rect.h, + }; + + SDL_RenderSetClipRect(renderer, &r); +} + +void render_clear(mu_Color c) +{ + SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); + SDL_RenderClear(renderer); +} + +void render_present() +{ + SDL_RenderPresent(renderer); +} + +uint32_t render_get_window_id() +{ + return SDL_GetWindowID(window); +} diff --git a/src/microui_render.h b/src/microui_render.h new file mode 100644 index 0000000..bc4bc48 --- /dev/null +++ b/src/microui_render.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "microui.h" + +enum DebugIcon +{ + DBGICON_CURRENT = 6, + DBGICON_BREAKPOINT, +}; + +int render_text_width(mu_Font font, const char *text, int len); +int render_text_height(mu_Font font); +void render_text(mu_Font font, const char *text, mu_Vec2 pos, mu_Color color); +int render_init(); +void render_deinit(); +void render_draw_rect(mu_Rect rect, mu_Color c); +void render_draw_icon(int id, mu_Rect rect, mu_Color c); +void render_clip_rect(mu_Rect rect); +void render_clear(mu_Color c); +void render_present(); +uint32_t render_get_window_id(); diff --git a/src/palette.c b/src/palette.c index 6f7b7e6..3fd841d 100644 --- a/src/palette.c +++ b/src/palette.c @@ -148,6 +148,10 @@ void palette_set_by_index(size_t index) void palette_set_by_name(const char *name) { + if (palette_list == NULL) { + return; + } + for (size_t i = 0; palette_list[i] != NULL; i++) { int result = strcmp(name, palette_list[i]); if (result == 0) { @@ -171,6 +175,15 @@ size_t palette_get_index() return palette_current; } +const char *palette_get_name() +{ + if (palette_list == NULL) { + return NULL; + } + + return palette_list[palette_current]; +} + bool palette_has_changed() { if (palette_changed) { diff --git a/src/palette.h b/src/palette.h index d4a1757..cc1ef86 100644 --- a/src/palette.h +++ b/src/palette.h @@ -27,4 +27,5 @@ void palette_set_by_index(size_t index); void palette_set_by_name(const char *name); void palette_set_default(); size_t palette_get_index(); +const char *palette_get_name(); bool palette_has_changed();