Merge pull request #2587 from yuriks/status-bar

Replace built-in Profiler with indicators in status bar
This commit is contained in:
Yuri Kunde Schlesner 2017-02-26 17:51:15 -08:00 committed by GitHub
commit b250ce21b9
28 changed files with 321 additions and 449 deletions

View file

@ -69,7 +69,6 @@ set(HEADERS
set(UIS set(UIS
debugger/callstack.ui debugger/callstack.ui
debugger/disassembler.ui debugger/disassembler.ui
debugger/profiler.ui
debugger/registers.ui debugger/registers.ui
configure.ui configure.ui
configure_audio.ui configure_audio.ui

View file

@ -146,6 +146,7 @@ void Config::ReadValues() {
UISettings::values.single_window_mode = qt_config->value("singleWindowMode", true).toBool(); UISettings::values.single_window_mode = qt_config->value("singleWindowMode", true).toBool();
UISettings::values.display_titlebar = qt_config->value("displayTitleBars", true).toBool(); UISettings::values.display_titlebar = qt_config->value("displayTitleBars", true).toBool();
UISettings::values.show_status_bar = qt_config->value("showStatusBar", true).toBool();
UISettings::values.confirm_before_closing = qt_config->value("confirmClose", true).toBool(); UISettings::values.confirm_before_closing = qt_config->value("confirmClose", true).toBool();
UISettings::values.first_start = qt_config->value("firstStart", true).toBool(); UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
@ -252,6 +253,7 @@ void Config::SaveValues() {
qt_config->setValue("singleWindowMode", UISettings::values.single_window_mode); qt_config->setValue("singleWindowMode", UISettings::values.single_window_mode);
qt_config->setValue("displayTitleBars", UISettings::values.display_titlebar); qt_config->setValue("displayTitleBars", UISettings::values.display_titlebar);
qt_config->setValue("showStatusBar", UISettings::values.show_status_bar);
qt_config->setValue("confirmClose", UISettings::values.confirm_before_closing); qt_config->setValue("confirmClose", UISettings::values.confirm_before_closing);
qt_config->setValue("firstStart", UISettings::values.first_start); qt_config->setValue("firstStart", UISettings::values.first_start);

View file

@ -4,6 +4,7 @@
#include "citra_qt/configure_system.h" #include "citra_qt/configure_system.h"
#include "citra_qt/ui_settings.h" #include "citra_qt/ui_settings.h"
#include "core/core.h"
#include "core/hle/service/cfg/cfg.h" #include "core/hle/service/cfg/cfg.h"
#include "core/hle/service/fs/archive.h" #include "core/hle/service/fs/archive.h"
#include "ui_configure_system.h" #include "ui_configure_system.h"

View file

@ -2,6 +2,8 @@
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
#include <QAction>
#include <QLayout>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QString> #include <QString>
@ -9,121 +11,12 @@
#include "citra_qt/util/util.h" #include "citra_qt/util/util.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "common/microprofile.h" #include "common/microprofile.h"
#include "common/profiler_reporting.h"
// Include the implementation of the UI in this file. This isn't in microprofile.cpp because the // Include the implementation of the UI in this file. This isn't in microprofile.cpp because the
// non-Qt frontends don't need it (and don't implement the UI drawing hooks either). // non-Qt frontends don't need it (and don't implement the UI drawing hooks either).
#if MICROPROFILE_ENABLED #if MICROPROFILE_ENABLED
#define MICROPROFILEUI_IMPL 1 #define MICROPROFILEUI_IMPL 1
#include "common/microprofileui.h" #include "common/microprofileui.h"
#endif
using namespace Common::Profiling;
static QVariant GetDataForColumn(int col, const AggregatedDuration& duration) {
static auto duration_to_float = [](Duration dur) -> float {
using FloatMs = std::chrono::duration<float, std::chrono::milliseconds::period>;
return std::chrono::duration_cast<FloatMs>(dur).count();
};
switch (col) {
case 1:
return duration_to_float(duration.avg);
case 2:
return duration_to_float(duration.min);
case 3:
return duration_to_float(duration.max);
default:
return QVariant();
}
}
ProfilerModel::ProfilerModel(QObject* parent) : QAbstractItemModel(parent) {
updateProfilingInfo();
}
QVariant ProfilerModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section) {
case 0:
return tr("Category");
case 1:
return tr("Avg");
case 2:
return tr("Min");
case 3:
return tr("Max");
}
}
return QVariant();
}
QModelIndex ProfilerModel::index(int row, int column, const QModelIndex& parent) const {
return createIndex(row, column);
}
QModelIndex ProfilerModel::parent(const QModelIndex& child) const {
return QModelIndex();
}
int ProfilerModel::columnCount(const QModelIndex& parent) const {
return 4;
}
int ProfilerModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) {
return 0;
} else {
return 2;
}
}
QVariant ProfilerModel::data(const QModelIndex& index, int role) const {
if (role == Qt::DisplayRole) {
if (index.row() == 0) {
if (index.column() == 0) {
return tr("Frame");
} else {
return GetDataForColumn(index.column(), results.frame_time);
}
} else if (index.row() == 1) {
if (index.column() == 0) {
return tr("Frame (with swapping)");
} else {
return GetDataForColumn(index.column(), results.interframe_time);
}
}
}
return QVariant();
}
void ProfilerModel::updateProfilingInfo() {
results = GetTimingResultsAggregator()->GetAggregatedResults();
emit dataChanged(createIndex(0, 1), createIndex(rowCount() - 1, 3));
}
ProfilerWidget::ProfilerWidget(QWidget* parent) : QDockWidget(parent) {
ui.setupUi(this);
model = new ProfilerModel(this);
ui.treeView->setModel(model);
connect(this, SIGNAL(visibilityChanged(bool)), SLOT(setProfilingInfoUpdateEnabled(bool)));
connect(&update_timer, SIGNAL(timeout()), model, SLOT(updateProfilingInfo()));
}
void ProfilerWidget::setProfilingInfoUpdateEnabled(bool enable) {
if (enable) {
update_timer.start(100);
model->updateProfilingInfo();
} else {
update_timer.stop();
}
}
#if MICROPROFILE_ENABLED
class MicroProfileWidget : public QWidget { class MicroProfileWidget : public QWidget {
public: public:

View file

@ -8,46 +8,6 @@
#include <QDockWidget> #include <QDockWidget>
#include <QTimer> #include <QTimer>
#include "common/microprofile.h" #include "common/microprofile.h"
#include "common/profiler_reporting.h"
#include "ui_profiler.h"
class ProfilerModel : public QAbstractItemModel {
Q_OBJECT
public:
explicit ProfilerModel(QObject* parent);
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
QModelIndex index(int row, int column,
const QModelIndex& parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex& child) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
public slots:
void updateProfilingInfo();
private:
Common::Profiling::AggregatedFrameResult results;
};
class ProfilerWidget : public QDockWidget {
Q_OBJECT
public:
explicit ProfilerWidget(QWidget* parent = nullptr);
private slots:
void setProfilingInfoUpdateEnabled(bool enable);
private:
Ui::Profiler ui;
ProfilerModel* model;
QTimer update_timer;
};
class MicroProfileDialog : public QWidget { class MicroProfileDialog : public QWidget {
Q_OBJECT Q_OBJECT

View file

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Profiler</class>
<widget class="QDockWidget" name="Profiler">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Profiler</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="treeView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -45,6 +45,7 @@ GameList::GameList(QWidget* parent) : QWidget{parent} {
// with signals/slots. In this case, QList falls under the umbrells of custom types. // with signals/slots. In this case, QList falls under the umbrells of custom types.
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(tree_view); layout->addWidget(tree_view);
setLayout(layout); setLayout(layout);
} }

View file

@ -95,6 +95,26 @@ void GMainWindow::InitializeWidgets() {
game_list = new GameList(); game_list = new GameList();
ui.horizontalLayout->addWidget(game_list); ui.horizontalLayout->addWidget(game_list);
// Create status bar
emu_speed_label = new QLabel();
emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% "
"indicate emulation is running faster or slower than a 3DS."));
game_fps_label = new QLabel();
game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. "
"This will vary from game to game and scene to scene."));
emu_frametime_label = new QLabel();
emu_frametime_label->setToolTip(
tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For "
"full-speed emulation this should be at most 16.67 ms."));
for (auto& label : {emu_speed_label, game_fps_label, emu_frametime_label}) {
label->setVisible(false);
label->setFrameStyle(QFrame::NoFrame);
label->setContentsMargins(4, 0, 4, 0);
statusBar()->addPermanentWidget(label);
}
statusBar()->setVisible(true);
} }
void GMainWindow::InitializeDebugWidgets() { void GMainWindow::InitializeDebugWidgets() {
@ -103,11 +123,6 @@ void GMainWindow::InitializeDebugWidgets() {
QMenu* debug_menu = ui.menu_View_Debugging; QMenu* debug_menu = ui.menu_View_Debugging;
profilerWidget = new ProfilerWidget(this);
addDockWidget(Qt::BottomDockWidgetArea, profilerWidget);
profilerWidget->hide();
debug_menu->addAction(profilerWidget->toggleViewAction());
#if MICROPROFILE_ENABLED #if MICROPROFILE_ENABLED
microProfileDialog = new MicroProfileDialog(this); microProfileDialog = new MicroProfileDialog(this);
microProfileDialog->hide(); microProfileDialog->hide();
@ -230,6 +245,9 @@ void GMainWindow::RestoreUIState() {
ui.action_Display_Dock_Widget_Headers->setChecked(UISettings::values.display_titlebar); ui.action_Display_Dock_Widget_Headers->setChecked(UISettings::values.display_titlebar);
OnDisplayTitleBars(ui.action_Display_Dock_Widget_Headers->isChecked()); OnDisplayTitleBars(ui.action_Display_Dock_Widget_Headers->isChecked());
ui.action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar);
statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
} }
void GMainWindow::ConnectWidgetEvents() { void GMainWindow::ConnectWidgetEvents() {
@ -240,6 +258,8 @@ void GMainWindow::ConnectWidgetEvents() {
connect(this, SIGNAL(EmulationStarting(EmuThread*)), render_window, connect(this, SIGNAL(EmulationStarting(EmuThread*)), render_window,
SLOT(OnEmulationStarting(EmuThread*))); SLOT(OnEmulationStarting(EmuThread*)));
connect(this, SIGNAL(EmulationStopping()), render_window, SLOT(OnEmulationStopping())); connect(this, SIGNAL(EmulationStopping()), render_window, SLOT(OnEmulationStopping()));
connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar);
} }
void GMainWindow::ConnectMenuEvents() { void GMainWindow::ConnectMenuEvents() {
@ -262,6 +282,7 @@ void GMainWindow::ConnectMenuEvents() {
&GMainWindow::ToggleWindowMode); &GMainWindow::ToggleWindowMode);
connect(ui.action_Display_Dock_Widget_Headers, &QAction::triggered, this, connect(ui.action_Display_Dock_Widget_Headers, &QAction::triggered, this,
&GMainWindow::OnDisplayTitleBars); &GMainWindow::OnDisplayTitleBars);
connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
} }
void GMainWindow::OnDisplayTitleBars(bool show) { void GMainWindow::OnDisplayTitleBars(bool show) {
@ -387,6 +408,8 @@ void GMainWindow::BootGame(const QString& filename) {
if (ui.action_Single_Window_Mode->isChecked()) { if (ui.action_Single_Window_Mode->isChecked()) {
game_list->hide(); game_list->hide();
} }
status_bar_update_timer.start(2000);
render_window->show(); render_window->show();
render_window->setFocus(); render_window->setFocus();
@ -421,6 +444,12 @@ void GMainWindow::ShutdownGame() {
render_window->hide(); render_window->hide();
game_list->show(); game_list->show();
// Disable status bar updates
status_bar_update_timer.stop();
emu_speed_label->setVisible(false);
game_fps_label->setVisible(false);
emu_frametime_label->setVisible(false);
emulation_running = false; emulation_running = false;
} }
@ -600,6 +629,23 @@ void GMainWindow::OnCreateGraphicsSurfaceViewer() {
graphicsSurfaceViewerWidget->show(); graphicsSurfaceViewerWidget->show();
} }
void GMainWindow::UpdateStatusBar() {
if (emu_thread == nullptr) {
status_bar_update_timer.stop();
return;
}
auto results = Core::System::GetInstance().GetAndResetPerfStats();
emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0));
game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0));
emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2));
emu_speed_label->setVisible(true);
game_fps_label->setVisible(true);
emu_frametime_label->setVisible(true);
}
bool GMainWindow::ConfirmClose() { bool GMainWindow::ConfirmClose() {
if (emu_thread == nullptr || !UISettings::values.confirm_before_closing) if (emu_thread == nullptr || !UISettings::values.confirm_before_closing)
return true; return true;
@ -625,6 +671,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
#endif #endif
UISettings::values.single_window_mode = ui.action_Single_Window_Mode->isChecked(); UISettings::values.single_window_mode = ui.action_Single_Window_Mode->isChecked();
UISettings::values.display_titlebar = ui.action_Display_Dock_Widget_Headers->isChecked(); UISettings::values.display_titlebar = ui.action_Display_Dock_Widget_Headers->isChecked();
UISettings::values.show_status_bar = ui.action_Show_Status_Bar->isChecked();
UISettings::values.first_start = false; UISettings::values.first_start = false;
game_list->SaveInterfaceLayout(); game_list->SaveInterfaceLayout();

View file

@ -127,17 +127,26 @@ private slots:
void OnCreateGraphicsSurfaceViewer(); void OnCreateGraphicsSurfaceViewer();
private: private:
void UpdateStatusBar();
Ui::MainWindow ui; Ui::MainWindow ui;
GRenderWindow* render_window; GRenderWindow* render_window;
GameList* game_list; GameList* game_list;
// Status bar elements
QLabel* emu_speed_label = nullptr;
QLabel* game_fps_label = nullptr;
QLabel* emu_frametime_label = nullptr;
QTimer status_bar_update_timer;
std::unique_ptr<Config> config; std::unique_ptr<Config> config;
// Whether emulation is currently running in Citra. // Whether emulation is currently running in Citra.
bool emulation_running = false; bool emulation_running = false;
std::unique_ptr<EmuThread> emu_thread; std::unique_ptr<EmuThread> emu_thread;
// Debugger panes
ProfilerWidget* profilerWidget; ProfilerWidget* profilerWidget;
MicroProfileDialog* microProfileDialog; MicroProfileDialog* microProfileDialog;
DisassemblerWidget* disasmWidget; DisassemblerWidget* disasmWidget;

View file

@ -88,6 +88,7 @@
</widget> </widget>
<addaction name="action_Single_Window_Mode"/> <addaction name="action_Single_Window_Mode"/>
<addaction name="action_Display_Dock_Widget_Headers"/> <addaction name="action_Display_Dock_Widget_Headers"/>
<addaction name="action_Show_Status_Bar"/>
<addaction name="menu_View_Debugging"/> <addaction name="menu_View_Debugging"/>
</widget> </widget>
<widget class="QMenu" name="menu_Help"> <widget class="QMenu" name="menu_Help">
@ -101,7 +102,6 @@
<addaction name="menu_View"/> <addaction name="menu_View"/>
<addaction name="menu_Help"/> <addaction name="menu_Help"/>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="action_Load_File"> <action name="action_Load_File">
<property name="text"> <property name="text">
<string>Load File...</string> <string>Load File...</string>
@ -167,6 +167,14 @@
<string>Display Dock Widget Headers</string> <string>Display Dock Widget Headers</string>
</property> </property>
</action> </action>
<action name="action_Show_Status_Bar">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Show Status Bar</string>
</property>
</action>
<action name="action_Select_Game_List_Root"> <action name="action_Select_Game_List_Root">
<property name="text"> <property name="text">
<string>Select Game Directory...</string> <string>Select Game Directory...</string>

View file

@ -27,6 +27,7 @@ struct Values {
bool single_window_mode; bool single_window_mode;
bool display_titlebar; bool display_titlebar;
bool show_status_bar;
bool confirm_before_closing; bool confirm_before_closing;
bool first_start; bool first_start;

View file

@ -35,7 +35,6 @@ set(SRCS
memory_util.cpp memory_util.cpp
microprofile.cpp microprofile.cpp
misc.cpp misc.cpp
profiler.cpp
scm_rev.cpp scm_rev.cpp
string_util.cpp string_util.cpp
symbols.cpp symbols.cpp
@ -68,7 +67,6 @@ set(HEADERS
microprofile.h microprofile.h
microprofileui.h microprofileui.h
platform.h platform.h
profiler_reporting.h
quaternion.h quaternion.h
scm_rev.h scm_rev.h
scope_exit.h scope_exit.h

View file

@ -1,101 +0,0 @@
// Copyright 2015 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <cstddef>
#include <vector>
#include "common/assert.h"
#include "common/profiler_reporting.h"
#include "common/synchronized_wrapper.h"
namespace Common {
namespace Profiling {
ProfilingManager::ProfilingManager()
: last_frame_end(Clock::now()), this_frame_start(Clock::now()) {}
void ProfilingManager::BeginFrame() {
this_frame_start = Clock::now();
}
void ProfilingManager::FinishFrame() {
Clock::time_point now = Clock::now();
results.interframe_time = now - last_frame_end;
results.frame_time = now - this_frame_start;
last_frame_end = now;
}
TimingResultsAggregator::TimingResultsAggregator(size_t window_size)
: max_window_size(window_size), window_size(0) {
interframe_times.resize(window_size, Duration::zero());
frame_times.resize(window_size, Duration::zero());
}
void TimingResultsAggregator::Clear() {
window_size = cursor = 0;
}
void TimingResultsAggregator::AddFrame(const ProfilingFrameResult& frame_result) {
interframe_times[cursor] = frame_result.interframe_time;
frame_times[cursor] = frame_result.frame_time;
++cursor;
if (cursor == max_window_size)
cursor = 0;
if (window_size < max_window_size)
++window_size;
}
static AggregatedDuration AggregateField(const std::vector<Duration>& v, size_t len) {
AggregatedDuration result;
result.avg = Duration::zero();
result.min = result.max = (len == 0 ? Duration::zero() : v[0]);
for (size_t i = 0; i < len; ++i) {
Duration value = v[i];
result.avg += value;
result.min = std::min(result.min, value);
result.max = std::max(result.max, value);
}
if (len != 0)
result.avg /= len;
return result;
}
static float tof(Common::Profiling::Duration dur) {
using FloatMs = std::chrono::duration<float, std::chrono::milliseconds::period>;
return std::chrono::duration_cast<FloatMs>(dur).count();
}
AggregatedFrameResult TimingResultsAggregator::GetAggregatedResults() const {
AggregatedFrameResult result;
result.interframe_time = AggregateField(interframe_times, window_size);
result.frame_time = AggregateField(frame_times, window_size);
if (result.interframe_time.avg != Duration::zero()) {
result.fps = 1000.0f / tof(result.interframe_time.avg);
} else {
result.fps = 0.0f;
}
return result;
}
ProfilingManager& GetProfilingManager() {
// Takes advantage of "magic" static initialization for race-free initialization.
static ProfilingManager manager;
return manager;
}
SynchronizedRef<TimingResultsAggregator> GetTimingResultsAggregator() {
static SynchronizedWrapper<TimingResultsAggregator> aggregator(30);
return SynchronizedRef<TimingResultsAggregator>(aggregator);
}
} // namespace Profiling
} // namespace Common

View file

@ -1,83 +0,0 @@
// Copyright 2015 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <chrono>
#include <cstddef>
#include <vector>
#include "common/synchronized_wrapper.h"
namespace Common {
namespace Profiling {
using Clock = std::chrono::high_resolution_clock;
using Duration = Clock::duration;
struct ProfilingFrameResult {
/// Time since the last delivered frame
Duration interframe_time;
/// Time spent processing a frame, excluding VSync
Duration frame_time;
};
class ProfilingManager final {
public:
ProfilingManager();
/// This should be called after swapping screen buffers.
void BeginFrame();
/// This should be called before swapping screen buffers.
void FinishFrame();
/// Get the timing results from the previous frame. This is updated when you call FinishFrame().
const ProfilingFrameResult& GetPreviousFrameResults() const {
return results;
}
private:
Clock::time_point last_frame_end;
Clock::time_point this_frame_start;
ProfilingFrameResult results;
};
struct AggregatedDuration {
Duration avg, min, max;
};
struct AggregatedFrameResult {
/// Time since the last delivered frame
AggregatedDuration interframe_time;
/// Time spent processing a frame, excluding VSync
AggregatedDuration frame_time;
float fps;
};
class TimingResultsAggregator final {
public:
TimingResultsAggregator(size_t window_size);
void Clear();
void AddFrame(const ProfilingFrameResult& frame_result);
AggregatedFrameResult GetAggregatedResults() const;
size_t max_window_size;
size_t window_size;
size_t cursor;
std::vector<Duration> interframe_times;
std::vector<Duration> frame_times;
};
ProfilingManager& GetProfilingManager();
SynchronizedRef<TimingResultsAggregator> GetTimingResultsAggregator();
} // namespace Profiling
} // namespace Common

View file

@ -9,25 +9,8 @@
namespace Common { namespace Common {
/**
* Wraps an object, only allowing access to it via a locking reference wrapper. Good to ensure no
* one forgets to lock a mutex before acessing an object. To access the wrapped object construct a
* SyncronizedRef on this wrapper. Inspired by Rust's Mutex type
* (http://doc.rust-lang.org/std/sync/struct.Mutex.html).
*/
template <typename T> template <typename T>
class SynchronizedWrapper { class SynchronizedWrapper;
public:
template <typename... Args>
SynchronizedWrapper(Args&&... args) : data(std::forward<Args>(args)...) {}
private:
template <typename U>
friend class SynchronizedRef;
std::mutex mutex;
T data;
};
/** /**
* Synchronized reference, that keeps a SynchronizedWrapper's mutex locked during its lifetime. This * Synchronized reference, that keeps a SynchronizedWrapper's mutex locked during its lifetime. This
@ -75,4 +58,28 @@ private:
SynchronizedWrapper<T>* wrapper; SynchronizedWrapper<T>* wrapper;
}; };
/**
* Wraps an object, only allowing access to it via a locking reference wrapper. Good to ensure no
* one forgets to lock a mutex before acessing an object. To access the wrapped object construct a
* SyncronizedRef on this wrapper. Inspired by Rust's Mutex type
* (http://doc.rust-lang.org/std/sync/struct.Mutex.html).
*/
template <typename T>
class SynchronizedWrapper {
public:
template <typename... Args>
SynchronizedWrapper(Args&&... args) : data(std::forward<Args>(args)...) {}
SynchronizedRef<T> Lock() {
return {*this};
}
private:
template <typename U>
friend class SynchronizedRef;
std::mutex mutex;
T data;
};
} // namespace Common } // namespace Common

View file

@ -173,6 +173,7 @@ set(SRCS
loader/smdh.cpp loader/smdh.cpp
tracer/recorder.cpp tracer/recorder.cpp
memory.cpp memory.cpp
perf_stats.cpp
settings.cpp settings.cpp
) )
@ -363,6 +364,7 @@ set(HEADERS
memory.h memory.h
memory_setup.h memory_setup.h
mmio.h mmio.h
perf_stats.h
settings.h settings.h
) )

View file

@ -109,6 +109,10 @@ void System::PrepareReschedule() {
reschedule_pending = true; reschedule_pending = true;
} }
PerfStats::Results System::GetAndResetPerfStats() {
return perf_stats.GetAndResetStats(CoreTiming::GetGlobalTimeUs());
}
void System::Reschedule() { void System::Reschedule() {
if (!reschedule_pending) { if (!reschedule_pending) {
return; return;
@ -140,6 +144,10 @@ System::ResultStatus System::Init(EmuWindow* emu_window, u32 system_mode) {
LOG_DEBUG(Core, "Initialized OK"); LOG_DEBUG(Core, "Initialized OK");
// Reset counters and set time origin to current frame
GetAndResetPerfStats();
perf_stats.BeginSystemFrame();
return ResultStatus::Success; return ResultStatus::Success;
} }

View file

@ -6,9 +6,9 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include "common/common_types.h" #include "common/common_types.h"
#include "core/memory.h" #include "core/memory.h"
#include "core/perf_stats.h"
class EmuWindow; class EmuWindow;
class ARM_Interface; class ARM_Interface;
@ -83,6 +83,8 @@ public:
/// Prepare the core emulation for a reschedule /// Prepare the core emulation for a reschedule
void PrepareReschedule(); void PrepareReschedule();
PerfStats::Results GetAndResetPerfStats();
/** /**
* Gets a reference to the emulated CPU. * Gets a reference to the emulated CPU.
* @returns A reference to the emulated CPU. * @returns A reference to the emulated CPU.
@ -91,6 +93,9 @@ public:
return *cpu_core; return *cpu_core;
} }
PerfStats perf_stats;
FrameLimiter frame_limiter;
private: private:
/** /**
* Initialize the emulated system. * Initialize the emulated system.

View file

@ -5,7 +5,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include "common/assert.h" #include "common/assert.h"
#include "common/profiler_reporting.h" #include "core/core.h"
#include "core/frontend/emu_window.h" #include "core/frontend/emu_window.h"
#include "core/frontend/key_map.h" #include "core/frontend/key_map.h"
#include "video_core/video_core.h" #include "video_core/video_core.h"
@ -104,8 +104,7 @@ void EmuWindow::AccelerometerChanged(float x, float y, float z) {
void EmuWindow::GyroscopeChanged(float x, float y, float z) { void EmuWindow::GyroscopeChanged(float x, float y, float z) {
constexpr float FULL_FPS = 60; constexpr float FULL_FPS = 60;
float coef = GetGyroscopeRawToDpsCoefficient(); float coef = GetGyroscopeRawToDpsCoefficient();
float stretch = float stretch = Core::System::GetInstance().perf_stats.GetLastFrameTimeScale();
FULL_FPS / Common::Profiling::GetTimingResultsAggregator()->GetAggregatedResults().fps;
std::lock_guard<std::mutex> lock(gyro_mutex); std::lock_guard<std::mutex> lock(gyro_mutex);
gyro_x = static_cast<s16>(x * coef * stretch); gyro_x = static_cast<s16>(x * coef * stretch);
gyro_y = static_cast<s16>(y * coef * stretch); gyro_y = static_cast<s16>(y * coef * stretch);

View file

@ -4,6 +4,7 @@
#pragma once #pragma once
#include <memory>
#include <string> #include <string>
#include "common/assert.h" #include "common/assert.h"
#include "common/common_types.h" #include "common/common_types.h"

View file

@ -11,7 +11,6 @@
#include <boost/container/flat_set.hpp> #include <boost/container/flat_set.hpp>
#include "common/common_types.h" #include "common/common_types.h"
#include "core/arm/arm_interface.h" #include "core/arm/arm_interface.h"
#include "core/core.h"
#include "core/hle/kernel/kernel.h" #include "core/hle/kernel/kernel.h"
#include "core/hle/result.h" #include "core/hle/result.h"

View file

@ -4,6 +4,7 @@
#include "common/bit_field.h" #include "common/bit_field.h"
#include "common/microprofile.h" #include "common/microprofile.h"
#include "core/core.h"
#include "core/hle/kernel/event.h" #include "core/hle/kernel/event.h"
#include "core/hle/kernel/shared_memory.h" #include "core/hle/kernel/shared_memory.h"
#include "core/hle/result.h" #include "core/hle/result.h"
@ -280,6 +281,7 @@ ResultCode SetBufferSwap(u32 screen_id, const FrameBufferInfo& info) {
if (screen_id == 0) { if (screen_id == 0) {
MicroProfileFlip(); MicroProfileFlip();
Core::System::GetInstance().perf_stats.EndGameFrame();
} }
return RESULT_SUCCESS; return RESULT_SUCCESS;

View file

@ -6,6 +6,7 @@
#include "common/common_types.h" #include "common/common_types.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "core/arm/arm_interface.h" #include "core/arm/arm_interface.h"
#include "core/core.h"
#include "core/hle/kernel/process.h" #include "core/hle/kernel/process.h"
#include "core/hle/kernel/vm_manager.h" #include "core/hle/kernel/vm_manager.h"
#include "core/hle/service/ldr_ro/cro_helper.h" #include "core/hle/service/ldr_ro/cro_helper.h"

View file

@ -8,17 +8,13 @@
#include "common/color.h" #include "common/color.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/math_util.h"
#include "common/microprofile.h" #include "common/microprofile.h"
#include "common/thread.h"
#include "common/timer.h"
#include "common/vector_math.h" #include "common/vector_math.h"
#include "core/core_timing.h" #include "core/core_timing.h"
#include "core/hle/service/gsp_gpu.h" #include "core/hle/service/gsp_gpu.h"
#include "core/hw/gpu.h" #include "core/hw/gpu.h"
#include "core/hw/hw.h" #include "core/hw/hw.h"
#include "core/memory.h" #include "core/memory.h"
#include "core/settings.h"
#include "core/tracer/recorder.h" #include "core/tracer/recorder.h"
#include "video_core/command_processor.h" #include "video_core/command_processor.h"
#include "video_core/debug_utils/debug_utils.h" #include "video_core/debug_utils/debug_utils.h"
@ -32,19 +28,9 @@ namespace GPU {
Regs g_regs; Regs g_regs;
/// 268MHz CPU clocks / 60Hz frames per second /// 268MHz CPU clocks / 60Hz frames per second
const u64 frame_ticks = BASE_CLOCK_RATE_ARM11 / 60; const u64 frame_ticks = BASE_CLOCK_RATE_ARM11 / SCREEN_REFRESH_RATE;
/// Event id for CoreTiming /// Event id for CoreTiming
static int vblank_event; static int vblank_event;
/// Total number of frames drawn
static u64 frame_count;
/// Start clock for frame limiter
static u32 time_point;
/// Total delay caused by slow frames
static float time_delay;
constexpr float FIXED_FRAME_TIME = 1000.0f / 60;
// Max lag caused by slow frames. Can be adjusted to compensate for too many slow frames. Higher
// values increases time needed to limit frame rate after spikes
constexpr float MAX_LAG_TIME = 18;
template <typename T> template <typename T>
inline void Read(T& var, const u32 raw_addr) { inline void Read(T& var, const u32 raw_addr) {
@ -522,24 +508,8 @@ template void Write<u32>(u32 addr, const u32 data);
template void Write<u16>(u32 addr, const u16 data); template void Write<u16>(u32 addr, const u16 data);
template void Write<u8>(u32 addr, const u8 data); template void Write<u8>(u32 addr, const u8 data);
static void FrameLimiter() {
time_delay += FIXED_FRAME_TIME;
time_delay = MathUtil::Clamp(time_delay, -MAX_LAG_TIME, MAX_LAG_TIME);
s32 desired_time = static_cast<s32>(time_delay);
s32 elapsed_time = static_cast<s32>(Common::Timer::GetTimeMs() - time_point);
if (elapsed_time < desired_time) {
Common::SleepCurrentThread(desired_time - elapsed_time);
}
u32 frame_time = Common::Timer::GetTimeMs() - time_point;
time_delay -= frame_time;
}
/// Update hardware /// Update hardware
static void VBlankCallback(u64 userdata, int cycles_late) { static void VBlankCallback(u64 userdata, int cycles_late) {
frame_count++;
VideoCore::g_renderer->SwapBuffers(); VideoCore::g_renderer->SwapBuffers();
// Signal to GSP that GPU interrupt has occurred // Signal to GSP that GPU interrupt has occurred
@ -550,12 +520,6 @@ static void VBlankCallback(u64 userdata, int cycles_late) {
Service::GSP::SignalInterrupt(Service::GSP::InterruptId::PDC0); Service::GSP::SignalInterrupt(Service::GSP::InterruptId::PDC0);
Service::GSP::SignalInterrupt(Service::GSP::InterruptId::PDC1); Service::GSP::SignalInterrupt(Service::GSP::InterruptId::PDC1);
if (!Settings::values.use_vsync && Settings::values.toggle_framelimit) {
FrameLimiter();
}
time_point = Common::Timer::GetTimeMs();
// Reschedule recurrent event // Reschedule recurrent event
CoreTiming::ScheduleEvent(frame_ticks - cycles_late, vblank_event); CoreTiming::ScheduleEvent(frame_ticks - cycles_late, vblank_event);
} }
@ -590,9 +554,6 @@ void Init() {
framebuffer_sub.color_format.Assign(Regs::PixelFormat::RGB8); framebuffer_sub.color_format.Assign(Regs::PixelFormat::RGB8);
framebuffer_sub.active_fb = 0; framebuffer_sub.active_fb = 0;
frame_count = 0;
time_point = Common::Timer::GetTimeMs();
vblank_event = CoreTiming::RegisterEvent("GPU::VBlankCallback", VBlankCallback); vblank_event = CoreTiming::RegisterEvent("GPU::VBlankCallback", VBlankCallback);
CoreTiming::ScheduleEvent(frame_ticks, vblank_event); CoreTiming::ScheduleEvent(frame_ticks, vblank_event);

View file

@ -13,6 +13,8 @@
namespace GPU { namespace GPU {
constexpr float SCREEN_REFRESH_RATE = 60;
// Returns index corresponding to the Regs member labeled by field_name // Returns index corresponding to the Regs member labeled by field_name
// TODO: Due to Visual studio bug 209229, offsetof does not return constant expressions // TODO: Due to Visual studio bug 209229, offsetof does not return constant expressions
// when used with array elements (e.g. GPU_REG_INDEX(memory_fill_config[0])). // when used with array elements (e.g. GPU_REG_INDEX(memory_fill_config[0])).

105
src/core/perf_stats.cpp Normal file
View file

@ -0,0 +1,105 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <chrono>
#include <mutex>
#include <thread>
#include "common/math_util.h"
#include "core/hw/gpu.h"
#include "core/perf_stats.h"
#include "core/settings.h"
using namespace std::chrono_literals;
using DoubleSecs = std::chrono::duration<double, std::chrono::seconds::period>;
using std::chrono::duration_cast;
using std::chrono::microseconds;
namespace Core {
void PerfStats::BeginSystemFrame() {
std::lock_guard<std::mutex> lock(object_mutex);
frame_begin = Clock::now();
}
void PerfStats::EndSystemFrame() {
std::lock_guard<std::mutex> lock(object_mutex);
auto frame_end = Clock::now();
accumulated_frametime += frame_end - frame_begin;
system_frames += 1;
previous_frame_length = frame_end - previous_frame_end;
previous_frame_end = frame_end;
}
void PerfStats::EndGameFrame() {
std::lock_guard<std::mutex> lock(object_mutex);
game_frames += 1;
}
PerfStats::Results PerfStats::GetAndResetStats(u64 current_system_time_us) {
std::lock_guard<std::mutex> lock(object_mutex);
auto now = Clock::now();
// Walltime elapsed since stats were reset
auto interval = duration_cast<DoubleSecs>(now - reset_point).count();
auto system_us_per_second =
static_cast<double>(current_system_time_us - reset_point_system_us) / interval;
Results results{};
results.system_fps = static_cast<double>(system_frames) / interval;
results.game_fps = static_cast<double>(game_frames) / interval;
results.frametime = duration_cast<DoubleSecs>(accumulated_frametime).count() /
static_cast<double>(system_frames);
results.emulation_speed = system_us_per_second / 1'000'000.0;
// Reset counters
reset_point = now;
reset_point_system_us = current_system_time_us;
accumulated_frametime = Clock::duration::zero();
system_frames = 0;
game_frames = 0;
return results;
}
double PerfStats::GetLastFrameTimeScale() {
std::lock_guard<std::mutex> lock(object_mutex);
constexpr double FRAME_LENGTH = 1.0 / GPU::SCREEN_REFRESH_RATE;
return duration_cast<DoubleSecs>(previous_frame_length).count() / FRAME_LENGTH;
}
void FrameLimiter::DoFrameLimiting(u64 current_system_time_us) {
// Max lag caused by slow frames. Can be adjusted to compensate for too many slow frames. Higher
// values increase the time needed to recover and limit framerate again after spikes.
constexpr microseconds MAX_LAG_TIME_US = 25ms;
if (!Settings::values.toggle_framelimit) {
return;
}
auto now = Clock::now();
frame_limiting_delta_err += microseconds(current_system_time_us - previous_system_time_us);
frame_limiting_delta_err -= duration_cast<microseconds>(now - previous_walltime);
frame_limiting_delta_err =
MathUtil::Clamp(frame_limiting_delta_err, -MAX_LAG_TIME_US, MAX_LAG_TIME_US);
if (frame_limiting_delta_err > microseconds::zero()) {
std::this_thread::sleep_for(frame_limiting_delta_err);
auto now_after_sleep = Clock::now();
frame_limiting_delta_err -= duration_cast<microseconds>(now_after_sleep - now);
now = now_after_sleep;
}
previous_system_time_us = current_system_time_us;
previous_walltime = now;
}
} // namespace Core

83
src/core/perf_stats.h Normal file
View file

@ -0,0 +1,83 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <chrono>
#include <mutex>
#include "common/common_types.h"
namespace Core {
/**
* Class to manage and query performance/timing statistics. All public functions of this class are
* thread-safe unless stated otherwise.
*/
class PerfStats {
public:
using Clock = std::chrono::high_resolution_clock;
struct Results {
/// System FPS (LCD VBlanks) in Hz
double system_fps;
/// Game FPS (GSP frame submissions) in Hz
double game_fps;
/// Walltime per system frame, in seconds, excluding any waits
double frametime;
/// Ratio of walltime / emulated time elapsed
double emulation_speed;
};
void BeginSystemFrame();
void EndSystemFrame();
void EndGameFrame();
Results GetAndResetStats(u64 current_system_time_us);
/**
* Gets the ratio between walltime and the emulated time of the previous system frame. This is
* useful for scaling inputs or outputs moving between the two time domains.
*/
double GetLastFrameTimeScale();
private:
std::mutex object_mutex;
/// Point when the cumulative counters were reset
Clock::time_point reset_point = Clock::now();
/// System time when the cumulative counters were reset
u64 reset_point_system_us = 0;
/// Cumulative duration (excluding v-sync/frame-limiting) of frames since last reset
Clock::duration accumulated_frametime = Clock::duration::zero();
/// Cumulative number of system frames (LCD VBlanks) presented since last reset
u32 system_frames = 0;
/// Cumulative number of game frames (GSP frame submissions) since last reset
u32 game_frames = 0;
/// Point when the previous system frame ended
Clock::time_point previous_frame_end = reset_point;
/// Point when the current system frame began
Clock::time_point frame_begin = reset_point;
/// Total visible duration (including frame-limiting, etc.) of the previous system frame
Clock::duration previous_frame_length = Clock::duration::zero();
};
class FrameLimiter {
public:
using Clock = std::chrono::high_resolution_clock;
void DoFrameLimiting(u64 current_system_time_us);
private:
/// Emulated system time (in microseconds) at the last limiter invocation
u64 previous_system_time_us = 0;
/// Walltime at the last limiter invocation
Clock::time_point previous_walltime = Clock::now();
/// Accumulated difference between walltime and emulated time
std::chrono::microseconds frame_limiting_delta_err{0};
};
} // namespace Core

View file

@ -10,8 +10,8 @@
#include "common/assert.h" #include "common/assert.h"
#include "common/bit_field.h" #include "common/bit_field.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/profiler_reporting.h" #include "core/core.h"
#include "common/synchronized_wrapper.h" #include "core/core_timing.h"
#include "core/frontend/emu_window.h" #include "core/frontend/emu_window.h"
#include "core/hw/gpu.h" #include "core/hw/gpu.h"
#include "core/hw/hw.h" #include "core/hw/hw.h"
@ -145,21 +145,16 @@ void RendererOpenGL::SwapBuffers() {
DrawScreens(); DrawScreens();
auto& profiler = Common::Profiling::GetProfilingManager(); Core::System::GetInstance().perf_stats.EndSystemFrame();
profiler.FinishFrame();
{
auto aggregator = Common::Profiling::GetTimingResultsAggregator();
aggregator->AddFrame(profiler.GetPreviousFrameResults());
}
// Swap buffers // Swap buffers
render_window->PollEvents(); render_window->PollEvents();
render_window->SwapBuffers(); render_window->SwapBuffers();
Core::System::GetInstance().frame_limiter.DoFrameLimiting(CoreTiming::GetGlobalTimeUs());
Core::System::GetInstance().perf_stats.BeginSystemFrame();
prev_state.Apply(); prev_state.Apply();
profiler.BeginFrame();
RefreshRasterizerSetting(); RefreshRasterizerSetting();
if (Pica::g_debug_context && Pica::g_debug_context->recorder) { if (Pica::g_debug_context && Pica::g_debug_context->recorder) {