@@ -0,0 +1,342 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include "DolphinQt2/FIFOPlayerWindow.h"

#include <QCheckBox>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QSpinBox>
#include <QVBoxLayout>

#include <algorithm>

#include "Core/Core.h"
#include "Core/FifoPlayer/FifoDataFile.h"
#include "Core/FifoPlayer/FifoPlaybackAnalyzer.h"
#include "Core/FifoPlayer/FifoPlayer.h"
#include "Core/FifoPlayer/FifoRecorder.h"

#include "DolphinQt2/QtUtils/QueueOnObject.h"
#include "DolphinQt2/Settings.h"

FIFOPlayerWindow::FIFOPlayerWindow(QWidget* parent) : QDialog(parent)
{
setWindowTitle(tr("FIFO Player"));

CreateWidgets();
ConnectWidgets();

UpdateInfo();

UpdateControls();

FifoPlayer::GetInstance().SetFileLoadedCallback(
[this] { QueueOnObject(this, &FIFOPlayerWindow::OnFIFOLoaded); });
FifoPlayer::GetInstance().SetFrameWrittenCallback(
[this] { QueueOnObject(this, &FIFOPlayerWindow::OnFIFOLoaded); });

connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [this](Core::State state) {
if (state == Core::State::Running)
OnEmulationStarted();
else if (state == Core::State::Uninitialized)
OnEmulationStopped();
});
}

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 QCheckBox(tr("Early Memory Updates"));

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, 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);

setLayout(layout);
}

void FIFOPlayerWindow::ConnectWidgets()
{
connect(m_load, &QPushButton::pressed, this, &FIFOPlayerWindow::LoadRecording);
connect(m_save, &QPushButton::pressed, this, &FIFOPlayerWindow::SaveRecording);
connect(m_record, &QPushButton::pressed, this, &FIFOPlayerWindow::StartRecording);
connect(m_stop, &QPushButton::pressed, this, &FIFOPlayerWindow::StopRecording);
connect(m_button_box, &QDialogButtonBox::rejected, this, &FIFOPlayerWindow::reject);
connect(m_early_memory_updates, &QCheckBox::toggled, this,
&FIFOPlayerWindow::OnEarlyMemoryUpdatesChanged);
connect(m_frame_range_from, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
&FIFOPlayerWindow::OnLimitsChanged);
connect(m_frame_range_to, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
&FIFOPlayerWindow::OnLimitsChanged);

connect(m_object_range_from, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
&FIFOPlayerWindow::OnLimitsChanged);
connect(m_object_range_to, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
&FIFOPlayerWindow::OnLimitsChanged);
}

void FIFOPlayerWindow::LoadRecording()
{
QString path = QFileDialog::getOpenFileName(this, tr("Open FIFO log"), QString(),
tr("Dolphin FIFO Log (*.dff)"));

if (path.isEmpty())
return;

emit LoadFIFORequested(path);
}

void FIFOPlayerWindow::SaveRecording()
{
QString path = QFileDialog::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)
QMessageBox::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();
}

void FIFOPlayerWindow::OnEmulationStopped()
{
// If we have previously been recording, stop now.
if (FifoRecorder::GetInstance().IsRecording())
StopRecording();

UpdateControls();
}

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().GetFrameObjectCount()),
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().GetFrameObjectCount();
auto frame_count = file->GetFrameCount();

m_frame_range_to->setMaximum(frame_count);
m_object_range_to->setMaximum(object_count);

m_frame_range_to->setValue(frame_count);
m_object_range_to->setValue(object_count);

UpdateInfo();
UpdateLimits();
UpdateControls();
}

void FIFOPlayerWindow::OnEarlyMemoryUpdatesChanged(bool enabled)
{
FifoPlayer::GetInstance().SetEarlyMemoryUpdates(enabled);
}

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(std::max(m_frame_range_to->value() - 1, 0));
m_frame_range_to->setMinimum(m_frame_range_from->value() + 1);
m_object_range_from->setMaximum(std::max(m_object_range_to->value() - 1, 0));
m_object_range_to->setMinimum(m_object_range_from->value() + 1);
}

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);

m_early_memory_updates->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());
}
@@ -0,0 +1,62 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#pragma once

#include <QDialog>

class QCheckBox;
class QDialogButtonBox;
class QLabel;
class QPushButton;
class QSpinBox;

