diff options
| author | crueter <crueter@eden-emu.dev> | 2026-02-15 19:22:19 +0100 |
|---|---|---|
| committer | crueter <crueter@eden-emu.dev> | 2026-02-15 19:22:19 +0100 |
| commit | 45c9f9bbb31ed06976bfacbc7bbf8851191ffb84 (patch) | |
| tree | 810047121e5619a88cb5013c6307321797d24619 /src/yuzu/render/performance_overlay.cpp | |
| parent | 8e373eb714aa01117ec885680477501cdb572112 (diff) | |
[desktop] Add basic Frametime/FPS overlay (#3537)
Just displays min, max, avg frametime/fps, alongside a chart of FPS in
the last 30 seconds.
Notes:
- Qt Charts is now required
- FPS/frametime collector now runs 2x as often. TODO: keep status bar at
500ms, but put perf overlay at 250ms
Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3537
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Diffstat (limited to 'src/yuzu/render/performance_overlay.cpp')
| -rw-r--r-- | src/yuzu/render/performance_overlay.cpp | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/src/yuzu/render/performance_overlay.cpp b/src/yuzu/render/performance_overlay.cpp new file mode 100644 index 0000000000..13003dce7b --- /dev/null +++ b/src/yuzu/render/performance_overlay.cpp @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "core/perf_stats.h" +#include "performance_overlay.h" +#include "ui_performance_overlay.h" + +#include "main_window.h" + +#include <QChart> +#include <QChartView> +#include <QGraphicsLayout> +#include <QLineSeries> +#include <QMouseEvent> +#include <QPainter> +#include <QValueAxis> + +// TODO(crueter): Reset samples when user changes turbo, slow, etc. +PerformanceOverlay::PerformanceOverlay(MainWindow* parent) + : QWidget(parent), m_mainWindow{parent}, ui(new Ui::PerformanceOverlay) { + ui->setupUi(this); + + setAttribute(Qt::WA_TranslucentBackground); + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); + raise(); + + // chart setup + m_fpsSeries = new QLineSeries(this); + + QPen pen(Qt::red); + pen.setWidth(2); + m_fpsSeries->setPen(pen); + + m_fpsChart = new QChart; + m_fpsChart->addSeries(m_fpsSeries); + m_fpsChart->legend()->hide(); + m_fpsChart->setBackgroundBrush(Qt::black); + m_fpsChart->setBackgroundVisible(true); + m_fpsChart->layout()->setContentsMargins(2, 2, 2, 2); + m_fpsChart->setMargins(QMargins{4, 4, 4, 4}); + + // axes + m_fpsX = new QValueAxis(this); + m_fpsX->setRange(0, NUM_FPS_SAMPLES); + m_fpsX->setVisible(false); + + m_fpsY = new QValueAxis(this); + m_fpsY->setRange(0, 60); + m_fpsY->setLabelFormat(QStringLiteral("%d")); + m_fpsY->setLabelsColor(Qt::white); + + QFont axisFont = m_fpsY->labelsFont(); + axisFont.setPixelSize(10); + m_fpsY->setLabelsFont(axisFont); + m_fpsY->setTickCount(3); + + // gray-ish label w/ white lines + m_fpsY->setLabelsVisible(true); + m_fpsY->setGridLineColor(QColor(50, 50, 50)); + m_fpsY->setLinePenColor(Qt::white); + + m_fpsChart->addAxis(m_fpsX, Qt::AlignBottom); + m_fpsChart->addAxis(m_fpsY, Qt::AlignLeft); + m_fpsSeries->attachAxis(m_fpsX); + m_fpsSeries->attachAxis(m_fpsY); + + // chart view + m_fpsChartView = new QChartView(m_fpsChart, this); + m_fpsChartView->setRenderHint(QPainter::Antialiasing); + m_fpsChartView->setMinimumHeight(100); + + ui->verticalLayout->addWidget(m_fpsChartView, 1); + + // thanks Debian. + QFont font = ui->fps->font(); + font.setWeight(QFont::DemiBold); + + ui->fps->setFont(font); + ui->frametime->setFont(font); + + // pos/stats + resetPosition(m_mainWindow->pos()); + connect(parent, &MainWindow::positionChanged, this, &PerformanceOverlay::resetPosition); + connect(m_mainWindow, &MainWindow::statsUpdated, this, &PerformanceOverlay::updateStats); +} + +PerformanceOverlay::~PerformanceOverlay() { + delete ui; +} + +void PerformanceOverlay::resetPosition(const QPoint& _) { + auto pos = m_mainWindow->pos(); + move(pos.x() + m_offset.x(), pos.y() + m_offset.y()); +} + +void PerformanceOverlay::updateStats(const Core::PerfStatsResults& results, + const VideoCore::ShaderNotify& shaders) { + auto fps = results.average_game_fps; + if (!std::isnan(fps)) { + // don't sample measurements < 3 fps because they are probably outliers or freezes + static constexpr double FPS_SAMPLE_THRESHOLD = 3.0; + + QString fpsText = tr("%1 fps").arg(std::round(fps), 0, 'f', 0); + // if (!m_fpsSuffix.isEmpty()) fpsText = fpsText % QStringLiteral(" (%1)").arg(m_fpsSuffix); + ui->fps->setText(fpsText); + + // sampling + if (fps > FPS_SAMPLE_THRESHOLD) { + m_fpsSamples.push_back(fps); + m_fpsPoints.push_back(QPointF{m_xPos++, fps}); + } + + if (m_fpsSamples.size() > NUM_FPS_SAMPLES) { + m_fpsSamples.pop_front(); + m_fpsPoints.pop_front(); + } + + // For the average only go back 10 samples max + if (m_fpsSamples.size() >= 2) { + const int back_search = std::min(size_t(10), m_fpsSamples.size() - 1); + double sum = std::accumulate(m_fpsSamples.end() - back_search, m_fpsSamples.end(), 0.0); + double avg = sum / back_search; + + ui->fps_avg->setText(tr("Avg: %1").arg(avg, 0, 'f', 0)); + } + + // chart it :) + if (!m_fpsPoints.empty()) { + auto [min_it, max_it] = std::minmax_element(m_fpsSamples.begin(), m_fpsSamples.end()); + double min_fps = *min_it; + double max_fps = *max_it; + + ui->fps_min->setText(tr("Min: %1").arg(min_fps, 0, 'f', 0)); + ui->fps_max->setText(tr("Max: %1").arg(max_fps, 0, 'f', 0)); + + m_fpsSeries->replace(QList<QPointF>(m_fpsPoints.begin(), m_fpsPoints.end())); + + qreal x_min = std::max(0.0, m_xPos - NUM_FPS_SAMPLES); + qreal x_max = std::max(qreal(10), m_xPos); + m_fpsX->setRange(x_min, x_max); + m_fpsY->setRange(0.0, max_fps); + } + } + + auto ft = results.frametime; + if (!std::isnan(ft)) { + // don't sample measurements > 500 ms because they are probably outliers + static constexpr double FT_SAMPLE_THRESHOLD = 500.0; + + double ft_ms = results.frametime * 1000.0; + ui->frametime->setText(tr("%1 ms").arg(ft_ms, 0, 'f', 2)); + + // sampling + if (ft_ms <= FT_SAMPLE_THRESHOLD) + m_frametimeSamples.push_back(ft_ms); + + if (m_frametimeSamples.size() > NUM_FRAMETIME_SAMPLES) + m_frametimeSamples.pop_front(); + + if (!m_frametimeSamples.empty()) { + auto [min_it, max_it] = + std::minmax_element(m_frametimeSamples.begin(), m_frametimeSamples.end()); + ui->ft_min->setText(tr("Min: %1").arg(*min_it, 0, 'f', 1)); + ui->ft_max->setText(tr("Max: %1").arg(*max_it, 0, 'f', 1)); + } + + // For the average only go back 10 samples max + if (m_frametimeSamples.size() >= 2) { + const int back_search = std::min(size_t(10), m_frametimeSamples.size() - 1); + double sum = std::accumulate(m_frametimeSamples.end() - back_search, + m_frametimeSamples.end(), 0.0); + double avg = sum / back_search; + + ui->ft_avg->setText(tr("Avg: %1").arg(avg, 0, 'f', 1)); + } + } +} + +void PerformanceOverlay::paintEvent(QPaintEvent* event) { + QPainter painter(this); + + painter.setRenderHint(QPainter::Antialiasing); + + painter.setBrush(m_background); + painter.setPen(Qt::NoPen); + + painter.drawRoundedRect(rect(), 10.0, 10.0); +} + +void PerformanceOverlay::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton) { + m_drag_start_pos = event->pos(); + } +} + +void PerformanceOverlay::mouseMoveEvent(QMouseEvent* event) { + // drag + if (event->buttons() & Qt::LeftButton) { + QPoint new_global_pos = event->globalPosition().toPoint() - m_drag_start_pos; + m_offset = new_global_pos - m_mainWindow->pos(); + move(new_global_pos); + } +} + +void PerformanceOverlay::closeEvent(QCloseEvent* event) { + emit closed(); +} |
