OXYGENHUB
# 2980b7b6d7cd enhanced-x16-emulator / src / debug_ui / panels / sprite_multiplex_tab.cpp
26.0 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// Commander X16 Emulator — the VERA panel's sprite multiplexing tab.
2
//
3
// The VERA panel's Sprites tab shows the attribute table as it stands right
4
// now. For a program that multiplexes -- rewriting slots from a raster IRQ so
5
// 128 hardware sprites show far more than 128 sprites -- that view is close to
6
// useless: it shows whichever band of the screen happened to be written last,
7
// and the rest of the frame is simply gone. This tab shows the frame instead of
8
// the table, which is why it sits directly alongside it.
9
//
10
// Everything here reads sprite_trace, which records what render_sprite_line()
11
// actually did on every scanline. Nothing on screen is inferred from the
12
// attribute table's current contents.
13
14
#include "imgui.h"
15
#include "debug_ui_bridge.h"
16
#include "debug_ui.h"
17
#include "sprite_multiplex_tab.h"
18
19
#include "sprite_trace.h"
20
21
#include <SDL.h>
22
#include <float.h>
23
#include <stdint.h>
24
#include <stdio.h>
25
#include <stdlib.h>
26
#include <string.h>
27
28
namespace {
29
30
constexpr int NUM_SLOTS = SPRITE_TRACE_SLOTS;
31
constexpr int NUM_LINES = SPRITE_TRACE_LINES;
32
33
inline int clampi(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }
34
35
// A stable colour per distinct sprite graphic, so the same artwork reused in
36
// several slots reads as the same thing across the whole ribbon.
37
ImU32
38
gen_color(const sprite_gen_t &g, float alpha)
39
{
40
    uint32_t h = g.address * 2654435761u;
41
    h ^= (uint32_t)g.palette_offset * 40503u;
42
    h ^= (uint32_t)g.color_mode << 19;
43
    float hue = (float)((h >> 8) & 0xFFFF) / 65535.0f;
44
    float r, gg, b;
45
    ImGui::ColorConvertHSVtoRGB(hue, 0.55f, 0.98f, r, gg, b);
46
    return ImGui::GetColorU32(ImVec4(r, gg, b, alpha));
47
}
48
49
struct GfxTexture {
50
    SDL_Texture *tex = nullptr;
51
    int          w = 0, h = 0;
52
53
    bool ensure(int width, int height)
54
    {
55
        if (width <= 0 || height <= 0)
56
            return false;
57
        if (tex && w == width && h == height)
58
            return true;
59
        if (tex) {
60
            SDL_DestroyTexture(tex);
61
            tex = nullptr;
62
        }
63
        SDL_Renderer *ren = debug_ui_get_renderer();
64
        if (!ren)
65
            return false;
66
        tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING,
67
                                width, height);
68
        if (!tex)
69
            return false;
70
        SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
71
        SDL_SetTextureScaleMode(tex, SDL_ScaleModeNearest);
72
        w = width;
73
        h = height;
74
        return true;
75
    }
76
    void update(const uint32_t *px) { if (tex) SDL_UpdateTexture(tex, nullptr, px, w * 4); }
77
    ImTextureID id() const { return (ImTextureID)(intptr_t)tex; }
78
};
79
80
// ---------------------------------------------------------------------------
81
// Panel state
82
// ---------------------------------------------------------------------------
83
bool  s_enabled       = false;
84
bool  s_alloc_failed  = false;
85
int   s_burst_gap     = 128;
86
int   s_px_per_line   = 2;   // ribbon horizontal zoom
87
int   s_px_per_slot   = 4;   // ribbon vertical zoom
88
bool  s_only_drawn    = true;
89
bool  s_mark_overrun  = true;
90
int   s_selected_gen  = -1;
91
92
GfxTexture s_ghost_tex;
93
uint32_t  *s_ghost_px    = nullptr;
94
uint32_t   s_ghost_frame = 0xFFFFFFFFu;
95
bool       s_ghost_valid = false;
96
97
const char *
98
gen_kind(const sprite_gen_t &g)
99
{
100
    if (g.flags & SPRITE_GEN_LATE)    return "LATE";
101
    if (g.flags & SPRITE_GEN_DREW)    return (g.flags & SPRITE_GEN_CARRIED) ? "static" : "muxed";
102
    if (g.flags & SPRITE_GEN_CARRIED) return "idle";
103
    return "phantom";
104
}
105
106
// ---------------------------------------------------------------------------
107
// Overview
108
// ---------------------------------------------------------------------------
109
void
110
draw_overview(const sprite_trace_frame_t *f)
111
{
112
    const sprite_frame_summary_t &s = f->summary;
113
114
    ImGui::Text("Frame %u", s.frame);
115
    ImGui::SameLine();
116
    if (s.multiplexing)
117
        ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.5f, 1.0f), "— multiplexing detected");
