diff --git a/hw/xbox/mcpx/apu/apu.c b/hw/xbox/mcpx/apu/apu.c
index bdb6d6db54..265c4485cf 100644
--- a/hw/xbox/mcpx/apu/apu.c
+++ b/hw/xbox/mcpx/apu/apu.c
@@ -102,30 +102,97 @@ static const MemoryRegionOps mcpx_apu_mmio_ops = {
.write = mcpx_apu_write,
};
+static void throttle_update_debug(MCPXAPUState *d, int64_t now_us)
+{
+ if (d->throttle.deviation.last_us > 0) {
+ int64_t dev = (now_us - d->throttle.deviation.last_us) - EP_FRAME_US;
+ if (d->throttle.deviation.count == 0) {
+ d->throttle.deviation.min_us = d->throttle.deviation.max_us = dev;
+ } else {
+ d->throttle.deviation.min_us = MIN(dev, d->throttle.deviation.min_us);
+ d->throttle.deviation.max_us = MAX(dev, d->throttle.deviation.max_us);
+ }
+ d->throttle.deviation.sum_us += dev;
+ d->throttle.deviation.count++;
+ }
+ d->throttle.deviation.last_us = now_us;
+}
+
+static void throttle_publish_debug(MCPXAPUState *d)
+{
+ int pacing_sum = d->throttle.pacing.backoff + d->throttle.pacing.ok +
+ d->throttle.pacing.speedup;
+ if (d->throttle.deviation.count == 0 || pacing_sum == 0) {
+ return;
+ }
+ g_dbg.throttle.pacing.backoff = (float)d->throttle.pacing.backoff / pacing_sum;
+ g_dbg.throttle.pacing.ok = (float)d->throttle.pacing.ok / pacing_sum;
+ g_dbg.throttle.pacing.speedup = (float)d->throttle.pacing.speedup / pacing_sum;
+ g_dbg.throttle.deviation.min_us = d->throttle.deviation.min_us;
+ g_dbg.throttle.deviation.avg_us = d->throttle.deviation.sum_us / d->throttle.deviation.count;
+ g_dbg.throttle.deviation.max_us = d->throttle.deviation.max_us;
+ if (d->throttle.queued_bytes_count > 0) {
+ g_dbg.throttle.latency.min_ms = d->throttle.queued_bytes_min / (float)MONITOR_BYTES_PER_MS;
+ g_dbg.throttle.latency.avg_ms = d->throttle.queued_bytes_sum /
+ (d->throttle.queued_bytes_count * (float)MONITOR_BYTES_PER_MS);
+ g_dbg.throttle.latency.max_ms = d->throttle.queued_bytes_max / (float)MONITOR_BYTES_PER_MS;
+ g_dbg.throttle.latency.low_ms = d->monitor.queued_bytes_low / (float)MONITOR_BYTES_PER_MS;
+ g_dbg.throttle.latency.high_ms = d->monitor.queued_bytes_high / (float)MONITOR_BYTES_PER_MS;
+ }
+ d->throttle.pacing.backoff = d->throttle.pacing.ok = d->throttle.pacing.speedup = 0;
+ d->throttle.deviation.min_us = d->throttle.deviation.max_us = d->throttle.deviation.sum_us = 0;
+ d->throttle.deviation.count = 0;
+ d->throttle.queued_bytes_min = d->throttle.queued_bytes_max = 0;
+ d->throttle.queued_bytes_sum = 0;
+ d->throttle.queued_bytes_count = 0;
+}
+
+static void throttle_record_queue(MCPXAPUState *d, int queued_bytes)
+{
+ if (d->throttle.queued_bytes_count == 0) {
+ d->throttle.queued_bytes_min = d->throttle.queued_bytes_max = queued_bytes;
+ } else {
+ d->throttle.queued_bytes_min = MIN(queued_bytes, d->throttle.queued_bytes_min);
+ d->throttle.queued_bytes_max = MAX(queued_bytes, d->throttle.queued_bytes_max);
+ }
+ d->throttle.queued_bytes_sum += queued_bytes;
+ d->throttle.queued_bytes_count++;
+ if (queued_bytes >= d->monitor.queued_bytes_high) {
+ d->throttle.pacing.backoff++;
+ } else if (queued_bytes <= d->monitor.queued_bytes_low) {
+ d->throttle.pacing.speedup++;
+ } else {
+ d->throttle.pacing.ok++;
+ }
+}
+
static void throttle(MCPXAPUState *d)
{
if (d->ep_frame_div % 8) {
return;
}
- const int64_t ep_frame_us = 5333; /* 256/48000 sec (~5.33ms) */
int64_t start_us = qemu_clock_get_us(QEMU_CLOCK_REALTIME);
+ throttle_update_debug(d, start_us);
int queued_bytes = -1;
if (d->monitor.stream) {
- while (!d->pause_requested) {
- queued_bytes = SDL_GetAudioStreamQueued(d->monitor.stream);
- if (queued_bytes >= d->monitor.queued_bytes_high) {
- qemu_cond_timedwait(&d->cond, &d->lock, ep_frame_us / 1000);
- } else {
+ queued_bytes = SDL_GetAudioStreamQueued(d->monitor.stream);
+ if (queued_bytes >= 0) {
+ throttle_record_queue(d, queued_bytes);
+ }
+ while (!d->pause_requested && queued_bytes >= d->monitor.queued_bytes_high) {
+ qemu_cond_timedwait(&d->cond, &d->lock, EP_FRAME_US / 1000);
+ if (d->pause_requested) {
break;
}
+ queued_bytes = SDL_GetAudioStreamQueued(d->monitor.stream);
}
}
if (queued_bytes < 0 || queued_bytes > d->monitor.queued_bytes_low) {
if (d->next_frame_time_us == 0 ||
- start_us - d->next_frame_time_us > ep_frame_us) {
+ start_us - d->next_frame_time_us > EP_FRAME_US) {
d->next_frame_time_us = start_us;
}
while (!d->pause_requested) {
@@ -137,7 +204,7 @@ static void throttle(MCPXAPUState *d)
break;
}
}
- d->next_frame_time_us += ep_frame_us;
+ d->next_frame_time_us += EP_FRAME_US;
}
d->sleep_acc_us += qemu_clock_get_us(QEMU_CLOCK_REALTIME) - start_us;
@@ -161,6 +228,7 @@ static void se_frame(MCPXAPUState *d)
*/
g_dbg.utilization = 1.0 - d->sleep_acc_us / (elapsed_ms * 1000.0);
g_dbg.frames_processed = (int)(d->frame_count * 1000.0 / elapsed_ms + 0.5);
+ throttle_publish_debug(d);
d->frame_count_time_ms = now_ms;
d->frame_count = 0;
diff --git a/hw/xbox/mcpx/apu/apu_debug.h b/hw/xbox/mcpx/apu/apu_debug.h
index 65a00cfa81..d3dabb8491 100644
--- a/hw/xbox/mcpx/apu/apu_debug.h
+++ b/hw/xbox/mcpx/apu/apu_debug.h
@@ -76,6 +76,18 @@ struct McpxApuDebug
struct McpxApuDebugDsp gp, ep;
int frames_processed;
float utilization;
+ struct {
+ struct {
+ float backoff, ok, speedup;
+ } pacing;
+ struct {
+ int64_t min_us, avg_us, max_us;
+ } deviation;
+ struct {
+ float min_ms, avg_ms, max_ms;
+ float low_ms, high_ms;
+ } latency;
+ } throttle;
bool gp_realtime, ep_realtime;
};
diff --git a/hw/xbox/mcpx/apu/apu_int.h b/hw/xbox/mcpx/apu/apu_int.h
index 7a247d10c9..1e81605590 100644
--- a/hw/xbox/mcpx/apu/apu_int.h
+++ b/hw/xbox/mcpx/apu/apu_int.h
@@ -101,6 +101,20 @@ typedef struct MCPXAPUState {
int64_t frame_count_time_ms;
int64_t next_frame_time_us;
+ struct {
+ struct {
+ int backoff, ok, speedup;
+ } pacing;
+ struct {
+ int64_t last_us;
+ int64_t min_us, max_us, sum_us;
+ int count;
+ } deviation;
+ int queued_bytes_min, queued_bytes_max;
+ int64_t queued_bytes_sum;
+ int queued_bytes_count;
+ } throttle;
+
struct {
McpxApuDebugMonitorPoint point;
int16_t frame_buf[256][2]; // 1 EP frame (0x400 bytes)
diff --git a/hw/xbox/mcpx/apu/apu_regs.h b/hw/xbox/mcpx/apu/apu_regs.h
index 5b63612147..2deee37f69 100644
--- a/hw/xbox/mcpx/apu/apu_regs.h
+++ b/hw/xbox/mcpx/apu/apu_regs.h
@@ -360,4 +360,7 @@ enum MCPX_HW_NOTIFIER {
// clang-format on
+#define EP_FRAME_US 5333 /* 256/48000 sec (~5.33ms) */
+#define MONITOR_BYTES_PER_MS 192 /* 48000 Hz * 2ch * 2 bytes / 1000 */
+
#endif
diff --git a/ui/xui/debug.cc b/ui/xui/debug.cc
index 52f3fa82b7..6c98ede711 100644
--- a/ui/xui/debug.cc
+++ b/ui/xui/debug.cc
@@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
+#include
#include "debug.hh"
#include "common.hh"
#include "misc.hh"
@@ -228,6 +229,72 @@ void DebugApuWindow::Draw()
if (color) ImGui::PopStyleColor();
ImGui::Text("Frames: %04d", dbg->frames_processed);
+ if (ImGui::TreeNode("Throttle")) {
+ ImGui::Text("Deviation: %" PRId64 "/%" PRId64 "/%" PRId64 " us",
+ dbg->throttle.deviation.min_us,
+ dbg->throttle.deviation.avg_us,
+ dbg->throttle.deviation.max_us);
+ ImGui::Text("Latency: %.1f/%.1f/%.1f ms",
+ dbg->throttle.latency.min_ms,
+ dbg->throttle.latency.avg_ms,
+ dbg->throttle.latency.max_ms);
+ float high = dbg->throttle.latency.high_ms;
+ if (high > 0) {
+ float scale = fmax(high, dbg->throttle.latency.max_ms);
+ float h = ImGui::GetFrameHeight() * 0.25f;
+
+ ImGui::Text("Queue/Pacing:");
+ ImVec2 pos = ImGui::GetCursorScreenPos();
+ float w = ImGui::GetContentRegionAvail().x;
+ float low_x = pos.x + w * (dbg->throttle.latency.low_ms / scale);
+ float high_x = pos.x + w * (high / scale);
+ float min_x = pos.x + w * (dbg->throttle.latency.min_ms / scale);
+ float avg_x = pos.x + w * (dbg->throttle.latency.avg_ms / scale);
+ float max_x = pos.x + w * (dbg->throttle.latency.max_ms / scale);
+ const ImU32 col_bg = IM_COL32(40, 40, 40, 255);
+ const ImU32 col_band = IM_COL32(60, 120, 60, 255);
+ const ImU32 col_avg = IM_COL32(100, 220, 100, 255);
+ const ImU32 col_low = IM_COL32(255, 255, 0, 180);
+ const ImU32 col_high = IM_COL32(255, 80, 80, 180);
+
+ ImDrawList *dl = ImGui::GetWindowDrawList();
+ dl->AddRectFilled(pos, ImVec2(pos.x + w, pos.y + h), col_bg);
+ dl->AddRectFilled(ImVec2(min_x, pos.y), ImVec2(max_x, pos.y + h),
+ col_band);
+ dl->AddLine(ImVec2(low_x, pos.y), ImVec2(low_x, pos.y + h),
+ col_low, 1.0f);
+ dl->AddLine(ImVec2(avg_x, pos.y), ImVec2(avg_x, pos.y + h),
+ col_avg, 2.0f);
+ dl->AddLine(ImVec2(high_x, pos.y), ImVec2(high_x, pos.y + h),
+ col_high, 1.0f);
+ ImGui::Dummy(ImVec2(w, h));
+
+ pos.y += h * 1.25;
+
+ const ImU32 col_speedup = col_low;
+ const ImU32 col_ok = col_band;
+ const ImU32 col_backoff = col_high;
+ float sp = dbg->throttle.pacing.speedup;
+ float ok = dbg->throttle.pacing.ok;
+ float bo = dbg->throttle.pacing.backoff;
+ float sp_x = pos.x + w * sp;
+ float ok_x = sp_x + w * ok;
+ dl->AddRectFilled(pos, ImVec2(pos.x + w, pos.y + h), col_bg);
+ if (sp > 0) {
+ dl->AddRectFilled(pos, ImVec2(sp_x, pos.y + h), col_speedup);
+ }
+ if (ok > 0) {
+ dl->AddRectFilled(ImVec2(sp_x, pos.y), ImVec2(ok_x, pos.y + h),
+ col_ok);
+ }
+ if (bo > 0) {
+ dl->AddRectFilled(ImVec2(ok_x, pos.y), ImVec2(pos.x + w, pos.y + h),
+ col_backoff);
+ }
+ ImGui::Dummy(ImVec2(w, h));
+ }
+ ImGui::TreePop();
+ }
ImGui::Text("VP: %4d us", dbg->vp.total_worker_time_us);
if (ImGui::TreeNode("VP Workers")) {
ImGui::Text(" W: # us");