/******************************************************************************
*
* FILENAME: debugger.c
*
* DESCRIPTION: Debug interface implementation using Nuklear moveable windows
*
******************************************************************************/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#include "debugger.h"
#include "gui.h"
#include "theme.h"
#include "widget.h"
#include "../kat5200.h"
#include "../core/console.h"
#include "../core/boom6502.h"
#include "../core/antic.h"
#include "../core/gtia.h"
#include "../core/pokey.h"
#include "../core/debug.h"
#include "../core/memory.h"
#include "guitop.h"
#include "filebrowser.h"
#include "../interface/katconfig.h"
#include "../interface/sdl_if.h"

extern struct theme g_theme;
extern t_sdl_if g_pc_if;
extern t_cpu cpu6502;
extern t_debug debug;

/* Edit buffer structure for hex fields */
struct hex_edit {
    char buf[5];  /* Max 4 hex chars + null */
    int width;    /* Number of hex digits (2 or 4) */
};

/* Debug state with all edit buffers */
static struct {
    int active;
    int paused;
    int in_halt;
    int trace_enabled;
    char trace_file[257];
    int trace_recording;
    int mem_page;
    int mem_page_editing;

    struct nk_rect cpu_bounds;
    struct nk_rect antic_bounds;
    struct nk_rect gtia_bounds;
    struct nk_rect pokey_bounds;
    struct nk_rect control_bounds;
    struct nk_rect video_bounds;
    struct nk_rect disasm_bounds;

    /* Last saved positions and sizes for change detection */
    int cpu_saved_x, cpu_saved_y, cpu_saved_w, cpu_saved_h;
    int antic_saved_x, antic_saved_y, antic_saved_w, antic_saved_h;
    int gtia_saved_x, gtia_saved_y, gtia_saved_w, gtia_saved_h;
    int pokey_saved_x, pokey_saved_y, pokey_saved_w, pokey_saved_h;
    int control_saved_x, control_saved_y, control_saved_w, control_saved_h;
    int video_saved_x, video_saved_y, video_saved_w, video_saved_h;
    int disasm_saved_x, disasm_saved_y, disasm_saved_w, disasm_saved_h;

    /* CPU register edit buffers */
    char pc_buf[5];
    char sp_buf[3];
    char a_buf[3];
    char x_buf[3];
    char y_buf[3];
    char mem_page_buf[3];

    /* Memory grid edit buffers - 16x16 = 256 cells */
    char mem_buf[256][3];

    /* Disassembly window state */
    KWORD disasm_addr;
    KWORD cached_pc;
    char disasm_addr_buf[5];

    /* Disassembly snapshot — built only while paused so the window never
       reads live memory during a run (live reads flicker and, on bank-switch-
       on-read carts like 32K BE, would swap the running game's bank). */
    char disasm_c_addr[256][8];
    char disasm_c_bytes[256][10];
    char disasm_c_mnem[256][8];
    char disasm_c_oper[256][16];
    char disasm_c_cyc[256][4];
    unsigned char disasm_c_is_pc[256];
    int disasm_cache_count;

    /* ANTIC register edit buffers */
    char antic_dmactl[3];
    char antic_chactl[3];
    char antic_dlistl[3];
    char antic_dlisth[3];
    char antic_hscrol[3];
    char antic_vscrol[3];
    char antic_pmbase[3];
    char antic_chbase[3];
    char antic_nmien[3];

    /* GTIA register edit buffers */
    char gtia_hposp[4][3];
    char gtia_hposm[4][3];
    char gtia_sizep[4][3];
    char gtia_sizem[3];
    char gtia_grafp[4][3];
    char gtia_grafm[3];
    char gtia_colpm[4][3];
    char gtia_colpf[4][3];
    char gtia_colbk[3];
    char gtia_prior[3];
    char gtia_gractl[3];
    char gtia_consol[3];

    /* POKEY register edit buffers */
    char pokey_audf[4][3];
    char pokey_audc[4][3];
    char pokey_audctl[3];
    char pokey_skctl[3];
    char pokey_irqen[3];
    char pokey_pot[8][3];
    char pokey_stimer[3];
    char pokey_serout[3];

    /* Breakpoint UI state */
    int bp_type;          /* 0 = Break, 1 = Watch */
    int bp_subtype;       /* 0 = Memory, 1 = Scanline */
    char bp_value_buf[6]; /* Hex (4 chars) or decimal (3 chars) + null */
    int bp_selected;      /* Selected breakpoint in list (-1 = none) */
    char control_scanline_buf[8];
    char control_cycle_buf[12];

    /* Video window texture */
    SDL_Texture *video_texture;   /* debug display texture, ARGB8888 */
    int video_tex_w;              /* current texture width (for change detection) */
    int video_tex_h;              /* current texture height */

    /* Dirty flag: 1 = display buffers need refresh from emulator state */
    int dirty;

    /* CPU cached status flags (was a static local in draw_cpu_window) */
    int cached_status;

    /* ANTIC read-only display buffers */
    char antic_vcount_buf[4];
    char antic_nmist_buf[4];

    /* NMIEN cached bit flags for checkboxes */
    int cached_nmien_dli;
    int cached_nmien_vbi;
    int cached_nmien_rst;

    /* GTIA read-only collision/trigger buffers */
    char gtia_right_bufs[20][4];

    /* POKEY read-only display buffers */
    char pokey_allpot_buf[4];
    char pokey_kbcode_buf[4];
    char pokey_random_buf[4];
    char pokey_serin_buf[4];
    char pokey_irqst_buf[4];
    char pokey_skstat_buf[4];

    /* Display list cache */
    char dlist_addr_str[200][9];   /* "$XXXX:" */
    char dlist_bytes_str[200][10]; /* "XX" or "XX XX XX" */
    char dlist_mode_str[200][24];  /* "BLK1", "M2 D", "JMP $4000", etc. */
    KWORD dlist_entry_addr[200];   /* raw start address per decoded entry */
    int  dlist_line_count;
    int  dlist_hl_row;             /* row to highlight as the active DL instruction (-1 = none) */
    KWORD cached_dlist_addr;
    KWORD cached_antic_pc;         /* antic.pc snapshot (next instruction to fetch) */

    /* 1 = video texture needs SDL_UpdateTexture upload */
    int video_dirty;
    int video_has_focus;

    /* Deferred reset state for resets requested from inside breakpoint halt */
    int reset_pending;
    int reset_restore_breakpoint_flag;
    int draw_breakpoint_flag;

} dbg_state = {0};

/* Default window positions and sizes */
#define CPU_WIN_X      10
#define CPU_WIN_Y      10
#define CPU_WIN_W      380
#define CPU_WIN_H      480

#define ANTIC_WIN_X    400
#define ANTIC_WIN_Y    10
#define ANTIC_WIN_W    500
#define ANTIC_WIN_H    620

#define GTIA_WIN_X     630
#define GTIA_WIN_Y     10
#define GTIA_WIN_W     280
#define GTIA_WIN_H     700

#define POKEY_WIN_X    940
#define POKEY_WIN_Y    10
#define POKEY_WIN_W    280
#define POKEY_WIN_H    400

#define CTRL_WIN_X     400
#define CTRL_WIN_Y     340
#define CTRL_WIN_W     260
#define CTRL_WIN_H     380

#define VIDEO_WIN_X    630
#define VIDEO_WIN_Y    540
#define VIDEO_WIN_W    340
#define VIDEO_WIN_H    260

#define DISASM_WIN_X   10
#define DISASM_WIN_Y   500
#define DISASM_WIN_W   390
#define DISASM_WIN_H   540

/* Helper: hex character filter for text input */
static nk_bool hex_filter(const struct nk_text_edit *box, nk_rune unicode)
{
    NK_UNUSED(box);
    return (unicode >= '0' && unicode <= '9') ||
           (unicode >= 'a' && unicode <= 'f') ||
           (unicode >= 'A' && unicode <= 'F');
}

/* Helper: decimal character filter for text input */
static nk_bool decimal_filter(const struct nk_text_edit *box, nk_rune unicode)
{
    NK_UNUSED(box);
    return (unicode >= '0' && unicode <= '9');
}

/* Helper: convert hex string to unsigned int */
static unsigned int hex_to_uint(const char *str)
{
    unsigned int val = 0;
    while (*str) {
        char c = *str++;
        if (c >= '0' && c <= '9') val = (val << 4) | (c - '0');
        else if (c >= 'a' && c <= 'f') val = (val << 4) | (c - 'a' + 10);
        else if (c >= 'A' && c <= 'F') val = (val << 4) | (c - 'A' + 10);
    }
    return val;
}

/* Helper: format unsigned int as hex string */
static void uint_to_hex(unsigned int val, char *buf, int width)
{
    static const char hex_chars[] = "0123456789ABCDEF";
    int i;
    for (i = width - 1; i >= 0; i--) {
        buf[i] = hex_chars[val & 0xF];
        val >>= 4;
    }
    buf[width] = '\0';
}

/* Helper: check if window position/size changed and save if so */
static void check_and_save_window_pos(const char *key, struct nk_rect *bounds,
                                       int *saved_x, int *saved_y,
                                       int *saved_w, int *saved_h)
{
    int new_x = (int)bounds->x;
    int new_y = (int)bounds->y;
    int new_w = (int)bounds->w;
    int new_h = (int)bounds->h;

    if (new_x != *saved_x || new_y != *saved_y ||
        new_w != *saved_w || new_h != *saved_h) {
        *saved_x = new_x;
        *saved_y = new_y;
        *saved_w = new_w;
        *saved_h = new_h;
        config_save_debugger_window_pos(key, new_x, new_y, new_w, new_h);
    }
}

