Browse Source
[desktop] Add basic Frametime/FPS overlay (#3537)
[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>pull/3558/head
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
10 changed files with 563 additions and 39 deletions
-
4CMakeLists.txt
-
70src/qt_common/config/uisettings.h
-
4src/yuzu/CMakeLists.txt
-
4src/yuzu/configuration/configure_filesystem.cpp
-
9src/yuzu/main.ui
-
39src/yuzu/main_window.cpp
-
11src/yuzu/main_window.h
-
207src/yuzu/render/performance_overlay.cpp
-
73src/yuzu/render/performance_overlay.h
-
181src/yuzu/render/performance_overlay.ui
@ -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(); |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <deque> |
|||
#include <QWidget> |
|||
|
|||
namespace VideoCore { |
|||
class ShaderNotify; |
|||
} |
|||
namespace Core { |
|||
struct PerfStatsResults; |
|||
} |
|||
namespace Ui { |
|||
class PerformanceOverlay; |
|||
} |
|||
|
|||
class QLineSeries; |
|||
class QChart; |
|||
class QChartView; |
|||
class QValueAxis; |
|||
class MainWindow; |
|||
|
|||
class PerformanceOverlay : public QWidget { |
|||
Q_OBJECT |
|||
|
|||
public: |
|||
explicit PerformanceOverlay(MainWindow* parent = nullptr); |
|||
~PerformanceOverlay(); |
|||
|
|||
protected: |
|||
void paintEvent(QPaintEvent* event) override; |
|||
|
|||
void mousePressEvent(QMouseEvent* event) override; |
|||
void mouseMoveEvent(QMouseEvent* event) override; |
|||
void closeEvent(QCloseEvent *event) override; |
|||
|
|||
private: |
|||
void resetPosition(const QPoint& pos); |
|||
void updateStats(const Core::PerfStatsResults &results, const VideoCore::ShaderNotify &shaders); |
|||
|
|||
MainWindow *m_mainWindow = nullptr; |
|||
Ui::PerformanceOverlay* ui; |
|||
|
|||
// colors |
|||
QColor m_background{127, 127, 127, 190}; |
|||
|
|||
QPoint m_offset{25, 75}; |
|||
|
|||
// frametime |
|||
const size_t NUM_FRAMETIME_SAMPLES = 300; |
|||
std::deque<double> m_frametimeSamples; |
|||
|
|||
// fps |
|||
const size_t NUM_FPS_SAMPLES = 120; |
|||
qreal m_xPos = 0; |
|||
std::deque<double> m_fpsSamples; |
|||
std::deque<QPointF> m_fpsPoints; |
|||
|
|||
// drag |
|||
QPoint m_drag_start_pos; |
|||
|
|||
// fps chart |
|||
QLineSeries *m_fpsSeries = nullptr; |
|||
QChart *m_fpsChart = nullptr; |
|||
QChartView *m_fpsChartView = nullptr; |
|||
QValueAxis *m_fpsX = nullptr; |
|||
QValueAxis *m_fpsY = nullptr; |
|||
|
|||
signals: |
|||
void closed(); |
|||
}; |
|||
@ -0,0 +1,181 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<ui version="4.0"> |
|||
<class>PerformanceOverlay</class> |
|||
<widget class="QWidget" name="PerformanceOverlay"> |
|||
<property name="geometry"> |
|||
<rect> |
|||
<x>0</x> |
|||
<y>0</y> |
|||
<width>225</width> |
|||
<height>250</height> |
|||
</rect> |
|||
</property> |
|||
<property name="windowTitle"> |
|||
<string>Form</string> |
|||
</property> |
|||
<layout class="QVBoxLayout" name="verticalLayout"> |
|||
<item> |
|||
<layout class="QHBoxLayout" name="ft_label_layout"> |
|||
<item> |
|||
<widget class="QLabel" name="label_3"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>12</pointsize> |
|||
<bold>true</bold> |
|||
</font> |
|||
</property> |
|||
<property name="styleSheet"> |
|||
<string notr="true">color: #0000ff;</string> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Frametime</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QLabel" name="frametime"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>12</pointsize> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>0 ms</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
<item> |
|||
<layout class="QHBoxLayout" name="ft_layout"> |
|||
<item> |
|||
<widget class="QLabel" name="ft_min"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>10</pointsize> |
|||
<bold>false</bold> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Min: 0</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QLabel" name="ft_max"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>10</pointsize> |
|||
<bold>false</bold> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Max: 0</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QLabel" name="ft_avg"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>10</pointsize> |
|||
<bold>false</bold> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Avg: 0</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
<item> |
|||
<layout class="QHBoxLayout" name="fps_label_layout"> |
|||
<item> |
|||
<widget class="QLabel" name="label_4"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>12</pointsize> |
|||
<bold>true</bold> |
|||
</font> |
|||
</property> |
|||
<property name="styleSheet"> |
|||
<string notr="true">color: #ff0000;</string> |
|||
</property> |
|||
<property name="text"> |
|||
<string>FPS</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QLabel" name="fps"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>12</pointsize> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>0 fps</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
<item> |
|||
<layout class="QHBoxLayout" name="horizontalLayout"> |
|||
<item> |
|||
<widget class="QLabel" name="fps_min"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>10</pointsize> |
|||
<bold>false</bold> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Min: 0</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QLabel" name="fps_max"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>10</pointsize> |
|||
<bold>false</bold> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Max: 0</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QLabel" name="fps_avg"> |
|||
<property name="font"> |
|||
<font> |
|||
<family>Sans Serif</family> |
|||
<pointsize>10</pointsize> |
|||
<bold>false</bold> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>Avg: 0</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
<resources/> |
|||
<connections/> |
|||
</ui> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue