mirror of
				https://github.com/dolphin-emu/dolphin.git
				synced 2025-10-25 01:19:19 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			419 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			419 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // Copyright 2017 Dolphin Emulator Project
 | |
| // SPDX-License-Identifier: GPL-2.0-or-later
 | |
| 
 | |
| #include "DolphinQt/FIFO/FIFOPlayerWindow.h"
 | |
| 
 | |
| #include <QCheckBox>
 | |
| #include <QDialogButtonBox>
 | |
| #include <QEvent>
 | |
| #include <QGroupBox>
 | |
| #include <QHBoxLayout>
 | |
| #include <QIcon>
 | |
| #include <QKeyEvent>
 | |
| #include <QKeySequence>
 | |
| #include <QLabel>
 | |
| #include <QPushButton>
 | |
| #include <QSpinBox>
 | |
| #include <QTabWidget>
 | |
| #include <QVBoxLayout>
 | |
| 
 | |
| #include <algorithm>
 | |
| 
 | |
| #include "Core/Core.h"
 | |
| #include "Core/FifoPlayer/FifoDataFile.h"
 | |
| #include "Core/FifoPlayer/FifoPlayer.h"
 | |
| #include "Core/FifoPlayer/FifoRecorder.h"
 | |
| 
 | |
| #include "DolphinQt/Config/ToolTipControls/ToolTipCheckBox.h"
 | |
| #include "DolphinQt/FIFO/FIFOAnalyzer.h"
 | |
| #include "DolphinQt/QtUtils/DolphinFileDialog.h"
 | |
| #include "DolphinQt/QtUtils/ModalMessageBox.h"
 | |
| #include "DolphinQt/QtUtils/QueueOnObject.h"
 | |
| #include "DolphinQt/Resources.h"
 | |
| #include "DolphinQt/Settings.h"
 | |
| 
 | |
| FIFOPlayerWindow::FIFOPlayerWindow(QWidget* parent) : QWidget(parent)
 | |
| {
 | |
|   setWindowTitle(tr("FIFO Player"));
 | |
|   setWindowIcon(Resources::GetAppIcon());
 | |
| 
 | |
|   CreateWidgets();
 | |
|   LoadSettings();
 | |
|   ConnectWidgets();
 | |
|   AddDescriptions();
 | |
| 
 | |
|   UpdateInfo();
 | |
| 
 | |
|   UpdateControls();
 | |
| 
 | |
|   FifoPlayer::GetInstance().SetFileLoadedCallback(
 | |
|       [this] { QueueOnObject(this, &FIFOPlayerWindow::OnFIFOLoaded); });
 | |
|   FifoPlayer::GetInstance().SetFrameWrittenCallback([this] {
 | |
|     QueueOnObject(this, [this] {
 | |
|       UpdateInfo();
 | |
|       UpdateControls();
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [this](Core::State state) {
 | |
|     if (state == Core::State::Running && m_emu_state != Core::State::Paused)
 | |
|       OnEmulationStarted();
 | |
|     else if (state == Core::State::Uninitialized)
 | |
|       OnEmulationStopped();
 | |
|     m_emu_state = state;
 | |
|   });
 | |
| 
 | |
|   installEventFilter(this);
 | |
| }
 | |
| 
 | |
| FIFOPlayerWindow::~FIFOPlayerWindow()
 | |
| {
 | |
|   FifoPlayer::GetInstance().SetFileLoadedCallback({});
 | |
|   FifoPlayer::GetInstance().SetFrameWrittenCallback({});
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::CreateWidgets()
 | |
| {
 | |
|   auto* layout = new QVBoxLayout;
 | |
| 
 | |
|   // Info
 | |
|   auto* info_group = new QGroupBox(tr("File Info"));
 | |
|   auto* info_layout = new QHBoxLayout;
 | |
| 
 | |
|   m_info_label = new QLabel;
 | |
|   info_layout->addWidget(m_info_label);
 | |
|   info_group->setLayout(info_layout);
 | |
| 
 | |
|   m_info_label->setFixedHeight(QFontMetrics(font()).lineSpacing() * 3);
 | |
| 
 | |
|   // Object Range
 | |
|   auto* object_range_group = new QGroupBox(tr("Object Range"));
 | |
|   auto* object_range_layout = new QHBoxLayout;
 | |
| 
 | |
|   m_object_range_from = new QSpinBox;
 | |
|   m_object_range_from_label = new QLabel(tr("From:"));
 | |
|   m_object_range_to = new QSpinBox;
 | |
|   m_object_range_to_label = new QLabel(tr("To:"));
 | |
| 
 | |
|   object_range_layout->addWidget(m_object_range_from_label);
 | |
|   object_range_layout->addWidget(m_object_range_from);
 | |
|   object_range_layout->addWidget(m_object_range_to_label);
 | |
|   object_range_layout->addWidget(m_object_range_to);
 | |
|   object_range_group->setLayout(object_range_layout);
 | |
| 
 | |
|   // Frame Range
 | |
|   auto* frame_range_group = new QGroupBox(tr("Frame Range"));
 | |
|   auto* frame_range_layout = new QHBoxLayout;
 | |
| 
 | |
|   m_frame_range_from = new QSpinBox;
 | |
|   m_frame_range_from_label = new QLabel(tr("From:"));
 | |
|   m_frame_range_to = new QSpinBox;
 | |
|   m_frame_range_to_label = new QLabel(tr("To:"));
 | |
| 
 | |
|   frame_range_layout->addWidget(m_frame_range_from_label);
 | |
|   frame_range_layout->addWidget(m_frame_range_from);
 | |
|   frame_range_layout->addWidget(m_frame_range_to_label);
 | |
|   frame_range_layout->addWidget(m_frame_range_to);
 | |
|   frame_range_group->setLayout(frame_range_layout);
 | |
| 
 | |
|   // Playback Options
 | |
|   auto* playback_group = new QGroupBox(tr("Playback Options"));
 | |
|   auto* playback_layout = new QGridLayout;
 | |
|   m_early_memory_updates = new ToolTipCheckBox(tr("Early Memory Updates"));
 | |
|   m_loop = new ToolTipCheckBox(tr("Loop"));
 | |
| 
 | |
|   playback_layout->addWidget(object_range_group, 0, 0);
 | |
|   playback_layout->addWidget(frame_range_group, 0, 1);
 | |
|   playback_layout->addWidget(m_early_memory_updates, 1, 0);
 | |
|   playback_layout->addWidget(m_loop, 1, 1);
 | |
|   playback_group->setLayout(playback_layout);
 | |
| 
 | |
|   // Recording Options
 | |
|   auto* recording_group = new QGroupBox(tr("Recording Options"));
 | |
|   auto* recording_layout = new QHBoxLayout;
 | |
|   m_frame_record_count = new QSpinBox;
 | |
|   m_frame_record_count_label = new QLabel(tr("Frames to Record:"));
 | |
| 
 | |
|   m_frame_record_count->setMinimum(1);
 | |
|   m_frame_record_count->setMaximum(3600);
 | |
|   m_frame_record_count->setValue(3);
 | |
| 
 | |
|   recording_layout->addWidget(m_frame_record_count_label);
 | |
|   recording_layout->addWidget(m_frame_record_count);
 | |
|   recording_group->setLayout(recording_layout);
 | |
| 
 | |
|   m_button_box = new QDialogButtonBox(QDialogButtonBox::Close);
 | |
| 
 | |
|   // Action Buttons
 | |
|   m_load = m_button_box->addButton(tr("Load..."), QDialogButtonBox::ActionRole);
 | |
|   m_save = m_button_box->addButton(tr("Save..."), QDialogButtonBox::ActionRole);
 | |
|   m_record = m_button_box->addButton(tr("Record"), QDialogButtonBox::ActionRole);
 | |
|   m_stop = m_button_box->addButton(tr("Stop"), QDialogButtonBox::ActionRole);
 | |
| 
 | |
|   layout->addWidget(info_group);
 | |
|   layout->addWidget(playback_group);
 | |
|   layout->addWidget(recording_group);
 | |
|   layout->addWidget(m_button_box);
 | |
| 
 | |
|   m_main_widget = new QWidget(this);
 | |
|   m_main_widget->setLayout(layout);
 | |
| 
 | |
|   m_tab_widget = new QTabWidget(this);
 | |
| 
 | |
|   m_analyzer = new FIFOAnalyzer;
 | |
| 
 | |
|   m_tab_widget->addTab(m_main_widget, tr("Play / Record"));
 | |
|   m_tab_widget->addTab(m_analyzer, tr("Analyze"));
 | |
| 
 | |
|   auto* tab_layout = new QVBoxLayout;
 | |
|   tab_layout->addWidget(m_tab_widget);
 | |
| 
 | |
|   setLayout(tab_layout);
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::LoadSettings()
 | |
| {
 | |
|   m_early_memory_updates->setChecked(Config::Get(Config::MAIN_FIFOPLAYER_EARLY_MEMORY_UPDATES));
 | |
|   m_loop->setChecked(Config::Get(Config::MAIN_FIFOPLAYER_LOOP_REPLAY));
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::ConnectWidgets()
 | |
| {
 | |
|   connect(m_load, &QPushButton::clicked, this, &FIFOPlayerWindow::LoadRecording);
 | |
|   connect(m_save, &QPushButton::clicked, this, &FIFOPlayerWindow::SaveRecording);
 | |
|   connect(m_record, &QPushButton::clicked, this, &FIFOPlayerWindow::StartRecording);
 | |
|   connect(m_stop, &QPushButton::clicked, this, &FIFOPlayerWindow::StopRecording);
 | |
|   connect(m_button_box, &QDialogButtonBox::rejected, this, &FIFOPlayerWindow::hide);
 | |
|   connect(m_early_memory_updates, &QCheckBox::toggled, this, &FIFOPlayerWindow::OnConfigChanged);
 | |
|   connect(m_loop, &QCheckBox::toggled, this, &FIFOPlayerWindow::OnConfigChanged);
 | |
| 
 | |
|   connect(m_frame_range_from, qOverload<int>(&QSpinBox::valueChanged), this,
 | |
|           &FIFOPlayerWindow::OnLimitsChanged);
 | |
|   connect(m_frame_range_to, qOverload<int>(&QSpinBox::valueChanged), this,
 | |
|           &FIFOPlayerWindow::OnLimitsChanged);
 | |
| 
 | |
|   connect(m_object_range_from, qOverload<int>(&QSpinBox::valueChanged), this,
 | |
|           &FIFOPlayerWindow::OnLimitsChanged);
 | |
|   connect(m_object_range_to, qOverload<int>(&QSpinBox::valueChanged), this,
 | |
|           &FIFOPlayerWindow::OnLimitsChanged);
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::AddDescriptions()
 | |
| {
 | |
|   static const char TR_MEMORY_UPDATES_DESCRIPTION[] = QT_TR_NOOP(
 | |
|       "If enabled, then all memory updates happen at once before the first frame.<br><br>"
 | |
|       "Causes issues with many fifologs, but can be useful for testing.<br><br>"
 | |
|       "<dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>");
 | |
|   static const char TR_LOOP_DESCRIPTION[] =
 | |
|       QT_TR_NOOP("If unchecked, then playback of the fifolog stops after the final frame.<br><br>"
 | |
|                  "This is generally only useful when a frame-dumping option is enabled.<br><br>"
 | |
|                  "<dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>");
 | |
| 
 | |
|   m_early_memory_updates->SetDescription(tr(TR_MEMORY_UPDATES_DESCRIPTION));
 | |
|   m_loop->SetDescription(tr(TR_LOOP_DESCRIPTION));
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::LoadRecording()
 | |
| {
 | |
|   QString path = DolphinFileDialog::getOpenFileName(this, tr("Open FIFO log"), QString(),
 | |
|                                                     tr("Dolphin FIFO Log (*.dff)"));
 | |
| 
 | |
|   if (path.isEmpty())
 | |
|     return;
 | |
| 
 | |
|   emit LoadFIFORequested(path);
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::SaveRecording()
 | |
| {
 | |
|   QString path = DolphinFileDialog::getSaveFileName(this, tr("Save FIFO log"), QString(),
 | |
|                                                     tr("Dolphin FIFO Log (*.dff)"));
 | |
| 
 | |
|   if (path.isEmpty())
 | |
|     return;
 | |
| 
 | |
|   FifoDataFile* file = FifoRecorder::GetInstance().GetRecordedFile();
 | |
| 
 | |
|   bool result = file->Save(path.toStdString());
 | |
| 
 | |
|   if (!result)
 | |
|   {
 | |
|     ModalMessageBox::critical(this, tr("Error"), tr("Failed to save FIFO log."));
 | |
|   }
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::StartRecording()
 | |
| {
 | |
|   // Start recording
 | |
|   FifoRecorder::GetInstance().StartRecording(m_frame_record_count->value(), [this] {
 | |
|     QueueOnObject(this, [this] { OnRecordingDone(); });
 | |
|   });
 | |
| 
 | |
|   UpdateControls();
 | |
| 
 | |
|   UpdateInfo();
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::StopRecording()
 | |
| {
 | |
|   FifoRecorder::GetInstance().StopRecording();
 | |
| 
 | |
|   UpdateControls();
 | |
|   UpdateInfo();
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::OnEmulationStarted()
 | |
| {
 | |
|   UpdateControls();
 | |
| 
 | |
|   if (FifoPlayer::GetInstance().GetFile())
 | |
|     OnFIFOLoaded();
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::OnEmulationStopped()
 | |
| {
 | |
|   // If we have previously been recording, stop now.
 | |
|   if (FifoRecorder::GetInstance().IsRecording())
 | |
|     StopRecording();
 | |
| 
 | |
|   UpdateControls();
 | |
|   // When emulation stops, switch away from the analyzer tab, as it no longer shows anything useful
 | |
|   m_tab_widget->setCurrentWidget(m_main_widget);
 | |
|   m_analyzer->Update();
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::OnRecordingDone()
 | |
| {
 | |
|   UpdateInfo();
 | |
|   UpdateControls();
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::UpdateInfo()
 | |
| {
 | |
|   if (FifoPlayer::GetInstance().IsPlaying())
 | |
|   {
 | |
|     FifoDataFile* file = FifoPlayer::GetInstance().GetFile();
 | |
|     m_info_label->setText(
 | |
|         tr("%1 frame(s)\n%2 object(s)\nCurrent Frame: %3")
 | |
|             .arg(QString::number(file->GetFrameCount()),
 | |
|                  QString::number(FifoPlayer::GetInstance().GetCurrentFrameObjectCount()),
 | |
|                  QString::number(FifoPlayer::GetInstance().GetCurrentFrameNum())));
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (FifoRecorder::GetInstance().IsRecordingDone())
 | |
|   {
 | |
|     FifoDataFile* file = FifoRecorder::GetInstance().GetRecordedFile();
 | |
|     size_t fifo_bytes = 0;
 | |
|     size_t mem_bytes = 0;
 | |
| 
 | |
|     for (u32 i = 0; i < file->GetFrameCount(); ++i)
 | |
|     {
 | |
|       fifo_bytes += file->GetFrame(i).fifoData.size();
 | |
|       for (const auto& mem_update : file->GetFrame(i).memoryUpdates)
 | |
|         mem_bytes += mem_update.data.size();
 | |
|     }
 | |
| 
 | |
|     m_info_label->setText(tr("%1 FIFO bytes\n%2 memory bytes\n%3 frames")
 | |
|                               .arg(QString::number(fifo_bytes), QString::number(mem_bytes),
 | |
|                                    QString::number(file->GetFrameCount())));
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (Core::IsRunning() && FifoRecorder::GetInstance().IsRecording())
 | |
|   {
 | |
|     m_info_label->setText(tr("Recording..."));
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   m_info_label->setText(tr("No file loaded / recorded."));
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::OnFIFOLoaded()
 | |
| {
 | |
|   FifoDataFile* file = FifoPlayer::GetInstance().GetFile();
 | |
| 
 | |
|   auto object_count = FifoPlayer::GetInstance().GetMaxObjectCount();
 | |
|   auto frame_count = file->GetFrameCount();
 | |
| 
 | |
|   m_frame_range_to->setMaximum(frame_count - 1);
 | |
|   m_object_range_to->setMaximum(object_count - 1);
 | |
| 
 | |
|   m_frame_range_from->setValue(0);
 | |
|   m_object_range_from->setValue(0);
 | |
|   m_frame_range_to->setValue(frame_count - 1);
 | |
|   m_object_range_to->setValue(object_count - 1);
 | |
| 
 | |
|   UpdateInfo();
 | |
|   UpdateLimits();
 | |
|   UpdateControls();
 | |
| 
 | |
|   m_analyzer->Update();
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::OnConfigChanged()
 | |
| {
 | |
|   Config::SetBase(Config::MAIN_FIFOPLAYER_EARLY_MEMORY_UPDATES,
 | |
|                   m_early_memory_updates->isChecked());
 | |
|   Config::SetBase(Config::MAIN_FIFOPLAYER_LOOP_REPLAY, m_loop->isChecked());
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::OnLimitsChanged()
 | |
| {
 | |
|   FifoPlayer& player = FifoPlayer::GetInstance();
 | |
| 
 | |
|   player.SetFrameRangeStart(m_frame_range_from->value());
 | |
|   player.SetFrameRangeEnd(m_frame_range_to->value());
 | |
|   player.SetObjectRangeStart(m_object_range_from->value());
 | |
|   player.SetObjectRangeEnd(m_object_range_to->value());
 | |
|   UpdateLimits();
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::UpdateLimits()
 | |
| {
 | |
|   m_frame_range_from->setMaximum(m_frame_range_to->value());
 | |
|   m_frame_range_to->setMinimum(m_frame_range_from->value());
 | |
|   m_object_range_from->setMaximum(m_object_range_to->value());
 | |
|   m_object_range_to->setMinimum(m_object_range_from->value());
 | |
| }
 | |
| 
 | |
| void FIFOPlayerWindow::UpdateControls()
 | |
| {
 | |
|   bool running = Core::IsRunning();
 | |
|   bool is_recording = FifoRecorder::GetInstance().IsRecording();
 | |
|   bool is_playing = FifoPlayer::GetInstance().IsPlaying();
 | |
| 
 | |
|   m_frame_range_from->setEnabled(is_playing);
 | |
|   m_frame_range_from_label->setEnabled(is_playing);
 | |
|   m_frame_range_to->setEnabled(is_playing);
 | |
|   m_frame_range_to_label->setEnabled(is_playing);
 | |
|   m_object_range_from->setEnabled(is_playing);
 | |
|   m_object_range_from_label->setEnabled(is_playing);
 | |
|   m_object_range_to->setEnabled(is_playing);
 | |
|   m_object_range_to_label->setEnabled(is_playing);
 | |
| 
 | |
|   bool enable_frame_record_count = !is_playing && !is_recording;
 | |
| 
 | |
|   m_frame_record_count_label->setEnabled(enable_frame_record_count);
 | |
|   m_frame_record_count->setEnabled(enable_frame_record_count);
 | |
| 
 | |
|   m_load->setEnabled(!running);
 | |
|   m_record->setEnabled(running && !is_playing);
 | |
| 
 | |
|   m_stop->setVisible(running && is_recording);
 | |
|   m_record->setVisible(!m_stop->isVisible());
 | |
| 
 | |
|   m_save->setEnabled(FifoRecorder::GetInstance().IsRecordingDone());
 | |
| }
 | |
| 
 | |
| bool FIFOPlayerWindow::eventFilter(QObject* object, QEvent* event)
 | |
| {
 | |
|   // Close when escape is pressed
 | |
|   if (event->type() == QEvent::KeyPress)
 | |
|   {
 | |
|     if (static_cast<QKeyEvent*>(event)->matches(QKeySequence::Cancel))
 | |
|       hide();
 | |
|   }
 | |
| 
 | |
|   return false;
 | |
| }
 |