void debugger_init(void)
{
    int x, y, w, h;

    memset(&dbg_state, 0, sizeof(dbg_state));

    /* Set default window bounds */
    dbg_state.cpu_bounds = nk_rect(CPU_WIN_X, CPU_WIN_Y, CPU_WIN_W, CPU_WIN_H);
    dbg_state.antic_bounds = nk_rect(ANTIC_WIN_X, ANTIC_WIN_Y, ANTIC_WIN_W, ANTIC_WIN_H);
    dbg_state.gtia_bounds = nk_rect(GTIA_WIN_X, GTIA_WIN_Y, GTIA_WIN_W, GTIA_WIN_H);
    dbg_state.pokey_bounds = nk_rect(POKEY_WIN_X, POKEY_WIN_Y, POKEY_WIN_W, POKEY_WIN_H);
    dbg_state.control_bounds = nk_rect(CTRL_WIN_X, CTRL_WIN_Y, CTRL_WIN_W, CTRL_WIN_H);
    dbg_state.video_bounds = nk_rect(VIDEO_WIN_X, VIDEO_WIN_Y, VIDEO_WIN_W, VIDEO_WIN_H);

    /* Load saved positions and sizes from config (if they exist) */
    x = (int)dbg_state.cpu_bounds.x;
    y = (int)dbg_state.cpu_bounds.y;
    w = (int)dbg_state.cpu_bounds.w;
    h = (int)dbg_state.cpu_bounds.h;
    config_load_debugger_window_pos("cpu", &x, &y, &w, &h);
    dbg_state.cpu_bounds.x = x;
    dbg_state.cpu_bounds.y = y;
    dbg_state.cpu_bounds.w = w;
    dbg_state.cpu_bounds.h = h;
    dbg_state.cpu_saved_x = x;
    dbg_state.cpu_saved_y = y;
    dbg_state.cpu_saved_w = w;
    dbg_state.cpu_saved_h = h;

    x = (int)dbg_state.antic_bounds.x;
    y = (int)dbg_state.antic_bounds.y;
    w = (int)dbg_state.antic_bounds.w;
    h = (int)dbg_state.antic_bounds.h;
    config_load_debugger_window_pos("antic", &x, &y, &w, &h);
    dbg_state.antic_bounds.x = x;
    dbg_state.antic_bounds.y = y;
    dbg_state.antic_bounds.w = w;
    dbg_state.antic_bounds.h = h;
    dbg_state.antic_saved_x = x;
    dbg_state.antic_saved_y = y;
    dbg_state.antic_saved_w = w;
    dbg_state.antic_saved_h = h;

    x = (int)dbg_state.gtia_bounds.x;
    y = (int)dbg_state.gtia_bounds.y;
    w = (int)dbg_state.gtia_bounds.w;
    h = (int)dbg_state.gtia_bounds.h;
    config_load_debugger_window_pos("gtia", &x, &y, &w, &h);
    dbg_state.gtia_bounds.x = x;
    dbg_state.gtia_bounds.y = y;
    dbg_state.gtia_bounds.w = w;
    dbg_state.gtia_bounds.h = h;
    dbg_state.gtia_saved_x = x;
    dbg_state.gtia_saved_y = y;
    dbg_state.gtia_saved_w = w;
    dbg_state.gtia_saved_h = h;

    x = (int)dbg_state.pokey_bounds.x;
    y = (int)dbg_state.pokey_bounds.y;
    w = (int)dbg_state.pokey_bounds.w;
    h = (int)dbg_state.pokey_bounds.h;
    config_load_debugger_window_pos("pokey", &x, &y, &w, &h);
    dbg_state.pokey_bounds.x = x;
    dbg_state.pokey_bounds.y = y;
    dbg_state.pokey_bounds.w = w;
    dbg_state.pokey_bounds.h = h;
    dbg_state.pokey_saved_x = x;
    dbg_state.pokey_saved_y = y;
    dbg_state.pokey_saved_w = w;
    dbg_state.pokey_saved_h = h;

    x = (int)dbg_state.control_bounds.x;
    y = (int)dbg_state.control_bounds.y;
    w = (int)dbg_state.control_bounds.w;
    h = (int)dbg_state.control_bounds.h;
    config_load_debugger_window_pos("control", &x, &y, &w, &h);
    dbg_state.control_bounds.x = x;
    dbg_state.control_bounds.y = y;
    dbg_state.control_bounds.w = w;
    dbg_state.control_bounds.h = h;
    dbg_state.control_saved_x = x;
    dbg_state.control_saved_y = y;
    dbg_state.control_saved_w = w;
    dbg_state.control_saved_h = h;

    x = (int)dbg_state.video_bounds.x;
    y = (int)dbg_state.video_bounds.y;
    w = (int)dbg_state.video_bounds.w;
    h = (int)dbg_state.video_bounds.h;
    config_load_debugger_window_pos("video", &x, &y, &w, &h);
    dbg_state.video_bounds.x = x;
    dbg_state.video_bounds.y = y;
    dbg_state.video_bounds.w = w;
    dbg_state.video_bounds.h = h;
    dbg_state.video_saved_x = x;
    dbg_state.video_saved_y = y;
    dbg_state.video_saved_w = w;
    dbg_state.video_saved_h = h;

    dbg_state.disasm_bounds = nk_rect(DISASM_WIN_X, DISASM_WIN_Y, DISASM_WIN_W, DISASM_WIN_H);
    x = (int)dbg_state.disasm_bounds.x;
    y = (int)dbg_state.disasm_bounds.y;
    w = (int)dbg_state.disasm_bounds.w;
    h = (int)dbg_state.disasm_bounds.h;
    config_load_debugger_window_pos("disasm", &x, &y, &w, &h);
    dbg_state.disasm_bounds.x = x;
    dbg_state.disasm_bounds.y = y;
    dbg_state.disasm_bounds.w = w;
    dbg_state.disasm_bounds.h = h;
    dbg_state.disasm_saved_x = x;
    dbg_state.disasm_saved_y = y;
    dbg_state.disasm_saved_w = w;
    dbg_state.disasm_saved_h = h;

    dbg_state.mem_page = 0;
    dbg_state.paused = 1;
    dbg_state.dirty = 1;
    dbg_state.bp_selected = -1;
    dbg_state.bp_type = 0;
    dbg_state.bp_subtype = 0;
    dbg_state.bp_value_buf[0] = '\0';

    config_load_debugger_breakpoints();
}

void debugger_show(void)
{
    int i;

    dbg_state.active = 1;
    debug_set_breakpoint_handler(debugger_breakpoint_halt);

    /* Re-arm memory/watch dispatch for breakpoints loaded by debugger_init(). */
    if (!debug.breakpoint_flag)
        return;

    for (i = 0; i < DEBUG_MAX_BK; ++i) {
        if (debug.breakpoint[i] == BK_MEM || debug.breakpoint[i] == BK_WATCH) {
            t_atari *console = console_get_ptr();
            if (console)
                mem_assign_ops_debug(console->cpu, &debug);
            break;
        }
    }
}

static void debugger_clear_crash_status(void)
{
    debug.cpu_crash_flag = 0;
    debug.cpu_crash_pc = 0;
    debug.cpu_crash_opcode = 0;
}

void debugger_hide(void)
{
    t_atari *console = console_get_ptr();

    if (dbg_state.trace_recording) {
        debug_close_trace();
        dbg_state.trace_recording = 0;
    }

    /*
     * Breakpoints are persisted in config and reloaded by debugger_init(), but
     * they should not stay live after the GUI debugger is closed.  Otherwise
     * scanline breakpoints continue to fire in normal emulation, printing to
     * the console while debugger_breakpoint_halt() immediately returns because
     * the debugger is no longer active.
     */
    debug.single_step_flag = 0;
    debug.single_scanline_flag = 0;
    debug.single_frame_flag = 0;
    debugger_clear_crash_status();
    dbg_state.reset_pending = 0;
    dbg_state.reset_restore_breakpoint_flag = 0;
    dbg_state.video_has_focus = 0;
    debug_set_breakpoint_flag(0);
    debug_set_breakpoint_handler(NULL);
    if (console)
        mem_assign_ops(console->cpu);

    dbg_state.active = 0;
    dbg_state.paused = 1;
    console_set_debug_mode(0);
    gui_set_run_current(0);
}

int debugger_is_active(void)
{
    return dbg_state.active;
}

int debugger_wants_input(void)
{
    return dbg_state.active && !dbg_state.paused && dbg_state.video_has_focus;
}

void debugger_activate(struct option_widget *wgt, void *data)
{
    (void)data;
    (void)wgt;

    /* Defensive only: the Misc row is hidden when unavailable. */
    if (!pc_debugger_available())
        return;

    /* Check if the system is initialized by checking if memory read functions exist */
    if (cpu6502.read_mem[0] == NULL) {
        t_config *cfg = config_get_global_cfg();
        console_reset(cfg->general.machine, cfg->machine.tv_standard,
                cfg->machine.ram);
    }

    debugger_init();
    debugger_show();
}

static void debugger_clear_execution_flags(void)
{
    debug.single_step_flag = 0;
    debug.single_scanline_flag = 0;
    debug.single_frame_flag = 0;
}

static void debugger_perform_reset(void)
{
    t_atari *console = console_get_ptr();

    debugger_clear_execution_flags();
    debugger_clear_crash_status();

    /* Full power-cycle: reopen the ROM so bankswitched carts get their default
     * bank reloaded into memory[] (and the bank latches reset) before reset.
     * A bare console_reset() would leave the last-switched bank resident.  Fall
     * back to console_reset() only if the reload fails (e.g. ROM now missing). */
    if (kat5200_launch(KSTATUS_LAUNCH) != 0)
        console_reset(console->machine_type, console->tv_standard, console->ram_size);

    /* The reload restores normal memory dispatch; re-arm debugger hooks. */
    mem_assign_ops_debug(console->cpu, &debug);

    dbg_state.reset_pending = 0;
    dbg_state.reset_restore_breakpoint_flag = 0;
    dbg_state.paused = 1;
    dbg_state.dirty = 1;
    dbg_state.video_dirty = 1;

    gui_set_run_current(0);
    console_set_debug_mode(1);
}

static void debugger_request_reset(void)
{
    debugger_clear_execution_flags();
    debugger_clear_crash_status();

    if (dbg_state.in_halt) {
        dbg_state.reset_pending = 1;
        dbg_state.reset_restore_breakpoint_flag = dbg_state.draw_breakpoint_flag;

        /*
         * The halt loop is nested inside the emulation stack.  Let it unwind
         * before resetting so a suspended instruction cannot resume on top of
         * freshly reset CPU/chip state.
         */
        dbg_state.paused = 0;
        gui_set_run_current(0);
        debug_set_breakpoint_flag(0);
        return;
    }

    debugger_perform_reset();
}

/* Return instruction size (1-3 bytes) from op_name without formatting */
static int get_op_size(const char *name)
{
    if (strstr(name, "_i_aaaa") || strstr(name, "_aaaa_x") ||
        strstr(name, "_aaaa_y") || strstr(name, "_aaaa"))
        return 3;
    if (strstr(name, "_i_aa_x") || strstr(name, "_i_aa_y") ||
        strstr(name, "_d_aa")   || strstr(name, "_aa_x")   ||
        strstr(name, "_aa_y")   || strstr(name, "_aa"))
        return 2;
    static const char *branches[] = {
        "bpl","bmi","bvc","bvs","bcc","bcs","bne","beq", NULL
    };
    for (int k = 0; branches[k]; k++)
        if (strcmp(name, branches[k]) == 0) return 2;
    return 1;
}

#define DISASM_BEFORE  10
#define MAX_BACK_BYTES 40

/* Find a starting address so that ~DISASM_BEFORE instructions appear before target */
static KWORD find_disasm_start(KWORD target)
{
    for (int offset = MAX_BACK_BYTES; offset >= 1; offset--) {
        KWORD try_addr = (target - offset) & 0xFFFF;
        KWORD addr = try_addr;
        int count = 0;

        while (count < DISASM_BEFORE + 10) {
            if (addr == target && count >= DISASM_BEFORE)
                return try_addr;
            KBYTE op = rd_6502(addr);
            int sz = get_op_size(cpu6502.op_name[op]);
            addr = (addr + sz) & 0xFFFF;
            count++;
        }
    }
    /* Fallback: start 20 bytes before target */
    return (target - DISASM_BEFORE * 2) & 0xFFFF;
}

/* Disassemble one 6502 instruction at addr into out (caller provides buf of >= 40 chars).
   Returns the number of bytes consumed by the instruction. */
struct disasm_row {
    char addr_str[8];    /* "$XXXX:" */
    char bytes_str[10];  /* "XX XX XX" */
    char mnem_str[8];    /* "LDA" */
    char oper_str[16];   /* "#$FF" etc */
    char cyc_str[4];     /* ";3" */
    int  size;           /* 1, 2, or 3 */
};

static void disasm_format(struct disasm_row *row, KWORD addr)
{
    KBYTE op = rd_6502(addr);

    /* Clean copy of op_name: strip undocumented-opcode marker '*' */
    char clean[16];
    strncpy(clean, cpu6502.op_name[op], 15);
    clean[15] = 0;
    char *star = strchr(clean, '*');
    if (star) *star = 0;

    /* Extract mnemonic (text before first '_', uppercased) */
    int j = 0;
    while (clean[j] && clean[j] != '_' && j < 7) {
        row->mnem_str[j] = (char)toupper((unsigned char)clean[j]);
        j++;
    }
    row->mnem_str[j] = 0;

    KBYTE b1   = rd_6502((addr + 1) & 0xFFFF);
    KBYTE b2   = rd_6502((addr + 2) & 0xFFFF);
    KWORD abs16 = (KWORD)b1 | ((KWORD)b2 << 8);

    row->oper_str[0] = 0;
    row->size = 1;

    /* Match addressing mode from the name — longer/more-specific patterns first */
    if      (strstr(clean, "_i_aa_x"))  { sprintf(row->oper_str, "($%02X,X)", b1);  row->size = 2; }
    else if (strstr(clean, "_i_aa_y"))  { sprintf(row->oper_str, "($%02X),Y", b1);  row->size = 2; }
    else if (strstr(clean, "_i_aaaa"))  { sprintf(row->oper_str, "($%04X)", abs16); row->size = 3; }
    else if (strstr(clean, "_d_aa"))    { sprintf(row->oper_str, "#$%02X", b1);     row->size = 2; }
    else if (strstr(clean, "_aaaa_x"))  { sprintf(row->oper_str, "$%04X,X", abs16); row->size = 3; }
    else if (strstr(clean, "_aaaa_y"))  { sprintf(row->oper_str, "$%04X,Y", abs16); row->size = 3; }
    else if (strstr(clean, "_aaaa"))    { sprintf(row->oper_str, "$%04X", abs16);   row->size = 3; }
    else if (strstr(clean, "_aa_x"))    { sprintf(row->oper_str, "$%02X,X", b1);    row->size = 2; }
    else if (strstr(clean, "_aa_y"))    { sprintf(row->oper_str, "$%02X,Y", b1);    row->size = 2; }
    else if (strstr(clean, "_aa"))      { sprintf(row->oper_str, "$%02X", b1);      row->size = 2; }
    else if (strstr(clean, "_A"))       { strcpy(row->oper_str, "A");               row->size = 1; }
    else {
        static const char *branches[] = {
            "bpl","bmi","bvc","bvs","bcc","bcs","bne","beq", NULL
        };
        for (int k = 0; branches[k]; k++) {
            if (strcmp(clean, branches[k]) == 0) {
                KWORD target = (addr + 2 + (signed char)b1) & 0xFFFF;
                sprintf(row->oper_str, "$%04X", target);
                row->size = 2;
                break;
            }
        }
    }

    /* Format fixed-width fields */
    sprintf(row->addr_str, "$%04X:", addr);

    if      (row->size == 1) sprintf(row->bytes_str, "%02X",       op);
    else if (row->size == 2) sprintf(row->bytes_str, "%02X %02X",  op, b1);
    else                     sprintf(row->bytes_str, "%02X %02X %02X", op, b1, b2);

    sprintf(row->cyc_str, ";%d", cpu6502.op_cycles[op]);
}

#define DISASM_CACHE_ROWS 256

/* Snapshot the disassembly starting at dbg_state.disasm_addr into the cache.
   Called only while paused (on stop/step and on manual scroll); the draw path
   renders from this cache so it never reads live memory during a run. */
static void disasm_build_cache(void)
{
    KWORD addr = dbg_state.disasm_addr;
    int n;

    for (n = 0; n < DISASM_CACHE_ROWS; n++) {
        struct disasm_row row;
        disasm_format(&row, addr);
        strcpy(dbg_state.disasm_c_addr[n],  row.addr_str);
        strcpy(dbg_state.disasm_c_bytes[n], row.bytes_str);
        strcpy(dbg_state.disasm_c_mnem[n],  row.mnem_str);
        strcpy(dbg_state.disasm_c_oper[n],  row.oper_str);
        strcpy(dbg_state.disasm_c_cyc[n],   row.cyc_str);
        dbg_state.disasm_c_is_pc[n] =
            (addr <= dbg_state.cached_pc &&
             dbg_state.cached_pc < addr + (KWORD)row.size) ? 1 : 0;
        addr = (addr + row.size) & 0xFFFF;
    }
    dbg_state.disasm_cache_count = DISASM_CACHE_ROWS;
}

/* Draw disassembly window */
static void draw_disasm_window(struct nk_context *ctx)
{
    float    row_height = g_theme.row_height * 1.5f;

    nk_flags flags = NK_WINDOW_TITLE | NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
                     NK_WINDOW_BORDER | NK_WINDOW_MINIMIZABLE;

    if (nk_begin(ctx, "Disassembly", dbg_state.disasm_bounds, flags)) {
        dbg_state.disasm_bounds = nk_window_get_bounds(ctx);
        check_and_save_window_pos("disasm", &dbg_state.disasm_bounds,
                                  &dbg_state.disasm_saved_x, &dbg_state.disasm_saved_y,
                                  &dbg_state.disasm_saved_w, &dbg_state.disasm_saved_h);

        /* Address bar: [Addr XXXX] [To PC] */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 4);
        nk_layout_row_push(ctx, 45);
        nk_label(ctx, "Addr:", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 55);
        uint_to_hex(dbg_state.disasm_addr, dbg_state.disasm_addr_buf, 4);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.disasm_addr_buf,
                                           sizeof(dbg_state.disasm_addr_buf), hex_filter)
                & NK_EDIT_COMMITED) {
            dbg_state.disasm_addr = hex_to_uint(dbg_state.disasm_addr_buf) & 0xFFFF;
            disasm_build_cache();
        }
        nk_layout_row_push(ctx, 55);
        if (nk_button_label(ctx, "To PC")) {
            dbg_state.disasm_addr = find_disasm_start(dbg_state.cached_pc);
            disasm_build_cache();
        }
        nk_layout_row_push(ctx, 55);
        if (nk_button_label(ctx, "+Page")) {
            dbg_state.disasm_addr = (dbg_state.disasm_addr + 0x40) & 0xFFFF;
            disasm_build_cache();
        }
        nk_layout_row_end(ctx);

        /* Scrollable disassembly group — fills remaining window height */
        struct nk_rect cr = nk_window_get_content_region(ctx);
        float group_h = cr.h - row_height - 8;
        if (group_h < row_height * 3) group_h = row_height * 3;

        nk_layout_row_dynamic(ctx, group_h, 1);
        if (nk_group_begin(ctx, "disasm_list", NK_WINDOW_BORDER)) {
            int rows = (int)(group_h / row_height) + 34; /* extra rows for backward scan + scroll */
            if (rows > dbg_state.disasm_cache_count)
                rows = dbg_state.disasm_cache_count;

            /* Render from the paused snapshot — no live memory reads here. */
            for (int k = 0; k < rows; k++) {
                struct nk_color color = dbg_state.disasm_c_is_pc[k]
                    ? g_theme.dbg_pc_highlight
                    : ctx->style.text.color;

                nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);
                nk_layout_row_push(ctx, 60);
                nk_label_colored(ctx, dbg_state.disasm_c_addr[k], NK_TEXT_LEFT, color);
                nk_layout_row_push(ctx, 80);
                nk_label_colored(ctx, dbg_state.disasm_c_bytes[k], NK_TEXT_LEFT, color);
                nk_layout_row_push(ctx, 35);
                nk_label_colored(ctx, dbg_state.disasm_c_mnem[k], NK_TEXT_LEFT, color);
                nk_layout_row_push(ctx, 75);
                nk_label_colored(ctx, dbg_state.disasm_c_oper[k], NK_TEXT_LEFT, color);
                nk_layout_row_push(ctx, 25);
                nk_label_colored(ctx, dbg_state.disasm_c_cyc[k], NK_TEXT_LEFT, color);
                nk_layout_row_end(ctx);
            }
            nk_group_end(ctx);
        }
    }
    nk_end(ctx);
}

/* Refresh all display buffers from current emulator state.
   Called once when dbg_state.dirty is set — not every frame. */
