OXYGENHUB
◆ rrdc-perf enhanced-x16-emulator / src / debug_ui / panels / fm_panel.cpp
31.2 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
1
// YM2151 panel — live FM synthesis inspection for the Commander X16.
2
//
3
// Tabs:
4
//   * Channels  — the 8 OPM channels: note, pan, algorithm, key-on, EG and level.
5
//   * Operators — all parameters for the selected channel's M1/M2/C1/C2 slots.
6
//   * Algorithm — a live connection diagram for the selected channel's algorithm.
7
//   * Global    — LFO, noise, timers, CT pins, status and chip/sample clocks.
8
//   * Registers — all 256 accepted YM register shadows with compact decodes.
9
//   * Scope     — per-channel and summed waveforms using the shared scope widget.
10
//
11
// Technique: the panel reads only the debugger bridge's side-effect-free YM2151
12
// shadow/live accessors. Expensive work is gated by ImGui::BeginTabItem(), and
13
// YM_debug_scope_read() is called only from the Scope tab: reading it self-arms
14
// the audio core's per-channel capture for a short time, so hidden tabs do not
15
// keep ymfm doing extra per-sample output work.
16
#include "imgui.h"
17
#include "debug_ui_panels.h"
18
#include "debug_ui_bridge.h"
19
#include "debug_ui_widgets.h"
20
#include "debug_ui_scope.h"
21
22
#include <math.h>
23
#include <stdint.h>
24
#include <stdio.h>
25
#include <string.h>
26
27
namespace {
28
29
constexpr double YM_CLOCK_HZ = 3579545.0;
30
constexpr double YM_SAMPLE_HZ = YM_CLOCK_HZ / 64.0;
31
constexpr int    OP_COUNT = 4;
32
33
static int s_selected_ch = 0;
34
35
struct AlgorithmInfo {
36
    uint8_t inputs[4];      // opout source selector used for this operator's phase input
37
    uint8_t carriers_mask;  // bits 0..3: operator output is summed to the channel output
38
    const char *desc;
39
};
40
41
// Derived from fm_channel<RegisterType>::output_4op() in ymfm_fm.ipp, and kept
42
// in ymfm's own O1..O4 order so it can be re-checked against that table
43
// verbatim. ymfm's O order is NOT the OPM register-slot order: ymfm's
44
// operator_map() gives channel N the operator list (N, N+16, N+8, N+24), and an
45
// operator's registers live at base + index, so
46
//
47
//     O1 -> slot 0 (M1)   O2 -> slot 2 (C1)   O3 -> slot 1 (M2)   O4 -> slot 3 (C2)
48
//
49
// Everything else in this panel is slot-indexed (op_reg/op_index use
50
// base + slot*8 + ch), so `inputs` and `carriers_mask` are permuted through
51
// kOpToSlot at the point of use rather than being pre-swapped here.
52
static const AlgorithmInfo s_alg[8] = {
53
    {{0, 1, 2, 3}, 0x8, "M1 -> C1 -> M2 -> C2 -> out"},
54
    {{0, 0, 5, 3}, 0x8, "(M1 + C1) -> M2 -> C2 -> out"},
55
    {{0, 0, 2, 6}, 0x8, "(M1 + (C1 -> M2)) -> C2 -> out"},
56
    {{0, 1, 0, 7}, 0x8, "((M1 -> C1) + M2) -> C2 -> out"},
57
    {{0, 1, 0, 3}, 0xA, "(M1 -> C1) + (M2 -> C2) -> out"},
58
    {{0, 1, 1, 1}, 0xE, "M1 modulates C1, M2 and C2; all three out"},
59
    {{0, 1, 0, 0}, 0xE, "(M1 -> C1) + M2 + C2 -> out"},
60
    {{0, 0, 0, 0}, 0xF, "M1 + M2 + C1 + C2 -> out"},
61
};
62
63
// ymfm operator index (O1..O4) -> OPM register slot. Self-inverse.
64
static const int kOpToSlot[OP_COUNT] = { 0, 2, 1, 3 };
65
66
// Re-index a mask of ymfm O-indices into a mask of register slots.
67
inline uint8_t op_mask_to_slot_mask(uint8_t op_mask)
68
{
69
    uint8_t slots = 0;
70
    for (int o = 0; o < OP_COUNT; ++o) {
71
        if ((op_mask & (1u << o)) != 0) slots |= (uint8_t)(1u << kOpToSlot[o]);
72
    }
73
    return slots;
74
}
75
76
static const char *s_slot_names[OP_COUNT] = { "M1", "M2", "C1", "C2" };
77
static const char *s_eg_names[5] = { "-", "Attack", "Decay", "Sustain", "Release" };
78
static const char *s_lfo_waves[4] = { "saw", "square", "triangle", "noise" };
79
80
inline uint8_t ym_reg(int reg) { return YM_debug_get_reg((uint8_t)reg); }
81
inline uint8_t ch_reg(int base, int ch) { return ym_reg(base + ch); }
82
inline uint8_t op_reg(int base, int slot, int ch) { return ym_reg(base + slot * 8 + ch); }
83
inline int op_index(int slot, int ch) { return slot * 8 + ch; }
84
85
struct NoteInfo {
86
    bool valid;
87
    char text[48];
88
    double hz;
89
};
90
91
float clamp01(float v)
92
{
93
    if (v < 0.0f) return 0.0f;
94
    if (v > 1.0f) return 1.0f;
95
    return v;
96
}
97
98
uint8_t alg_for_ch(int ch) { return (uint8_t)(ch_reg(0x20, ch) & 0x07); }
99
100
// Carriers as a SLOT mask, so it can be indexed the same way as everything else
101
// in this panel. s_alg holds ymfm O-indices, hence the permutation.
102
uint8_t carriers_for_alg(int alg) { return op_mask_to_slot_mask(s_alg[alg & 7].carriers_mask); }
103
104
float operator_level(int slot, int ch)
105
{
106
    ym_debug_operator op = {};
107
    if (!YM_debug_get_operator(op_index(slot, ch), &op)) {
108
        return 0.0f;
109
    }
110
    return clamp01(1.0f - (float)op.eg_attenuation / 1023.0f);
111
}
112
113
float channel_carrier_level(int ch)
114
{
115
    const uint8_t carriers = carriers_for_alg(alg_for_ch(ch));
116
    float level = 0.0f;
117
    for (int slot = 0; slot < OP_COUNT; ++slot) {
118
        if ((carriers & (1u << slot)) != 0) {
119
            const float l = operator_level(slot, ch);
120
            if (l > level) level = l;
121
        }
122
    }
123
    return level;
124
}
125
126
bool channel_effectively_silent(int ch)
127
{
128
    for (int slot = 0; slot < OP_COUNT; ++slot) {
129
        ym_debug_operator op = {};
130
        if (YM_debug_get_operator(op_index(slot, ch), &op) && op.eg_attenuation < 0x3f0) {
131
            return false;
132
        }
133
    }
134
    return true;
135
}
136
137
ImU32 eg_color(uint8_t state)
138
{
139
    switch (state) {
140
    case YM_EG_ATTACK:  return IM_COL32(105, 220, 125, 255);
141
    case YM_EG_DECAY:   return IM_COL32(235, 205, 90, 255);
142
    case YM_EG_SUSTAIN: return IM_COL32(105, 170, 255, 255);
143
    case YM_EG_RELEASE: return IM_COL32(195, 130, 230, 255);
144
    default:            return IM_COL32(120, 126, 136, 255);
145
    }
146
}
147
148
void note_from_kc_kf(uint8_t kc, uint8_t kf, NoteInfo *out)
149
{
150
    static const char *names[12] = { "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "C" };
151
    const int oct = kc >> 4;
152
    const int low = kc & 0x0f;
153
    const int semi = (kc & 3) + 3 * ((kc >> 2) & 3);
154
    out->valid = (low != 3 && low != 7 && low != 11 && low != 15);
155
    if (!out->valid) {
156
        out->hz = 0.0;
157
        snprintf(out->text, sizeof(out->text), "invalid KC %02X", kc);
158
        return;
159
    }
160
    const double fine_semi = (double)(kf >> 2) / 64.0;
161
    const double midi = 12.0 * (double)(oct + 1) + 1.0 + (double)semi + fine_semi;
162
    out->hz = 440.0 * pow(2.0, (midi - 69.0) / 12.0);
163
    snprintf(out->text, sizeof(out->text), "%s%d %+d c  %.2f Hz", names[semi], oct, (int)(fine_semi * 100.0 + 0.5), out->hz);
164
}
165
166
void draw_key_mask(uint8_t mask)
167
{
168
    // YM_debug_get_keyon() is already repacked into slot order. Raw $08 bits are
169
    // M1,C1,M2,C2, which is why the Registers tab calls that out separately.
170
    for (int slot = 0; slot < OP_COUNT; ++slot) {
171
        if (slot != 0) ImGui::SameLine(0.0f, 4.0f);
172
        ImGui::TextColored((mask & (1u << slot)) ? ImVec4(0.45f, 1.0f, 0.55f, 1.0f)
173
                                                 : ImVec4(0.35f, 0.36f, 0.38f, 1.0f),
174
                           "%s", s_slot_names[slot]);
175
    }
176
}
177
178
void draw_eg_summary(int ch)
179
{
180
    for (int slot = 0; slot < OP_COUNT; ++slot) {
181
        ym_debug_operator op = {};
182
        uint8_t state = 0;
183
        if (YM_debug_get_operator(op_index(slot, ch), &op)) {
184
            state = op.eg_state;
185
        }
186
        const char c = (state == YM_EG_ATTACK) ? 'A' : (state == YM_EG_DECAY) ? 'D' :
187
                       (state == YM_EG_SUSTAIN) ? 'S' : (state == YM_EG_RELEASE) ? 'R' : '-';
188
        if (slot != 0) ImGui::SameLine(0.0f, 4.0f);
189
        ImGui::TextColored(ImColor(eg_color(state)), "%c", c);
190
    }
191
}
192
193
void draw_channels_tab()
194
{
195
    // Channels are numbered 0-7 throughout this panel, matching the register
196
    // offsets ($20+ch, $28+ch, ...) and the channel field of the $08 key-on
197
    // register — which is what you cross-reference while debugging. (The OPM
198
    // datasheet numbers them 1-8.)
199
    ImGui::TextDisabled("Channels are 0-7, matching the $20+ch / $28+ch register offsets "
200
                        "and the $08 key-on channel field.");
201
    if (ImGui::BeginTable("ym_channels", 11, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) {
202
        ImGui::TableSetupColumn("Ch", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Ch", "00"));
203
        ImGui::TableSetupColumn("KC / note");
204
        ImGui::TableSetupColumn("KF", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("KF", "$FF"));
205
        ImGui::TableSetupColumn("ALG", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("ALG", "000"));
206
        ImGui::TableSetupColumn("FB", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("FB", "00"));
207
        ImGui::TableSetupColumn("Pan", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Pan", "L+R"));
208
        ImGui::TableSetupColumn("PMS", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("PMS", "000"));
209
        ImGui::TableSetupColumn("AMS", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("AMS", "000"));
210
        ImGui::TableSetupColumn("Key-on");
211
        ImGui::TableSetupColumn("EG");
212
        ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Level", "-00.0 dB"));
213
        ImGui::TableHeadersRow();
214
        for (int ch = 0; ch < YM_NUM_CHANNELS; ++ch) {
215
            const uint8_t r20 = ch_reg(0x20, ch);
216
            const uint8_t kc = ch_reg(0x28, ch) & 0x7f;
217
            const uint8_t kf = ch_reg(0x30, ch) & 0xfc;
218
            const uint8_t r38 = ch_reg(0x38, ch);
219
            NoteInfo note;
220
            note_from_kc_kf(kc, kf, &note);
221
            const bool silent = channel_effectively_silent(ch);
222
223
            ImGui::TableNextRow();
224
            if (silent) ImGui::BeginDisabled();
225
            ImGui::TableSetColumnIndex(0);
226
            char id[16];
227
            snprintf(id, sizeof(id), "Ch %d", ch);
228
            if (ImGui::Selectable(id, s_selected_ch == ch, ImGuiSelectableFlags_SpanAllColumns)) {
229
                s_selected_ch = ch;
230
            }
231
            ImGui::TableSetColumnIndex(1);
232
            ImGui::Text("$%02X  %s", kc, note.text);
233
            if (!note.valid) ImGui::SameLine(), ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "!");
234
            ImGui::TableSetColumnIndex(2); ImGui::Text("%2u", (unsigned)(kf >> 2));
235
            ImGui::TableSetColumnIndex(3); ImGui::Text("%u", (unsigned)(r20 & 7));
236
            ImGui::TableSetColumnIndex(4); ImGui::Text("%u", (unsigned)((r20 >> 3) & 7));
237
            ImGui::TableSetColumnIndex(5); ImGui::Text("%c/%c", (r20 & 0x40) ? 'L' : '-', (r20 & 0x80) ? 'R' : '-');
238
            ImGui::TableSetColumnIndex(6); ImGui::Text("%u", (unsigned)((r38 >> 4) & 7));
239
            ImGui::TableSetColumnIndex(7); ImGui::Text("%u", (unsigned)(r38 & 3));
240
            ImGui::TableSetColumnIndex(8); draw_key_mask(YM_debug_get_keyon(ch));
241
            ImGui::TableSetColumnIndex(9); draw_eg_summary(ch);
242
            ImGui::TableSetColumnIndex(10);
243
            snprintf(id, sizeof(id), "##lev%d", ch);
244
            dbgui_level_meter(id, channel_carrier_level(ch), ImVec2(80.0f, 0.0f), IM_COL32(80, 190, 110, 255));
245
            if (silent) ImGui::EndDisabled();
246
        }
247
        ImGui::EndTable();
248
    }
249
}
250
251
void draw_env_shape(int slot, int ch, ImVec2 size)
252
{
253
    ImGui::PushID(slot * 16 + ch);
254
    ImGui::InvisibleButton("##env", size);
255
    const ImVec2 p0 = ImGui::GetItemRectMin();
256
    const ImVec2 p1 = ImGui::GetItemRectMax();
257
    ImGui::PopID();
258
    ImDrawList *dl = ImGui::GetWindowDrawList();
259
    dl->AddRectFilled(p0, p1, IM_COL32(16, 18, 22, 255));
260
    dl->AddRect(p0, p1, IM_COL32(70, 78, 90, 255));
261
262
    const uint8_t tl = op_reg(0x60, slot, ch) & 0x7f;
263
    const uint8_t ar = op_reg(0x80, slot, ch) & 0x1f;
264
    const uint8_t d1r = op_reg(0xa0, slot, ch) & 0x1f;
265
    const uint8_t d2r = op_reg(0xc0, slot, ch) & 0x1f;
266
    const uint8_t d1l = (op_reg(0xe0, slot, ch) >> 4) & 0x0f;
267
    const uint8_t rr = op_reg(0xe0, slot, ch) & 0x0f;
268
269
    const float w = p1.x - p0.x;
270
    const float h = p1.y - p0.y;
271
    const float loud = (float)tl / 127.0f;
272
    const float sustain = (float)d1l / 15.0f;
273
    const float x1 = p0.x + w * (0.12f + (31 - ar) * 0.0025f);
274
    const float x2 = x1 + w * (0.14f + (31 - d1r) * 0.003f);
275
    const float x3 = x2 + w * (0.18f + (31 - d2r) * 0.002f);
276
    const float x4 = p1.x - w * (0.08f + (15 - rr) * 0.004f);
277
    const float y0 = p1.y - 2.0f;
278
    const float y_peak = p0.y + 2.0f + h * loud;
279
    const float y_sus = p0.y + 2.0f + h * (loud + (1.0f - loud) * sustain);
280
    ImVec2 pts[5] = { ImVec2(p0.x + 2.0f, y0), ImVec2(x1, y_peak), ImVec2(x2, y_sus), ImVec2(x3, y_sus), ImVec2(x4, y0) };
281
    dl->AddPolyline(pts, 5, IM_COL32(120, 190, 255, 255), false, 1.5f);
282
}
283
284
void draw_operator_row(int ch, int slot)
285
{
286
    const uint8_t r40 = op_reg(0x40, slot, ch);
287
    const uint8_t r60 = op_reg(0x60, slot, ch);
288
    const uint8_t r80 = op_reg(0x80, slot, ch);
289
    const uint8_t ra0 = op_reg(0xa0, slot, ch);
290
    const uint8_t rc0 = op_reg(0xc0, slot, ch);
291
    const uint8_t re0 = op_reg(0xe0, slot, ch);
292
    ym_debug_operator op = {};
293
    YM_debug_get_operator(op_index(slot, ch), &op);
294
295
    ImGui::TableNextRow();
296
    ImGui::TableSetColumnIndex(0); ImGui::Text("Ch %d %s", ch, s_slot_names[slot]);
297
    ImGui::TableSetColumnIndex(1); ImGui::Text("%u", (unsigned)((r40 >> 4) & 7));
298
    ImGui::TableSetColumnIndex(2); ImGui::Text("%u", (unsigned)(r40 & 0x0f));
299
    ImGui::TableSetColumnIndex(3); ImGui::Text("%u", (unsigned)(r60 & 0x7f));
300
    ImGui::TableSetColumnIndex(4); ImGui::Text("%u", (unsigned)((r80 >> 6) & 3));
301
    ImGui::TableSetColumnIndex(5); ImGui::Text("%u", (unsigned)(r80 & 0x1f));
302
    ImGui::TableSetColumnIndex(6); ImGui::Text("%u", (unsigned)(ra0 & 0x1f));
303
    ImGui::TableSetColumnIndex(7); ImGui::Text("%u", (unsigned)((rc0 >> 6) & 3));
304
    ImGui::TableSetColumnIndex(8); ImGui::Text("%u", (unsigned)(rc0 & 0x1f));
305
    ImGui::TableSetColumnIndex(9); ImGui::Text("%u", (unsigned)((re0 >> 4) & 0x0f));
306
    ImGui::TableSetColumnIndex(10); ImGui::Text("%u", (unsigned)(re0 & 0x0f));
307
    ImGui::TableSetColumnIndex(11); ImGui::TextUnformatted((ra0 & 0x80) ? "yes" : "no");
308
    ImGui::TableSetColumnIndex(12); ImGui::TextColored(ImColor(eg_color(op.eg_state)), "%s", s_eg_names[(op.eg_state <= YM_EG_RELEASE) ? op.eg_state : 0]);
309
    ImGui::TableSetColumnIndex(13);
310
    char id[32];
311
    snprintf(id, sizeof(id), "##att%d_%d", ch, slot);
312
    dbgui_level_meter(id, operator_level(slot, ch), ImVec2(90.0f, 0.0f), IM_COL32(90, 170, 240, 255));
313
    ImGui::TableSetColumnIndex(14);
314
    draw_env_shape(slot, ch, ImVec2(110.0f, 28.0f));
315
}
316
317
void draw_operators_tab()
318
{
319
    static bool show_all = false;
320
    ImGui::Checkbox("Show all 32 operators grouped by channel", &show_all);
321
    ImGui::TextDisabled("Envelope sketches are qualitative ADSR/TL shapes, not cycle-accurate ymfm EG traces.");
322
    const int first_ch = show_all ? 0 : s_selected_ch;
323
    const int last_ch = show_all ? YM_NUM_CHANNELS : (s_selected_ch + 1);
324
    if (ImGui::BeginTable("ym_ops", 15, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0.0f, 0.0f))) {
325
        const char *cols[15] = { "Slot", "DT1", "MUL", "TL", "KS", "AR", "D1R", "DT2", "D2R", "D1L", "RR", "AMS-EN", "EG state", "Atten", "ADSR sketch" };
326
        for (int i = 0; i < 15; ++i) ImGui::TableSetupColumn(cols[i]);
327
        ImGui::TableHeadersRow();
328
        for (int ch = first_ch; ch < last_ch; ++ch) {
329
            for (int slot = 0; slot < OP_COUNT; ++slot) draw_operator_row(ch, slot);
330
        }
331
        ImGui::EndTable();
332
    }
333
}
334
335
uint8_t source_mask(uint8_t selector)
336
{
337
    switch (selector) {
338
    case 1: return 0x1;
339
    case 2: return 0x2;
340
    case 3: return 0x4;
341
    case 5: return 0x3;
342
    case 6: return 0x5;
343
    case 7: return 0x6;
344
    default: return 0x0;
345
    }
346
}
347
348
void draw_arrow(ImDrawList *dl, ImVec2 a, ImVec2 b, ImU32 color)
349
{
350
    dl->AddLine(a, b, color, 2.0f);
351
    const float ang = atan2f(b.y - a.y, b.x - a.x);
352
    const float len = 8.0f;
353
    const ImVec2 p1(b.x - cosf(ang - 0.45f) * len, b.y - sinf(ang - 0.45f) * len);
354
    const ImVec2 p2(b.x - cosf(ang + 0.45f) * len, b.y - sinf(ang + 0.45f) * len);
355
    dl->AddTriangleFilled(b, p1, p2, color);
356
}
357
358
void draw_algorithm_tab()
359
{
360
    const int ch = s_selected_ch;
361
    const int alg = alg_for_ch(ch);
362
    const uint8_t fb = (ch_reg(0x20, ch) >> 3) & 7;
363
    ImGui::Text("Channel %d  ALG %d: %s", ch, alg, s_alg[alg].desc);
364
    ImGui::TextDisabled("Algorithm table is derived from ymfm_fm.ipp output_4op(). Box brightness follows live EG attenuation.");
365
366
    const ImVec2 canvas = ImVec2(ImGui::GetContentRegionAvail().x, 330.0f);
367
    ImGui::InvisibleButton("alg_canvas", canvas);
368
    const ImVec2 p0 = ImGui::GetItemRectMin();
369
    const ImVec2 p1 = ImGui::GetItemRectMax();
370
    ImDrawList *dl = ImGui::GetWindowDrawList();
371
    dl->AddRectFilled(p0, p1, IM_COL32(14, 16, 20, 255));
372
    dl->AddRect(p0, p1, IM_COL32(70, 78, 90, 255));
373
374
    ImVec2 c[OP_COUNT] = {
375
        ImVec2(p0.x + canvas.x * 0.16f, p0.y + canvas.y * 0.42f),
376
        ImVec2(p0.x + canvas.x * 0.38f, p0.y + canvas.y * 0.25f),
377
        ImVec2(p0.x + canvas.x * 0.38f, p0.y + canvas.y * 0.62f),
378
        ImVec2(p0.x + canvas.x * 0.64f, p0.y + canvas.y * 0.42f),
379
    };
380
    const ImVec2 box(70.0f, 42.0f);
381
    const uint8_t carriers = carriers_for_alg(alg);
382
    const ImU32 mod_col = IM_COL32(140, 170, 220, 255);
383
    const ImU32 out_col = IM_COL32(110, 230, 130, 255);
384
385
    // s_alg[].inputs is indexed by ymfm operator (O1..O4) and source_mask()
386
    // yields an O-index mask, so both ends of each arrow are permuted into slot
387
    // space to match the box layout below.
388
    for (int dst_op = 1; dst_op < OP_COUNT; ++dst_op) {
389
        const uint8_t srcs = source_mask(s_alg[alg].inputs[dst_op]);
390
        const int dst = kOpToSlot[dst_op];
391
        for (int src_op = 0; src_op < OP_COUNT; ++src_op) {
392
            if ((srcs & (1u << src_op)) != 0) {
393
                const int src = kOpToSlot[src_op];
394
                draw_arrow(dl, ImVec2(c[src].x + box.x * 0.5f, c[src].y), ImVec2(c[dst].x - box.x * 0.5f, c[dst].y), mod_col);
395
            }
396
        }
397
    }
398
    if (fb != 0) {
399
        const ImVec2 a(c[0].x - 18.0f, c[0].y - box.y * 0.5f);
400
        const ImVec2 b(c[0].x + 18.0f, c[0].y - box.y * 0.5f);
401
        dl->AddBezierCubic(a, ImVec2(a.x - 45.0f, a.y - 60.0f), ImVec2(b.x + 45.0f, b.y - 60.0f), b, IM_COL32(235, 170, 80, 255), 2.0f);
402
        dl->AddText(ImVec2(c[0].x - 18.0f, a.y - 58.0f), IM_COL32(235, 185, 100, 255), "FB");
403
    }
404
    const ImVec2 out(p0.x + canvas.x * 0.86f, p0.y + canvas.y * 0.42f);
405
    for (int slot = 0; slot < OP_COUNT; ++slot) {
406
        if ((carriers & (1u << slot)) != 0) {
407
            draw_arrow(dl, ImVec2(c[slot].x + box.x * 0.5f, c[slot].y), out, out_col);
408
        }
409
    }
410
    dl->AddText(ImVec2(out.x + 8.0f, out.y - 8.0f), out_col, "OUT");
411
412
    for (int slot = 0; slot < OP_COUNT; ++slot) {
413
        const float lvl = operator_level(slot, ch);
414
        const int base = (int)(55.0f + lvl * 150.0f);
415
        const bool carrier = (carriers & (1u << slot)) != 0;
416
        const ImU32 fill = carrier ? IM_COL32(35, base, 65, 255) : IM_COL32(35, 55, base, 255);
417
        const ImVec2 b0(c[slot].x - box.x * 0.5f, c[slot].y - box.y * 0.5f);
418
        const ImVec2 b1(c[slot].x + box.x * 0.5f, c[slot].y + box.y * 0.5f);
419
        dl->AddRectFilled(b0, b1, fill, 5.0f);
420
        dl->AddRect(b0, b1, carrier ? out_col : mod_col, 5.0f, 0, 2.0f);
421
        char label[32];
422
        snprintf(label, sizeof(label), "%s\n%.0f%%", s_slot_names[slot], lvl * 100.0f);
423
        dl->AddText(ImVec2(b0.x + 8.0f, b0.y + 5.0f), IM_COL32(235, 240, 245, 255), label);
424
    }
425
}
426
427
void draw_global_tab()
428
{
429
    const uint8_t r0f = ym_reg(0x0f);
430
    const uint8_t r14 = ym_reg(0x14);
431
    const uint8_t r18 = ym_reg(0x18);
432
    const uint8_t r19 = ym_reg(0x19);
433
    const uint8_t r1a = ym_reg(0x1a);
434
    const uint8_t r1b = ym_reg(0x1b);
435
    const uint16_t ta = (uint16_t)((ym_reg(0x10) << 2) | (ym_reg(0x11) & 3));
436
    const uint8_t tb = ym_reg(0x12);
437
    const double ta_ms = (1024.0 - (double)ta) / YM_SAMPLE_HZ * 1000.0;
438
    const double tb_ms = (256.0 - (double)tb) * 16.0 / YM_SAMPLE_HZ * 1000.0;
439
    const uint8_t status = YM_debug_get_status();
440
441
    ImGui::Text("Clock: %.0f Hz   ymfm sample rate: %.3f Hz", YM_CLOCK_HZ, YM_SAMPLE_HZ);
442
    ImGui::Separator();
443
    ImGui::Text("LFO: rate $%02X, waveform %s, AM depth %u, PM depth %u", r18, s_lfo_waves[r1b & 3], (unsigned)(r19 & 0x7f), (unsigned)(r1a & 0x7f));
444
    ImGui::TextDisabled("$19 writes select AM (bit7=0) or PM (bit7=1); PM is shown from the shadowed internal $1A.");
445
    ImGui::Text("Noise: %s, frequency %u", (r0f & 0x80) ? "enabled" : "disabled", (unsigned)(r0f & 0x1f));
446
    ImGui::Text("CT pins: CT1=%u CT2=%u", (unsigned)((r1b >> 6) & 1), (unsigned)((r1b >> 7) & 1));
447
    ImGui::Separator();
448
    ImGui::Text("Timer A: %u  approx %.3f ms", (unsigned)ta, ta_ms);
449
    ImGui::Text("Timer B: %u  approx %.3f ms", (unsigned)tb, tb_ms);
450
    ImGui::Text("Mode $14: CSM=%u reset TB/TA=%u/%u enable TB/TA=%u/%u load TB/TA=%u/%u",
451
                (unsigned)((r14 >> 7) & 1), (unsigned)((r14 >> 5) & 1), (unsigned)((r14 >> 4) & 1),
452
                (unsigned)((r14 >> 3) & 1), (unsigned)((r14 >> 2) & 1), (unsigned)((r14 >> 1) & 1), (unsigned)(r14 & 1));
453
    ImGui::Separator();
454
    ImGui::Text("Status $%02X: Timer A ovf=%u  Timer B ovf=%u  busy bit=%u", status, (unsigned)(status & 1), (unsigned)((status >> 1) & 1), (unsigned)((status >> 7) & 1));
455
    ImGui::Text("YM_debug_is_busy(): %s", YM_debug_is_busy() ? "true" : "false");
456
}
457
458
const char *system_name(int reg)
459
{
460
    switch (reg) {
461
    case 0x01: return "TEST / LFO reset";
462
    case 0x08: return "Key-on";
463
    case 0x0f: return "Noise";
464
    case 0x10: return "Timer A high";
465
    case 0x11: return "Timer A low";
466
    case 0x12: return "Timer B";
467
    case 0x14: return "Timer control";
468
    case 0x18: return "LFO frequency";
469
    case 0x19: return "AM/PM depth write";
470
    case 0x1a: return "PM depth shadow";
471
    case 0x1b: return "CT / LFO wave";
472
    default: return "System";
473
    }
474
}
475
476
void decode_reg(int reg, uint8_t v, char *out, size_t outsz)
477
{
478
    if (reg < 0x20) {
479
        switch (reg) {
480
        case 0x01: snprintf(out, outsz, "test=$%02X LFO reset=%u", v, (unsigned)((v >> 1) & 1)); break;
481
        case 0x08: snprintf(out, outsz, "ch=%u raw op bits b3-b6=M1,C1,M2,C2", (unsigned)(v & 7)); break;
482
        case 0x0f: snprintf(out, outsz, "noise %s freq=%u", (v & 0x80) ? "on" : "off", (unsigned)(v & 0x1f)); break;
483
        case 0x14: snprintf(out, outsz, "CSM=%u en TB/TA=%u/%u load TB/TA=%u/%u", (unsigned)(v >> 7), (unsigned)((v >> 3) & 1), (unsigned)((v >> 2) & 1), (unsigned)((v >> 1) & 1), (unsigned)(v & 1)); break;
484
        case 0x18: snprintf(out, outsz, "LFO freq=%u", (unsigned)v); break;
485
        case 0x19: snprintf(out, outsz, "%s depth write=%u", (v & 0x80) ? "PM" : "AM", (unsigned)(v & 0x7f)); break;
486
        case 0x1b: snprintf(out, outsz, "CT=%u wave=%s", (unsigned)((v >> 6) & 3), s_lfo_waves[v & 3]); break;
487
        default: snprintf(out, outsz, "%s", system_name(reg)); break;
488
        }
489
    } else if (reg < 0x40) {
490
        const int ch = reg & 7;
491
        const int base = reg & 0xf8;
492
        if (base == 0x20) snprintf(out, outsz, "Ch%d pan L/R=%u/%u FB=%u ALG=%u", ch, (unsigned)((v >> 6) & 1), (unsigned)((v >> 7) & 1), (unsigned)((v >> 3) & 7), (unsigned)(v & 7));
493
        else if (base == 0x28) snprintf(out, outsz, "Ch%d KC=%u", ch, (unsigned)(v & 0x7f));
494
        else if (base == 0x30) snprintf(out, outsz, "Ch%d KF=%u", ch, (unsigned)(v >> 2));
495
        else if (base == 0x38) snprintf(out, outsz, "Ch%d PMS=%u AMS=%u", ch, (unsigned)((v >> 4) & 7), (unsigned)(v & 3));
496
        else snprintf(out, outsz, "Ch%d unused/shadow", ch);
497
    } else {
498
        const int off = reg - 0x40;
499
        const int group = reg & 0xe0;
500
        const int slot = (off >> 3) & 3;
501
        const int ch = off & 7;
502
        if (group == 0x40) snprintf(out, outsz, "%s Ch%d DT1=%u MUL=%u", s_slot_names[slot], ch, (unsigned)((v >> 4) & 7), (unsigned)(v & 0x0f));
503
        else if (group == 0x60) snprintf(out, outsz, "%s Ch%d TL=%u", s_slot_names[slot], ch, (unsigned)(v & 0x7f));
504
        else if (group == 0x80) snprintf(out, outsz, "%s Ch%d KS=%u AR=%u", s_slot_names[slot], ch, (unsigned)((v >> 6) & 3), (unsigned)(v & 0x1f));
505
        else if (group == 0xa0) snprintf(out, outsz, "%s Ch%d AMS-EN=%u D1R=%u", s_slot_names[slot], ch, (unsigned)((v >> 7) & 1), (unsigned)(v & 0x1f));
506
        else if (group == 0xc0) snprintf(out, outsz, "%s Ch%d DT2=%u D2R=%u", s_slot_names[slot], ch, (unsigned)((v >> 6) & 3), (unsigned)(v & 0x1f));
507
        else snprintf(out, outsz, "%s Ch%d D1L=%u RR=%u", s_slot_names[slot], ch, (unsigned)((v >> 4) & 0x0f), (unsigned)(v & 0x0f));
508
    }
509
}
510
511
void draw_register_range(const char *label, int first, int last)
512
{
513
    ImGui::TextUnformatted(label);
514
    if (ImGui::BeginTable(label, 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) {
515
        ImGui::TableSetupColumn("Reg", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Reg", "$FF"));
516
        ImGui::TableSetupColumn("Layout", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Layout", "1111 1111"));
517
        ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Name", "AMS-EN / D1R"));
518
        ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Value", "$FF (255)"));
519
        ImGui::TableSetupColumn("Decode");
520
        ImGui::TableHeadersRow();
521
        for (int reg = first; reg <= last; ++reg) {
522
            const uint8_t v = ym_reg(reg);
523
            char bin[16];
524
            char layout[32];
525
            char name[64];
526
            char dec[160];
527
            dbgui_format_binary(bin, sizeof(bin), v, 8);
528
            if (reg < 0x20) snprintf(layout, sizeof(layout), "system");
529
            else if (reg < 0x40) snprintf(layout, sizeof(layout), "ch %d", (reg & 7));
530
            else snprintf(layout, sizeof(layout), "%s ch %d", s_slot_names[((reg - 0x40) >> 3) & 3], (reg & 7));
531
            if (reg < 0x20) snprintf(name, sizeof(name), "%s", system_name(reg));
532
            else if (reg < 0x40) snprintf(name, sizeof(name), "%s", ((reg & 0xf8) == 0x20) ? "Pan/FB/ALG" : ((reg & 0xf8) == 0x28) ? "KC" : ((reg & 0xf8) == 0x30) ? "KF" : ((reg & 0xf8) == 0x38) ? "PMS/AMS" : "unused");
533
            else snprintf(name, sizeof(name), "%s", ((reg & 0xe0) == 0x40) ? "DT1/MUL" : ((reg & 0xe0) == 0x60) ? "TL" : ((reg & 0xe0) == 0x80) ? "KS/AR" : ((reg & 0xe0) == 0xa0) ? "AMS-EN/D1R" : ((reg & 0xe0) == 0xc0) ? "DT2/D2R" : "D1L/RR");
534
            decode_reg(reg, v, dec, sizeof(dec));
535
            ImGui::TableNextRow();
536
            ImGui::TableSetColumnIndex(0); ImGui::Text("$%02X", reg); dbgui_hover_value_tooltip(dec, v, 1);
537
            ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(layout);
538
            ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(name);
539
            ImGui::TableSetColumnIndex(3); ImGui::Text("$%02X  %s", v, bin); dbgui_hover_value_tooltip(dec, v, 1);
540
            ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(dec);
541
        }
542
        ImGui::EndTable();
543
    }
544
}
545
546
void draw_registers_tab()
547
{
548
    ImGui::TextDisabled("Shadowed registers show only writes the chip accepted; YM_write_reg drops writes while the chip is busy.");
549
    ImGui::TextDisabled("Per-operator address layout is base + (slot << 3) + channel; slots are M1, M2, C1, C2. Raw $08 key-on bits are M1,C1,M2,C2, but live key masks are normalized.");
550
    if (ImGui::CollapsingHeader("$00-$1F system", ImGuiTreeNodeFlags_DefaultOpen)) draw_register_range("system_regs", 0x00, 0x1f);
551
    if (ImGui::CollapsingHeader("$20-$3F per channel", ImGuiTreeNodeFlags_DefaultOpen)) draw_register_range("channel_regs", 0x20, 0x3f);
552
    if (ImGui::CollapsingHeader("$40-$FF per operator", ImGuiTreeNodeFlags_DefaultOpen)) draw_register_range("operator_regs", 0x40, 0xff);
553
}
554
555
void draw_scope_tab()
556
{
557
    static DbgScopeControls ctl;
558
    static int16_t samples[YM_SCOPE_STREAMS][YM_SCOPE_SAMPLES];
559
    static unsigned counts[YM_SCOPE_STREAMS] = {};
560
    static bool captured = false; // buffers hold real audio, not a projection
561
562
    dbgui_scope_controls(&ctl, YM_SCOPE_SAMPLES);
563
    const bool held = dbgui_audio_hold();
564
    bool       predicted = false;
565
566
    if (!ctl.freeze && !held) {
567
        captured = false;
568
        for (int stream = 0; stream < YM_SCOPE_STREAMS; ++stream) {
569
            counts[stream] = YM_debug_scope_read(stream, samples[stream], YM_SCOPE_SAMPLES);
570
            if (counts[stream] > 0) captured = true;
571
        }
572
    } else if (held && !captured && !ctl.freeze) {
573
        // Envelopes, LFO and operator phase are live chip state, so the chip can
574
        // be run forward to show what it is about to emit.
575
        const unsigned n = YM_debug_scope_predict(samples, YM_SCOPE_SAMPLES);
576
        for (int stream = 0; stream < YM_SCOPE_STREAMS; ++stream) {
577
            counts[stream] = n;
578
        }
579
        predicted = true;
580
    }
581
582
    if (held) {
583
        if (predicted) {
584
            dbgui_audio_predicted_note("the YM2151's current envelope and phase state");
585
        } else {
586
            dbgui_audio_hold_note();
587
        }
588
    }
589
    ImGui::TextDisabled("Per-channel capture is self-armed only while this Scope tab reads it; hidden tabs let it disarm.");
590
591
    if (ImGui::BeginTable("ym_scope_grid", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg)) {
592
        for (int ch = 0; ch < YM_NUM_CHANNELS; ++ch) {
593
            if ((ch % 2) == 0) ImGui::TableNextRow();
594
            ImGui::TableSetColumnIndex(ch % 2);
595
            NoteInfo note;
596
            note_from_kc_kf(ch_reg(0x28, ch) & 0x7f, ch_reg(0x30, ch) & 0xfc, &note);
597
            char overlay[96];
598
            snprintf(overlay, sizeof(overlay), "Ch %d ALG %u %s", ch, (unsigned)alg_for_ch(ch), note.text);
599
            char id[32];
600
            snprintf(id, sizeof(id), "mini%d", ch);
601
            dbgui_scope(id, samples[ch], (int)counts[ch], ImVec2(0.0f, 58.0f), IM_COL32(90, 190, 245, 255), ctl.window, ctl.gain, ctl.trigger, overlay);
602
            if (ImGui::IsItemClicked()) s_selected_ch = ch;
603
        }
604
        ImGui::EndTable();
605
    }
606
    ImGui::Separator();
607
    char overlay[96];
608
    NoteInfo note;
609
    note_from_kc_kf(ch_reg(0x28, s_selected_ch) & 0x7f, ch_reg(0x30, s_selected_ch) & 0xfc, &note);
610
    snprintf(overlay, sizeof(overlay), "Selected Ch %d ALG %u %s", s_selected_ch, (unsigned)alg_for_ch(s_selected_ch), note.text);
611
    dbgui_scope("selected", samples[s_selected_ch], (int)counts[s_selected_ch], ImVec2(0.0f, 120.0f), IM_COL32(105, 225, 140, 255), ctl.window, ctl.gain, ctl.trigger, overlay);
612
    ImGui::TextUnformatted("Summed YM output");
613
    dbgui_scope("mix_l", samples[YM_SCOPE_MIX_L], (int)counts[YM_SCOPE_MIX_L], ImVec2(0.0f, 84.0f), IM_COL32(245, 185, 90, 255), ctl.window, ctl.gain, ctl.trigger, "Mix L");
614
    dbgui_scope("mix_r", samples[YM_SCOPE_MIX_R], (int)counts[YM_SCOPE_MIX_R], ImVec2(0.0f, 84.0f), IM_COL32(245, 120, 160, 255), ctl.window, ctl.gain, ctl.trigger, "Mix R");
615
}
616
617
void fm_panel_render(bool *p_open)
618
{
619
    if (ImGui::Begin("YM2151", p_open)) {
620
        dbgui_window_zoom("ym2151");
621
        if (s_selected_ch < 0 || s_selected_ch >= YM_NUM_CHANNELS) s_selected_ch = 0;
622
        if (ImGui::BeginTabBar("ym2151_tabs", ImGuiTabBarFlags_DrawSelectedOverline)) {
623
            if (ImGui::BeginTabItem("Channels")) {
624
                draw_channels_tab();
625
                ImGui::EndTabItem();
626
            }
627
            if (ImGui::BeginTabItem("Operators")) {
628
                draw_operators_tab();
629
                ImGui::EndTabItem();
630
            }
631
            if (ImGui::BeginTabItem("Algorithm")) {
632
                draw_algorithm_tab();
633
                ImGui::EndTabItem();
634
            }
635
            if (ImGui::BeginTabItem("Global")) {
636
                draw_global_tab();
637
                ImGui::EndTabItem();
638
            }
639
            if (ImGui::BeginTabItem("Registers")) {
640
                draw_registers_tab();
641
                ImGui::EndTabItem();
642
            }
643
            if (ImGui::BeginTabItem("Scope")) {
644
                draw_scope_tab();
645
                ImGui::EndTabItem();
646
            }
647
            ImGui::EndTabBar();
648
        }
649
    }
650
    dbgui_window_end();
651
}
652
653
} // namespace
654
655
static DebugPanelRegistration s_reg("YM2151", fm_panel_render, true);