118
    else
119
        ImGui::TextDisabled("— no multiplexing (no slot held more than one drawn value)");
120
121
    ImGui::Separator();
122
123
    // The headline: what the player actually saw, versus the 128 the hardware has.
124
    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.9f, 1.0f, 1.0f));
125
    ImGui::Text("Effective sprites: %u", s.effective_sprites);
126
    ImGui::PopStyleColor();
127
    ImGui::SameLine();
128
    float ratio = s.slots_used ? (float)s.effective_sprites / (float)s.slots_used : 0.0f;
129
    ImGui::Text("  (%u of 128 slots used, %.1f\xC3\x97 multiplex)", s.slots_used, ratio);
130
131
    if (ImGui::BeginTable("mux_summary", 2, ImGuiTableFlags_SizingFixedFit)) {
132
        ImGui::TableNextRow();
133
        ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted("Slots multiplexed");
134
        ImGui::TableSetColumnIndex(1); ImGui::Text("%u", s.slots_multiplexed);
135
        ImGui::TableNextRow();
136
        ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted("Deepest slot");
137
        ImGui::TableSetColumnIndex(1); ImGui::Text("%u values", s.max_gens_in_slot);
138
        ImGui::TableNextRow();
139
        ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted("Attribute writes");
140
        ImGui::TableSetColumnIndex(1);
141
        ImGui::Text("%u (%u while the beam was in the display)", s.mutations, s.mutations_onscreen);
142
        ImGui::TableNextRow();
143
        ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted("Raster bursts");
144
        ImGui::TableSetColumnIndex(1); ImGui::Text("%u", s.mutation_bursts);
145
        ImGui::EndTable();
146
    }
147
148
    ImGui::Separator();
149
    ImGui::TextUnformatted("Wasted work");
150
    if (s.phantom_writes || s.late_writes) {
151
        if (s.phantom_writes)
152
            ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f),
153
                               "  %u phantom: written to a slot but never drew a pixel",
154
                               s.phantom_writes);
155
        if (s.late_writes)
156
            ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f),
157
                               "  %u late: completed after the beam had passed their own Y span",
158
                               s.late_writes);
159
    } else {
160
        ImGui::TextDisabled("  none — every value written was displayed");
161
    }
162
163
    // ── Cost, requirement 4a: what the multiplexing costs the 6502 ──────────
164
    ImGui::Separator();
165
    ImGui::TextUnformatted("CPU cost of multiplexing");
166
    float cpu_pct = s.cpu_cycles ? 100.0f * (float)s.mutation_cycles / (float)s.cpu_cycles : 0.0f;
167
    ImGui::Text("  %u cycles/frame of %u (%.1f%% of the frame budget)",
168
                s.mutation_cycles, s.cpu_cycles, cpu_pct);
169
    ImGui::ProgressBar(cpu_pct / 100.0f, ImVec2(-FLT_MIN, 0), "");
170
    ImGui::TextDisabled("  Summed span of each burst of attribute writes (gap <= %u cycles).",
171
                        s.burst_gap);