static void debugger_refresh_buffers(void)
{
    t_atari *console = console_get_ptr();
    t_cpu   *cpu     = console->cpu;
    t_antic *antic   = console->antic;
    t_gtia  *gtia    = console->gtia;
    t_pokey *pokey   = console->pokey;
    int i;

    /* CPU registers */
    dbg_state.cached_pc = cpu->pc;
    dbg_state.disasm_addr = find_disasm_start(cpu->pc);
    disasm_build_cache();
    uint_to_hex(cpu->pc,    dbg_state.pc_buf, 4);
    uint_to_hex(cpu->s_reg, dbg_state.sp_buf, 2);
    uint_to_hex(cpu->a_reg, dbg_state.a_buf,  2);
    uint_to_hex(cpu->x_reg, dbg_state.x_buf,  2);
    uint_to_hex(cpu->y_reg, dbg_state.y_buf,  2);
    dbg_state.cached_status = cpu->status;

    /* Memory grid (256 bytes of current page) */
    {
        KWORD base_addr = (KWORD)(dbg_state.mem_page << 8);
        for (i = 0; i < 256; i++)
            uint_to_hex(rd_6502(base_addr + i), dbg_state.mem_buf[i], 2);
    }

    /* ANTIC editable registers */
    uint_to_hex(antic->dmactl_reg,                    dbg_state.antic_dmactl, 2);
    uint_to_hex(antic->chactl_reg,                    dbg_state.antic_chactl, 2);
    uint_to_hex(antic->dlistl_reg,                    dbg_state.antic_dlistl, 2);
    uint_to_hex(antic->dlisth_reg,                    dbg_state.antic_dlisth, 2);
    uint_to_hex(antic->hscroll_reg_value >> 1,        dbg_state.antic_hscrol, 2);
    uint_to_hex(antic->vscroll_reg_value,             dbg_state.antic_vscrol, 2);
    uint_to_hex((KBYTE)(antic->pm_base_address >> 8), dbg_state.antic_pmbase, 2);
    uint_to_hex((KBYTE)(antic->ch_base_address >> 8), dbg_state.antic_chbase, 2);

    /* ANTIC read-only registers */
    sprintf(dbg_state.antic_vcount_buf, "%02X", antic->vcount);
    sprintf(dbg_state.control_scanline_buf, "%u", (unsigned int)antic->dl_scanline);
    sprintf(dbg_state.control_cycle_buf, "%d", antic->sl_cycle_count - cpu->i_cnt);

    /* NMIEN cached bits for checkboxes */
    dbg_state.cached_nmien_dli = antic->nmi_dli_en   ? 1 : 0;
    dbg_state.cached_nmien_vbi = antic->nmi_vbi_en   ? 1 : 0;
    dbg_state.cached_nmien_rst = antic->nmi_reset_en ? 1 : 0;
    sprintf(dbg_state.antic_nmist_buf, "%02X", antic->nmi_status_reg);

    /* ANTIC display list (pre-format all entries into separate columns) */
    {
        KWORD dlist_addr = ((KWORD)antic->dlisth_reg << 8) | antic->dlistl_reg;
        KWORD offset = 0;
        int done = 0;
        dbg_state.cached_dlist_addr = dlist_addr;
        dbg_state.cached_antic_pc = antic->pc;
        dbg_state.dlist_line_count = 0;
        while (!done && offset < 200 && dbg_state.dlist_line_count < 200) {
            int n = dbg_state.dlist_line_count;
            KWORD addr  = (dlist_addr + offset) & 0xFFFF;
            KBYTE instr = rd_6502(addr);
            KBYTE mode  = instr & 0x0F;
            int is_lms  = (instr >> 6) & 1;
            int is_dli  = (instr >> 7) & 1;

            sprintf(dbg_state.dlist_addr_str[n], "$%04X:", addr);
            dbg_state.dlist_entry_addr[n] = addr;

            if (mode == 0) {
                sprintf(dbg_state.dlist_bytes_str[n], "%02X", instr);
                sprintf(dbg_state.dlist_mode_str[n], "BLK%d%s",
                        ((instr >> 4) & 7) + 1, is_dli ? " D" : "");
                offset++;
            } else if (mode == 1) {
                KBYTE lo = rd_6502((addr + 1) & 0xFFFF);
                KBYTE hi = rd_6502((addr + 2) & 0xFFFF);
                KWORD target = ((KWORD)hi << 8) | lo;
                sprintf(dbg_state.dlist_bytes_str[n], "%02X %02X %02X", instr, lo, hi);
                sprintf(dbg_state.dlist_mode_str[n], "%s $%04X%s",
                        is_lms ? "JVB" : "JMP", target, is_dli ? " D" : "");
                offset += 3;
                if (is_lms) done = 1;
            } else if (is_lms) {
                KBYTE lo = rd_6502((addr + 1) & 0xFFFF);
                KBYTE hi = rd_6502((addr + 2) & 0xFFFF);
                KWORD data = ((KWORD)hi << 8) | lo;
                sprintf(dbg_state.dlist_bytes_str[n], "%02X %02X %02X", instr, lo, hi);
                sprintf(dbg_state.dlist_mode_str[n], "M%X@$%04X%s",
                        mode, data, is_dli ? " D" : "");
                offset += 3;
            } else {
                sprintf(dbg_state.dlist_bytes_str[n], "%02X", instr);
                sprintf(dbg_state.dlist_mode_str[n], "M%X%s",
                        mode, is_dli ? " D" : "");
                offset++;
            }
            dbg_state.dlist_line_count++;
        }

        /* antic.pc points at the NEXT instruction to fetch — every mode handler
           advances it before its scanlines are drawn.  So highlight the row
           before the one matching antic.pc (the instruction actually on screen),
           unless the match is the first row. */
        dbg_state.dlist_hl_row = -1;
        for (int n = 0; n < dbg_state.dlist_line_count; n++) {
            if (dbg_state.dlist_entry_addr[n] == dbg_state.cached_antic_pc) {
                dbg_state.dlist_hl_row = (n > 0) ? n - 1 : 0;
                break;
            }
        }
    }

    /* GTIA editable registers */
    {
        int *left_int_ptrs[] = {
            &gtia->hpos_p0_reg, &gtia->hpos_p1_reg, &gtia->hpos_p2_reg, &gtia->hpos_p3_reg,
            &gtia->hpos_m0_reg, &gtia->hpos_m1_reg, &gtia->hpos_m2_reg, &gtia->hpos_m3_reg
        };
        KBYTE *left_byte_ptrs[] = {
            &gtia->w_reg[SIZEP0], &gtia->w_reg[SIZEP1], &gtia->w_reg[SIZEP2], &gtia->w_reg[SIZEP3],
            &gtia->w_reg[SIZEM],
            &gtia->graph_p0_reg, &gtia->graph_p1_reg, &gtia->graph_p2_reg, &gtia->graph_p3_reg,
            &gtia->graph_m_reg,
            &gtia->color_pm0_reg, &gtia->color_pm1_reg, &gtia->color_pm2_reg, &gtia->color_pm3_reg,
            &gtia->color_pf0_reg, &gtia->color_pf1_reg, &gtia->color_pf2_reg, &gtia->color_pf3_reg,
            &gtia->color_bk_reg, &gtia->prior_reg,
            &gtia->w_reg[GRACTL], &gtia->console_reg
        };
        char (*left_bufs[])[3] = {
            &dbg_state.gtia_hposp[0], &dbg_state.gtia_hposp[1],
            &dbg_state.gtia_hposp[2], &dbg_state.gtia_hposp[3],
            &dbg_state.gtia_hposm[0], &dbg_state.gtia_hposm[1],
            &dbg_state.gtia_hposm[2], &dbg_state.gtia_hposm[3],
            &dbg_state.gtia_sizep[0], &dbg_state.gtia_sizep[1],
            &dbg_state.gtia_sizep[2], &dbg_state.gtia_sizep[3],
            &dbg_state.gtia_sizem,
            &dbg_state.gtia_grafp[0], &dbg_state.gtia_grafp[1],
            &dbg_state.gtia_grafp[2], &dbg_state.gtia_grafp[3],
            &dbg_state.gtia_grafm,
            &dbg_state.gtia_colpm[0], &dbg_state.gtia_colpm[1],
            &dbg_state.gtia_colpm[2], &dbg_state.gtia_colpm[3],
            &dbg_state.gtia_colpf[0], &dbg_state.gtia_colpf[1],
            &dbg_state.gtia_colpf[2], &dbg_state.gtia_colpf[3],
            &dbg_state.gtia_colbk, &dbg_state.gtia_prior,
            &dbg_state.gtia_gractl, &dbg_state.gtia_consol
        };
        for (i = 0; i < 30; i++) {
            if (i < 8)
                /* hpos regs stored as pixels (color clocks << 1); show real register value */
                uint_to_hex(*left_int_ptrs[i] >> 1, *left_bufs[i], 2);
            else
                uint_to_hex(*left_byte_ptrs[i - 8], *left_bufs[i], 2);
        }

        /* GTIA read-only */
        KBYTE *right_ptrs[] = {
            &gtia->m0pf_reg, &gtia->m1pf_reg, &gtia->m2pf_reg, &gtia->m3pf_reg,
            &gtia->p0pf_reg, &gtia->p1pf_reg, &gtia->p2pf_reg, &gtia->p3pf_reg,
            &gtia->m0pl_reg, &gtia->m1pl_reg, &gtia->m2pl_reg, &gtia->m3pl_reg,
            &gtia->p0pl_reg, &gtia->p1pl_reg, &gtia->p2pl_reg, &gtia->p3pl_reg,
            &gtia->trig[0],  &gtia->trig[1],  &gtia->trig[2],  &gtia->trig[3]
        };
        for (i = 0; i < 20; i++)
            sprintf(dbg_state.gtia_right_bufs[i], "%02X", *right_ptrs[i]);
    }

    /* POKEY editable registers */
    {
        int audf_regs[] = {AUDF1, AUDF2, AUDF3, AUDF4};
        int audc_regs[] = {AUDC1, AUDC2, AUDC3, AUDC4};
        for (i = 0; i < 4; i++) {
            uint_to_hex(pokey->w_reg[audf_regs[i]], dbg_state.pokey_audf[i], 2);
            uint_to_hex(pokey->w_reg[audc_regs[i]], dbg_state.pokey_audc[i], 2);
        }
        for (i = 0; i < 8; i++)
            uint_to_hex(pokey->pot[i], dbg_state.pokey_pot[i], 2);
        uint_to_hex(pokey->w_reg[AUDCTL], dbg_state.pokey_audctl, 2);
        uint_to_hex(pokey->w_reg[STIMER], dbg_state.pokey_stimer, 2);
        uint_to_hex(pokey->w_reg[SEROUT], dbg_state.pokey_serout, 2);
        uint_to_hex(pokey->irq_enable,    dbg_state.pokey_irqen,  2);
        uint_to_hex(pokey->sk_ctl,        dbg_state.pokey_skctl,  2);
    }

    /* POKEY read-only */
    sprintf(dbg_state.pokey_allpot_buf, "%02X", pokey->r_reg[ALLPOT]);
    sprintf(dbg_state.pokey_kbcode_buf, "%02X", pokey->kbcode);
    sprintf(dbg_state.pokey_random_buf, "%02X", pokey->r_reg[RANDOM]);
    sprintf(dbg_state.pokey_serin_buf,  "%02X", pokey->r_reg[SERIN]);
    sprintf(dbg_state.pokey_irqst_buf,  "%02X", pokey->irq_status);
    sprintf(dbg_state.pokey_skstat_buf, "%02X", pokey->sk_status);

    /*
     * GTIA writes INDEX8 pixels, but the debugger displays the converted ARGB
     * output surface.  Refresh it whenever paused debugger state changes so
     * halts/steps show the current frame rather than the last presented one.
     */
    pc_refresh_video_output();

    /* Video texture needs re-upload to reflect current frame */
    dbg_state.video_dirty = 1;
}

/* Draw CPU window */
static void draw_cpu_window(struct nk_context *ctx)
{
    t_atari *console = console_get_ptr();
    t_cpu *cpu = console->cpu;
    char buf[8];
    int i, j;
    float row_height = g_theme.row_height * 1.5f;

    nk_flags flags = NK_WINDOW_TITLE | NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
                     NK_WINDOW_BORDER | NK_WINDOW_MINIMIZABLE;

    if (nk_begin(ctx, "CPU", dbg_state.cpu_bounds, flags)) {
        dbg_state.cpu_bounds = nk_window_get_bounds(ctx);
        check_and_save_window_pos("cpu", &dbg_state.cpu_bounds,
                                  &dbg_state.cpu_saved_x, &dbg_state.cpu_saved_y,
                                  &dbg_state.cpu_saved_w, &dbg_state.cpu_saved_h);

        /* PC, SP row */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 4);
        nk_layout_row_push(ctx, 30);
        nk_label(ctx, "PC:", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 56);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.pc_buf,
                                           sizeof(dbg_state.pc_buf), hex_filter) & NK_EDIT_COMMITED) {
            cpu->pc = hex_to_uint(dbg_state.pc_buf) & 0xFFFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_push(ctx, 30);
        nk_label(ctx, "SP:", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 40);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.sp_buf,
                                           sizeof(dbg_state.sp_buf), hex_filter) & NK_EDIT_COMMITED) {
            cpu->s_reg = hex_to_uint(dbg_state.sp_buf) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_end(ctx);

        /* A, X, Y row */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 6);
        nk_layout_row_push(ctx, 20);
        nk_label(ctx, "A:", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 40);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.a_buf,
                                           sizeof(dbg_state.a_buf), hex_filter) & NK_EDIT_COMMITED) {
            cpu->a_reg = hex_to_uint(dbg_state.a_buf) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_push(ctx, 20);
        nk_label(ctx, "X:", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 40);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.x_buf,
                                           sizeof(dbg_state.x_buf), hex_filter) & NK_EDIT_COMMITED) {
            cpu->x_reg = hex_to_uint(dbg_state.x_buf) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_push(ctx, 20);
        nk_label(ctx, "Y:", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 40);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.y_buf,
                                           sizeof(dbg_state.y_buf), hex_filter) & NK_EDIT_COMMITED) {
            cpu->y_reg = hex_to_uint(dbg_state.y_buf) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_end(ctx);

        /* Status flags */
        nk_layout_row_dynamic(ctx, row_height, 1);
        nk_label(ctx, "Status Flags:", NK_TEXT_LEFT);

        nk_layout_row_begin(ctx, NK_STATIC, row_height, 8);
        static const char *flag_names[] = {"N", "V", "-", "B", "D", "I", "Z", "C"};
        static const int flag_bits[] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
        for (i = 0; i < 8; i++) {
            int flag_set = (dbg_state.cached_status & flag_bits[i]) ? 1 : 0;
            nk_layout_row_push(ctx, 40);
            if (nk_checkbox_label(ctx, flag_names[i], &flag_set)) {
                if (flag_set)
                    cpu->status |= flag_bits[i];
                else
                    cpu->status &= ~flag_bits[i];
                dbg_state.cached_status = cpu->status;
            }
        }
        nk_layout_row_end(ctx);

        /* Memory page selector */
        nk_layout_row_dynamic(ctx, row_height * 0.5f, 1);
        nk_spacing(ctx, 1);

        nk_layout_row_begin(ctx, NK_STATIC, row_height, 3);
        nk_layout_row_push(ctx, 130);
        nk_label(ctx, "Memory Page:", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 40);
        if (!dbg_state.mem_page_editing)
            uint_to_hex(dbg_state.mem_page, dbg_state.mem_page_buf, 2);
        {
            nk_flags ef = nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER,
                                                         dbg_state.mem_page_buf,
                                                         sizeof(dbg_state.mem_page_buf),
                                                         hex_filter);
            dbg_state.mem_page_editing = (ef & NK_EDIT_ACTIVE) != 0;
            if (ef & NK_EDIT_COMMITED) {
                dbg_state.mem_page = hex_to_uint(dbg_state.mem_page_buf) & 0xFF;
                dbg_state.mem_page_editing = 0;
                dbg_state.dirty = 1;
            }
        }
        nk_layout_row_push(ctx, 40);
        nk_label(ctx, "xx", NK_TEXT_LEFT);
        nk_layout_row_end(ctx);

        /* Memory grid header */
        nk_layout_row_begin(ctx, NK_STATIC, row_height * 0.8f, 17);
        nk_layout_row_push(ctx, row_height);
        nk_label(ctx, "", NK_TEXT_LEFT);
        float row_width = row_height + g_theme.row_height_eighth;
        for (i = 0; i < 16; i++) {
            nk_layout_row_push(ctx, row_width);
            sprintf(buf, "%X", i);
            nk_label(ctx, buf, NK_TEXT_CENTERED);
        }
        nk_layout_row_end(ctx);

        /* Memory grid - 16 rows of 16 bytes (editable) */
        KWORD base_addr = (KWORD)(dbg_state.mem_page << 8);
        for (i = 0; i < 16; i++) {
            //nk_layout_row_begin(ctx, NK_STATIC, g_theme.row_height*1.25f, 17);
            nk_layout_row_begin(ctx, NK_STATIC, g_theme.row_height_1_5, 17);
            nk_layout_row_push(ctx, row_height);
            sprintf(buf, "%X0:", i);
            nk_label(ctx, buf, NK_TEXT_LEFT);
            for (j = 0; j < 16; j++) {
                int idx = i * 16 + j;
                KWORD addr = base_addr + idx;
                nk_layout_row_push(ctx, row_width);
                if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.mem_buf[idx],
                    sizeof(dbg_state.mem_buf[idx]), hex_filter) & NK_EDIT_COMMITED) {
                    KBYTE new_val = hex_to_uint(dbg_state.mem_buf[idx]) & 0xFF;
                    wr_6502(addr, new_val);
                    dbg_state.dirty = 1;
                }
            }
            nk_layout_row_end(ctx);
        }
    }
    nk_end(ctx);
}

