OXYGENHUB
◆ main enhanced-x16-emulator / src / debug_ui / panels / vera_panel.cpp
41.1 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// VERA panel — graphical VRAM viewers for the Commander X16's VERA.
2
//
3
// Tabs:
4
//   * Palette  — the 256-entry VERA palette ($1FA00..$1FBFF) as a 16x16 grid.
5
//   * Tiles    — a freeform tile/character viewer (base addr, bpp, tile size).
6
//   * Sprites  — the 128 sprite attribute entries ($1FC00..$1FFFF), decoded.
7
//   * Bitmap   — a layer's bitmap-mode image (from the layer registers).
8
//   * Tilemap  — a layer's tile/text map (from the layer registers).
9
//
10
// Technique: each viewer decodes VRAM into an RGBA (ABGR8888) pixel buffer,
11
// uploads it to a STREAMING SDL_Texture via the debugger's SDL_Renderer, and
12
// shows it with ImGui::Image. Textures are function-static and only recreated
13
// when their dimensions change, so there is no per-frame allocation/leak. Only
14
// the active tab decodes each frame (ImGui::BeginTabItem gates the work), so the
15
// cost of the heavier viewers is only paid when they are visible. All VRAM
16
// access goes through video_space_read()/debug_ui_vram_read_range(), which mask
17
// the address into the 17-bit VRAM space, so reads can never go out of bounds.
18
#include "imgui.h"
19
#include "debug_ui_panels.h"
20
#include "debug_ui_bridge.h"
21
#include "debug_ui.h"
22
#include "debug_ui_widgets.h" // dbgui_hover_value_tooltip / dbgui_format_binary
23
#include "sprite_multiplex_tab.h"
24
25
#include <SDL.h>
26
#include <stdint.h>
27
#include <string.h>
28
29
namespace {
30
31
constexpr uint32_t ADDR_PALETTE     = 0x1FA00; // 256 * 2 bytes, 12-bit RGB
32
constexpr uint32_t ADDR_SPRITE_ATTR = 0x1FC00; // 128 * 8 bytes
33
constexpr int      NUM_SPRITES      = 128;
34
35
inline int clampi(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }
36
37
// Expand a 4-bit VERA color channel (0..15) to 8 bits by nibble replication,
38
// exactly like refresh_palette() in video.c (x<<4 | x).
39
inline uint8_t expand4(uint8_t v) { return (uint8_t)((v << 4) | v); }
40
41
// Build the 256-entry palette as ABGR8888 (alpha forced opaque). VERA stores
42
// each entry as two bytes: byte0 = GGGGBBBB, byte1 = 0000RRRR.
43
void
44
build_palette(uint32_t out[256])
45
{
46
    for (int i = 0; i < 256; ++i) {
47
        uint8_t b0 = video_space_read(ADDR_PALETTE + i * 2);     // GGGGBBBB
48
        uint8_t b1 = video_space_read(ADDR_PALETTE + i * 2 + 1); // 0000RRRR
49
        uint8_t r  = expand4(b1 & 0x0f);
50
        uint8_t g  = expand4((b0 >> 4) & 0x0f);
51
        uint8_t b  = expand4(b0 & 0x0f);
52
        out[i] = 0xFF000000u | ((uint32_t)b << 16) | ((uint32_t)g << 8) | (uint32_t)r;
53
    }
54
}
55
56
// A lazily-created, resizable STREAMING texture. Kept as a file-/function-static
57
// so it lives for the app's lifetime; recreated only when the size changes.
58
struct GfxTexture {
59
    SDL_Texture *tex = nullptr;
60
    int          w   = 0;
61
    int          h   = 0;
62
63
    bool ensure(int width, int height)
64
    {
65
        if (width <= 0 || height <= 0)
66
            return false;
67
        if (tex && w == width && h == height)
68
            return true;
69
        if (tex) {
70
            SDL_DestroyTexture(tex);
71
            tex = nullptr;
72
        }
73
        SDL_Renderer *ren = debug_ui_get_renderer();
74
        if (!ren)
75
            return false;
76
        tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ABGR8888,
77
                                SDL_TEXTUREACCESS_STREAMING, width, height);
78
        if (!tex)
79
            return false;
80
        SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
81
        SDL_SetTextureScaleMode(tex, SDL_ScaleModeNearest); // crisp zoomed pixels
82
        w = width;
83
        h = height;
84
        return true;
85
    }
86
87
    void update(const uint32_t *pixels)
88
    {
89
        if (tex)
90
            SDL_UpdateTexture(tex, nullptr, pixels, w * (int)sizeof(uint32_t));
91
    }
92
93
    ImTextureID id() const { return (ImTextureID)(intptr_t)tex; }
94
};
95
96
// A reusable, growable RGBA scratch buffer for building images.
97
struct PixelBuffer {
98
    uint32_t *data = nullptr;
99
    size_t    cap  = 0;
100
101
    uint32_t *ensure(size_t count)
102
    {
103
        if (count > cap) {
104
            free(data);
105
            data = (uint32_t *)malloc(count * sizeof(uint32_t));
106
            cap  = data ? count : 0;
107
        }
108
        return data;
109
    }
110
};
111
112
// Decode one indexed color to a display pixel. index 0 stays transparent when
113
// `transparent0` is set (sprites); otherwise it renders as palette[0] (tiles).
114
inline uint32_t
115
resolve_color(const uint32_t pal[256], int idx, int palette_offset, bool transparent0)
116
{
117
    if (idx == 0 && transparent0)
118
        return 0; // fully transparent
119
    int ci = idx;
120
    if (ci > 0 && ci < 16)
121
        ci += palette_offset;
122
    return pal[ci & 0xff];
123
}
124
125
// ---------------------------------------------------------------------------
126
// Palette tab
127
// ---------------------------------------------------------------------------
128
void
129
draw_palette_tab()
130
{
131
    ImGui::TextUnformatted("VERA palette — $1FA00..$1FBFF (256 entries, 12-bit RGB)");
132
    ImGui::Spacing();
133
134
    uint32_t pal[256];
135
    build_palette(pal);
136
137
    const ImGuiColorEditFlags flags =
138
        ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoAlpha |
139
        ImGuiColorEditFlags_NoDragDrop | ImGuiColorEditFlags_NoBorder;
140
    const ImVec2 sw(20.0f, 20.0f);
141
142
    for (int i = 0; i < 256; ++i) {
143
        ImGui::PushID(i);
144
        uint32_t c = pal[i];
145
        uint8_t  r = c & 0xff, g = (c >> 8) & 0xff, b = (c >> 16) & 0xff;
146
        ImVec4   col(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f);
147
        ImGui::ColorButton("##sw", col, flags, sw);
148
        if (ImGui::IsItemHovered()) {
149
            uint8_t b0 = video_space_read(ADDR_PALETTE + i * 2);
150
            uint8_t b1 = video_space_read(ADDR_PALETTE + i * 2 + 1);
151
            ImGui::SetTooltip("Index %3d ($%02X)\nRGB $%X%X%X  (%d, %d, %d)\nVRAM $%05X: $%02X $%02X",
152
                              i, i, (b1 & 0x0f), (b0 >> 4) & 0x0f, b0 & 0x0f, r, g, b,
153
                              ADDR_PALETTE + i * 2, b0, b1);
154
        }
155
        if ((i % 16) != 15)
156
            ImGui::SameLine();
157
        ImGui::PopID();
158
    }
159
}
160
161
// ---------------------------------------------------------------------------
162
// Tile / character viewer tab
163
// ---------------------------------------------------------------------------
164
void
165
draw_tiles_tab(const uint32_t pal[256])
166
{
167
    static int   base       = 0x00000;
168
    static int   bpp_idx    = 2;   // 0:1bpp 1:2bpp 2:4bpp 3:8bpp
169
    static int   tw_idx     = 0;   // 0:8   1:16
170
    static int   th_idx     = 0;   // 0:8   1:16
171
    static int   cols       = 16;
172
    static int   rows       = 16;
173
    static int   pal_off    = 0;   // 0..15 (times 16)
174
    static float zoom       = 2.0f;
175
    static int   selected   = -1;
176
177
    static const int   bpp_vals[4] = { 1, 2, 4, 8 };
178
    static const char *bpp_names   = "1 bpp\0" "2 bpp\0" "4 bpp\0" "8 bpp\0";
179
    static const char *dim_names   = "8\0" "16\0";
180
181
    // Row 1 — base address (type any hex address to jump straight there) + format.
182
    // A plain "%05X" field (no "$" in the format) keeps hex text-entry unambiguous.
183
    ImGui::TextUnformatted("Base $");
184
    ImGui::SameLine(0.0f, 2.0f);
185
    ImGui::PushItemWidth(90);
186
    ImGui::InputScalar("##tilebase", ImGuiDataType_S32, &base, nullptr, nullptr, "%05X",
187
                       ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase);
188
    ImGui::SameLine();
189
    ImGui::Combo("Depth", &bpp_idx, bpp_names);
190
    ImGui::SameLine();
191
    ImGui::Combo("W", &tw_idx, dim_names);
192
    ImGui::SameLine();
193
    ImGui::Combo("H", &th_idx, dim_names);
194
    ImGui::PopItemWidth();
195
196
    // Geometry (computed here so the navigation controls below can use it).
197
    const int bpp = bpp_vals[bpp_idx];
198
    const int tw  = tw_idx ? 16 : 8;
199
    const int th  = th_idx ? 16 : 8;
200
    const int bytes_per_tile = tw * th * bpp / 8;
201
    cols = clampi(cols, 1, 64);
202
    rows = clampi(rows, 1, 64);
203
    const int row_step  = cols * bytes_per_tile;
204
    const int page_step = cols * rows * bytes_per_tile;
205
206
    base = clampi(base, 0, 0x1FFFF);
207
208
    // Row 2 — jump by tile index + alignment.
209
    int tile_no = bytes_per_tile ? base / bytes_per_tile : 0;
210
    ImGui::PushItemWidth(110);
211
    if (ImGui::InputInt("Tile #", &tile_no, 1, 16))
212
        base = clampi(tile_no * bytes_per_tile, 0, 0x1FFFF);
213
    ImGui::PopItemWidth();
214
    ImGui::SameLine();
215
    if (ImGui::SmallButton("Align"))          // snap to the current tile stride
216
        base -= base % bytes_per_tile;
217
    ImGui::SameLine();
218
    if (ImGui::SmallButton("Align $800"))     // snap to VERA tile-base granularity (2KB)
219
        base &= ~0x7FF;
220
    ImGui::SameLine();
221
    ImGui::TextDisabled("(?)");
222
    if (ImGui::IsItemHovered())
223
        ImGui::SetTooltip("Type a hex address in Base, or a tile index in Tile #, to jump.\n"
224
                          "Step with the buttons below, or drag the scrollbar for fast scrolling.\n"
225
                          "-1/+1 nudge by one byte; Align snaps to the tile stride ($800 = VERA tilebase).");
226
227
    // Row 3 — stepping: home / page / row / tile / byte-nudge.
228
    if (ImGui::SmallButton("|< Home")) base = 0;
229
    ImGui::SameLine(); if (ImGui::SmallButton("<< Page")) base -= page_step;
230
    ImGui::SameLine(); if (ImGui::SmallButton("< Row"))   base -= row_step;
231
    ImGui::SameLine(); if (ImGui::SmallButton("< Tile"))  base -= bytes_per_tile;
232
    ImGui::SameLine(); if (ImGui::SmallButton("-1"))      base -= 1;
233
    ImGui::SameLine(); if (ImGui::SmallButton("+1"))      base += 1;
234
    ImGui::SameLine(); if (ImGui::SmallButton("Tile >"))  base += bytes_per_tile;
235
    ImGui::SameLine(); if (ImGui::SmallButton("Row >"))   base += row_step;
236
    ImGui::SameLine(); if (ImGui::SmallButton("Page >>")) base += page_step;
237
    base = clampi(base, 0, 0x1FFFF);
238
239
    // Row 4 — full-width fast-scroll bar across all of VRAM (tile-aligned).
240
    ImGui::PushItemWidth(-1);
241
    if (ImGui::SliderInt("##tilescroll", &base, 0, 0x1FFFF, "scroll -> $%05X"))
242
        base -= base % bytes_per_tile; // keep tiles aligned while dragging
243
    ImGui::PopItemWidth();
244
    base = clampi(base, 0, 0x1FFFF);
245
246
    // Row 5 — grid geometry.
247
    ImGui::PushItemWidth(110);
248
    ImGui::SliderInt("Columns", &cols, 1, 64);
249
    ImGui::SameLine();
250
    ImGui::SliderInt("Rows", &rows, 1, 64);
251
    ImGui::SameLine();
252
    ImGui::SliderInt("Pal offset", &pal_off, 0, 15);
253
    ImGui::SameLine();
254
    ImGui::SliderFloat("Zoom", &zoom, 1.0f, 8.0f, "%.0fx");
255
    ImGui::PopItemWidth();
256
257
    cols = clampi(cols, 1, 64);
258
    rows = clampi(rows, 1, 64);
259
    const int img_w = cols * tw;
260
    const int img_h = rows * th;
261
    const int mask  = (1 << bpp) - 1;
262
263
    static PixelBuffer buf;
264
    uint32_t *px = buf.ensure((size_t)img_w * img_h);
265
    if (!px)
266
        return;
267
268
    for (int t = 0; t < cols * rows; ++t) {
269
        const int      tx        = t % cols;
270
        const int      ty        = t / cols;
271
        const uint32_t tile_addr = (uint32_t)base + (uint32_t)t * bytes_per_tile;
272
        for (int y = 0; y < th; ++y) {
273
            for (int x = 0; x < tw; ++x) {
274
                const uint32_t bitpos = (uint32_t)(y * tw + x) * bpp;
275
                const uint8_t  byte   = video_space_read(tile_addr + (bitpos >> 3));
276
                const int      shift  = 8 - bpp - (int)(bitpos & 7);
277
                const int      idx    = (byte >> shift) & mask;
278
                const uint32_t color  = resolve_color(pal, idx, pal_off << 4, false);
279
                px[(size_t)(ty * th + y) * img_w + (tx * tw + x)] = color;
280
            }
281
        }
282
    }
283
284
    static GfxTexture gt;
285
    if (!gt.ensure(img_w, img_h))
286
        return;
287
    gt.update(px);
288
289
    ImGui::Separator();
290
    const ImVec2 img_pos = ImGui::GetCursorScreenPos();
291
    ImGui::Image(gt.id(), ImVec2(img_w * zoom, img_h * zoom));
292
293
    int hovered = -1;
294
    if (ImGui::IsItemHovered()) {
295
        const ImVec2 m  = ImGui::GetIO().MousePos;
296
        const int    lx = (int)((m.x - img_pos.x) / zoom);
297
        const int    ly = (int)((m.y - img_pos.y) / zoom);
298
        const int    htx = lx / tw;
299
        const int    hty = ly / th;
300
        if (htx >= 0 && htx < cols && hty >= 0 && hty < rows) {
301
            hovered = hty * cols + htx;
302
            const uint32_t a = (uint32_t)base + (uint32_t)hovered * bytes_per_tile;
303
            ImGui::SetTooltip("Tile %d  (col %d, row %d)\nVRAM $%05X..$%05X",
304
                              hovered, htx, hty, a, a + bytes_per_tile - 1);
305
            if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
306
                selected = hovered;
307
        }
308
    }
309
310
    if (selected >= 0 && selected < cols * rows) {
311
        const uint32_t a = (uint32_t)base + (uint32_t)selected * bytes_per_tile;
312
        ImGui::Text("Selected tile %d — VRAM $%05X (%d bytes/tile)", selected, a, bytes_per_tile);
313
    } else {
314
        ImGui::TextUnformatted("Hover a tile for its address; click to select.");
315
    }
316
}
317
318
// ---------------------------------------------------------------------------
319
// Sprite viewer tab
320
// ---------------------------------------------------------------------------
321
struct SpriteInfo {
322
    uint32_t address;
323
    int      mode; // 0:4bpp 1:8bpp
324
    int      w, h;
325
    int      x, y;
326
    int      zdepth;
327
    int      pal_off;
328
    bool     hflip, vflip;
329
    int      collision;
330
};
331
332
SpriteInfo
333
decode_sprite_attr(int i)
334
{
335
    uint8_t a[8];
336
    for (int b = 0; b < 8; ++b)
337
        a[b] = video_space_read(ADDR_SPRITE_ATTR + i * 8 + b);
338
339
    SpriteInfo s;
340
    s.address   = ((uint32_t)a[0] << 5) | ((uint32_t)(a[1] & 0x0f) << 13);
341
    s.mode      = (a[1] >> 7) & 1;
342
    s.w         = 1 << (((a[7] >> 4) & 3) + 3);
343
    s.h         = 1 << (((a[7] >> 6) & 3) + 3);
344
    s.pal_off   = (a[7] & 0x0f) << 4;
345
    s.zdepth    = (a[6] >> 2) & 3;
346
    s.collision = a[6] & 0xf0;
347
    s.hflip     = a[6] & 1;
348
    s.vflip     = (a[6] >> 1) & 1;
349
    s.x         = a[2] | ((a[3] & 3) << 8);
350
    s.y         = a[4] | ((a[5] & 3) << 8);
351
    if (s.x >= 0x400 - s.w)
352
        s.x -= 0x400;
353
    if (s.y >= 0x400 - s.h)
354
        s.y -= 0x400;
355
    return s;
356
}
357
358
// Decode sprite `s` into the atlas cell at (cx, cy). Cells are 64x64 and the
359
// atlas is atlas_w wide; the sprite is drawn top-left in its cell.
360
void
361
decode_sprite_pixels(const SpriteInfo &s, const uint32_t pal[256],
362
                     uint32_t *atlas, int atlas_w, int cx, int cy)