172
    ImGui::TextDisabled("  A lower bound: a burst's span excludes the handler's entry, exit");
173
    ImGui::TextDisabled("  and any work following its last store.");
174
    if (f->mutations_dropped)
175
        ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.3f, 1.0f),
176
                           "  %u writes past the buffer were counted but not timed.",
177
                           f->mutations_dropped);
178
179
    // ── Cost, requirement 4b: VERA's own per-scanline sprite render time ────
180
    ImGui::Separator();
181
    ImGui::TextUnformatted("VERA sprite render time");
182
    ImGui::Text("  Peak: line %u wanted %u of %d clocks", s.peak_line, s.peak_demand,
183
                SPRITE_TRACE_LINE_BUDGET);
184
    if (s.lines_exhausted) {
185
        ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.35f, 1.0f),
186
                           "  %u scanlines ran out of time — %u sprites were dropped.",
187
                           s.lines_exhausted, s.sprites_dropped);
188
        ImGui::TextDisabled("  Dropped exactly as VERA drops them: when render_time_r hits 798");
189
        ImGui::TextDisabled("  the sprite search and the line renderer both stop, so the sprite");
190
        ImGui::TextDisabled("  in flight is left part-drawn and every slot after it is skipped.");
191
        ImGui::TextDisabled("  Move sprites off these lines or make them narrower.");
192
    } else {
193
        float head = s.peak_demand ? 100.0f * (float)s.peak_demand / (float)SPRITE_TRACE_LINE_BUDGET
194
                                   : 0.0f;
195
        ImGui::TextDisabled("  No scanline ran out of time — worst line is at %.0f%% of it.", head);
196
    }