/* Draw ANTIC window */
static void draw_antic_window(struct nk_context *ctx)
{
    t_atari *console = console_get_ptr();
    t_antic *antic = console->antic;
    char buf[48];
    float row_height = g_theme.row_height * 1.5f;

    nk_flags flags = NK_WINDOW_TITLE | NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
                     NK_WINDOW_BORDER | NK_WINDOW_MINIMIZABLE;

    if (nk_begin(ctx, "ANTIC", dbg_state.antic_bounds, flags)) {
        dbg_state.antic_bounds = nk_window_get_bounds(ctx);
        check_and_save_window_pos("antic", &dbg_state.antic_bounds,
                                  &dbg_state.antic_saved_x, &dbg_state.antic_saved_y,
                                  &dbg_state.antic_saved_w, &dbg_state.antic_saved_h);

        /* Two-panel layout: registers left, display list right */
        float group_height = row_height * 20;
        nk_layout_row_begin(ctx, NK_STATIC, group_height, 2);
        nk_layout_row_push(ctx, 170);

        /* ---- Left panel: registers ---- */
        if (nk_group_begin(ctx, "antic_regs", NK_WINDOW_NO_SCROLLBAR | NK_WINDOW_BORDER)) {

            /* Editable macro to reduce repetition */
            #define ANTIC_REG_EDIT(label, field, buf_field, mask) \
                nk_layout_row_begin(ctx, NK_STATIC, row_height, 2); \
                nk_layout_row_push(ctx, 75); \
                nk_label(ctx, label, NK_TEXT_LEFT); \
                nk_layout_row_push(ctx, 45); \
                if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.buf_field, \
                                                   sizeof(dbg_state.buf_field), hex_filter) & NK_EDIT_COMMITED) { \
                    field = hex_to_uint(dbg_state.buf_field) & (mask); \
                    dbg_state.dirty = 1; \
                } \
                nk_layout_row_end(ctx)

            ANTIC_REG_EDIT("DMACTL", antic->dmactl_reg, antic_dmactl, 0xFF);
            ANTIC_REG_EDIT("CHACTL", antic->chactl_reg, antic_chactl, 0xFF);
            ANTIC_REG_EDIT("DLISTL", antic->dlistl_reg, antic_dlistl, 0xFF);
            ANTIC_REG_EDIT("DLISTH", antic->dlisth_reg, antic_dlisth, 0xFF);
            /* HSCROL is stored internally doubled for pixel positioning. */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 75);
            nk_label(ctx, "HSCROL", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 45);
            if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.antic_hscrol,
                                               sizeof(dbg_state.antic_hscrol), hex_filter) & NK_EDIT_COMMITED) {
                antic_write_register_debug(HSCROL, hex_to_uint(dbg_state.antic_hscrol) & 0x0F);
                dbg_state.dirty = 1;
            }
            nk_layout_row_end(ctx);
            ANTIC_REG_EDIT("VSCROL", antic->vscroll_reg_value, antic_vscrol, 0xFF);

            /* PMBASE - editable, writes via register handler for side effects */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 75);
            nk_label(ctx, "PMBASE", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 45);
            if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.antic_pmbase,
                                               sizeof(dbg_state.antic_pmbase), hex_filter) & NK_EDIT_COMMITED) {
                antic_write_register_debug(PMBASE, hex_to_uint(dbg_state.antic_pmbase) & 0xFF);
                dbg_state.dirty = 1;
            }
            nk_layout_row_end(ctx);

            /* CHBASE - editable, writes via register handler for side effects */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 75);
            nk_label(ctx, "CHBASE", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 45);
            if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.antic_chbase,
                                               sizeof(dbg_state.antic_chbase), hex_filter) & NK_EDIT_COMMITED) {
                antic_write_register_debug(CHBASE, hex_to_uint(dbg_state.antic_chbase) & 0xFF);
                dbg_state.dirty = 1;
            }
            nk_layout_row_end(ctx);

            /* WSYNC - write-only strobe, no stored state */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 75);
            nk_label(ctx, "WSYNC", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 45);
            nk_label(ctx, "---", NK_TEXT_LEFT);
            nk_layout_row_end(ctx);

            /* VCOUNT - read only */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 75);
            nk_label(ctx, "VCOUNT", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 45);
            nk_label(ctx, dbg_state.antic_vcount_buf, NK_TEXT_LEFT);
            nk_layout_row_end(ctx);

            /* PENH - light pen H, read only (always 00) */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 75);
            nk_label(ctx, "PENH", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 45);
            nk_label(ctx, "00", NK_TEXT_LEFT);
            nk_layout_row_end(ctx);

            /* PENV - light pen V, read only (always 00) */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 75);
            nk_label(ctx, "PENV", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 45);
            nk_label(ctx, "00", NK_TEXT_LEFT);
            nk_layout_row_end(ctx);

            /* NMIEN - toggleable checkboxes for DLI/VBI/RST enable bits */
            nk_layout_row_dynamic(ctx, row_height, 1);
            nk_label(ctx, "NMIEN", NK_TEXT_LEFT);

            nk_layout_row_dynamic(ctx, row_height, 1);
            if (nk_checkbox_label(ctx, "DLI Enable", &dbg_state.cached_nmien_dli)) {
                KBYTE nmien = (dbg_state.cached_nmien_dli ? 0x80 : 0)
                            | (dbg_state.cached_nmien_vbi ? 0x40 : 0)
                            | (dbg_state.cached_nmien_rst ? 0x20 : 0);
                antic_write_register_debug(NMIEN, nmien);
            }
            nk_layout_row_dynamic(ctx, row_height, 1);
            if (nk_checkbox_label(ctx, "VBI Enable", &dbg_state.cached_nmien_vbi)) {
                KBYTE nmien = (dbg_state.cached_nmien_dli ? 0x80 : 0)
                            | (dbg_state.cached_nmien_vbi ? 0x40 : 0)
                            | (dbg_state.cached_nmien_rst ? 0x20 : 0);
                antic_write_register_debug(NMIEN, nmien);
            }
            nk_layout_row_dynamic(ctx, row_height, 1);
            if (nk_checkbox_label(ctx, "RST Enable", &dbg_state.cached_nmien_rst)) {
                KBYTE nmien = (dbg_state.cached_nmien_dli ? 0x80 : 0)
                            | (dbg_state.cached_nmien_vbi ? 0x40 : 0)
                            | (dbg_state.cached_nmien_rst ? 0x20 : 0);
                antic_write_register_debug(NMIEN, nmien);
            }

            /* NMIST - read only */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 75);
            nk_label(ctx, "NMIST", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 45);
            nk_label(ctx, dbg_state.antic_nmist_buf, NK_TEXT_LEFT);
            nk_layout_row_end(ctx);

            #undef ANTIC_REG_EDIT

            nk_group_end(ctx);
        }

        /* ---- Right panel: display list ---- */
        nk_layout_row_push(ctx, 290);
        if (nk_group_begin(ctx, "antic_dlist", NK_WINDOW_BORDER)) {

            /* Header */
            nk_layout_row_dynamic(ctx, row_height, 1);
            sprintf(buf, "DLIST $%04X", dbg_state.cached_dlist_addr);
            nk_label(ctx, buf, NK_TEXT_LEFT);

            /* Render display list lines with aligned columns */
            for (int dl_i = 0; dl_i < dbg_state.dlist_line_count; dl_i++) {
                struct nk_color color =
                    (dl_i == dbg_state.dlist_hl_row)
                    ? g_theme.dbg_pc_highlight
                    : ctx->style.text.color;

                nk_layout_row_begin(ctx, NK_STATIC, row_height, 3);
                nk_layout_row_push(ctx, 60);
                nk_label_colored(ctx, dbg_state.dlist_addr_str[dl_i], NK_TEXT_LEFT, color);
                nk_layout_row_push(ctx, 80);
                nk_label_colored(ctx, dbg_state.dlist_bytes_str[dl_i], NK_TEXT_LEFT, color);
                nk_layout_row_push(ctx, 140);
                nk_label_colored(ctx, dbg_state.dlist_mode_str[dl_i], NK_TEXT_LEFT, color);
                nk_layout_row_end(ctx);
            }

            nk_group_end(ctx);
        }

        nk_layout_row_end(ctx);
    }
    nk_end(ctx);
}