363
{
364
    for (int y = 0; y < s.h; ++y) {
365
        for (int x = 0; x < s.w; ++x) {
366
            int idx;
367
            if (s.mode) { // 8bpp
368
                idx = video_space_read(s.address + (uint32_t)y * s.w + x);
369
            } else {      // 4bpp, high nibble = left pixel
370
                uint8_t byte = video_space_read(s.address + (uint32_t)y * (s.w / 2) + (x / 2));
371
                idx = (x & 1) ? (byte & 0x0f) : (byte >> 4);
372
            }
373
            atlas[(size_t)(cy + y) * atlas_w + (cx + x)] =
374
                resolve_color(pal, idx, s.pal_off, true);
375
        }
376
    }
377
}
378
379
void
380
draw_sprites_tab(const uint32_t pal[256])
381
{
382
    static bool  hide_disabled = false;
383
    static float zoom          = 2.0f;
384
385
    ImGui::Checkbox("Hide disabled (z=0)", &hide_disabled);
386
    ImGui::SameLine();
387
    ImGui::PushItemWidth(120);
388
    ImGui::SliderFloat("Preview zoom", &zoom, 1.0f, 4.0f, "%.0fx");
389
    ImGui::PopItemWidth();
390
391
    // One atlas texture holds every sprite: 16 columns x 8 rows of 64x64 cells.
392
    constexpr int CELL       = 64;
393
    constexpr int ATLAS_COLS = 16;
394
    constexpr int ATLAS_ROWS = 8;
395
    constexpr int ATLAS_W    = CELL * ATLAS_COLS; // 1024
396
    constexpr int ATLAS_H    = CELL * ATLAS_ROWS; // 512
397
398
    static PixelBuffer buf;
399
    uint32_t *atlas = buf.ensure((size_t)ATLAS_W * ATLAS_H);
400
    if (!atlas)
401
        return;
402
    memset(atlas, 0, (size_t)ATLAS_W * ATLAS_H * sizeof(uint32_t));
403
404
    SpriteInfo info[NUM_SPRITES];
405
    for (int i = 0; i < NUM_SPRITES; ++i) {
406
        info[i] = decode_sprite_attr(i);
407
        const int cx = (i % ATLAS_COLS) * CELL;
408
        const int cy = (i / ATLAS_COLS) * CELL;
409
        decode_sprite_pixels(info[i], pal, atlas, ATLAS_W, cx, cy);
410
    }
411
412
    static GfxTexture gt;
413
    if (!gt.ensure(ATLAS_W, ATLAS_H))
414
        return;
415
    gt.update(atlas);
416
417
    ImGui::Separator();
418
    const ImGuiTableFlags tflags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
419
                                   ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit |
420
                                   DBGUI_TABLE_FLAGS_RESIZABLE;
421
    if (ImGui::BeginTable("sprites", 9, tflags, ImVec2(0, 0))) {
422
        ImGui::TableSetupScrollFreeze(0, 1);
423
        ImGui::TableSetupColumn("#");
424
        ImGui::TableSetupColumn("Image");
425
        ImGui::TableSetupColumn("Addr");
426
        ImGui::TableSetupColumn("Mode");
427
        ImGui::TableSetupColumn("Size");
428
        ImGui::TableSetupColumn("X,Y");
429
        ImGui::TableSetupColumn("Z");
430
        ImGui::TableSetupColumn("PalOff");
431
        ImGui::TableSetupColumn("Flip");
432
        ImGui::TableHeadersRow();
433
434
        for (int i = 0; i < NUM_SPRITES; ++i) {
435
            const SpriteInfo &s = info[i];
436
            if (hide_disabled && s.zdepth == 0)
437
                continue;
438
439
            ImGui::TableNextRow();
440
            ImGui::TableNextColumn();
441
            ImGui::Text("%d", i);
442
443
            ImGui::TableNextColumn();
444
            const int cx = (i % ATLAS_COLS) * CELL;
445
            const int cy = (i / ATLAS_COLS) * CELL;
446
            ImVec2 uv0((float)cx / ATLAS_W, (float)cy / ATLAS_H);
447
            ImVec2 uv1((float)(cx + s.w) / ATLAS_W, (float)(cy + s.h) / ATLAS_H);
448
            ImGui::Image(gt.id(), ImVec2(s.w * zoom, s.h * zoom), uv0, uv1);
449
450
            ImGui::TableNextColumn();
451
            ImGui::Text("$%05X", s.address);
452
            ImGui::TableNextColumn();
453
            ImGui::TextUnformatted(s.mode ? "8bpp" : "4bpp");
454
            ImGui::TableNextColumn();
455
            ImGui::Text("%dx%d", s.w, s.h);
456
            ImGui::TableNextColumn();
457
            ImGui::Text("%d,%d", s.x, s.y);
458
            ImGui::TableNextColumn();
459
            ImGui::Text("%d", s.zdepth);
460
            ImGui::TableNextColumn();
461
            ImGui::Text("$%02X", s.pal_off);
462
            ImGui::TableNextColumn();
463
            ImGui::TextUnformatted(s.hflip && s.vflip ? "HV" : s.hflip ? "H" : s.vflip ? "V" : "-");
464
        }
465
        ImGui::EndTable();
466
    }
467
}
468
469
// ---------------------------------------------------------------------------
470
// Layer helpers (shared by Bitmap + Tilemap tabs)
471
// ---------------------------------------------------------------------------
472
struct LayerRegs {
473
    int      color_depth;   // 0..3
474
    int      bpp;           // 1,2,4,8
475
    bool     bitmap_mode;
476
    bool     text_mode;
477
    bool     text_256c;
478
    uint32_t map_base;
479
    uint32_t tile_base;
480
    int      mapw, maph;    // in tiles
481
    int      tilew, tileh;  // in pixels
482
    int      tile_size_log2;
483
    int      bitmap_w;      // 320 or 640
484
    int      bitmap_pal_off;
485
    int      hscroll, vscroll;
486
};
487
488
LayerRegs
489
decode_layer_regs(const uint8_t r[7])
490
{
491
    uint8_t r0 = r[0], r1 = r[1], r2 = r[2], r3 = r[3], r4 = r[4], r5 = r[5], r6 = r[6];
492
493
    LayerRegs L;
494
    L.color_depth    = r0 & 3;
495
    L.bpp            = 1 << L.color_depth;
496
    L.bitmap_mode    = (r0 & 4) != 0;
497
    L.text_mode      = (L.color_depth == 0) && !L.bitmap_mode;
498
    L.text_256c      = (r0 & 8) != 0;
499
    L.map_base       = (uint32_t)r1 << 9;
500
    L.tile_base      = (uint32_t)(r2 & 0xFC) << 9;
501
    L.mapw           = 1 << (5 + ((r0 >> 4) & 3));
502
    L.maph           = 1 << (5 + ((r0 >> 6) & 3));
503
    L.tilew          = 1 << (3 + (r2 & 1));
504
    L.tileh          = 1 << (3 + ((r2 >> 1) & 1));
505
    L.tile_size_log2 = (3 + (r2 & 1)) + (3 + ((r2 >> 1) & 1)) + L.color_depth - 3;
506
    L.bitmap_w       = (r2 & 1) ? 640 : 320;
507
    L.bitmap_pal_off = (r4 & 0x0f) << 4;
508
    L.hscroll        = r3 | ((r4 & 0x0f) << 8);
509
    L.vscroll        = r5 | ((r6 & 0x0f) << 8);
510
    return L;
511
}
512
513
LayerRegs
514
read_layer_regs(int layer)
515
{
516
    const uint8_t base = layer == 0 ? 0x0D : 0x14;
517
    uint8_t r[7];
518
    for (int i = 0; i < 7; ++i)
519
        r[i] = video_read(base + i, true);
520
    return decode_layer_regs(r);
521
}
522
523
// True when two snapshots describe the same tile geometry, i.e. a per-scanline
524
// snapshot can be substituted for the live one without changing image layout.
525
bool
526
same_layer_geometry(const LayerRegs &a, const LayerRegs &b)
527
{
528
    return a.color_depth == b.color_depth && a.bitmap_mode == b.bitmap_mode &&
529
           a.text_mode == b.text_mode && a.text_256c == b.text_256c &&
530
           a.mapw == b.mapw && a.maph == b.maph &&
531
           a.tilew == b.tilew && a.tileh == b.tileh;
532
}
533
534
// ---------------------------------------------------------------------------
535
// Raster-split support
536
//
537
// A program can rewrite the layer registers part-way down a frame (typically
538
// from a line IRQ) so that different horizontal bands of the screen use a
539
// different MAPBASE/TILEBASE/scroll. A single register snapshot therefore only
540
// describes the band that happened to be active when the debugger looked, and
541
// every band below it decodes as garbage. video_get_layer_line_state() reports
542
// what each scanline actually rendered with, so we index those snapshots by the
543
// layer row each scanline displayed and decode each pixel row with the
544
// registers that produced it.
545
// ---------------------------------------------------------------------------
546
// Largest layer height VERA can produce: MAPH (256) * TILEH (16).
547
constexpr int MAX_LAYER_ROWS = 256 * 16;
548
549
struct RasterRowRegs {
550
    uint8_t regs[MAX_LAYER_ROWS][7];
551
    bool    valid[MAX_LAYER_ROWS];
552
    bool    any_split; // layer registers changed part-way down the frame
553
    bool    any_valid; // at least one scanline mapped onto a layer row
554
};
555
556
void
557
build_raster_row_regs(int layer, const LayerRegs &live, RasterRowRegs &out)
558
{
559
    memset(out.valid, 0, sizeof(out.valid));
560
    out.any_split = false;
561
    out.any_valid = false;
562
563
    const uint16_t lines      = video_get_scanline_count();
564
    bool           have_first = false;
565
    uint8_t        first[7]   = {0};
566
567
    for (uint16_t line = 0; line < lines; ++line) {
568
        uint8_t  r[7];
569
        bool     enabled   = false;
570
        uint16_t layer_row = 0;
571
        if (!video_get_layer_line_state((uint8_t)layer, line, r, nullptr, &enabled, &layer_row))
572
            continue;
573
        if (!enabled)
574
            continue;
575
576
        if (!have_first) {
577
            memcpy(first, r, sizeof(first));
578
            have_first = true;
579
        } else if (memcmp(first, r, sizeof(first)) != 0) {
580
            out.any_split = true;
581
        }
582
583
        // Only substitute snapshots that keep the image layout identical;
584
        // anything else would change the texture geometry mid-frame.
585
        const LayerRegs LL = decode_layer_regs(r);
586
        if (!same_layer_geometry(LL, live))
587
            continue;
588
589
        // Which row of the layer image this scanline showed. Taken from the
590
        // renderer rather than recomputed here: the renderer applies VSCROLL
591
        // and the layer-height mask from a different register generation than
592
        // the layout registers above, so deriving it from `r` would be off by
593
        // whatever the scroll changed mid-frame -- in exactly the raster-split
594
        // case this path exists to show.
595
        const int ly = (int)layer_row;
596
        if (ly < 0 || ly >= MAX_LAYER_ROWS || out.valid[ly])
597
            continue; // first scanline to show this row wins
598
599
        memcpy(out.regs[ly], r, sizeof(out.regs[ly]));
600
        out.valid[ly] = true;
601
        out.any_valid = true;
602
    }
603
}
604
605
// ---------------------------------------------------------------------------
606
// Bitmap viewer tab
607
// ---------------------------------------------------------------------------
608
void
609
draw_bitmap_tab(const uint32_t pal[256])
610
{
611
    static int   layer         = 0;
612
    static int   height        = 240;
613
    static float zoom          = 1.0f;
614
    static bool  follow_raster = true;
615
    static bool  auto_fit      = true;
616
617
    // Default the height to whatever the composer is actually displaying, so a
618
    // 320x200-style mode shows 200 rows rather than a fixed guess. Any manual
619
    // edit turns auto-fit off; Fit turns it back on.
620
    int fit_h = 0;
621
    video_get_active_layer_size(nullptr, &fit_h);
622
    if (auto_fit && fit_h > 0)
623
        height = clampi(fit_h, 1, 480);
624
625
    ImGui::PushItemWidth(120);
626
    ImGui::Combo("Layer", &layer, "Layer 0\0Layer 1\0");
627
    ImGui::SameLine();
628
    // DragInt rather than SliderInt: a 120px slider cannot address every value
629
    // in a 1..480 range, so dragging skips numbers.
630
    if (ImGui::DragInt("Height", &height, 1.0f, 8, 480))
631
        auto_fit = false;
632
    ImGui::SameLine();
633
    ImGui::SliderFloat("Zoom", &zoom, 1.0f, 4.0f, "%.1fx");
634
    ImGui::PopItemWidth();
635
    ImGui::SameLine();
636
    if (ImGui::Button("Fit"))
637
        auto_fit = true;
638
    if (ImGui::IsItemHovered())
639
        ImGui::SetTooltip("Match the active video mode (%d lines)", fit_h);
640
641
    LayerRegs L = read_layer_regs(layer);
642
    ImGui::Text("depth=%dbpp  bitmap=%s  tile_base(data)=$%05X  width=%d  pal_off=$%02X",
643
                L.bpp, L.bitmap_mode ? "yes" : "no", L.tile_base, L.bitmap_w, L.bitmap_pal_off);
644
    if (!L.bitmap_mode) {
645
        ImGui::TextColored(ImVec4(1, 0.7f, 0.2f, 1),
646
                           "Layer %d is not in bitmap mode; showing raw decode anyway.", layer);
647
    }
648
649
    ImGui::Checkbox("Follow raster", &follow_raster);
650
    ImGui::SameLine();
651
    ImGui::TextDisabled("(decode each row with the registers that rendered it)");
652
653
    static RasterRowRegs rr;
654
    build_raster_row_regs(layer, L, rr);
655
    const bool raster_active = follow_raster && rr.any_valid;
656
657
    if (rr.any_split) {
658
        ImGui::TextColored(raster_active ? ImVec4(0.4f, 1, 0.4f, 1) : ImVec4(1, 0.7f, 0.2f, 1),
659
                           raster_active ? "Raster split detected - rows decoded per scanline."
660
                                         : "Raster split detected - enable \"Follow raster\".");
661
    }
662
663
    const int img_w = L.bitmap_w;
664
    const int img_h = clampi(height, 1, 480);
665
    const int mask  = (1 << L.bpp) - 1;
666
667
    static PixelBuffer buf;
668
    uint32_t *px = buf.ensure((size_t)img_w * img_h);
669
    if (!px)
670
        return;
671
672
    for (int y = 0; y < img_h; ++y) {
673
        // Registers in effect on the scanline that displayed this bitmap row.
674
        LayerRegs LR = L;
675
        if (raster_active && y < MAX_LAYER_ROWS && rr.valid[y])
676
            LR = decode_layer_regs(rr.regs[y]);
677
678
        const uint32_t row_base = LR.tile_base + (uint32_t)((y * img_w * LR.bpp) >> 3);
679
        for (int x = 0; x < img_w; ++x) {
680
            const uint32_t bitpos = (uint32_t)x * LR.bpp;
681
            const uint8_t  byte   = video_space_read(row_base + (bitpos >> 3));
682
            const int      shift  = 8 - LR.bpp - (int)(bitpos & 7);
683
            const int      idx    = (byte >> shift) & mask;
684
            px[(size_t)y * img_w + x] = resolve_color(pal, idx, LR.bitmap_pal_off, false);
685
        }
686
    }
687
688
    static GfxTexture gt;
689
    if (!gt.ensure(img_w, img_h))
690
        return;
691
    gt.update(px);
692
693
    ImGui::Separator();
694
    ImGui::BeginChild("bmp", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
695
    ImGui::Image(gt.id(), ImVec2(img_w * zoom, img_h * zoom));
696
    ImGui::EndChild();
697
}
698
699
// ---------------------------------------------------------------------------
700
// Tilemap viewer tab
701
// ---------------------------------------------------------------------------
702
void
703
draw_tilemap_tab(const uint32_t pal[256])
704
{
705
    static int   layer         = 0;
706
    static int   max_cols      = 32;
707
    static int   max_rows      = 32;
708
    static float zoom          = 1.0f;
709
    static bool  follow_raster = true;
710
    static bool  auto_fit      = true;
711
712
    ImGui::PushItemWidth(120);
713
    ImGui::Combo("Layer", &layer, "Layer 0\0Layer 1\0");
714
    ImGui::PopItemWidth();
715
716
    LayerRegs L = read_layer_regs(layer);
717
718
    // Default the viewport to the tile area the composer is actually showing,
719
    // so e.g. a 320x200 tile mode opens at 40x25 instead of a fixed 32x32.
720
    // Any manual edit turns auto-fit off; Fit turns it back on.
721
    int fit_w = 0, fit_h = 0;
722
    video_get_active_layer_size(&fit_w, &fit_h);
723
    const int fit_cols = L.tilew > 0 ? (fit_w + L.tilew - 1) / L.tilew : 0;
724
    const int fit_rows = L.tileh > 0 ? (fit_h + L.tileh - 1) / L.tileh : 0;
725
    if (auto_fit && fit_cols > 0 && fit_rows > 0) {
726
        max_cols = clampi(fit_cols, 1, 128);
727
        max_rows = clampi(fit_rows, 1, 128);
728
    }
729
730
    ImGui::PushItemWidth(120);
731
    // DragInt rather than SliderInt: a 120px slider cannot address every value
732
    // in a 1..128 range, so dragging skips numbers.
733
    if (ImGui::DragInt("Max cols", &max_cols, 0.25f, 1, 128))
734
        auto_fit = false;
735
    ImGui::SameLine();
736
    if (ImGui::DragInt("Max rows", &max_rows, 0.25f, 1, 128))
737
        auto_fit = false;
738
    ImGui::SameLine();
739
    ImGui::SliderFloat("Zoom", &zoom, 1.0f, 4.0f, "%.1fx");
740
    ImGui::PopItemWidth();
741
    ImGui::SameLine();
742
    if (ImGui::Button("Fit"))
743
        auto_fit = true;
744
    if (ImGui::IsItemHovered())
745
        ImGui::SetTooltip("Match the active video mode (%dx%d px = %dx%d tiles)",
746
                          fit_w, fit_h, fit_cols, fit_rows);
747
748
    ImGui::Text("mode=%s  depth=%dbpp  map_base=$%05X  tile_base=$%05X  map=%dx%d  tile=%dx%d",
749
                L.bitmap_mode ? "bitmap" : (L.text_mode ? (L.text_256c ? "text256" : "text") : "tile"),
750
                L.bpp, L.map_base, L.tile_base, L.mapw, L.maph, L.tilew, L.tileh);
751
752
    if (L.bitmap_mode) {
753
        ImGui::TextColored(ImVec4(1, 0.7f, 0.2f, 1),
754
                           "Layer %d is in bitmap mode — use the Bitmap tab.", layer);
755
        return;
756
    }
757
758
    ImGui::Checkbox("Follow raster", &follow_raster);
759
    ImGui::SameLine();
760
    ImGui::TextDisabled("(decode each row with the registers that rendered it)");
761
762
    static RasterRowRegs rr;
763
    build_raster_row_regs(layer, L, rr);
764
    const bool raster_active = follow_raster && rr.any_valid;
765
766
    if (rr.any_split) {
767
        if (raster_active)
768
            ImGui::TextColored(ImVec4(0.4f, 1, 0.4f, 1),
769
                               "Raster split detected - rows decoded per scanline.");
770
        else
771
            ImGui::TextColored(ImVec4(1, 0.7f, 0.2f, 1),
772
                               "Raster split detected - rows outside the active band will "
773
                               "decode incorrectly. Enable \"Follow raster\".");
774
    }
775
776
    // Clamp rendered region so the texture stays a sane size.
777
    int cols = clampi(max_cols < L.mapw ? max_cols : L.mapw, 1, 128);
778
    int rows = clampi(max_rows < L.maph ? max_rows : L.maph, 1, 128);
779
    while ((long)cols * L.tilew > 2048)
780
        --cols;
781
    while ((long)rows * L.tileh > 2048)
782
        --rows;
783
784
    const int img_w = cols * L.tilew;
785
    const int img_h = rows * L.tileh;
786
    const int mask  = (1 << L.bpp) - 1;
787
788
    static PixelBuffer buf;
789
    uint32_t *px = buf.ensure((size_t)img_w * img_h);
790
    if (!px)
791
        return;
792
793
    for (int ty = 0; ty < rows; ++ty) {
794
        for (int y = 0; y < L.tileh; ++y) {
795
            // Registers in effect on the scanline that displayed this pixel row.
796
            const int layer_row = ty * L.tileh + y;
797
            LayerRegs LR        = L;
798
            if (raster_active && layer_row < MAX_LAYER_ROWS && rr.valid[layer_row])
799
                LR = decode_layer_regs(rr.regs[layer_row]);
800
801
            uint32_t *dst = px + (size_t)(ty * L.tileh + y) * img_w;
802
803
            for (int tx = 0; tx < cols; ++tx) {
804
                const uint32_t map_addr = LR.map_base + (uint32_t)(ty * LR.mapw + tx) * 2;
805
                const uint8_t  byte0    = video_space_read(map_addr);
806
                const uint8_t  byte1    = video_space_read(map_addr + 1);
807
808
                if (LR.text_mode) {
809
                    // Map entry: byte0 = char, byte1 = fg/bg colors (or full fg in 256c).
810
                    const int      fg         = LR.text_256c ? byte1 : (byte1 & 0x0f);
811
                    const int      bg         = LR.text_256c ? 0 : ((byte1 >> 4) & 0x0f);
812
                    const uint32_t glyph_base = LR.tile_base + (uint32_t)byte0 * (LR.tilew * LR.tileh / 8);
813
                    for (int x = 0; x < LR.tilew; ++x) {
814
                        const uint32_t bitpos = (uint32_t)(y * LR.tilew + x);
815
                        const uint8_t  b      = video_space_read(glyph_base + (bitpos >> 3));
816
                        const int      on     = (b >> (7 - (bitpos & 7))) & 1;
817
                        dst[tx * L.tilew + x] = pal[on ? fg : bg];
818
                    }
819
                } else {
820
                    // Tile mode: byte0 low tile index, byte1 = pal offset + flips + index hi.
821
                    const int      tile_index = byte0 | ((byte1 & 3) << 8);
822
                    const int      pal_off    = byte1 & 0xf0;
823
                    const bool     vflip      = (byte1 >> 3) & 1;
824
                    const bool     hflip      = (byte1 >> 2) & 1;
825
                    const uint32_t tile_start = LR.tile_base + ((uint32_t)tile_index << LR.tile_size_log2);
826
                    const int      sy         = vflip ? (LR.tileh - 1 - y) : y;
827
                    for (int x = 0; x < LR.tilew; ++x) {
828
                        const int      sx     = hflip ? (LR.tilew - 1 - x) : x;
829
                        const uint32_t bitpos = (uint32_t)(sy * LR.tilew + sx) * LR.bpp;
830
                        const uint8_t  byte   = video_space_read(tile_start + (bitpos >> 3));
831
                        const int      shift  = 8 - LR.bpp - (int)(bitpos & 7);
832
                        const int      idx    = (byte >> shift) & mask;
833
                        dst[tx * L.tilew + x] = resolve_color(pal, idx, pal_off, false);
834
                    }
835
                }
836
            }
837
        }
838
    }
839
840
    static GfxTexture gt;
841
    if (!gt.ensure(img_w, img_h))
842
        return;
843
    gt.update(px);
844
845
    if (cols < L.mapw || rows < L.maph)
846
        ImGui::TextDisabled("Showing %dx%d of %dx%d tiles (raise Max cols/rows).",
847
                            cols, rows, L.mapw, L.maph);
848
849
    ImGui::Separator();
850
    ImGui::BeginChild("tmap", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
851
    ImGui::Image(gt.id(), ImVec2(img_w * zoom, img_h * zoom));
852
    ImGui::EndChild();
853
}
854
855
// ---------------------------------------------------------------------------
856
// Registers — the 32 external VERA registers ($9F20-$9F3F) with names, hex,
857
// binary, and a decoded summary of the most useful fields.
858
// ---------------------------------------------------------------------------
859
static const char *
860
vera_reg_name(int reg, int dcsel)
861
{
862
    static const char *fixed[0x20] = {
863
        "ADDR_L", "ADDR_M", "ADDR_H", "DATA0", "DATA1", "CTRL", "IEN", "ISR",
864
        "IRQLINE_L", nullptr, nullptr, nullptr, nullptr, // 0x09-0x0C: DCSEL-dependent
865
        "L0_CONFIG", "L0_MAPBASE", "L0_TILEBASE", "L0_HSCROLL_L", "L0_HSCROLL_H",
866
        "L0_VSCROLL_L", "L0_VSCROLL_H", "L1_CONFIG", "L1_MAPBASE", "L1_TILEBASE",
867
        "L1_HSCROLL_L", "L1_HSCROLL_H", "L1_VSCROLL_L", "L1_VSCROLL_H",
868
        "AUDIO_CTRL", "AUDIO_RATE", "AUDIO_DATA", "SPI_DATA", "SPI_CTRL"
869
    };
870
    if (reg >= 0x09 && reg <= 0x0C) {
871
        static const char *dc0[4] = {"DC_VIDEO", "DC_HSCALE", "DC_VSCALE", "DC_BORDER"};
872
        static const char *dc1[4] = {"DC_HSTART", "DC_HSTOP", "DC_VSTART", "DC_VSTOP"};
873
        if (dcsel == 0)
874
            return dc0[reg - 0x09];
875
        if (dcsel == 1)
876
            return dc1[reg - 0x09];
877
        return "FX";
878
    }
879
    return (reg >= 0 && reg < 0x20 && fixed[reg]) ? fixed[reg] : "?";
880
}
881
882
static void
883
draw_registers_tab()
884
{
885
    // CTRL selects which of the DCSEL-banked / data ports are visible.
886
    uint8_t ctrl    = video_read(0x05, true);
887
    int     addrsel = ctrl & 1;
888
    int     dcsel   = (ctrl >> 1) & 0x3f;
889
890
    ImGui::Text("CTRL $%02X    ADDRSEL = %d    DCSEL = %d", ctrl, addrsel, dcsel);
891
    ImGui::TextDisabled("VERA external registers $9F20-$9F3F. Hover a value for hex/dec/bin.");
892
    ImGui::Separator();
893
894
    ImGuiTableFlags tflags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
895
                             ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY |
896
                             DBGUI_TABLE_FLAGS_RESIZABLE;
897
    if (ImGui::BeginTable("vera_regs", 4, tflags, ImVec2(0, 300))) {
898
        ImGui::TableSetupScrollFreeze(0, 1);
899
        ImGui::TableSetupColumn("Reg");
900
        ImGui::TableSetupColumn("Name");
901
        ImGui::TableSetupColumn("Hex");
902
        ImGui::TableSetupColumn("Binary");
903
        ImGui::TableHeadersRow();
904
905
        for (int r = 0; r < 0x20; r++) {
906
            // 0x09-0x0C are write-only FX registers when DCSEL>=2 — reading a
907
            // write-only VERA register logs a warning.
908
            bool fx_wo = (r >= 0x09 && r <= 0x0C && dcsel >= 2);
909
            // AUDIO_CTRL (0x1B) and SPI_DATA (0x1E) used to be unreadable here:
910
            // video_read() ignored debugOn for them, so polling them every frame
911
            // flushed audio and clocked a real byte to the SD card. Both now
912
            // honour debugOn (vera_spi_peek, and the audio_render guard), so
913
            // they are read like any other register.
914
915
            ImGui::TableNextRow();
916
            ImGui::TableSetColumnIndex(0);
917
            ImGui::Text("$9F%02X", 0x20 + r);
918
            ImGui::TableSetColumnIndex(1);
919
            ImGui::TextUnformatted(vera_reg_name(r, dcsel));
920
921
            if (fx_wo) {
922
                ImGui::TableSetColumnIndex(2);
923
                ImGui::TextDisabled("--");
924
                ImGui::TableSetColumnIndex(3);
925
                ImGui::TextDisabled("(write-only)");
926
            } else {
927
                uint8_t v = video_read((uint8_t)r, true);
928
                ImGui::TableSetColumnIndex(2);
929
                ImGui::Text("$%02X", v);
930
                dbgui_hover_value_tooltip(vera_reg_name(r, dcsel), v, 1);
931
                ImGui::TableSetColumnIndex(3);
932
                char bin[12];
933
                dbgui_format_binary(bin, sizeof bin, v, 8);
934
                ImGui::TextUnformatted(bin);
935
            }
936
        }
937
        ImGui::EndTable();
938
    }
939
940
    ImGui::Separator();
941
    if (ImGui::CollapsingHeader("Decoded", ImGuiTreeNodeFlags_DefaultOpen)) {
942
        // Active data-port address + increment code.
943
        uint8_t  al   = video_read(0x00, true);
944
        uint8_t  am   = video_read(0x01, true);
945
        uint8_t  ah   = video_read(0x02, true);
946
        uint32_t addr = ((uint32_t)(ah & 1) << 16) | ((uint32_t)am << 8) | al;
947
        ImGui::Text("Data port %d address: $%05X   (increment code %d)", addrsel, addr, (ah >> 3) & 0x1f);
948
949
        if (dcsel == 0) {
950
            uint8_t            dcv      = video_read(0x09, true);
951
            static const char *omode[4] = {"disabled", "VGA", "NTSC", "RGB/component"};
952
            ImGui::Text("DC_VIDEO $%02X: output=%s  layer0=%s  layer1=%s  sprites=%s", dcv,
953
                        omode[dcv & 3], (dcv & 0x10) ? "ON" : "off",
954
                        (dcv & 0x20) ? "ON" : "off", (dcv & 0x40) ? "ON" : "off");
955
            uint8_t hs = video_read(0x0A, true);
956
            uint8_t vs = video_read(0x0B, true);
957
            ImGui::Text("Scale: H=%d (%.2fx)  V=%d (%.2fx)    Border color=%d", hs, hs / 128.0f,
958
                        vs, vs / 128.0f, video_read(0x0C, true));
959
        } else if (dcsel == 1) {
960
            ImGui::Text("Active area: H %d..%d,  V %d..%d", video_read(0x09, true) << 2,
961
                        video_read(0x0A, true) << 2, video_read(0x0B, true) << 1,
962
                        video_read(0x0C, true) << 1);
963
        } else {
964
            ImGui::TextDisabled("DCSEL=%d selects FX registers; set DCSEL=0 to see DC_VIDEO etc.", dcsel);
965
        }
966
967
        static const char *depth[4] = {"1bpp", "2bpp", "4bpp", "8bpp"};
968
        for (int layer = 0; layer < 2; layer++) {
969
            int     base  = layer == 0 ? 0x0D : 0x14;
970
            uint8_t cfg   = video_read((uint8_t)base, true);
971
            uint8_t mapb  = video_read((uint8_t)(base + 1), true);
972
            uint8_t tileb = video_read((uint8_t)(base + 2), true);
973
            ImGui::Text("Layer %d $%02X: %s%s%s  map %dx%d  mapbase $%05X  tilebase $%05X  tile %dx%d",
974
                        layer, cfg, depth[cfg & 3], (cfg & 4) ? " bitmap" : "",
975
                        (cfg & 8) ? " T256C" : "", 32 << ((cfg >> 4) & 3), 32 << ((cfg >> 6) & 3),
976
                        (uint32_t)mapb << 9, (uint32_t)(tileb & 0xFC) << 9, 8 << (tileb & 1),
977
                        8 << ((tileb >> 1) & 1));
978
        }
979
    }
980
}
981
982
// ---------------------------------------------------------------------------
983
// Panel entry point
984
// ---------------------------------------------------------------------------
985
void
986
vera_panel_render(bool *p_open)
987
{
988
    if (ImGui::Begin("VERA", p_open)) {
989
        dbgui_window_zoom("vera");
990
        if (debug_ui_get_renderer() == nullptr) {
991
            ImGui::TextUnformatted("Debugger renderer unavailable.");
992
            dbgui_window_end();
993
            return;
994
        }
995
996
        uint32_t pal[256];
997
        build_palette(pal);
998
999
        if (ImGui::BeginTabBar("vera_tabs", ImGuiTabBarFlags_DrawSelectedOverline)) {
1000
            if (ImGui::BeginTabItem("Registers")) {
1001
                draw_registers_tab();
1002
                ImGui::EndTabItem();
1003
            }
1004
            if (ImGui::BeginTabItem("Palette")) {
1005
                draw_palette_tab();
1006
                ImGui::EndTabItem();
1007
            }
1008
            if (ImGui::BeginTabItem("Tiles")) {
1009
                draw_tiles_tab(pal);
1010
                ImGui::EndTabItem();
1011
            }
1012
            if (ImGui::BeginTabItem("Sprites")) {
1013
                draw_sprites_tab(pal);
1014
                ImGui::EndTabItem();
1015
            }
1016
            // Directly after Sprites: that tab shows the attribute table as it
1017
            // stands, this one shows what the frame actually drew, and for a
1018
            // multiplexed frame the two are very different things.
1019
            if (ImGui::BeginTabItem("Multiplex")) {
1020
                draw_sprite_multiplex_tab(pal);
1021
                ImGui::EndTabItem();
1022
            }
1023
            if (ImGui::BeginTabItem("Bitmap")) {
1024
                draw_bitmap_tab(pal);
1025
                ImGui::EndTabItem();
1026
            }
1027
            if (ImGui::BeginTabItem("Tilemap")) {
1028
                draw_tilemap_tab(pal);
1029
                ImGui::EndTabItem();
1030
            }
1031
            ImGui::EndTabBar();
1032
        }
1033
    }
1034
    dbgui_window_end();
1035
}
1036
1037
} // namespace
1038
1039
static DebugPanelRegistration s_reg("VERA", vera_panel_render, true);