OXYGENHUB
# 9120e87480a6 enhanced-x16-emulator / src / debug_ui / panels / psg_panel.cpp
21.5 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// PSG panel — live VERA PSG voice, register and oscilloscope views.
2
//
3
// Tabs:
4
//   * Voices    — one row per PSG voice, decoded from the core's shadow state.
5
//   * Registers — the 64 raw PSG bytes ($1F9C0..$1F9FF), with bit summaries.
6
//   * Scope     — self-armed per-voice and summed-output oscilloscope traces.
7
//
8
// Technique: the expensive work is gated by ImGui::BeginTabItem(), so hidden
9
// tabs do not decode or refresh buffers. Voice peaks are read exactly once per
10
// visible Voices/Scope frame because psg_debug_get_channel() resets them. Scope
11
// capture is self-arming, so psg_debug_scope_read() is called only from the
12
// Scope tab; when hidden or frozen, capture naturally lapses in the audio core.
13
#include "imgui.h"
14
#include "debug_ui_panels.h"
15
#include "debug_ui_bridge.h"
16
#include "debug_ui_scope.h"
17
#include "debug_ui_widgets.h"
18
19
#include <math.h>
20
#include <stdint.h>
21
#include <stdio.h>
22
23
namespace {
24
25
constexpr uint32_t PSG_BASE_ADDR     = 0x1F9C0;
26
constexpr double   PSG_SAMPLE_RATE   = 25000000.0 / 512.0;
27
constexpr double   PSG_PHASE_STEPS   = 131072.0;
28
constexpr float    PSG_PEAK_FULLSCALE = 16352.0f;
29
constexpr float    LEVEL_METER_W      = 92.0f;
30
31
struct PsgSnapshot {
32
    psg_debug_channel ch[PSG_NUM_CHANNELS];
33
    int               audible = 0;
34
};
35
36
const char *
37
waveform_name(uint8_t wf)
38
{
39
    static const char *names[] = { "Pulse", "Saw", "Tri", "Noise" };
40
    return names[wf & 3];
41
}
42
43
ImU32
44
waveform_color(uint8_t wf, bool silent)
45
{
46
    if (silent)
47
        return IM_COL32(95, 100, 108, 255);
48
    switch (wf & 3) {
49
        case PSG_WF_PULSE:    return IM_COL32(100, 190, 255, 255);
50
        case PSG_WF_SAWTOOTH: return IM_COL32(255, 185, 90, 255);
51
        case PSG_WF_TRIANGLE: return IM_COL32(130, 225, 145, 255);
52
        default:              return IM_COL32(220, 130, 255, 255);
53
    }
54
}
55
56
bool
57
voice_silent(const psg_debug_channel &c)
58
{
59
    return c.vol_raw == 0 || (!c.left && !c.right);
60
}
61
62
double
63
freq_hz(uint16_t freq)
64
{
65
    return (double)freq * PSG_SAMPLE_RATE / PSG_PHASE_STEPS;
66
}
67
68
void
69
format_note(double hz, char *out, size_t outsz)
70
{
71
    if (hz <= 0.0) {
72
        snprintf(out, outsz, "--");
73
        return;
74
    }
75
76
    static const char *names[] = {
77
        "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
78
    };
79
    const double semis = 69.0 + 12.0 * log(hz / 440.0) / log(2.0);
80
    const int    note  = (int)floor(semis + 0.5);
81
    const int    idx   = ((note % 12) + 12) % 12;
82
    const int    oct   = note / 12 - 1;
83
    int cents = (int)floor((semis - (double)note) * 100.0 + ((semis >= (double)note) ? 0.5 : -0.5));
84
    // Pad name+octave to a constant width so the cents value stays put.
85
    char nb[12];
86
    snprintf(nb, sizeof nb, "%s%d", names[idx], oct);
87
    snprintf(out, outsz, "%-4s %+3d c", nb, cents);
88
}
89
90
void
91
read_channels(PsgSnapshot *snap)
92
{
93
    snap->audible = 0;
94
    for (int ch = 0; ch < PSG_NUM_CHANNELS; ++ch) {
95
        if (!psg_debug_get_channel(ch, &snap->ch[ch])) {
96
            snap->ch[ch] = {};
97
        }
98
        if (!voice_silent(snap->ch[ch])) {
99
            ++snap->audible;
100
        }
101
    }
102
}
103
104
// Voice state for this frame. psg_debug_get_channel() resets the peak meters as
105
// it reads, and nothing refills them while the machine is paused, so hold the
106
// last live snapshot rather than reporting silence. The first read always
107
// happens even when paused, otherwise a panel opened while stopped would show
108
// zeros until execution resumed.
109
const PsgSnapshot &
110
current_channels()
111
{
112
    static PsgSnapshot held;
113
    static bool        primed = false;
114
    if (!primed || !dbgui_audio_hold()) {
115
        read_channels(&held);
116
        primed = true;
117
    }
118
    return held;
119
}
120
121
int
122
audible_from_regs()
123
{
124
    int audible = 0;
125
    for (int ch = 0; ch < PSG_NUM_CHANNELS; ++ch) {
126
        uint8_t v = psg_debug_get_reg((uint8_t)(ch * 4 + 2));
127
        if ((v & 0x3F) != 0 && (v & 0xC0) != 0) {
128
            ++audible;
129
        }
130
    }
131
    return audible;
132
}
133
134
void
135
show_status_line()
136
{
137
    ImGui::Text("Sample %.3f Hz   audible voices %2d/%2d   noise LFSR $%04X",
138
                PSG_SAMPLE_RATE, audible_from_regs(), PSG_NUM_CHANNELS,
139
                psg_debug_get_noise_state());
140
}
141
142
void
143
text_value(const char *desc, const char *fmt, uint32_t v, int bytes)
144
{
145
    ImGui::Text(fmt, v);
146
    dbgui_hover_value_tooltip(desc, v, bytes);
147
}
148
149
// Width for a fixed-size table column, measured from a worst-case SAMPLE string
150
// rather than from the live cell contents.
151
//
152
// This matters: with ImGuiTableFlags_SizingFixedFit and no explicit width, each
153
// column re-fits to whatever it currently holds, so a value going from "0.000"
154
// to "762.939" (or "unused" to "63 (50.00%)") resized the column and shifted
155
// every column after it — the table visibly jittered while audio played. Sizing
156
// from a constant sample makes the layout depend only on the font, so it holds
157
// still. Columns are also Resizable, so a user can still widen one by hand.
158
float
159
col_w(const char *sample)
160
{
161
    return ImGui::CalcTextSize(sample).x + ImGui::GetStyle().CellPadding.x * 2.0f;
162
}
163
164
void
165
format_pw(const psg_debug_channel &c, char *out, size_t outsz)
166
{
167
    // Fixed field widths so the text does not shift around inside its column
168
    // as the value changes.
169
    switch (c.waveform & 3) {
170
        case PSG_WF_PULSE:
171
            snprintf(out, outsz, "%2u (%5.2f%%)", c.pw, (double)(c.pw + 1) * 100.0 / 128.0);
172
            break;
173
        case PSG_WF_SAWTOOTH:
174
        case PSG_WF_TRIANGLE:
175
            snprintf(out, outsz, "mask $%02X", (unsigned)((c.pw ^ 0x3F) & 0x3F));
176
            break;
177
        default:
178
            snprintf(out, outsz, "unused");
179
            break;
180
    }
181
}
182
183
const char *
184
reg_summary(int byte, uint8_t v)
185
{
186
    // Fixed field widths: these strings sit in a fixed-width table column and
187
    // are re-rendered every frame, so growing/shrinking numbers would jitter.
188
    static char buf[96];
189
    switch (byte & 3) {
190
        case 0:
191
            snprintf(buf, sizeof buf, "frequency low byte");
192
            break;
193
        case 1:
194
            snprintf(buf, sizeof buf, "frequency high byte");
195
            break;
196
        case 2:
197
            snprintf(buf, sizeof buf, "R=%-3s L=%-3s volume=%2u",
198
                     (v & 0x80) ? "on" : "off", (v & 0x40) ? "on" : "off",
199
                     (unsigned)(v & 0x3F));
200
            break;
201
        default:
202
            snprintf(buf, sizeof buf, "wave=%-5s PW/shape=%2u",
203
                     waveform_name((uint8_t)(v >> 6)), (unsigned)(v & 0x3F));
204
            break;
205
    }
206
    return buf;
207
}
208
209
void
210
draw_voice_detail(int selected, const psg_debug_channel &c)
211
{
212
    uint8_t regs[4];
213
    for (int i = 0; i < 4; ++i) {
214
        regs[i] = psg_debug_get_reg((uint8_t)(selected * 4 + i));
215
    }
216
217
    char note[32];
218
    char pw[48];
219
    const double hz = freq_hz(c.freq);
220
    format_note(hz, note, sizeof note);
221
    format_pw(c, pw, sizeof pw);
222
223
    ImGui::Separator();
224
    ImGui::Text("Voice %d detail", selected);
225
226
    // A fixed-height child: the detail body has an optional status note, one or
227
    // two PW lines and a noise-only line, so its natural height changes with the
228
    // selected voice's waveform. Reserving the space keeps everything below from
229
    // jumping as you click between voices.
230
    const float detail_h = ImGui::GetTextLineHeightWithSpacing() * 14.0f;
231
    if (!ImGui::BeginChild("psg_voice_detail", ImVec2(0.0f, detail_h), ImGuiChildFlags_None)) {
232
        ImGui::EndChild();
233
        return;
234
    }
235
236
    if (!c.left && !c.right) {
237
        ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
238
                           "L and R are both disabled: the core holds phase at 0 (normal off state).");
239
    } else if (c.vol_raw == 0) {
240
        ImGui::TextDisabled("Volume is zero: phase runs but output is silent.");
241
    } else {
242
        ImGui::TextDisabled("Voice is audible.");
243
    }
244
245
    if (ImGui::BeginTable("psg_detail_regs", 4, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg |
246
                                             ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable)) {
247
        ImGui::TableSetupColumn("Addr",   ImGuiTableColumnFlags_WidthFixed, col_w("$1F9FF_"));
248
        ImGui::TableSetupColumn("Hex",    ImGuiTableColumnFlags_WidthFixed, col_w("$FF_"));
249
        ImGui::TableSetupColumn("Bits",   ImGuiTableColumnFlags_WidthFixed, col_w("1111 1111_"));
250
        ImGui::TableSetupColumn("Decode", ImGuiTableColumnFlags_WidthFixed, col_w("R=off L=off volume=63__"));
251
        ImGui::TableHeadersRow();
252
        for (int i = 0; i < 4; ++i) {
253
            char bits[16];
254
            dbgui_format_binary(bits, sizeof bits, regs[i], 8);
255
            ImGui::TableNextRow();
256
            ImGui::TableSetColumnIndex(0);
257
            text_value("VRAM mirror address", "$%05X", PSG_BASE_ADDR + (uint32_t)selected * 4u + (uint32_t)i, 4);
258
            ImGui::TableSetColumnIndex(1);
259
            text_value("raw PSG shadow byte", "$%02X", regs[i], 1);
260
            ImGui::TableSetColumnIndex(2);
261
            ImGui::TextUnformatted(bits);
262
            ImGui::TableSetColumnIndex(3);
263
            ImGui::TextUnformatted(reg_summary(i, regs[i]));
264
        }
265
        ImGui::EndTable();
266
    }
267
268
    ImGui::Text("Frequency: $%04X / %5u = %10.6f Hz", c.freq, c.freq, hz);
269
    dbgui_hover_value_tooltip("16-bit frequency word", c.freq, 2);
270
    ImGui::Text("Nearest note: %s", note);
271
272
    if ((c.waveform & 3) == PSG_WF_PULSE) {
273
        ImGui::Text("PW: %s duty; core compares (phase >> 10) > PW.", pw);
274
        ImGui::TextDisabled("Duty cycle only applies to the Pulse waveform.");
275
    } else if ((c.waveform & 3) == PSG_WF_SAWTOOTH || (c.waveform & 3) == PSG_WF_TRIANGLE) {
276
        ImGui::Text("PW: %2u, shape mask $%02X", c.pw, (unsigned)((c.pw ^ 0x3F) & 0x3F));
277
        ImGui::TextDisabled("For Saw/Triangle this is an XOR wave-shaping mask, not duty cycle.");
278
    } else {
279
        ImGui::Text("PW: unused by Noise voices.");
280
        ImGui::TextDisabled("Noise latches a 6-bit value from the shared LFSR each phase wrap.");
281
    }
282
283
    ImGui::Text("Volume: raw %2u -> LUT %3u", (unsigned)c.vol_raw, (unsigned)c.vol_lut);
284
    ImGui::Text("Pan: L %-3s  R %-3s", c.left ? "on" : "off", c.right ? "on" : "off");
285
    ImGui::Text("Live phase: $%05X", (unsigned)(c.phase & 0x1FFFFu));
286
    dbgui_hover_value_tooltip("17-bit phase accumulator", c.phase & 0x1FFFFu, 4);
287
    ImGui::Text("Last sample: %6d   peak: %5u", (int)c.last_sample, (unsigned)c.peak);
288
    if ((c.waveform & 3) == PSG_WF_NOISE) {
289
        ImGui::Text("Latched noise value: %2u   shared LFSR: $%04X",
290
                    (unsigned)c.noiseval, (unsigned)psg_debug_get_noise_state());
291
    }
292
    ImGui::EndChild();
293
}
294
295
void
296
draw_voices_tab(int *selected_voice)
297
{
298
    PsgSnapshot snap = current_channels();
299
    if (dbgui_audio_hold()) {
300
        ImGui::TextColored(ImVec4(0.95f, 0.70f, 0.30f, 1.0f), "held (paused)");
301
        ImGui::SetItemTooltip("The machine is paused, so audio is silent and the peak meters\n"
302
                              "are no longer being fed. These are the values from the moment\n"
303
                              "it stopped.");
304
    }
305
306
    const ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
307
                                  ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY |
308
                                  ImGuiTableFlags_ScrollX | ImGuiTableFlags_Resizable;
309
    if (ImGui::BeginTable("psg_voices", 10, flags, ImVec2(0, 310))) {
310
        ImGui::TableSetupScrollFreeze(0, 1);
311
        // Samples are the widest value each column can ever hold, so the layout
312
        // never depends on the live values (see col_w).
313
        ImGui::TableSetupColumn("Ch",    ImGuiTableColumnFlags_WidthFixed, col_w("Ch__"));
314
        ImGui::TableSetupColumn("Wave",  ImGuiTableColumnFlags_WidthFixed, col_w("Noise_"));
315
        ImGui::TableSetupColumn("Freq",  ImGuiTableColumnFlags_WidthFixed, col_w("$FFFF / 65535_"));
316
        ImGui::TableSetupColumn("Hz",    ImGuiTableColumnFlags_WidthFixed, col_w("24414.062_"));
317
        ImGui::TableSetupColumn("Note",  ImGuiTableColumnFlags_WidthFixed, col_w("A#10 -50 c_"));
318
        ImGui::TableSetupColumn("PW",    ImGuiTableColumnFlags_WidthFixed, col_w("63 (50.00%)_"));
319
        ImGui::TableSetupColumn("Vol",   ImGuiTableColumnFlags_WidthFixed, col_w("63 -> 511_"));
320
        ImGui::TableSetupColumn("L",     ImGuiTableColumnFlags_WidthFixed, col_w("on_"));
321
        ImGui::TableSetupColumn("R",     ImGuiTableColumnFlags_WidthFixed, col_w("on_"));
322
        ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, LEVEL_METER_W);
323
        ImGui::TableHeadersRow();
324
325
        for (int ch = 0; ch < PSG_NUM_CHANNELS; ++ch) {
326
            const psg_debug_channel &c = snap.ch[ch];
327
            const bool silent = voice_silent(c);
328
            char note[32], pw[48], label[16];
329
            format_note(freq_hz(c.freq), note, sizeof note);
330
            format_pw(c, pw, sizeof pw);
331
            snprintf(label, sizeof label, "%2d", ch);
332
333
            ImGui::TableNextRow();
334
            if (silent) {
335
                ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.48f);
336
            }
337
338
            ImGui::TableSetColumnIndex(0);
339
            if (ImGui::Selectable(label, *selected_voice == ch, ImGuiSelectableFlags_SpanAllColumns)) {
340
                *selected_voice = ch;
341
            }
342
            ImGui::TableSetColumnIndex(1);
343
            ImGui::TextUnformatted(waveform_name(c.waveform));
344
            ImGui::TableSetColumnIndex(2);
345
            ImGui::Text("$%04X / %5u", (unsigned)c.freq, (unsigned)c.freq);
346
            dbgui_hover_value_tooltip("16-bit frequency word", c.freq, 2);
347
            ImGui::TableSetColumnIndex(3);
348
            ImGui::Text("%9.3f", freq_hz(c.freq));
349
            ImGui::TableSetColumnIndex(4);
350
            ImGui::TextUnformatted(note);
351
            ImGui::TableSetColumnIndex(5);
352
            ImGui::TextUnformatted(pw);
353
            ImGui::TableSetColumnIndex(6);
354
            ImGui::Text("%2u -> %3u", (unsigned)c.vol_raw, (unsigned)c.vol_lut);
355
            dbgui_hover_value_tooltip("volume register value", c.vol_raw, 1);
356
            ImGui::TableSetColumnIndex(7);
357
            ImGui::TextUnformatted(c.left ? "on" : "-");
358
            ImGui::TableSetColumnIndex(8);
359
            ImGui::TextUnformatted(c.right ? "on" : "-");
360
            ImGui::TableSetColumnIndex(9);
361
            char meter_id[16];
362
            snprintf(meter_id, sizeof meter_id, "##lvl%d", ch);
363
            dbgui_level_meter(meter_id, (float)c.peak / PSG_PEAK_FULLSCALE,
364
                              ImVec2(LEVEL_METER_W, ImGui::GetTextLineHeight()), waveform_color(c.waveform, silent));
365
366
            if (silent) {
367
                ImGui::PopStyleVar();
368
            }
369
        }