/* Draw GTIA window */
static void draw_gtia_window(struct nk_context *ctx)
{
    t_atari *console = console_get_ptr();
    t_gtia *gtia = console->gtia;
    char buf[32];
    int i;
    float row_height = g_theme.row_height * 1.5f;

    nk_flags flags = NK_WINDOW_TITLE | NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
                     NK_WINDOW_BORDER | NK_WINDOW_MINIMIZABLE;

    if (nk_begin(ctx, "GTIA", dbg_state.gtia_bounds, flags)) {
        dbg_state.gtia_bounds = nk_window_get_bounds(ctx);
        check_and_save_window_pos("gtia", &dbg_state.gtia_bounds,
                                  &dbg_state.gtia_saved_x, &dbg_state.gtia_saved_y,
                                  &dbg_state.gtia_saved_w, &dbg_state.gtia_saved_h);

        /* Left column: editable write registers */
        const char *left_labels[] = {
            "HPOSP0", "HPOSP1", "HPOSP2", "HPOSP3",
            "HPOSM0", "HPOSM1", "HPOSM2", "HPOSM3",
            "SIZEP0", "SIZEP1", "SIZEP2", "SIZEP3",
            "SIZEM",  "GRAFP0", "GRAFP1", "GRAFP2",
            "GRAFP3", "GRAFM",  "COLPM0", "COLPM1",
            "COLPM2", "COLPM3", "COLPF0", "COLPF1",
            "COLPF2", "COLPF3", "COLBK",  "PRIOR",
            "GRACTL", "CONSOL"
        };

        /* Pointers to int registers (HPOSP/HPOSM) */
        int *left_int_ptrs[] = {
            &gtia->hpos_p0_reg, &gtia->hpos_p1_reg, &gtia->hpos_p2_reg, &gtia->hpos_p3_reg,
            &gtia->hpos_m0_reg, &gtia->hpos_m1_reg, &gtia->hpos_m2_reg, &gtia->hpos_m3_reg
        };

        /* Pointers to KBYTE registers (rows 8-29) */
        KBYTE *left_byte_ptrs[] = {
            &gtia->w_reg[SIZEP0], &gtia->w_reg[SIZEP1], &gtia->w_reg[SIZEP2], &gtia->w_reg[SIZEP3],
            &gtia->w_reg[SIZEM],
            &gtia->graph_p0_reg, &gtia->graph_p1_reg, &gtia->graph_p2_reg, &gtia->graph_p3_reg,
            &gtia->graph_m_reg,
            &gtia->color_pm0_reg, &gtia->color_pm1_reg, &gtia->color_pm2_reg, &gtia->color_pm3_reg,
            &gtia->color_pf0_reg, &gtia->color_pf1_reg, &gtia->color_pf2_reg, &gtia->color_pf3_reg,
            &gtia->color_bk_reg, &gtia->prior_reg,
            &gtia->w_reg[GRACTL], &gtia->console_reg
        };

        /* Edit buffers: rows 0-3 = hposp[0-3], 4-7 = hposm[0-3], 8-11 = sizep[0-3],
           12 = sizem, 13-16 = grafp[0-3], 17 = grafm, 18-21 = colpm[0-3],
           22-25 = colpf[0-3], 26 = colbk, 27 = prior, 28 = gractl, 29 = consol */
        char (*left_bufs[])[3] = {
            &dbg_state.gtia_hposp[0], &dbg_state.gtia_hposp[1],
            &dbg_state.gtia_hposp[2], &dbg_state.gtia_hposp[3],
            &dbg_state.gtia_hposm[0], &dbg_state.gtia_hposm[1],
            &dbg_state.gtia_hposm[2], &dbg_state.gtia_hposm[3],
            &dbg_state.gtia_sizep[0], &dbg_state.gtia_sizep[1],
            &dbg_state.gtia_sizep[2], &dbg_state.gtia_sizep[3],
            &dbg_state.gtia_sizem,
            &dbg_state.gtia_grafp[0], &dbg_state.gtia_grafp[1],
            &dbg_state.gtia_grafp[2], &dbg_state.gtia_grafp[3],
            &dbg_state.gtia_grafm,
            &dbg_state.gtia_colpm[0], &dbg_state.gtia_colpm[1],
            &dbg_state.gtia_colpm[2], &dbg_state.gtia_colpm[3],
            &dbg_state.gtia_colpf[0], &dbg_state.gtia_colpf[1],
            &dbg_state.gtia_colpf[2], &dbg_state.gtia_colpf[3],
            &dbg_state.gtia_colbk, &dbg_state.gtia_prior,
            &dbg_state.gtia_gractl, &dbg_state.gtia_consol
        };

        /* Right column: read-only registers (20 entries, rows 0-19) */
        const char *right_labels[] = {
            "M0PF", "M1PF", "M2PF", "M3PF",
            "P0PF", "P1PF", "P2PF", "P3PF",
            "M0PL", "M1PL", "M2PL", "M3PL",
            "P0PL", "P1PL", "P2PL", "P3PL",
            "TRIG0", "TRIG1", "TRIG2", "TRIG3"
        };
        KBYTE *right_ptrs[] = {
            &gtia->m0pf_reg, &gtia->m1pf_reg, &gtia->m2pf_reg, &gtia->m3pf_reg,
            &gtia->p0pf_reg, &gtia->p1pf_reg, &gtia->p2pf_reg, &gtia->p3pf_reg,
            &gtia->m0pl_reg, &gtia->m1pl_reg, &gtia->m2pl_reg, &gtia->m3pl_reg,
            &gtia->p0pl_reg, &gtia->p1pl_reg, &gtia->p2pl_reg, &gtia->p3pl_reg,
            &gtia->trig[0], &gtia->trig[1], &gtia->trig[2], &gtia->trig[3]
        };

        #define GTIA_LEFT_COUNT  30
        #define GTIA_RIGHT_COUNT 20

        for (i = 0; i < GTIA_LEFT_COUNT; i++) {
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);

            /* Left: label + editable field */
            nk_layout_row_push(ctx, 70);
            nk_label(ctx, left_labels[i], NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 35);

            if (i < 8) {
                /* Int registers (HPOSP/HPOSM) */
                if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, *left_bufs[i],
                                                   3, hex_filter) & NK_EDIT_COMMITED) {
                    /* user enters real register value; store as pixels (<< 1) */
                    *left_int_ptrs[i] = (hex_to_uint(*left_bufs[i]) & 0xFF) << 1;
                    dbg_state.dirty = 1;
                }
            } else {
                /* KBYTE registers */
                if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, *left_bufs[i],
                                                   3, hex_filter) & NK_EDIT_COMMITED) {
                    KBYTE val = hex_to_uint(*left_bufs[i]) & 0xFF;
                    if (i >= 8 && i <= 12) {
                        /* SIZEP0-3/SIZEM: route through the register write so the
                           raw value decodes into the internal size fields used by
                           rendering (w_reg, which we display, is set there too). */
                        gtia_write_register(SIZEP0 + (i - 8), val);
                    } else {
                        *left_byte_ptrs[i - 8] = val;
                    }
                    dbg_state.dirty = 1;
                }
            }

            /* Spacer */
            nk_layout_row_push(ctx, 35);
            nk_spacing(ctx, 1);

            /* Right: read-only or empty */
            if (i < GTIA_RIGHT_COUNT) {
                nk_layout_row_push(ctx, 70);
                nk_label(ctx, right_labels[i], NK_TEXT_LEFT);
                nk_layout_row_push(ctx, 35);
                nk_label(ctx, dbg_state.gtia_right_bufs[i], NK_TEXT_LEFT);
            } else {
                nk_layout_row_push(ctx, 70);
                nk_label(ctx, "", NK_TEXT_LEFT);
                nk_layout_row_push(ctx, 35);
                nk_label(ctx, "", NK_TEXT_LEFT);
            }

            nk_layout_row_end(ctx);
        }

        #undef GTIA_LEFT_COUNT
        #undef GTIA_RIGHT_COUNT
    }
    nk_end(ctx);
}

/* Draw POKEY window */
static void draw_pokey_window(struct nk_context *ctx)
{
    t_atari *console = console_get_ptr();
    t_pokey *pokey = console->pokey;
    char buf[32];
    int i;
    float row_height = g_theme.row_height * 1.5f;

    nk_flags flags = NK_WINDOW_TITLE | NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
                     NK_WINDOW_BORDER | NK_WINDOW_MINIMIZABLE;

    if (nk_begin(ctx, "POKEY", dbg_state.pokey_bounds, flags)) {
        dbg_state.pokey_bounds = nk_window_get_bounds(ctx);
        check_and_save_window_pos("pokey", &dbg_state.pokey_bounds,
                                  &dbg_state.pokey_saved_x, &dbg_state.pokey_saved_y,
                                  &dbg_state.pokey_saved_w, &dbg_state.pokey_saved_h);

        /* Section 1: AUD + POT registers (8 rows) */
        int audf_regs[] = {AUDF1, AUDF2, AUDF3, AUDF4};
        int audc_regs[] = {AUDC1, AUDC2, AUDC3, AUDC4};
        const char *audf_labels[] = {"AUDF1", "AUDC1", "AUDF2", "AUDC2",
                                     "AUDF3", "AUDC3", "AUDF4", "AUDC4"};

        for (i = 0; i < 8; i++) {
            int aud_idx = i / 2;
            int is_ctrl = i & 1;
            int aud_reg = is_ctrl ? audc_regs[aud_idx] : audf_regs[aud_idx];
            char (*aud_buf)[3] = is_ctrl ? dbg_state.pokey_audc : dbg_state.pokey_audf;

            nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);
            nk_layout_row_push(ctx, 70);
            sprintf(buf, "%s", audf_labels[i]);
            nk_label(ctx, buf, NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 35);
            if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, aud_buf[aud_idx],
                                               3, hex_filter) & NK_EDIT_COMMITED) {
                pokey->w_reg[aud_reg] = hex_to_uint(aud_buf[aud_idx]) & 0xFF;
                dbg_state.dirty = 1;
            }
            nk_layout_row_push(ctx, 35);
            nk_spacing(ctx, 1);
            nk_layout_row_push(ctx, 70);
            sprintf(buf, "POT%d", i);
            nk_label(ctx, buf, NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 35);
            if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.pokey_pot[i],
                                               sizeof(dbg_state.pokey_pot[i]), hex_filter) & NK_EDIT_COMMITED) {
                pokey->pot[i] = hex_to_uint(dbg_state.pokey_pot[i]) & 0xFF;
                dbg_state.dirty = 1;
            }
            nk_layout_row_end(ctx);
        }

        /* Section 2: Control registers (3 rows) */
        /* AUDCTL | ALLPOT */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);
        nk_layout_row_push(ctx, 70);
        nk_label(ctx, "AUDCTL", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.pokey_audctl,
                                           sizeof(dbg_state.pokey_audctl), hex_filter) & NK_EDIT_COMMITED) {
            pokey->w_reg[AUDCTL] = hex_to_uint(dbg_state.pokey_audctl) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_push(ctx, 35);
        nk_spacing(ctx, 1);
        nk_layout_row_push(ctx, 75);
        nk_label(ctx, "ALLPOT", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        nk_label(ctx, dbg_state.pokey_allpot_buf, NK_TEXT_LEFT);
        nk_layout_row_end(ctx);

        /* STIMER | KBCODE */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);
        nk_layout_row_push(ctx, 70);
        nk_label(ctx, "STIMER", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.pokey_stimer,
                                           sizeof(dbg_state.pokey_stimer), hex_filter) & NK_EDIT_COMMITED) {
            pokey->w_reg[STIMER] = hex_to_uint(dbg_state.pokey_stimer) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_push(ctx, 35);
        nk_spacing(ctx, 1);
        nk_layout_row_push(ctx, 75);
        nk_label(ctx, "KBCODE", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        nk_label(ctx, dbg_state.pokey_kbcode_buf, NK_TEXT_LEFT);
        nk_layout_row_end(ctx);

        /* (empty) | RANDOM */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);
        nk_layout_row_push(ctx, 70);
        nk_spacing(ctx, 1);
        nk_layout_row_push(ctx, 35);
        nk_spacing(ctx, 1);
        nk_layout_row_push(ctx, 35);
        nk_spacing(ctx, 1);
        nk_layout_row_push(ctx, 75);
        nk_label(ctx, "RANDOM", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        nk_label(ctx, dbg_state.pokey_random_buf, NK_TEXT_LEFT);
        nk_layout_row_end(ctx);

        /* Section 3: Serial/IRQ/SK registers (3 rows) */
        /* SEROUT | SERIN */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);
        nk_layout_row_push(ctx, 70);
        nk_label(ctx, "SEROUT", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.pokey_serout,
                                           sizeof(dbg_state.pokey_serout), hex_filter) & NK_EDIT_COMMITED) {
            pokey->w_reg[SEROUT] = hex_to_uint(dbg_state.pokey_serout) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_push(ctx, 35);
        nk_spacing(ctx, 1);
        nk_layout_row_push(ctx, 75);
        nk_label(ctx, "SERIN", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        nk_label(ctx, dbg_state.pokey_serin_buf, NK_TEXT_LEFT);
        nk_layout_row_end(ctx);

        /* IRQEN | IRQST */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);
        nk_layout_row_push(ctx, 70);
        nk_label(ctx, "IRQEN", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.pokey_irqen,
                                           sizeof(dbg_state.pokey_irqen), hex_filter) & NK_EDIT_COMMITED) {
            pokey->irq_enable = hex_to_uint(dbg_state.pokey_irqen) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_push(ctx, 35);
        nk_spacing(ctx, 1);
        nk_layout_row_push(ctx, 75);
        nk_label(ctx, "IRQST", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        nk_label(ctx, dbg_state.pokey_irqst_buf, NK_TEXT_LEFT);
        nk_layout_row_end(ctx);

        /* SKCTL | SKSTAT */
        nk_layout_row_begin(ctx, NK_STATIC, row_height, 5);
        nk_layout_row_push(ctx, 70);
        nk_label(ctx, "SKCTL", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        if (nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.pokey_skctl,
                                           sizeof(dbg_state.pokey_skctl), hex_filter) & NK_EDIT_COMMITED) {
            pokey->sk_ctl = hex_to_uint(dbg_state.pokey_skctl) & 0xFF;
            dbg_state.dirty = 1;
        }
        nk_layout_row_push(ctx, 35);
        nk_spacing(ctx, 1);
        nk_layout_row_push(ctx, 75);
        nk_label(ctx, "SKSTAT", NK_TEXT_LEFT);
        nk_layout_row_push(ctx, 35);
        nk_label(ctx, dbg_state.pokey_skstat_buf, NK_TEXT_LEFT);
        nk_layout_row_end(ctx);
    }
    nk_end(ctx);
}