197
}
198
199
// ---------------------------------------------------------------------------
200
// Raster ribbon: slots down, scanlines across. One horizontal band per value a
201
// slot held, so a multiplexed slot reads as a stack of coloured segments.
202
// ---------------------------------------------------------------------------
203
void
204
draw_ribbon(const sprite_trace_frame_t *f)
205
{
206
    ImGui::SliderInt("Line width", &s_px_per_line, 1, 6, "%d px");
207
    ImGui::SameLine();
208
    ImGui::SetNextItemWidth(140);
209
    ImGui::SliderInt("Slot height", &s_px_per_slot, 2, 12, "%d px");
210
    ImGui::SameLine();
211
    ImGui::Checkbox("Only drawn", &s_only_drawn);
212
    ImGui::SameLine();
213
    ImGui::Checkbox("Mark overrun", &s_mark_overrun);
214
    ImGui::TextDisabled("Colour identifies the sprite graphic. Hover for the value a slot held "
215
                        "on that line.");
216
217
    const float cw = (float)s_px_per_line;
218
    const float ch = (float)s_px_per_slot;
219
220
    ImGui::BeginChild("ribbon_scroll", ImVec2(0, 0), true,
221
                      ImGuiWindowFlags_HorizontalScrollbar);
222
223
    ImDrawList  *dl     = ImGui::GetWindowDrawList();
224
    const ImVec2 origin = ImGui::GetCursorScreenPos();
225
    const ImVec2 size(NUM_LINES * cw, NUM_SLOTS * ch);
226
227
    ImGui::InvisibleButton("ribbon", size);
228
    const bool hovered = ImGui::IsItemHovered();
229
230
    dl->AddRectFilled(origin, ImVec2(origin.x + size.x, origin.y + size.y),
231
                      IM_COL32(18, 18, 22, 255));
232
233
    // Run-length merge along each slot row. Drawing every (line, slot) cell
234
    // would be 61,440 rectangles a frame; multiplexed content changes only a
235
    // handful of times per row, so merging collapses it to a few hundred.
236
    for (int slot = 0; slot < NUM_SLOTS; ++slot) {
237
        int      run_start = -1;
238
        uint16_t run_gen   = SPRITE_TRACE_NO_GEN;
239
        uint8_t  run_flags = 0;
240
241
        for (int line = 0; line <= NUM_LINES; ++line) {
242
            uint16_t g  = SPRITE_TRACE_NO_GEN;
243
            uint8_t  fl = 0;
244
            if (line < NUM_LINES) {
245
                const size_t i = (size_t)line * NUM_SLOTS + slot;
246
                g  = f->line_gen[i];
247
                fl = f->line_flags[i];
248
                if (s_only_drawn && !(fl & SPRITE_TRACE_DREW))
249
                    g = SPRITE_TRACE_NO_GEN;
250
            }
251
            const bool same = (g == run_gen) &&
252
                              ((fl & SPRITE_TRACE_DREW) == (run_flags & SPRITE_TRACE_DREW)) &&
253
                              ((fl & SPRITE_TRACE_CUT) == (run_flags & SPRITE_TRACE_CUT));
254
            if (!same) {
255
                if (run_start >= 0 && run_gen != SPRITE_TRACE_NO_GEN && run_gen < f->gen_count) {
256
                    const sprite_gen_t &gen = f->gens[run_gen];
257
                    float alpha = (run_flags & SPRITE_TRACE_DREW) ? 1.0f : 0.22f;
258
                    ImU32 col   = gen_color(gen, alpha);
259
                    ImVec2 a(origin.x + run_start * cw, origin.y + slot * ch);
260
                    ImVec2 b(origin.x + line * cw, origin.y + slot * ch + ch - 1.0f);
261
                    dl->AddRectFilled(a, b, col);
262
                    if (s_mark_overrun && (run_flags & SPRITE_TRACE_CUT))
263
                        dl->AddRectFilled(a, b, IM_COL32(255, 40, 40, 110));
264
                    if (s_selected_gen == (int)run_gen)
265
                        dl->AddRect(a, b, IM_COL32(255, 255, 255, 220));
266
                }
267
                run_start = line;
268
                run_gen   = g;
269
                run_flags = fl;
270
            }
271
        }
272
    }
273
274
    // Scanlines whose sprite demand exceeded the hardware budget.
275
    if (s_mark_overrun) {
276
        for (int line = 0; line < NUM_LINES; ++line) {
277
            if (!f->lines[line].exhausted)
278
                continue;
279
            dl->AddRectFilled(ImVec2(origin.x + line * cw, origin.y),
280
                              ImVec2(origin.x + line * cw + cw, origin.y + 3.0f),
281
                              IM_COL32(255, 60, 50, 230));
282
        }
283
    }
284
285
    if (hovered) {
286
        const ImVec2 mp   = ImGui::GetMousePos();
287
        const int    line = clampi((int)((mp.x - origin.x) / cw), 0, NUM_LINES - 1);
288
        const int    slot = clampi((int)((mp.y - origin.y) / ch), 0, NUM_SLOTS - 1);
289
        const size_t i    = (size_t)line * NUM_SLOTS + slot;
290
        const uint16_t gi = f->line_gen[i];
291
292
        dl->AddLine(ImVec2(origin.x + line * cw, origin.y),
293
                    ImVec2(origin.x + line * cw, origin.y + size.y),
294
                    IM_COL32(255, 255, 255, 60));
295
296
        ImGui::BeginTooltip();
297
        ImGui::Text("line %d, slot %d", line, slot);
298
        if (gi != SPRITE_TRACE_NO_GEN && gi < f->gen_count) {
299
            const sprite_gen_t &g  = f->gens[gi];
300
            const uint8_t       fl = f->line_flags[i];
301
            ImGui::Separator();
302
            ImGui::Text("value #%u in this slot (%s)", g.index_in_slot, gen_kind(g));
303
            ImGui::Text("gfx $%05X  %ux%u  %s", g.address, g.width, g.height,
304
                        g.color_mode ? "8bpp" : "4bpp");
305
            ImGui::Text("pos %d,%d  z%u  pal+%u", g.x, g.y, g.zdepth, g.palette_offset);
306
            if (g.write_line == SPRITE_TRACE_NO_LINE)
307
                ImGui::TextDisabled("carried in from the previous frame");
308
            else
309
                ImGui::Text("written on line %u (cycle %u, %u bytes)", g.write_line,
310
                            g.write_cycle, g.writes);
311
            ImGui::Text("resident %u lines, drew on %u", g.lines_resident, g.lines_drawn);
312
            ImGui::Text("this line: %s%s%s", (fl & SPRITE_TRACE_DREW) ? "drew " : "",
313
                        (fl & SPRITE_TRACE_ONSCREEN) ? "on-screen " : "",
314
                        (fl & SPRITE_TRACE_CUT) ? "DROPPED (out of render time)" : "");
315
            ImGui::Text("cost %u VERA clocks", f->line_budget[i]);
316
        } else {
317
            ImGui::TextDisabled("slot not reached by the renderer on this line");
318
        }
319
        ImGui::Separator();
320
        ImGui::Text("line wanted %u of %d clocks%s", f->lines[line].demand,
321
                    SPRITE_TRACE_LINE_BUDGET,
322
                    f->lines[line].exhausted ? "  — RAN OUT" : "");
323
        if (f->lines[line].dropped)
324
            ImGui::TextColored(ImVec4(1, 0.4f, 0.35f, 1), "%u sprite(s) dropped on this line",
325
                               f->lines[line].dropped);
326
        ImGui::EndTooltip();
327
328
        if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
329
            s_selected_gen = (gi == SPRITE_TRACE_NO_GEN) ? -1 : (int)gi;
330
    }
331
332
    ImGui::EndChild();
333
}
334
335
// ---------------------------------------------------------------------------
336
// Budget: per-scanline sprite fetch demand against the hardware ceiling.
337
// ---------------------------------------------------------------------------
338
void
339
draw_budget(const sprite_trace_frame_t *f)
340
{
341
    const sprite_frame_summary_t &s = f->summary;
342
    ImGui::Text("Peak: line %u wanted %u of %d clocks.  %u scanline(s) ran out, %u sprites dropped.",
343
                s.peak_line, s.peak_demand, SPRITE_TRACE_LINE_BUDGET, s.lines_exhausted,
344
                s.sprites_dropped);
345
    ImGui::TextDisabled("Height is the render time a scanline wanted. Anything above the white "
346
                        "line did not get drawn — on hardware or here.");
347
348
    ImDrawList  *dl = ImGui::GetWindowDrawList();
349
    const ImVec2 origin = ImGui::GetCursorScreenPos();
350
    const float  w = ImGui::GetContentRegionAvail().x;
351
    const float  h = 220.0f;
352
    ImGui::InvisibleButton("budget", ImVec2(w, h));
353
    const bool hovered = ImGui::IsItemHovered();
354
355
    dl->AddRectFilled(origin, ImVec2(origin.x + w, origin.y + h), IM_COL32(18, 18, 22, 255));
356
357
    // Scale to the peak, but never compress the ceiling below three-quarters
358
    // height, so an ordinary frame does not look alarmingly full.
359
    float top = (float)(s.peak_demand > (uint32_t)(SPRITE_TRACE_LINE_BUDGET * 4 / 3)
360
                            ? s.peak_demand
361
                            : (uint32_t)(SPRITE_TRACE_LINE_BUDGET * 4 / 3));
362
    const float bw = w / (float)NUM_LINES;
363
364
    for (int line = 0; line < NUM_LINES; ++line) {
365
        const sprite_line_stat_t &st = f->lines[line];
366
        if (!st.demand)
367
            continue;
368
        float frac = (float)st.demand / top;
369
        float bh   = frac * h;
370
        ImU32 col  = st.exhausted ? IM_COL32(235, 70, 60, 255) : IM_COL32(90, 170, 240, 255);
371
        dl->AddRectFilled(ImVec2(origin.x + line * bw, origin.y + h - bh),
372
                          ImVec2(origin.x + line * bw + (bw > 1.0f ? bw : 1.0f), origin.y + h),
373
                          col);
374
    }
375
376
    const float ceil_y = origin.y + h - ((float)SPRITE_TRACE_LINE_BUDGET / top) * h;
377
    dl->AddLine(ImVec2(origin.x, ceil_y), ImVec2(origin.x + w, ceil_y),
378
                IM_COL32(255, 255, 255, 200));
379
    dl->AddText(ImVec2(origin.x + 4, ceil_y - 16), IM_COL32(255, 255, 255, 200),
380
                "VERA render time (798 clocks)");
381
382
    if (hovered) {
383
        const int line = clampi((int)((ImGui::GetMousePos().x - origin.x) / bw), 0, NUM_LINES - 1);
384
        const sprite_line_stat_t &st = f->lines[line];
385
        ImGui::BeginTooltip();
386
        ImGui::Text("line %d", line);
387
        ImGui::Text("wanted %u clocks, spent %u of %d", st.demand, st.budget_used,
388
                    SPRITE_TRACE_LINE_BUDGET);
389
        ImGui::Text("slots reached: %u, drawing: %u", st.evaluated, st.drawn);
390
        if (st.exhausted)
391
            ImGui::TextColored(ImVec4(1, 0.4f, 0.35f, 1),
392
                               "ran out at slot %u — %u sprite(s) dropped", st.cut_slot,
393
                               st.dropped);
394
        ImGui::Text("CPU cycles on this line: %u", st.cpu_cycles);
395
        ImGui::EndTooltip();
396
    }
397
}
398
399
// ---------------------------------------------------------------------------
400
// Effective sprites: one row per value that actually reached the screen.
401
// ---------------------------------------------------------------------------
402
void
403
draw_effective(const sprite_trace_frame_t *f)
404
{
405
    static bool show_phantom = true;
406
407
    ImGui::Checkbox("Include phantom / late", &show_phantom);
408
    ImGui::SameLine();
409
    ImGui::TextDisabled("%u effective this frame", f->summary.effective_sprites);
410
411
    const ImGuiTableFlags tf = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
412
                               ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit;
413
    if (!ImGui::BeginTable("effective", 10, tf))
414
        return;
415
416
    ImGui::TableSetupScrollFreeze(0, 1);
417
    ImGui::TableSetupColumn("Slot");
418
    ImGui::TableSetupColumn("#");
419
    ImGui::TableSetupColumn("Kind");
420
    ImGui::TableSetupColumn("Gfx");
421
    ImGui::TableSetupColumn("Size");
422
    ImGui::TableSetupColumn("Pos");
423
    ImGui::TableSetupColumn("Lines");
424
    ImGui::TableSetupColumn("Drew");
425
    ImGui::TableSetupColumn("Written");
426
    ImGui::TableSetupColumn("VERA cyc");
427
    ImGui::TableHeadersRow();
428
429
    for (uint16_t i = 0; i < f->gen_count; ++i) {
430
        const sprite_gen_t &g = f->gens[i];
431
        const bool drew = (g.flags & SPRITE_GEN_DREW) != 0;
432
        if (!drew) {
433
            // An untouched idle slot is noise; a written value that never drew
434
            // is a finding.
435
            if (g.flags & SPRITE_GEN_CARRIED)
436
                continue;
437
            if (!show_phantom)
438
                continue;
439
        }
440
441
        ImGui::TableNextRow();
442
        if (!drew)
443
            ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
444
                                   (g.flags & SPRITE_GEN_LATE) ? IM_COL32(90, 30, 30, 140)
445
                                                               : IM_COL32(80, 60, 20, 140));
446
447
        ImGui::TableSetColumnIndex(0);
448
        char lbl[32];
449
        snprintf(lbl, sizeof(lbl), "%u##g%u", g.slot, i);
450
        if (ImGui::Selectable(lbl, s_selected_gen == (int)i, ImGuiSelectableFlags_SpanAllColumns))
451
            s_selected_gen = (int)i;
452
453
        ImGui::TableSetColumnIndex(1); ImGui::Text("%u", g.index_in_slot);
454
        ImGui::TableSetColumnIndex(2);
455
        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(gen_color(g, 1.0f)));