class FIFOPlayerWindow : public QDialog
{
Q_OBJECT
public:
explicit FIFOPlayerWindow(QWidget* parent = nullptr);
~FIFOPlayerWindow();

signals:
void LoadFIFORequested(const QString& path);

private:
void CreateWidgets();
void ConnectWidgets();

void LoadRecording();
void SaveRecording();
void StartRecording();
void StopRecording();

void OnEmulationStarted();
void OnEmulationStopped();
void OnLimitsChanged();
void OnEarlyMemoryUpdatesChanged(bool enabled);
void OnRecordingDone();
void OnFIFOLoaded();

void UpdateControls();
void UpdateInfo();
void UpdateLimits();

QLabel* m_info_label;
QPushButton* m_load;
QPushButton* m_save;
QPushButton* m_record;
QPushButton* m_stop;
QSpinBox* m_frame_range_from;
QLabel* m_frame_range_from_label;
QSpinBox* m_frame_range_to;
QLabel* m_frame_range_to_label;
QSpinBox* m_frame_record_count;
QLabel* m_frame_record_count_label;
QSpinBox* m_object_range_from;
QLabel* m_object_range_from_label;
QSpinBox* m_object_range_to;
QLabel* m_object_range_to_label;
QCheckBox* m_early_memory_updates;
QDialogButtonBox* m_button_box;
};
@@ -45,6 +45,7 @@
#include "DolphinQt2/Config/LogWidget.h"
#include "DolphinQt2/Config/Mapping/MappingWindow.h"
#include "DolphinQt2/Config/SettingsWindow.h"
#include "DolphinQt2/FIFOPlayerWindow.h"
#include "DolphinQt2/Host.h"
#include "DolphinQt2/HotkeyScheduler.h"
#include "DolphinQt2/MainWindow.h"
@@ -155,9 +156,15 @@ void MainWindow::CreateComponents()
m_stack = new QStackedWidget(this);
m_controllers_window = new ControllersWindow(this);
m_settings_window = new SettingsWindow(this);

m_hotkey_window = new MappingWindow(this, MappingWindow::Type::MAPPING_HOTKEYS, 0);

m_log_widget = new LogWidget(this);
m_log_config_widget = new LogConfigWidget(this);
m_fifo_window = new FIFOPlayerWindow(this);

connect(m_fifo_window, &FIFOPlayerWindow::LoadFIFORequested, this,
static_cast<void (MainWindow::*)(const QString&)>(&MainWindow::StartGame));

#if defined(HAVE_XRANDR) && HAVE_XRANDR
m_graphics_window = new GraphicsWindow(
@@ -215,6 +222,7 @@ void MainWindow::ConnectMenuBar()
connect(m_menu_bar, &MenuBar::PerformOnlineUpdate, this, &MainWindow::PerformOnlineUpdate);
connect(m_menu_bar, &MenuBar::BootWiiSystemMenu, this, &MainWindow::BootWiiSystemMenu);
connect(m_menu_bar, &MenuBar::StartNetPlay, this, &MainWindow::ShowNetPlaySetupDialog);
connect(m_menu_bar, &MenuBar::ShowFIFOPlayer, this, &MainWindow::ShowFIFOPlayer);

// Movie
connect(m_menu_bar, &MenuBar::PlayRecording, this, &MainWindow::OnPlayRecording);
@@ -590,6 +598,13 @@ void MainWindow::ShowNetPlaySetupDialog()
m_netplay_setup_dialog->activateWindow();
}

void MainWindow::ShowFIFOPlayer()
{
m_fifo_window->show();
m_fifo_window->raise();
m_fifo_window->activateWindow();
}

void MainWindow::StateLoad()
{
QString path = QFileDialog::getOpenFileName(this, tr("Select a File"), QDir::currentPath(),
@@ -17,6 +17,7 @@
#include "DolphinQt2/ToolBar.h"

struct BootParameters;
class FIFOPlayerWindow;
class HotkeyScheduler;
class LogConfigWidget;
class LogWidget;
@@ -98,6 +99,7 @@ class MainWindow final : public QMainWindow
void ShowAboutDialog();
void ShowHotkeyDialog();
void ShowNetPlaySetupDialog();
void ShowFIFOPlayer();

void NetPlayInit();
bool NetPlayJoin();
@@ -137,4 +139,5 @@ class MainWindow final : public QMainWindow
GraphicsWindow* m_graphics_window;
LogWidget* m_log_widget;
LogConfigWidget* m_log_config_widget;
FIFOPlayerWindow* m_fifo_window;
};
@@ -116,6 +116,8 @@ void MenuBar::AddToolsMenu()
AddAction(gc_ipl, tr("PAL"), this, [this] { emit BootGameCubeIPL(DiscIO::Region::PAL); });

AddAction(tools_menu, tr("Start &NetPlay..."), this, &MenuBar::StartNetPlay);
AddAction(tools_menu, tr("FIFO Player"), this, &MenuBar::ShowFIFOPlayer);

tools_menu->addSeparator();

// Label will be set by a NANDRefresh later
@@ -62,6 +62,8 @@ class MenuBar final : public QMenuBar

// Tools
void BootGameCubeIPL(DiscIO::Region region);
void ShowFIFOPlayer();
void ShowAboutDialog();

// Options
void Configure();
@@ -77,8 +79,6 @@ class MenuBar final : public QMenuBar
void GameListPlatformVisibilityToggled(const QString& row, bool visible);
void GameListRegionVisibilityToggled(const QString& row, bool visible);

void ShowAboutDialog();

// Movie
void PlayRecording();
void StartRecording();