370
        ImGui::EndTable();
371
    }
372
373
    if (*selected_voice < 0 || *selected_voice >= PSG_NUM_CHANNELS) {
374
        *selected_voice = 0;
375
    }
376
    draw_voice_detail(*selected_voice, snap.ch[*selected_voice]);
377
}
378
379
void
380
draw_registers_tab()
381
{
382
    ImGui::TextDisabled("VERA mirrors these at $1F9C0-$1F9FF; this panel reads the PSG shadow, which psg_reset() clears.");
383
384
    const ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
385
                                  ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY |
386
                                  ImGuiTableFlags_ScrollX | ImGuiTableFlags_Resizable;
387
    if (ImGui::BeginTable("psg_regs", 5, flags, ImVec2(0, 0))) {
388
        ImGui::TableSetupScrollFreeze(0, 1);
389
        ImGui::TableSetupColumn("Voice",  ImGuiTableColumnFlags_WidthFixed, col_w("15 +3_"));
390
        ImGui::TableSetupColumn("Addr",   ImGuiTableColumnFlags_WidthFixed, col_w("$1F9FF_"));
391
        ImGui::TableSetupColumn("Hex",    ImGuiTableColumnFlags_WidthFixed, col_w("$FF_"));
392
        ImGui::TableSetupColumn("Bits",   ImGuiTableColumnFlags_WidthFixed, col_w("1111 1111_"));
393
        ImGui::TableSetupColumn("Decode", ImGuiTableColumnFlags_WidthFixed, col_w("R=off L=off volume=63__"));
394
        ImGui::TableHeadersRow();
395
396
        for (int ch = 0; ch < PSG_NUM_CHANNELS; ++ch) {
397
            for (int i = 0; i < 4; ++i) {
398
                const int reg = ch * 4 + i;
399
                const uint8_t v = psg_debug_get_reg((uint8_t)reg);
400
                char bits[16];
401
                dbgui_format_binary(bits, sizeof bits, v, 8);
402
403
                ImGui::TableNextRow();
404
                ImGui::TableSetColumnIndex(0);
405
                ImGui::Text("%2d +%d", ch, i);
406
                ImGui::TableSetColumnIndex(1);
407
                text_value("VRAM mirror address", "$%05X", PSG_BASE_ADDR + (uint32_t)reg, 4);
408
                ImGui::TableSetColumnIndex(2);
409
                text_value("raw PSG shadow byte", "$%02X", v, 1);
410
                ImGui::TableSetColumnIndex(3);
411
                ImGui::TextUnformatted(bits);
412
                ImGui::TableSetColumnIndex(4);
413
                ImGui::TextUnformatted(reg_summary(i, v));
414
            }
415
        }
416
        ImGui::EndTable();
417
    }
418
}
419
420
void
421
read_scope_buffers(int16_t buffers[PSG_SCOPE_STREAMS][PSG_SCOPE_SAMPLES], unsigned counts[PSG_SCOPE_STREAMS])
422
{
423
    for (int i = 0; i < PSG_SCOPE_STREAMS; ++i) {
424
        counts[i] = psg_debug_scope_read(i, buffers[i], PSG_SCOPE_SAMPLES);
425
    }
426
}
427
428
void
429
draw_scope_tab(int *selected_voice)
430
{
431
    static DbgScopeControls ctl;
432
    static int16_t          buffers[PSG_SCOPE_STREAMS][PSG_SCOPE_SAMPLES];
433
    static unsigned         counts[PSG_SCOPE_STREAMS];
434
    static bool             captured = false; // buffers hold real audio, not a projection
435
436
    PsgSnapshot snap = current_channels();
437
438
    dbgui_scope_controls(&ctl, PSG_SCOPE_SAMPLES);
439
    // Only refresh the traces when the machine is actually producing audio; a
440
    // read while paused would drain the rings to nothing and blank the display.
441
    const bool held = dbgui_audio_hold();
442
    bool       predicted = false;
443
    if (!ctl.freeze && !held) {
444
        read_scope_buffers(buffers, counts);
445
        captured = false;
446
        for (int i = 0; i < PSG_SCOPE_STREAMS; ++i) {
447
            if (counts[i] > 0) {
448
                captured = true;
449
                break;
450
            }
451
        }
452
    } else if (held && !captured && !ctl.freeze) {
453
        // Paused with nothing ever captured - capture is self-arming, so a scope
454
        // first opened at a breakpoint has an empty ring. The voice registers
455
        // still describe the waveform completely, so project it forward instead
456
        // of showing the user a dead panel.
457
        const unsigned n = psg_debug_scope_predict(buffers, PSG_SCOPE_SAMPLES);
458
        for (int i = 0; i < PSG_SCOPE_STREAMS; ++i) {
459
            counts[i] = n;
460
        }
461
        predicted = true;
462
    }
463
    if (held) {
464
        if (predicted) {
465
            dbgui_audio_predicted_note("the current PSG voice registers");
466
        } else {
467
            dbgui_audio_hold_note();
468
        }
469
    } else if (ctl.freeze) {
470
        ImGui::SameLine();
471
        ImGui::TextDisabled("frozen; capture will lapse");
472
    }
473
474
    const float mini_h = ImGui::GetTextLineHeight() * 4.0f;
475
    if (ImGui::BeginTable("psg_scope_grid", 4, ImGuiTableFlags_SizingStretchSame)) {
476
        for (int ch = 0; ch < PSG_NUM_CHANNELS; ++ch) {
477
            const psg_debug_channel &c = snap.ch[ch];
478
            const bool silent = voice_silent(c);
479
            char id[24], overlay[32];
480
            snprintf(id, sizeof id, "psg_scope_%d", ch);
481
            snprintf(overlay, sizeof overlay, "%02d %s%s", ch, waveform_name(c.waveform),
482
                     (ch == *selected_voice) ? " *" : "");
483
484
            ImGui::TableNextColumn();
485
            dbgui_scope(id, buffers[ch], (int)counts[ch], ImVec2(0, mini_h),
486
                        waveform_color(c.waveform, silent), ctl.window, ctl.gain, ctl.trigger, overlay);
487
            if (ImGui::IsItemClicked()) {
488
                *selected_voice = ch;
489
            }
490
        }
491
        ImGui::EndTable();
492
    }
493
494
    if (*selected_voice < 0 || *selected_voice >= PSG_NUM_CHANNELS) {
495
        *selected_voice = 0;
496
    }
497
498
    ImGui::Separator();
499
    char overlay[64];
500
    snprintf(overlay, sizeof overlay, "Voice %02d %s", *selected_voice, waveform_name(snap.ch[*selected_voice].waveform));
501
    ImGui::Text("Selected voice %d", *selected_voice);
502
    dbgui_scope("psg_scope_selected", buffers[*selected_voice], (int)counts[*selected_voice],
503
                ImVec2(0, ImGui::GetTextLineHeight() * 8.0f),
504
                waveform_color(snap.ch[*selected_voice].waveform, voice_silent(snap.ch[*selected_voice])),
505
                ctl.window, ctl.gain, ctl.trigger, overlay);
506
507
    ImGui::TextUnformatted("Summed PSG output");
508
    dbgui_scope("psg_scope_mix_l", buffers[PSG_SCOPE_MIX_L], (int)counts[PSG_SCOPE_MIX_L],
509
                ImVec2(0, ImGui::GetTextLineHeight() * 5.0f), IM_COL32(100, 210, 255, 255),
510
                ctl.window, ctl.gain, ctl.trigger, "Mix L");
511
    dbgui_scope("psg_scope_mix_r", buffers[PSG_SCOPE_MIX_R], (int)counts[PSG_SCOPE_MIX_R],
512
                ImVec2(0, ImGui::GetTextLineHeight() * 5.0f), IM_COL32(255, 170, 105, 255),
513
                ctl.window, ctl.gain, ctl.trigger, "Mix R");
514
}
515
516
// ---------------------------------------------------------------------------
517
// Panel entry point
518
// ---------------------------------------------------------------------------
519
void
520
psg_panel_render(bool *p_open)
521
{
522
    static int selected_voice = 0;
523
524
    if (ImGui::Begin("PSG", p_open)) {
525
        dbgui_window_zoom("psg");
526
        show_status_line();
527
528
        if (ImGui::BeginTabBar("psg_tabs", ImGuiTabBarFlags_DrawSelectedOverline)) {
529
            if (ImGui::BeginTabItem("Voices")) {
530
                draw_voices_tab(&selected_voice);
531
                ImGui::EndTabItem();
532
            }
533
            if (ImGui::BeginTabItem("Registers")) {
534
                draw_registers_tab();
535
                ImGui::EndTabItem();
536
            }
537
            if (ImGui::BeginTabItem("Scope")) {
538
                draw_scope_tab(&selected_voice);
539
                ImGui::EndTabItem();
540
            }
541
            ImGui::EndTabBar();
542
        }
543
    }
544
    dbgui_window_end();
545
}
546
547
} // namespace
548
549
static DebugPanelRegistration s_reg("PSG", psg_panel_render, true);