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