/* Helper: Get breakpoint type name */
static const char *get_bp_type_name(e_debug_breakpoint type)
{
    switch (type) {
        case BK_MEM: return "Break Mem";
        case BK_SCANLINE: return "Break SL";
        case BK_WATCH: return "Watch Mem";
        case BK_VBLANK: return "Break VBL";
        default: return "---";
    }
}

/* Helper: Add a breakpoint using the current UI selections */
static void add_breakpoint(void)
{
    int i;
    e_debug_breakpoint bp_type;
    int value;

    /* Find an empty slot */
    for (i = 0; i < DEBUG_MAX_BK; ++i) {
        if (debug.breakpoint[i] == BK_NONE)
            break;
    }
    if (i >= DEBUG_MAX_BK) {
        return;  /* No slots available */
    }

    /* Determine breakpoint type from UI selections */
    if (dbg_state.bp_type == 0) {  /* Break */
        if (dbg_state.bp_subtype == 0) {  /* Memory */
            bp_type = BK_MEM;
            value = hex_to_uint(dbg_state.bp_value_buf) & 0xFFFF;
        } else {  /* Scanline */
            bp_type = BK_SCANLINE;
            value = atoi(dbg_state.bp_value_buf);
            if (value > 311) value = 311;  /* Max scanline */
        }
    } else {  /* Watch */
        bp_type = BK_WATCH;
        value = hex_to_uint(dbg_state.bp_value_buf) & 0xFFFF;
    }

    /* Add the breakpoint */
    debug.breakpoint[i] = bp_type;
    debug.breakpoint_place[i] = value;
    debug.breakpoint_flag = 1;

    /* This runs inside debugger_draw(), which saved the pre-draw breakpoint
       flag and restores it after drawing.  Update that saved value so the new
       breakpoint stays armed instead of being wiped at end of frame. */
    dbg_state.draw_breakpoint_flag = 1;

    /* If memory breakpoint or watch, update memory dispatch */
    if (bp_type == BK_MEM || bp_type == BK_WATCH) {
        t_atari *console = console_get_ptr();
        mem_assign_ops_debug(console->cpu, &debug);
    }

    /* Clear input buffer */
    dbg_state.bp_value_buf[0] = '\0';

    config_save_debugger_breakpoint(bp_type, value);
}

/* Helper: Delete a breakpoint by index */
static void delete_breakpoint(int index)
{
    int i;

    if (index < 0 || index >= DEBUG_MAX_BK)
        return;

    e_debug_breakpoint saved_type = debug.breakpoint[index];
    int saved_value = debug.breakpoint_place[index];

    debug.breakpoint[index] = BK_NONE;

    /* Check if any breakpoints remain */
    debug.breakpoint_flag = 0;
    for (i = 0; i < DEBUG_MAX_BK; ++i) {
        if (debug.breakpoint[i] != BK_NONE) {
            debug.breakpoint_flag = 1;
            break;
        }
    }

    /* Keep the value debugger_draw() restores at end of frame in sync (see
       add_breakpoint), otherwise this delete is undone until the next entry. */
    dbg_state.draw_breakpoint_flag = debug.breakpoint_flag;

    /* Update memory dispatch */
    t_atari *console = console_get_ptr();
    mem_assign_ops_debug(console->cpu, &debug);

    dbg_state.bp_selected = -1;

    config_delete_debugger_breakpoint(saved_type, saved_value);
}

static void trace_file_selected(struct modal_config *cfg)
{
    if (cfg->result == 1) {
        char *path = filebrowser_result();
        if (path && path[0]) {
            strncpy(dbg_state.trace_file, path, sizeof(dbg_state.trace_file) - 1);
            dbg_state.trace_file[sizeof(dbg_state.trace_file) - 1] = '\0';
        }
    }
}

/* Draw Control window */
static void draw_control_window(struct nk_context *ctx)
{
    float row_height = g_theme.row_height * 1.5f;
    int i;
    char bp_line[32];
    char crash_line[40];
    static const char *type_names[] = {"Break", "Watch"};
    static const char *subtype_break[] = {"Memory", "Scanline"};
    static const char *subtype_watch[] = {"Memory"};

    nk_flags flags = NK_WINDOW_TITLE | NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
                     NK_WINDOW_BORDER | NK_WINDOW_MINIMIZABLE;

    if (nk_begin(ctx, "Control", dbg_state.control_bounds, flags)) {
        dbg_state.control_bounds = nk_window_get_bounds(ctx);
        check_and_save_window_pos("control", &dbg_state.control_bounds,
                                  &dbg_state.control_saved_x, &dbg_state.control_saved_y,
                                  &dbg_state.control_saved_w, &dbg_state.control_saved_h);

        /* Two-column layout: breakpoints on left, buttons on right */
        struct nk_rect cr = nk_window_get_content_region(ctx);
        float panel_h = cr.h - row_height - 16;
        if (panel_h < row_height * 6) panel_h = row_height * 6;
        float right_w = 145;

        nk_layout_row_begin(ctx, NK_STATIC, panel_h, 2);
        nk_layout_row_push(ctx, cr.w - right_w - 32);

        /* === Left group: Breakpoints === */
        if (nk_group_begin(ctx, "bp_group", 0)) {
            /* Header: label + delete button */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 2);
            nk_layout_row_push(ctx, 100);
            nk_label(ctx, "Breakpoints:", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 135);
            if (nk_button_label(ctx, "Delete Selected")) {
                if (dbg_state.bp_selected >= 0) {
                    delete_breakpoint(dbg_state.bp_selected);
                }
            }
            nk_layout_row_end(ctx);

            /* Breakpoint list */
            nk_layout_row_dynamic(ctx, row_height * 6, 1);
            if (nk_group_begin(ctx, "bp_list", NK_WINDOW_BORDER)) {
                nk_layout_row_dynamic(ctx, row_height * 0.9f, 1);
                for (i = 0; i < DEBUG_MAX_BK; ++i) {
                    if (debug.breakpoint[i] != BK_NONE) {
                        if (debug.breakpoint[i] == BK_SCANLINE) {
                            sprintf(bp_line, "%d: %s %d", i + 1,
                                    get_bp_type_name(debug.breakpoint[i]),
                                    debug.breakpoint_place[i]);
                        } else {
                            sprintf(bp_line, "%d: %s $%04X", i + 1,
                                    get_bp_type_name(debug.breakpoint[i]),
                                    debug.breakpoint_place[i]);
                        }
                        if (nk_selectable_label(ctx, bp_line, NK_TEXT_LEFT,
                                               (nk_bool *)(dbg_state.bp_selected == i ? &(nk_bool){1} : &(nk_bool){0}))) {
                            dbg_state.bp_selected = (dbg_state.bp_selected == i) ? -1 : i;
                        }
                    }
                }
                nk_group_end(ctx);
            }

            /* Add breakpoint controls */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 7);

            nk_layout_row_push(ctx, 90);
            dbg_state.bp_type = nk_combo(ctx, type_names, 2, dbg_state.bp_type, row_height, nk_vec2(100, 120));

            nk_layout_row_push(ctx, 8);
            nk_spacing(ctx, 1);

            nk_layout_row_push(ctx, 105);
            if (dbg_state.bp_type == 0) {
                dbg_state.bp_subtype = nk_combo(ctx, subtype_break, 2, dbg_state.bp_subtype, row_height, nk_vec2(115, 120));
            } else {
                dbg_state.bp_subtype = 0;
                nk_combo(ctx, subtype_watch, 1, 0, row_height, nk_vec2(115, 120));
            }

            nk_layout_row_push(ctx, 8);
            nk_spacing(ctx, 1);

            nk_layout_row_push(ctx, 60);
            if (dbg_state.bp_type == 0 && dbg_state.bp_subtype == 1) {
                nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.bp_value_buf,
                                               sizeof(dbg_state.bp_value_buf), decimal_filter);
            } else {
                nk_edit_string_zero_terminated(ctx, NK_EDIT_FIELD | NK_EDIT_SIG_ENTER, dbg_state.bp_value_buf,
                                               sizeof(dbg_state.bp_value_buf), hex_filter);
            }

            nk_layout_row_push(ctx, 8);
            nk_spacing(ctx, 1);

            nk_layout_row_push(ctx, 45);
            if (nk_button_label(ctx, "Add")) {
                add_breakpoint();
            }
            nk_layout_row_end(ctx);

            /* Current ANTIC position at the last debugger stop */
            nk_layout_row_begin(ctx, NK_STATIC, row_height, 4);
            nk_layout_row_push(ctx, 70);
            nk_label(ctx, "Scanline:", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 55);
            nk_label(ctx, dbg_state.control_scanline_buf, NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 50);
            nk_label(ctx, "Cycle:", NK_TEXT_LEFT);
            nk_layout_row_push(ctx, 55);
            nk_label(ctx, dbg_state.control_cycle_buf, NK_TEXT_LEFT);
            nk_layout_row_end(ctx);

            if (debug.cpu_crash_flag) {
                sprintf(crash_line, "CPU Crash: PC $%04X OP $%02X",
                        debug.cpu_crash_pc, debug.cpu_crash_opcode);
                nk_layout_row_dynamic(ctx, row_height, 1);
                nk_label_colored(ctx, crash_line, NK_TEXT_LEFT, g_theme.error);
            }

            nk_group_end(ctx);
        }

        /* === Right group: Execution controls === */
        nk_layout_row_push(ctx, right_w);
        if (nk_group_begin(ctx, "ctrl_group", 0)) {
            nk_layout_row_dynamic(ctx, row_height, 1);
            if (nk_button_label(ctx, dbg_state.paused ? "Run" : "Pause")) {
                dbg_state.paused = !dbg_state.paused;
                gui_set_run_current(!dbg_state.paused);
                console_set_debug_mode(dbg_state.paused);
                if (!dbg_state.paused) {
                    /* Ensure debug memory dispatch is active so breakpoints fire */
                    t_atari *console = console_get_ptr();
                    mem_assign_ops_debug(console->cpu, &debug);
                    /* Resuming full speed: clear any pending step flags */
                    debugger_clear_execution_flags();
                    debugger_clear_crash_status();
                } else {
                    /* Pausing: mark buffers dirty so they refresh */
                    dbg_state.dirty = 1;
                }
            }

            if (dbg_state.paused) {
                nk_layout_row_dynamic(ctx, row_height, 1);
                if (nk_button_label(ctx, "Step")) {
                    debugger_clear_crash_status();
                    debug.single_step_flag = 1;
                    dbg_state.paused = 0;
                    if (!dbg_state.in_halt)
                        gui_set_run_current(1);
                }

                nk_layout_row_dynamic(ctx, row_height, 1);
                if (nk_button_label(ctx, "Scanline")) {
                    debugger_clear_crash_status();
                    debug.single_scanline_flag = 1;
                    dbg_state.paused = 0;
                    if (!dbg_state.in_halt)
                        gui_set_run_current(1);
                }

                nk_layout_row_dynamic(ctx, row_height, 1);
                if (nk_button_label(ctx, "Frame")) {
                    debugger_clear_crash_status();
                    if (dbg_state.in_halt) {
                        /* Mid-frame: exit halt, run_frame() finishes, detect flag in draw */
                        debug.single_frame_flag = 1;
                        dbg_state.paused = 0;
                    } else {
                        /* Between frames: execute one frame directly */
                        run_frame();
                        pc_render_frame_sound();
                        dbg_state.dirty = 1;
                    }
                }

                nk_layout_row_dynamic(ctx, row_height, 1);
                if (nk_button_label(ctx, "Reset")) {
                    debugger_request_reset();
                }
            }

            /* Trace file selector */
            nk_layout_row_dynamic(ctx, row_height, 1);
            if (nk_button_label(ctx, "Trace File...")) {
                struct modal_config *cfg = guitop_get_modal_cfg();
                memset(cfg, 0, sizeof(struct modal_config));
                cfg->modal_type = MODAL_FB_SAVE;
                cfg->modal_active = 2;
                strcpy(cfg->title, "Trace File");
                strcpy(cfg->accept_btn_label, "Save");
                strcpy(cfg->cancel_btn_label, "Cancel");
                cfg->post_show = trace_file_selected;
                filebrowser_init(cfg);
            }

            nk_layout_row_dynamic(ctx, row_height, 1);
            {
                const char *display;
                if (dbg_state.trace_file[0]) {
                    const char *name = strrchr(dbg_state.trace_file, '/');
                    display = name ? name + 1 : dbg_state.trace_file;
                } else {
                    display = "No file selected";
                }
                nk_label(ctx, display, NK_TEXT_LEFT);
            }

            nk_layout_row_dynamic(ctx, row_height, 1);
            if (!dbg_state.trace_recording) {
                if (!dbg_state.trace_file[0]) {
                    nk_style_push_style_item(ctx, &ctx->style.button.normal,
                        nk_style_item_color(nk_rgb(60, 60, 60)));
                    nk_style_push_style_item(ctx, &ctx->style.button.hover,
                        nk_style_item_color(nk_rgb(60, 60, 60)));
                    nk_style_push_style_item(ctx, &ctx->style.button.active,
                        nk_style_item_color(nk_rgb(60, 60, 60)));
                    nk_style_push_color(ctx, &ctx->style.button.text_normal, nk_rgb(100, 100, 100));
                    nk_style_push_color(ctx, &ctx->style.button.text_hover,  nk_rgb(100, 100, 100));
                    nk_style_push_color(ctx, &ctx->style.button.text_active, nk_rgb(100, 100, 100));
                    nk_button_label(ctx, "\xef\x84\x91 Start Trace");
                    nk_style_pop_color(ctx);
                    nk_style_pop_color(ctx);
                    nk_style_pop_color(ctx);
                    nk_style_pop_style_item(ctx);
                    nk_style_pop_style_item(ctx);
                    nk_style_pop_style_item(ctx);
                } else {
                    if (nk_button_label(ctx, "\xef\x84\x91 Start Trace")) {
                        debug_open_trace(dbg_state.trace_file);
                        dbg_state.trace_recording = 1;
                    }
                }
            } else {
                if (nk_button_label(ctx, "\xef\x81\x8d Stop Trace")) {
                    debug_close_trace();
                    dbg_state.trace_recording = 0;
                }
            }

            nk_group_end(ctx);
        }
        nk_layout_row_end(ctx);

        /* Back button - full width below columns */
        nk_layout_row_dynamic(ctx, row_height, 1);
        if (nk_button_label(ctx, "Back to Menu")) {
            debugger_hide();
        }
    }
    nk_end(ctx);
}

static void draw_video_window(struct nk_context *ctx)
{
    nk_flags flags = NK_WINDOW_TITLE | NK_WINDOW_MOVABLE | NK_WINDOW_SCALABLE |
                     NK_WINDOW_BORDER | NK_WINDOW_MINIMIZABLE;

    dbg_state.video_has_focus = 0;

    if (nk_begin(ctx, "Video", dbg_state.video_bounds, flags)) {
        dbg_state.video_has_focus = nk_window_has_focus(ctx);
        dbg_state.video_bounds = nk_window_get_bounds(ctx);
        check_and_save_window_pos("video", &dbg_state.video_bounds,
                                  &dbg_state.video_saved_x, &dbg_state.video_saved_y,
                                  &dbg_state.video_saved_w, &dbg_state.video_saved_h);

        /* Guard: surface must be initialised and draw pointer set up */
        if (!g_pc_if.surface || !g_pc_if.draw_pixel_start) {
            nk_layout_row_dynamic(ctx, 20, 1);
            nk_label(ctx, "No video surface", NK_TEXT_CENTERED);
        } else {
            int img_w = g_pc_if.draw_video_rect.w;
            int img_h = g_pc_if.draw_video_rect.h;

            /* Recreate texture if first use or dimensions changed (e.g. NTSC toggle) */
            if (dbg_state.video_texture &&
                (dbg_state.video_tex_w != img_w || dbg_state.video_tex_h != img_h)) {
                SDL_DestroyTexture(dbg_state.video_texture);
                dbg_state.video_texture = NULL;
            }
            if (!dbg_state.video_texture) {
                dbg_state.video_texture = SDL_CreateTexture(g_pc_if.renderer,
                                                            SDL_PIXELFORMAT_ARGB8888,
                                                            SDL_TEXTUREACCESS_STREAMING,
                                                            img_w, img_h);
                if (dbg_state.video_texture) {
                    SDL_SetTextureBlendMode(dbg_state.video_texture, SDL_BLENDMODE_NONE);
                    SDL_SetTextureScaleMode(dbg_state.video_texture, SDL_SCALEMODE_NEAREST);
                    dbg_state.video_tex_w = img_w;
                    dbg_state.video_tex_h = img_h;
                    dbg_state.video_dirty = 1; /* newly created — must upload once */
                }
            }

            if (dbg_state.video_texture) {
                /* While running, update every frame; while paused, only on dirty */
                if (!dbg_state.paused)
                    dbg_state.video_dirty = 1;
                if (dbg_state.video_dirty) {
                    SDL_UpdateTexture(dbg_state.video_texture, NULL,
                                      g_pc_if.draw_pixel_start,
                                      g_pc_if.draw_pitch);
                    dbg_state.video_dirty = 0;
                }

                struct nk_vec2 avail = nk_window_get_content_region_size(ctx);

                if (avail.x >= img_w && avail.y >= img_h) {
                    /* Window is larger — scale up to fit, preserve aspect ratio */
                    float scale = (avail.x / img_w < avail.y / img_h)
                                  ? avail.x / img_w : avail.y / img_h;
                    int draw_w = (int)(img_w * scale);
                    int draw_h = (int)(img_h * scale);
                    nk_layout_row_static(ctx, (float)draw_h, draw_w, 1);
                    nk_image(ctx, nk_image_ptr((void *)dbg_state.video_texture));
                } else {
                    /* Window is smaller — show at native size with scrollbars */
                    nk_layout_row_dynamic(ctx, avail.y, 1);
                    if (nk_group_begin(ctx, "VideoContent", 0)) {
                        nk_layout_row_static(ctx, (float)img_h, img_w, 1);
                        nk_image(ctx, nk_image_ptr((void *)dbg_state.video_texture));
                        nk_group_end(ctx);
                    }
                }
            }
        }
    }
    nk_end(ctx);
}

void debugger_draw(struct nk_context *ctx, int window_w, int window_h)
{
    (void)window_w;
    (void)window_h;

    /* Suppress breakpoints during all debugger drawing to avoid re-entrancy:
       any rd_6502 -> debug_search_for_breakpoint -> halt -> gui_run_frame -> debugger_draw */
    int saved_bp_flag = debug_get_breakpoint_flag();

    if (dbg_state.reset_pending && !dbg_state.in_halt)
        saved_bp_flag = dbg_state.reset_restore_breakpoint_flag;

    dbg_state.draw_breakpoint_flag = saved_bp_flag;
    debug_set_breakpoint_flag(0);

    if (dbg_state.reset_pending && !dbg_state.in_halt)
        debugger_perform_reset();

    /* Single-frame step complete: re-pause after run_frame() finished */
    if (debug.single_frame_flag) {
        debug.single_frame_flag = 0;
        dbg_state.paused = 1;
        dbg_state.dirty = 1;
        gui_set_run_current(0);
    }

    /* Refresh all display buffers once when state has changed */
    if (dbg_state.paused && dbg_state.dirty) {
        debugger_refresh_buffers();
        dbg_state.dirty = 0;
    }

    /* Draw all debug windows */
    draw_cpu_window(ctx);
    draw_disasm_window(ctx);
    draw_antic_window(ctx);
    draw_gtia_window(ctx);
    draw_pokey_window(ctx);
    draw_control_window(ctx);
    draw_video_window(ctx);

    /*
     * debugger_hide() may be invoked from a button during this draw.  If that
     * happens, do not resurrect the breakpoint state we saved on entry; the
     * debugger close path intentionally disarms it for normal emulation.
     */
    if (dbg_state.active) {
        if (dbg_state.reset_pending && dbg_state.in_halt)
            debug_set_breakpoint_flag(0);
        else
            /* Use draw_breakpoint_flag (initialized to saved_bp_flag above) so
               add/delete_breakpoint changes made during this draw survive. */
            debug_set_breakpoint_flag(dbg_state.draw_breakpoint_flag);
    }
}

void debugger_breakpoint_halt(void)
{
    t_config *p_config = config_get_global_cfg();
    t_atari *p_console = console_get_ptr();
    double throttle_time = 0;
    double next_frame_time;
    Uint64 stop;

    /* Skip if debugger was closed */
    if (!dbg_state.active) return;

    /* Pause emulation and mark display buffers dirty */
    dbg_state.paused = 1;
    dbg_state.dirty = 1;
    dbg_state.in_halt = 1;
    gui_set_run_current(0);

    if (p_config->general.throttle) {
        throttle_time = 100000000.0 / p_console->frames_per_sec /
                        (double)p_config->general.throttle;
    }
    next_frame_time = (double)pc_get_ticks_us();

    /* Mini event loop — mirrors run_system() GUI path */
    while (dbg_state.paused && dbg_state.active) {
        unsigned int status = gui_run_frame();
        kat5200_set_status(status);

        if (status & KSTATUS_EXIT) {
            debug.single_step_flag = 0;
            debug.single_scanline_flag = 0;
            debug.single_frame_flag = 0;
            break;
        }

        if (pc_poll_events() < 0) {
            debug.single_step_flag = 0;
            debug.single_scanline_flag = 0;
            debug.single_frame_flag = 0;
            kat5200_set_status(kat5200_get_status() | KSTATUS_EXIT);
            break;
        }

        /* Frame throttle */
        if (p_config->general.throttle) {
            throttle_time = 100000000.0 / p_console->frames_per_sec /
                            (double)p_config->general.throttle;
            next_frame_time += throttle_time;
            stop = pc_get_ticks_us();
            if ((double)stop < next_frame_time)
                pc_delay_precise_ns((Uint64)((next_frame_time - (double)stop) * 1000.0));
            stop = pc_get_ticks_us();
            if (((double)stop - next_frame_time) > throttle_time)
                next_frame_time = (double)stop;
        }
    }

    dbg_state.in_halt = 0;

    /* User hit Run — restore run state if debugger still active */
    if (dbg_state.active && !(kat5200_get_status() & KSTATUS_EXIT)) {
        gui_set_run_current(1);
    }
}