456
        ImGui::TextUnformatted(gen_kind(g));
457
        ImGui::PopStyleColor();
458
        ImGui::TableSetColumnIndex(3); ImGui::Text("$%05X", g.address);
459
        ImGui::TableSetColumnIndex(4); ImGui::Text("%ux%u", g.width, g.height);
460
        ImGui::TableSetColumnIndex(5); ImGui::Text("%d,%d", g.x, g.y);
461
        ImGui::TableSetColumnIndex(6);
462
        if (g.first_line == SPRITE_TRACE_NO_LINE)
463
            ImGui::TextDisabled("—");
464
        else
465
            ImGui::Text("%u..%u", g.first_line, g.last_line);
466
        ImGui::TableSetColumnIndex(7); ImGui::Text("%u", g.lines_drawn);
467
        ImGui::TableSetColumnIndex(8);
468
        if (g.write_line == SPRITE_TRACE_NO_LINE)
469
            ImGui::TextDisabled("carried");
470
        else
471
            ImGui::Text("line %u", g.write_line);
472
        ImGui::TableSetColumnIndex(9); ImGui::Text("%u", g.budget_cycles);
473
    }
474
    ImGui::EndTable();
475
}
476
477
// ---------------------------------------------------------------------------
478
// Frame ghost: every effective sprite composited into one image, each drawn
479
// only on the scanlines it was actually resident and visible for. This is the
480
// frame the player saw, which no single read of the attribute table can show.
481
// ---------------------------------------------------------------------------
482
void
483
rebuild_ghost(const sprite_trace_frame_t *f, const uint32_t pal[256])
484
{
485
    if (!s_ghost_px)
486
        s_ghost_px = (uint32_t *)malloc((size_t)640 * 480 * 4);
487
    if (!s_ghost_px)
488
        return;
489
    memset(s_ghost_px, 0, (size_t)640 * 480 * 4);
490
491
    // Walk the residency map so each sprite contributes exactly the scanlines
492
    // it drew on -- reconstructing from first_line/last_line would paint over
493
    // gaps where the slot had been handed to a different sprite.
494
    for (int line = 0; line < NUM_LINES; ++line) {
495
        for (int slot = 0; slot < NUM_SLOTS; ++slot) {
496
            const size_t i = (size_t)line * NUM_SLOTS + slot;
497
            if (!(f->line_flags[i] & SPRITE_TRACE_DREW))
498
                continue;
499
            const uint16_t gi = f->line_gen[i];
500
            if (gi == SPRITE_TRACE_NO_GEN || gi >= f->gen_count)
501
                continue;
502
            const sprite_gen_t &g = f->gens[gi];
503
504
            int row = line - g.y;
505
            if (row < 0 || row >= (int)g.height)
506
                continue;
507
            if (g.vflip)
508
                row = (int)g.height - 1 - row;
509
510
            for (int sx = 0; sx < (int)g.width; ++sx) {
511
                const int px = g.x + sx;
512
                if (px < 0 || px >= 640)
513
                    continue;
514
                const int tx = g.hflip ? (int)g.width - 1 - sx : sx;
515
516
                int idx;
517
                if (g.color_mode) {
518
                    idx = video_space_read(g.address + (uint32_t)row * g.width + tx);
519
                } else {
520
                    uint8_t byte = video_space_read(g.address + (uint32_t)row * (g.width / 2) +
521
                                                    (tx / 2));
522
                    idx = (tx & 1) ? (byte & 0x0f) : (byte >> 4);
523
                }
524
                if (idx == 0)
525
                    continue;
526
                if (idx < 16)
527
                    idx += g.palette_offset;
528
                s_ghost_px[(size_t)line * 640 + px] = pal[idx & 0xff];
529
            }
530
        }
531
    }
532
    s_ghost_valid = true;
533
}
534
535
void
536
draw_ghost(const sprite_trace_frame_t *f, const uint32_t pal[256])
537
{
538
    static bool auto_refresh = true;
539
    ImGui::Checkbox("Follow live frames", &auto_refresh);
540
    ImGui::SameLine();
541
    if (ImGui::Button("Rebuild") || (auto_refresh && f->summary.frame != s_ghost_frame)) {
542
        rebuild_ghost(f, pal);
543
        s_ghost_frame = f->summary.frame;
544
    }
545
    ImGui::SameLine();
546
    ImGui::TextDisabled("every sprite the frame showed, on the lines it showed them");
547
548
    if (!s_ghost_valid) {
549
        ImGui::TextDisabled("Nothing composited yet.");
550
        return;
551
    }
552
    if (!s_ghost_tex.ensure(640, 480)) {
553
        ImGui::TextDisabled("No renderer available.");
554
        return;
555
    }
556
    s_ghost_tex.update(s_ghost_px);
557
558
    const float w = ImGui::GetContentRegionAvail().x;
559
    const float scale = w < 640.0f ? w / 640.0f : 1.0f;
560
    ImGui::Image(s_ghost_tex.id(), ImVec2(640 * scale, 480 * scale));
561
}
562
563
// ---------------------------------------------------------------------------
564
} // namespace
565
566
void
567
draw_sprite_multiplex_tab(const uint32_t pal[256])
568
{
569
    if (ImGui::Checkbox("Record sprite raster activity", &s_enabled)) {
570
        s_alloc_failed = !sprite_trace_set_enabled(s_enabled);
571
        if (s_alloc_failed)
572
            s_enabled = false;
573
        s_ghost_frame = 0xFFFFFFFFu;
574
        s_ghost_valid = false;
575
    }
576
    ImGui::SameLine();
577
    ImGui::SetNextItemWidth(160);
578
    if (ImGui::SliderInt("Burst gap", &s_burst_gap, 8, 2048, "%d cyc"))
579
        sprite_trace_set_burst_gap((uint32_t)s_burst_gap);
580
581
    if (s_enabled) {
582
        ImGui::SameLine();
583
        ImGui::TextDisabled("(%zu KB)", sprite_trace_memory_usage() / 1024);
584
    }
585
    if (s_alloc_failed)
586
        ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "Could not allocate the trace buffers.");
587
588
    const sprite_trace_frame_t *f = sprite_trace_last_frame();
589
    if (!f) {
590
        ImGui::Separator();
591
        ImGui::TextWrapped(
592
            "Enable recording and let a frame complete.\n\n"
593
            "The Sprites tab reads the attribute table as it stands now. This one "
594
            "reconstructs a frame from what render_sprite_line() actually did on each "
595
            "scanline, so a program that rewrites sprite attributes from a raster IRQ shows "
596
            "up as what it really drew rather than whatever the table happens to hold when "
597
            "you look at it.");
598
        return;
599
    }
600
601
    if (ImGui::BeginTabBar("mux_subtabs", ImGuiTabBarFlags_DrawSelectedOverline)) {
602
        if (ImGui::BeginTabItem("Overview")) { draw_overview(f); ImGui::EndTabItem(); }
603
        if (ImGui::BeginTabItem("Raster ribbon")) { draw_ribbon(f); ImGui::EndTabItem(); }
604
        if (ImGui::BeginTabItem("Render time")) { draw_budget(f); ImGui::EndTabItem(); }
605
        if (ImGui::BeginTabItem("Effective sprites")) { draw_effective(f); ImGui::EndTabItem(); }
606
        if (ImGui::BeginTabItem("Frame ghost")) { draw_ghost(f, pal); ImGui::EndTabItem(); }
607
        ImGui::EndTabBar();
608
    }
609
}