GS: Add video dumping via ffmpeg

This commit is contained in:
Connor McLaughlin
2022-12-22 04:27:30 +00:00
committed by refractionpcsx2
parent e5009398e0
commit 699c3d80b7
29 changed files with 1038 additions and 1588 deletions
+195
View File
@@ -0,0 +1,195 @@
#[==[
Provides the following variables:
* `FFMPEG_INCLUDE_DIRS`: Include directories necessary to use FFMPEG.
* `FFMPEG_LIBRARIES`: Libraries necessary to use FFMPEG. Note that this only
includes libraries for the components requested.
* `FFMPEG_VERSION`: The version of FFMPEG found.
The following components are supported:
* `avcodec`
* `avdevice`
* `avfilter`
* `avformat`
* `avresample`
* `avutil`
* `swresample`
* `swscale`
For each component, the following are provided:
* `FFMPEG_<component>_FOUND`: Libraries for the component.
* `FFMPEG_<component>_INCLUDE_DIRS`: Include directories for
the component.
* `FFMPEG_<component>_LIBRARIES`: Libraries for the component.
* `FFMPEG::<component>`: A target to use with `target_link_libraries`.
Note that only components requested with `COMPONENTS` or `OPTIONAL_COMPONENTS`
are guaranteed to set these variables or provide targets.
#]==]
function (_ffmpeg_find component headername)
find_path("FFMPEG_${component}_INCLUDE_DIR"
NAMES
"lib${component}/${headername}"
PATHS
"${FFMPEG_ROOT}/include"
~/Library/Frameworks
/Library/Frameworks
/usr/local/include
/usr/include
/sw/include # Fink
/opt/local/include # DarwinPorts
/opt/csw/include # Blastwave
/opt/include
/usr/freeware/include
PATH_SUFFIXES
ffmpeg
DOC "FFMPEG's ${component} include directory")
mark_as_advanced("FFMPEG_${component}_INCLUDE_DIR")
# On Windows, static FFMPEG is sometimes built as `lib<name>.a`.
if (WIN32)
list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".lib")
list(APPEND CMAKE_FIND_LIBRARY_PREFIXES "" "lib")
endif ()
find_library("FFMPEG_${component}_LIBRARY"
NAMES
"${component}"
PATHS
"${FFMPEG_ROOT}/lib"
~/Library/Frameworks
/Library/Frameworks
/usr/local/lib
/usr/local/lib64
/usr/lib
/usr/lib64
/sw/lib
/opt/local/lib
/opt/csw/lib
/opt/lib
/usr/freeware/lib64
"${FFMPEG_ROOT}/bin"
DOC "FFMPEG's ${component} library")
mark_as_advanced("FFMPEG_${component}_LIBRARY")
if (FFMPEG_${component}_LIBRARY AND FFMPEG_${component}_INCLUDE_DIR)
set(_deps_found TRUE)
set(_deps_link)
foreach (_ffmpeg_dep IN LISTS ARGN)
if (TARGET "FFMPEG::${_ffmpeg_dep}")
list(APPEND _deps_link "FFMPEG::${_ffmpeg_dep}")
else ()
set(_deps_found FALSE)
endif ()
endforeach ()
if (_deps_found)
if (NOT TARGET "FFMPEG::${component}")
add_library("FFMPEG::${component}" UNKNOWN IMPORTED)
set_target_properties("FFMPEG::${component}" PROPERTIES
IMPORTED_LOCATION "${FFMPEG_${component}_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_${component}_INCLUDE_DIR}"
IMPORTED_LINK_INTERFACE_LIBRARIES "${_deps_link}")
endif ()
set("FFMPEG_${component}_FOUND" 1
PARENT_SCOPE)
set(version_header_path "${FFMPEG_${component}_INCLUDE_DIR}/lib${component}/version.h")
if (EXISTS "${version_header_path}")
string(TOUPPER "${component}" component_upper)
file(STRINGS "${version_header_path}" version
REGEX "#define *LIB${component_upper}_VERSION_(MAJOR|MINOR|MICRO) ")
string(REGEX REPLACE ".*_MAJOR *\([0-9]*\).*" "\\1" major "${version}")
string(REGEX REPLACE ".*_MINOR *\([0-9]*\).*" "\\1" minor "${version}")
string(REGEX REPLACE ".*_MICRO *\([0-9]*\).*" "\\1" micro "${version}")
if (NOT major STREQUAL "" AND
NOT minor STREQUAL "" AND
NOT micro STREQUAL "")
set("FFMPEG_${component}_VERSION" "${major}.${minor}.${micro}"
PARENT_SCOPE)
endif ()
endif ()
else ()
set("FFMPEG_${component}_FOUND" 0
PARENT_SCOPE)
set(what)
if (NOT FFMPEG_${component}_LIBRARY)
set(what "library")
endif ()
if (NOT FFMPEG_${component}_INCLUDE_DIR)
if (what)
string(APPEND what " or headers")
else ()
set(what "headers")
endif ()
endif ()
set("FFMPEG_${component}_NOT_FOUND_MESSAGE"
"Could not find the ${what} for ${component}."
PARENT_SCOPE)
endif ()
endif ()
endfunction ()
_ffmpeg_find(avutil avutil.h)
_ffmpeg_find(avresample avresample.h
avutil)
_ffmpeg_find(swresample swresample.h
avutil)
_ffmpeg_find(swscale swscale.h
avutil)
_ffmpeg_find(avcodec avcodec.h
avutil)
_ffmpeg_find(avformat avformat.h
avcodec avutil)
_ffmpeg_find(avfilter avfilter.h
avutil)
_ffmpeg_find(avdevice avdevice.h
avformat avutil)
if (TARGET FFMPEG::avutil)
set(_ffmpeg_version_header_path "${FFMPEG_avutil_INCLUDE_DIR}/libavutil/ffversion.h")
if (EXISTS "${_ffmpeg_version_header_path}")
file(STRINGS "${_ffmpeg_version_header_path}" _ffmpeg_version
REGEX "FFMPEG_VERSION")
string(REGEX REPLACE ".*\"n?\(.*\)\"" "\\1" FFMPEG_VERSION "${_ffmpeg_version}")
unset(_ffmpeg_version)
else ()
set(FFMPEG_VERSION FFMPEG_VERSION-NOTFOUND)
endif ()
unset(_ffmpeg_version_header_path)
endif ()
set(FFMPEG_INCLUDE_DIRS)
set(FFMPEG_LIBRARIES)
set(_ffmpeg_required_vars)
foreach (_ffmpeg_component IN LISTS FFMPEG_FIND_COMPONENTS)
if (TARGET "FFMPEG::${_ffmpeg_component}")
set(FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS
"${FFMPEG_${_ffmpeg_component}_INCLUDE_DIR}")
set(FFMPEG_${_ffmpeg_component}_LIBRARIES
"${FFMPEG_${_ffmpeg_component}_LIBRARY}")
list(APPEND FFMPEG_INCLUDE_DIRS
"${FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS}")
list(APPEND FFMPEG_LIBRARIES
"${FFMPEG_${_ffmpeg_component}_LIBRARIES}")
if (FFMEG_FIND_REQUIRED_${_ffmpeg_component})
list(APPEND _ffmpeg_required_vars
"FFMPEG_${_ffmpeg_required_vars}_INCLUDE_DIRS"
"FFMPEG_${_ffmpeg_required_vars}_LIBRARIES")
endif ()
endif ()
endforeach ()
unset(_ffmpeg_component)
if (FFMPEG_INCLUDE_DIRS)
list(REMOVE_DUPLICATES FFMPEG_INCLUDE_DIRS)
endif ()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(FFMPEG
REQUIRED_VARS FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARIES ${_ffmpeg_required_vars}
VERSION_VAR FFMPEG_VERSION
HANDLE_COMPONENTS)
unset(_ffmpeg_required_vars)
+9
View File
@@ -18,6 +18,7 @@ if (WIN32)
endif()
add_subdirectory(3rdparty/xz EXCLUDE_FROM_ALL)
add_subdirectory(3rdparty/D3D12MemAlloc EXCLUDE_FROM_ALL)
set(FFMPEG_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/3rdparty/ffmpeg/include")
else()
find_package(PCAP REQUIRED)
find_package(Gettext) # translation tool
@@ -38,6 +39,14 @@ else()
set(CMAKE_FIND_FRAMEWORK ${FIND_FRAMEWORK_BACKUP})
find_package(Vtune)
# Use bundled ffmpeg v4.x.x headers if we can't locate it in the system.
# We'll try to load it dynamically at runtime.
find_package(FFMPEG)
if(NOT FFMPEG_VERSION)
message(WARNING "FFmpeg not found, using bundled headers.")
set(FFMPEG_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/3rdparty/ffmpeg/include")
endif()
if(NOT PCSX2_CORE)
# Does not require the module (allow to compile non-wx plugins)
# Force the unicode build (the variable is only supported on cmake 2.8.3 and above)
+33
View File
@@ -32,6 +32,7 @@
#include "pcsx2/CDVD/CDVDdiscReader.h"
#include "pcsx2/Frontend/GameList.h"
#include "pcsx2/Frontend/LogSink.h"
#include "pcsx2/GS/GS.h"
#include "pcsx2/GSDumpReplayer.h"
#include "pcsx2/HostDisplay.h"
#include "pcsx2/HostSettings.h"
@@ -383,6 +384,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionSaveBlockDump, &QAction::toggled, this, &MainWindow::onBlockDumpActionToggled);
connect(m_ui.actionShowAdvancedSettings, &QAction::toggled, this, &MainWindow::onShowAdvancedSettingsToggled);
connect(m_ui.actionSaveGSDump, &QAction::triggered, this, &MainWindow::onSaveGSDumpActionTriggered);
connect(m_ui.actionToolsVideoCapture, &QAction::toggled, this, &MainWindow::onToolsVideoCaptureToggled);
// Input Recording
connect(m_ui.actionInputRecNew, &QAction::triggered, this, &MainWindow::onInputRecNewActionTriggered);
@@ -877,6 +879,33 @@ void MainWindow::onShowAdvancedSettingsToggled(bool checked)
recreateSettings();
}
void MainWindow::onToolsVideoCaptureToggled(bool checked)
{
if (!s_vm_valid)
return;
if (!checked)
{
g_emu_thread->endCapture();
return;
}
const QString container(QString::fromStdString(
Host::GetStringSettingValue("EmuCore/GS", "VideoCaptureContainer", Pcsx2Config::GSOptions::DEFAULT_VIDEO_CAPTURE_CONTAINER)));
const QString filter(tr("%1 Files (*.%2)").arg(container.toUpper()).arg(container));
QString path(QStringLiteral("%1.%2").arg(QString::fromStdString(GSGetBaseSnapshotFilename())).arg(container));
path = QFileDialog::getSaveFileName(this, tr("Video Capture"), path, filter);
if (path.isEmpty())
{
QSignalBlocker sb(m_ui.actionToolsVideoCapture);
m_ui.actionToolsVideoCapture->setChecked(false);
return;
}
g_emu_thread->beginCapture(path);
}
void MainWindow::saveStateToConfig()
{
if (!isVisible())
@@ -952,6 +981,10 @@ void MainWindow::updateEmulationActions(bool starting, bool running)
m_ui.actionViewGameProperties->setEnabled(running);
m_ui.actionToolsVideoCapture->setEnabled(running);
if (!running && m_ui.actionToolsVideoCapture->isChecked())
m_ui.actionToolsVideoCapture->setChecked(false);
m_game_list_widget->setDisabled(starting && !running);
if (!starting && !running)
+1
View File
@@ -169,6 +169,7 @@ private Q_SLOTS:
void onSaveGSDumpActionTriggered();
void onBlockDumpActionToggled(bool checked);
void onShowAdvancedSettingsToggled(bool checked);
void onToolsVideoCaptureToggled(bool checked);
// Input Recording
void onInputRecNewActionTriggered();
+9
View File
@@ -188,6 +188,7 @@
<addaction name="actionToggleSoftwareRendering"/>
<addaction name="actionReloadPatches"/>
<addaction name="menuInput_Recording"/>
<addaction name="actionToolsVideoCapture"/>
<addaction name="separator"/>
<addaction name="actionEnableSystemConsole"/>
<addaction name="actionEnableFileLogging"/>
@@ -882,6 +883,14 @@
<string>Recording Viewer</string>
</property>
</action>
<action name="actionToolsVideoCapture">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Video Capture</string>
</property>
</action>
</widget>
<resources>
<include location="resources/resources.qrc"/>
+30
View File
@@ -847,6 +847,36 @@ void EmuThread::queueSnapshot(quint32 gsdump_frames)
GetMTGS().RunOnGSThread([gsdump_frames]() { GSQueueSnapshot(std::string(), gsdump_frames); });
}
void EmuThread::beginCapture(const QString& path)
{
if (!isOnEmuThread())
{
QMetaObject::invokeMethod(this, "beginCapture", Qt::QueuedConnection, Q_ARG(const QString&, path));
return;
}
if (!VMManager::HasValidVM())
return;
GetMTGS().RunOnGSThread([path = path.toStdString()]() {
GSBeginCapture(std::move(path));
});
}
void EmuThread::endCapture()
{
if (!isOnEmuThread())
{
QMetaObject::invokeMethod(this, "endCapture", Qt::QueuedConnection);
return;
}
if (!VMManager::HasValidVM())
return;
GetMTGS().RunOnGSThread(&GSEndCapture);
}
void EmuThread::updateDisplay()
{
pxAssertRel(!isOnEmuThread(), "Not on emu thread");
+2
View File
@@ -112,6 +112,8 @@ public Q_SLOTS:
void enumerateVibrationMotors();
void runOnCPUThread(const std::function<void()>& func);
void queueSnapshot(quint32 gsdump_frames);
void beginCapture(const QString& path);
void endCapture();
Q_SIGNALS:
bool messageConfirmed(const QString& title, const QString& message);
+39 -1
View File
@@ -23,6 +23,7 @@
#include "pcsx2/HostSettings.h"
#include "pcsx2/GS/GS.h"
#include "pcsx2/GS/GSCapture.h"
#include "pcsx2/GS/GSUtil.h"
#ifdef ENABLE_VULKAN
@@ -314,7 +315,7 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget*
if (!m_dialog->isPerGameSettings())
{
// Only allow disabling readbacks for per-game settings, it's too dangerous.
m_ui.advancedDebugFormLayout->removeRow(2);
m_ui.advancedOptionsFormLayout->removeRow(0);
m_ui.gsDownloadMode = nullptr;
// Remove texture offset and skipdraw range for global settings.
@@ -327,6 +328,23 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget*
}
#endif
// Capture settings
{
for (const char** container = Pcsx2Config::GSOptions::VideoCaptureContainers; *container; container++)
{
const QString name(QString::fromUtf8(*container));
m_ui.videoCaptureContainer->addItem(name.toUpper(), name);
}
SettingWidgetBinder::BindWidgetToStringSetting(sif, m_ui.videoCaptureContainer, "EmuCore/GS", "VideoCaptureContainer");
connect(m_ui.videoCaptureContainer, &QComboBox::currentIndexChanged, this, &GraphicsSettingsWidget::onVideoCaptureContainerChanged);
SettingWidgetBinder::BindWidgetToIntSetting(
sif, m_ui.videoCaptureBitrate, "EmuCore/GS", "VideoCaptureBitrate", Pcsx2Config::GSOptions::DEFAULT_VIDEO_CAPTURE_BITRATE);
onVideoCaptureContainerChanged();
}
// Display tab
{
@@ -705,6 +723,26 @@ void GraphicsSettingsWidget::onShadeBoostChanged()
m_ui.shadeBoostSaturation->setEnabled(enabled);
}
void GraphicsSettingsWidget::onVideoCaptureContainerChanged()
{
const std::string container(
m_dialog->getEffectiveStringValue("EmuCore/GS", "VideoCaptureContainer", Pcsx2Config::GSOptions::DEFAULT_VIDEO_CAPTURE_CONTAINER));
m_ui.videoCaptureCodec->disconnect();
m_ui.videoCaptureCodec->clear();
const std::vector<std::pair<std::string, std::string>> vcapture_formats(GSCapture::GetVideoCodecList(container.c_str()));
m_ui.videoCaptureCodec->addItem(tr("Default"), QString());
for (const auto& [format, name] : vcapture_formats)
{
const QString qformat(QString::fromStdString(format));
const QString qname(QString::fromStdString(name));
m_ui.videoCaptureCodec->addItem(QStringLiteral("%1 [%2]").arg(qformat).arg(qname), qformat);
}
SettingWidgetBinder::BindWidgetToStringSetting(
m_dialog->getSettingsInterface(), m_ui.videoCaptureCodec, "EmuCore/GS", "VideoCaptureCodec");
}
void GraphicsSettingsWidget::onGpuPaletteConversionChanged(int state)
{
const bool enabled = state == Qt::CheckState::PartiallyChecked ? Host::GetBaseBoolSettingValue("EmuCore/GS", "paltex", false) : state;
@@ -44,6 +44,7 @@ private Q_SLOTS:
void onGpuPaletteConversionChanged(int state);
void onFullscreenModeChanged(int index);
void onShadeBoostChanged();
void onVideoCaptureContainerChanged();
private:
GSRendererType getEffectiveRenderer() const;
+137 -84
View File
@@ -1629,11 +1629,135 @@
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<widget class="QGroupBox" name="advancedOptions">
<property name="title">
<string>Debug Options</string>
<string>Advanced Options</string>
</property>
<layout class="QFormLayout" name="advancedDebugFormLayout">
<layout class="QFormLayout" name="advancedOptionsFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_42">
<property name="text">
<string>Hardware Download Mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="gsDownloadMode">
<item>
<property name="text">
<string>Accurate (Recommended)</string>
</property>
</item>
<item>
<property name="text">
<string>Disable Readbacks (Synchronize GS Thread)</string>
</property>
</item>
<item>
<property name="text">
<string>Unsynchronized (Non-Deterministic)</string>
</property>
</item>
<item>
<property name="text">
<string>Disabled (Ignore Transfers)</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_34">
<property name="text">
<string>GS Dump Compression:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="gsDumpCompression">
<item>
<property name="text">
<string>Uncompressed</string>
</property>
</item>
<item>
<property name="text">
<string>LZMA (xz)</string>
</property>
</item>
<item>
<property name="text">
<string>Zstandard (zst)</string>
</property>
</item>
</widget>
</item>
<item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_9">
<item row="0" column="0">
<widget class="QCheckBox" name="useBlitSwapChain">
<property name="text">
<string>Use Blit Swap Chain</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="skipPresentingDuplicateFrames">
<property name="text">
<string>Skip Presenting Duplicate Frames</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_45">
<property name="text">
<string>Video Capture Codec:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0,0,0">
<item>
<widget class="QComboBox" name="videoCaptureCodec"/>
</item>
<item>
<widget class="QComboBox" name="videoCaptureContainer"/>
</item>
<item>
<widget class="QLabel" name="label_46">
<property name="text">
<string>Bitrate:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="videoCaptureBitrate">
<property name="suffix">
<string> kbps</string>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>100000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="debuggingOptions">
<property name="title">
<string>Debugging Options</string>
</property>
<layout class="QFormLayout" name="debuggingOptionsFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
@@ -1686,102 +1810,31 @@
</item>
</widget>
</item>
<item row="5" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_7">
<item row="2" column="0">
<widget class="QCheckBox" name="skipPresentingDuplicateFrames">
<property name="text">
<string>Skip Presenting Duplicate Frames</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="useBlitSwapChain">
<property name="text">
<string>Use Blit Swap Chain</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="disableFramebufferFetch">
<property name="text">
<string>Disable Framebuffer Fetch</string>
</property>
</widget>
</item>
<item row="0" column="1">
<item row="1" column="0">
<widget class="QCheckBox" name="useDebugDevice">
<property name="text">
<string>Use Debug Device</string>
</property>
</widget>
</item>
<item row="1" column="0">
<item row="0" column="0">
<widget class="QCheckBox" name="disableDualSource">
<property name="text">
<string>Disable Dual Source Blending</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="disableFramebufferFetch">
<property name="text">
<string>Disable Framebuffer Fetch</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_34">
<property name="text">
<string>GS Dump Compression:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="gsDumpCompression">
<item>
<property name="text">
<string>Uncompressed</string>
</property>
</item>
<item>
<property name="text">
<string>LZMA (xz)</string>
</property>
</item>
<item>
<property name="text">
<string>Zstandard (zst)</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_42">
<property name="text">
<string>Hardware Download Mode:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="gsDownloadMode">
<item>
<property name="text">
<string>Accurate (Recommended)</string>
</property>
</item>
<item>
<property name="text">
<string>Disable Readbacks (Synchronize GS Thread)</string>
</property>
</item>
<item>
<property name="text">
<string>Unsynchronized (Non-Deterministic)</string>
</property>
</item>
<item>
<property name="text">
<string>Disabled (Ignore Transfers)</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
+1 -4
View File
@@ -757,8 +757,6 @@ set(pcsx2GSHeaders
GS/Renderers/SW/GSTextureCacheSW.h
GS/Renderers/SW/GSTextureSW.h
GS/Renderers/SW/GSVertexSW.h
GS/Window/GSCaptureDlg.h
GS/Window/GSDialog.h
GS/Window/GSSetting.h
)
@@ -884,8 +882,6 @@ if(WIN32)
GS/Renderers/DX11/GSTextureFX11.cpp
GS/Renderers/DX12/GSDevice12.cpp
GS/Renderers/DX12/GSTexture12.cpp
GS/Window/GSCaptureDlg.cpp
GS/Window/GSDialog.cpp
)
list(APPEND pcsx2GSHeaders
GS/Renderers/DX11/D3D.h
@@ -1704,6 +1700,7 @@ target_include_directories(PCSX2_FLAGS INTERFACE
${CMAKE_BINARY_DIR}/common/include/
"${CMAKE_SOURCE_DIR}/3rdparty/jpgd/"
"${CMAKE_SOURCE_DIR}/3rdparty/xbyak/"
"${FFMPEG_INCLUDE_DIRS}"
)
if(COMMAND target_precompile_headers)
+8
View File
@@ -598,12 +598,16 @@ struct Pcsx2Config
{
static const char* AspectRatioNames[];
static const char* FMVAspectRatioSwitchNames[];
static const char* VideoCaptureContainers[];
static const char* GetRendererName(GSRendererType type);
static constexpr float DEFAULT_FRAME_RATE_NTSC = 59.94f;
static constexpr float DEFAULT_FRAME_RATE_PAL = 50.00f;
static constexpr u32 DEFAULT_VIDEO_CAPTURE_BITRATE = 6000;
static const char* DEFAULT_VIDEO_CAPTURE_CONTAINER;
union
{
u64 bitset;
@@ -738,6 +742,10 @@ struct Pcsx2Config
GSScreenshotFormat ScreenshotFormat{GSScreenshotFormat::PNG};
int ScreenshotQuality{50};
std::string VideoCaptureContainer{DEFAULT_VIDEO_CAPTURE_CONTAINER};
std::string VideoCaptureCodec;
int VideoCaptureBitrate{DEFAULT_VIDEO_CAPTURE_BITRATE};
std::string Adapter;
GSOptions();
+32 -46
View File
@@ -24,6 +24,7 @@
#endif
#include "GS.h"
#include "GSCapture.h"
#include "GSGL.h"
#include "GSUtil.h"
#include "GSExtra.h"
@@ -545,6 +546,20 @@ void GSStopGSDump()
g_gs_renderer->StopGSDump();
}
bool GSBeginCapture(std::string filename)
{
if (g_gs_renderer)
return g_gs_renderer->BeginCapture(std::move(filename));
else
return false;
}
void GSEndCapture()
{
if (g_gs_renderer)
g_gs_renderer->EndCapture();
}
void GSPresentCurrentFrame()
{
g_gs_renderer->PresentCurrentFrame();
@@ -613,6 +628,8 @@ void GSconfigure()
}
}
#endif
int GStest()
{
if (!GSUtil::CheckSSE())
@@ -621,52 +638,6 @@ int GStest()
return 0;
}
static void pt(const char* str)
{
struct tm* current;
time_t now;
time(&now);
current = localtime(&now);
printf("%02i:%02i:%02i%s", current->tm_hour, current->tm_min, current->tm_sec, str);
}
bool GSsetupRecording(std::string& filename)
{
if (g_gs_renderer == NULL)
{
printf("GS: no s_gs for recording\n");
return false;
}
#if defined(__unix__) || defined(__APPLE__)
if (!theApp.GetConfigB("capture_enabled"))
{
printf("GS: Recording is disabled\n");
return false;
}
#endif
printf("GS: Recording start command\n");
if (g_gs_renderer->BeginCapture(filename))
{
pt(" - Capture started\n");
return true;
}
else
{
pt(" - Capture cancelled\n");
return false;
}
}
void GSendRecording()
{
printf("GS: Recording end command\n");
g_gs_renderer->EndCapture();
pt(" - Capture ended\n");
}
#endif
void GSsetGameCRC(u32 crc, int options)
{
g_gs_renderer->SetGameCRC(crc, options);
@@ -1588,6 +1559,21 @@ BEGIN_HOTKEY_LIST(g_gs_hotkeys)
});
}
}},
{"ToggleVideoCapture", "Graphics", "Toggle Video Capture", [](s32 pressed) {
if (!pressed)
{
GetMTGS().RunOnGSThread([]() {
if (GSCapture::IsCapturing())
{
g_gs_renderer->EndCapture();
return;
}
std::string filename(fmt::format("{}.{}", GSGetBaseSnapshotFilename(), GSConfig.VideoCaptureContainer));
g_gs_renderer->BeginCapture(std::move(filename));
});
}
}},
{"GSDumpSingleFrame", "Graphics", "Save Single Frame GS Dump", [](s32 pressed) {
if (!pressed)
{
+3 -2
View File
@@ -70,16 +70,17 @@ void GSgifTransfer2(u8* mem, u32 size);
void GSgifTransfer3(u8* mem, u32 size);
void GSvsync(u32 field, bool registers_written);
int GSfreeze(FreezeAction mode, freezeData* data);
std::string GSGetBaseSnapshotFilename();
void GSQueueSnapshot(const std::string& path, u32 gsdump_frames = 0);
void GSStopGSDump();
bool GSBeginCapture(std::string filename);
void GSEndCapture();
void GSPresentCurrentFrame();
void GSThrottlePresentation();
#ifndef PCSX2_CORE
void GSkeyEvent(const HostKeyEvent& e);
void GSconfigure();
int GStest();
bool GSsetupRecording(std::string& filename);
void GSendRecording();
#endif
void GSsetGameCRC(u32 crc, int options);
+446 -511
View File
File diff suppressed because it is too large Load Diff
+10 -34
View File
@@ -14,44 +14,20 @@
*/
#pragma once
#include <string>
#include <utility>
#include <vector>
#include "GSVector.h"
#include "GSPng.h"
#ifdef _WIN32
#include "Window/GSCaptureDlg.h"
#include <wil/com.h>
#endif
class GSCapture
namespace GSCapture
{
std::recursive_mutex m_lock;
bool m_capturing;
GSVector2i m_size;
std::string m_out_dir;
int m_threads;
#ifdef _WIN32
wil::com_ptr_failfast<IGraphBuilder> m_graph;
wil::com_ptr_failfast<IBaseFilter> m_src;
#elif defined(__unix__)
u64 m_frame;
std::vector<std::unique_ptr<GSPng::Worker>> m_workers;
int m_compression_level;
#endif
public:
GSCapture();
virtual ~GSCapture();
bool BeginCapture(float fps, GSVector2i recommendedResolution, float aspect, std::string& filename);
bool BeginCapture(float fps, GSVector2i recommendedResolution, float aspect, std::string filename);
bool DeliverFrame(const void* bits, int pitch, bool rgba);
bool EndCapture();
bool IsCapturing() { return m_capturing; }
GSVector2i GetSize() { return m_size; }
};
bool IsCapturing();
GSVector2i GetSize();
std::vector<std::pair<std::string, std::string>> GetVideoCodecList(const char* container);
}; // namespace GSCapture
+60 -53
View File
@@ -15,6 +15,7 @@
#include "PrecompiledHeader.h"
#include "GSRenderer.h"
#include "GS/GSCapture.h"
#include "GS/GSGL.h"
#include "Host.h"
#include "HostDisplay.h"
@@ -95,6 +96,7 @@ void GSRenderer::Reset(bool hardware_reset)
void GSRenderer::Destroy()
{
GSCapture::EndCapture();
}
bool GSRenderer::Merge(int field)
@@ -824,13 +826,12 @@ void GSRenderer::VSync(u32 field, bool registers_written)
}
}
#ifndef PCSX2_CORE
// capture
if (m_capture.IsCapturing())
if (GSCapture::IsCapturing())
{
if (GSTexture* current = g_gs_device->GetCurrent())
{
GSVector2i size = m_capture.GetSize();
GSVector2i size = GSCapture::GetSize();
bool res;
GSTexture::GSMap m;
@@ -841,12 +842,11 @@ void GSRenderer::VSync(u32 field, bool registers_written)
if (res)
{
m_capture.DeliverFrame(m.bits, m.pitch, !g_gs_device->IsRBSwapped());
GSCapture::DeliverFrame(m.bits, m.pitch, !g_gs_device->IsRBSwapped());
g_gs_device->DownloadTextureComplete();
}
}
}
#endif
}
void GSRenderer::QueueSnapshot(const std::string& path, u32 gsdump_frames)
@@ -861,49 +861,7 @@ void GSRenderer::QueueSnapshot(const std::string& path, u32 gsdump_frames)
}
else
{
m_snapshot = "";
// append the game serial and title
if (std::string name(GetDumpName()); !name.empty())
{
Path::SanitizeFileName(&name);
if (name.length() > 219)
name.resize(219);
m_snapshot += name;
}
if (std::string serial(GetDumpSerial()); !serial.empty())
{
Path::SanitizeFileName(&serial);
m_snapshot += '_';
m_snapshot += serial;
}
time_t cur_time = time(nullptr);
char local_time[16];
if (strftime(local_time, sizeof(local_time), "%Y%m%d%H%M%S", localtime(&cur_time)))
{
static time_t prev_snap;
// The variable 'n' is used for labelling the screenshots when multiple screenshots are taken in
// a single second, we'll start using this variable for naming when a second screenshot request is detected
// at the same time as the first one. Hence, we're initially setting this counter to 2 to imply that
// the captured image is the 2nd image captured at this specific time.
static int n = 2;
m_snapshot += '_';
if (cur_time == prev_snap)
m_snapshot += fmt::format("{0}_({1})", local_time, n++);
else
{
n = 2;
m_snapshot += fmt::format("{}", local_time);
}
prev_snap = cur_time;
}
// prepend snapshots directory
m_snapshot = Path::Combine(EmuFolders::Snapshots, m_snapshot);
m_snapshot = GSGetBaseSnapshotFilename();
}
// this is really gross, but wx we get the snapshot request after shift...
@@ -912,6 +870,53 @@ void GSRenderer::QueueSnapshot(const std::string& path, u32 gsdump_frames)
#endif
}
std::string GSGetBaseSnapshotFilename()
{
std::string filename;
// append the game serial and title
if (std::string name(GetDumpName()); !name.empty())
{
Path::SanitizeFileName(&name);
if (name.length() > 219)
name.resize(219);
filename += name;
}
if (std::string serial(GetDumpSerial()); !serial.empty())
{
Path::SanitizeFileName(&serial);
filename += '_';
filename += serial;
}
time_t cur_time = time(nullptr);
char local_time[16];
if (strftime(local_time, sizeof(local_time), "%Y%m%d%H%M%S", localtime(&cur_time)))
{
static time_t prev_snap;
// The variable 'n' is used for labelling the screenshots when multiple screenshots are taken in
// a single second, we'll start using this variable for naming when a second screenshot request is detected
// at the same time as the first one. Hence, we're initially setting this counter to 2 to imply that
// the captured image is the 2nd image captured at this specific time.
static int n = 2;
filename += '_';
if (cur_time == prev_snap)
filename += fmt::format("{0}_({1})", local_time, n++);
else
{
n = 2;
filename += fmt::format("{}", local_time);
}
prev_snap = cur_time;
}
// prepend snapshots directory
return Path::Combine(EmuFolders::Snapshots, filename);
}
void GSRenderer::StopGSDump()
{
m_snapshot = {};
@@ -962,18 +967,20 @@ void GSTranslateWindowToDisplayCoordinates(float window_x, float window_y, float
*display_y = rel_y / draw_height;
}
#ifndef PCSX2_CORE
bool GSRenderer::BeginCapture(std::string& filename)
bool GSRenderer::BeginCapture(std::string filename)
{
return m_capture.BeginCapture(GetTvRefreshRate(), GetInternalResolution(), GetCurrentAspectRatioFloat(GetVideoMode() == GSVideoMode::SDTV_480P || (GSConfig.PCRTCOverscan && GSConfig.PCRTCOffsets)), filename);
return GSCapture::BeginCapture(GetTvRefreshRate(), GetInternalResolution(),
GetCurrentAspectRatioFloat(GetVideoMode() == GSVideoMode::SDTV_480P || (GSConfig.PCRTCOverscan && GSConfig.PCRTCOffsets)),
std::move(filename));
}
void GSRenderer::EndCapture()
{
m_capture.EndCapture();
GSCapture::EndCapture();
}
#ifndef PCSX2_CORE
void GSRenderer::KeyEvent(const HostKeyEvent& e)
{
#ifdef _WIN32
+3 -4
View File
@@ -16,8 +16,8 @@
#pragma once
#include "GS/GSState.h"
#include "GS/GSCapture.h"
#include <memory>
#include <string>
#ifndef PCSX2_CORE
#include <mutex>
@@ -33,7 +33,6 @@ private:
u64 m_shader_time_start = 0;
#ifndef PCSX2_CORE
GSCapture m_capture;
std::mutex m_snapshot_mutex;
bool m_shift_key = false;
bool m_control_key = false;
@@ -73,9 +72,9 @@ public:
void StopGSDump();
void PresentCurrentFrame();
#ifndef PCSX2_CORE
bool BeginCapture(std::string& filename);
bool BeginCapture(std::string filename);
void EndCapture();
#ifndef PCSX2_CORE
void KeyEvent(const HostKeyEvent& e);
#endif
};
-260
View File
@@ -1,260 +0,0 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2021 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#include "GS.h"
#include "GSCaptureDlg.h"
#include "GS/GSExtra.h"
#include "common/StringUtil.h"
#include <commdlg.h>
// Ideally this belongs in WIL, but CAUUID is used by a *single* COM function in WinAPI.
// That's presumably why it's omitted and is unlikely to make it to upstream WIL.
static void __stdcall CloseCAUUID(_Inout_ CAUUID* cauuid) WI_NOEXCEPT
{
::CoTaskMemFree(cauuid->pElems);
}
using unique_cauuid = wil::unique_struct<CAUUID, decltype(&::CloseCAUUID), ::CloseCAUUID>;
using unique_olestr = wil::unique_any<LPOLESTR, decltype(&::CoTaskMemFree), ::CoTaskMemFree>;
template<typename Func>
static void EnumSysDev(const GUID& clsid, Func&& f)
{
if (auto devEnum = wil::CoCreateInstanceNoThrow<ICreateDevEnum>(CLSID_SystemDeviceEnum))
{
wil::com_ptr_nothrow<IEnumMoniker> classEnum;
if (SUCCEEDED(devEnum->CreateClassEnumerator(clsid, classEnum.put(), 0)))
{
wil::com_ptr_nothrow<IMoniker> moniker;
while (classEnum->Next(1, moniker.put(), nullptr) == S_OK)
{
std::forward<Func>(f)(moniker.get());
}
}
}
}
void GSCaptureDlg::InvalidFile()
{
const std::wstring message = L"GS couldn't open file for capturing: " + m_filename + L".\nCapture aborted.";
MessageBox(GetActiveWindow(), message.c_str(), L"GS System Message", MB_OK | MB_SETFOREGROUND);
}
GSCaptureDlg::GSCaptureDlg()
: GSDialog(IDD_CAPTURE)
{
m_width = theApp.GetConfigI("CaptureWidth");
m_height = theApp.GetConfigI("CaptureHeight");
m_filename = StringUtil::UTF8StringToWideString(theApp.GetConfigS("CaptureFileName"));
}
int GSCaptureDlg::GetSelCodec(Codec& c)
{
INT_PTR data = 0;
if (ComboBoxGetSelData(IDC_CODECS, data))
{
if (data == 0)
return 2;
c = *(Codec*)data;
if (!c.filter)
{
c.moniker->BindToObject(NULL, NULL, IID_PPV_ARGS(c.filter.put()));
if (!c.filter)
return 0;
}
return 1;
}
return 0;
}
void GSCaptureDlg::UpdateConfigureButton()
{
Codec c;
bool enable = false;
if (GetSelCodec(c) != 1)
{
EnableWindow(GetDlgItem(m_hWnd, IDC_CONFIGURE), false);
return;
}
if (auto pSPP = c.filter.try_query<ISpecifyPropertyPages>())
{
unique_cauuid caGUID;
enable = SUCCEEDED(pSPP->GetPages(&caGUID));
}
else if (auto pAMVfWCD = c.filter.try_query<IAMVfwCompressDialogs>())
{
enable = pAMVfWCD->ShowDialog(VfwCompressDialog_QueryConfig, nullptr) == S_OK;
}
EnableWindow(GetDlgItem(m_hWnd, IDC_CONFIGURE), enable);
}
void GSCaptureDlg::OnInit()
{
__super::OnInit();
SetTextAsInt(IDC_WIDTH, m_width);
SetTextAsInt(IDC_HEIGHT, m_height);
SetText(IDC_FILENAME, m_filename.c_str());
m_codecs.clear();
const std::wstring selected = StringUtil::UTF8StringToWideString(theApp.GetConfigS("CaptureVideoCodecDisplayName"));
ComboBoxAppend(IDC_CODECS, "Uncompressed", 0, true);
ComboBoxAppend(IDC_COLORSPACE, "YUY2", 0, true);
ComboBoxAppend(IDC_COLORSPACE, "RGB32", 1, false);
CoInitialize(0); // this is obviously wrong here, each thread should call this on start, and where is CoUninitalize?
EnumSysDev(CLSID_VideoCompressorCategory, [&](IMoniker* moniker)
{
Codec c;
c.moniker = moniker;
unique_olestr str;
if (FAILED(moniker->GetDisplayName(NULL, NULL, str.put())))
return;
std::wstring prefix;
if (wcsstr(str.get(), L"@device:dmo:")) prefix = L"(DMO) ";
else if (wcsstr(str.get(), L"@device:sw:")) prefix = L"(DS) ";
else if (wcsstr(str.get(), L"@device:cm:")) prefix = L"(VfW) ";
c.DisplayName = str.get();
wil::com_ptr_nothrow<IPropertyBag> pPB;
if (FAILED(moniker->BindToStorage(0, 0, IID_PPV_ARGS(pPB.put()))))
return;
wil::unique_variant var;
if (FAILED(pPB->Read(L"FriendlyName", &var, nullptr)))
return;
c.FriendlyName = prefix + var.bstrVal;
m_codecs.push_back(c);
ComboBoxAppend(IDC_CODECS, c.FriendlyName.c_str(), (LPARAM)&m_codecs.back(), c.DisplayName == selected);
});
UpdateConfigureButton();
}
bool GSCaptureDlg::OnCommand(HWND hWnd, UINT id, UINT code)
{
switch (id)
{
case IDC_FILENAME:
{
EnableWindow(GetDlgItem(m_hWnd, IDOK), GetText(IDC_FILENAME).length() != 0);
return false;
}
case IDC_BROWSE:
{
if (code == BN_CLICKED)
{
wchar_t buff[MAX_PATH] = {0};
OPENFILENAME ofn;
memset(&ofn, 0, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = m_hWnd;
ofn.lpstrFile = buff;
ofn.nMaxFile = std::size(buff);
ofn.lpstrFilter = L"Avi files (*.avi)\0*.avi\0";
ofn.Flags = OFN_EXPLORER | OFN_ENABLESIZING | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST;
wcscpy(ofn.lpstrFile, m_filename.c_str());
if (GetSaveFileName(&ofn))
{
m_filename = ofn.lpstrFile;
SetText(IDC_FILENAME, m_filename.c_str());
}
return true;
}
break;
}
case IDC_CONFIGURE:
{
if (code == BN_CLICKED)
{
Codec c;
if (GetSelCodec(c) == 1)
{
if (auto pSPP = c.filter.try_query<ISpecifyPropertyPages>())
{
unique_cauuid caGUID;
if (SUCCEEDED(pSPP->GetPages(&caGUID)))
{
auto lpUnk = pSPP.try_query<IUnknown>();
OleCreatePropertyFrame(m_hWnd, 0, 0, c.FriendlyName.c_str(), 1, lpUnk.addressof(), caGUID.cElems, caGUID.pElems, 0, 0, NULL);
}
}
else if (auto pAMVfWCD = c.filter.try_query<IAMVfwCompressDialogs>())
{
if (pAMVfWCD->ShowDialog(VfwCompressDialog_QueryConfig, NULL) == S_OK)
pAMVfWCD->ShowDialog(VfwCompressDialog_Config, m_hWnd);
}
}
return true;
}
break;
}
case IDC_CODECS:
{
UpdateConfigureButton();
break;
}
case IDOK:
{
m_width = GetTextAsInt(IDC_WIDTH);
m_height = GetTextAsInt(IDC_HEIGHT);
m_filename = GetText(IDC_FILENAME);
ComboBoxGetSelData(IDC_COLORSPACE, m_colorspace);
Codec c;
int ris = GetSelCodec(c);
if (ris == 0)
return false;
m_enc = c.filter;
theApp.SetConfig("CaptureWidth", m_width);
theApp.SetConfig("CaptureHeight", m_height);
theApp.SetConfig("CaptureFileName", StringUtil::WideStringToUTF8String(m_filename).c_str());
if (ris != 2)
theApp.SetConfig("CaptureVideoCodecDisplayName", StringUtil::WideStringToUTF8String(c.DisplayName).c_str());
else
theApp.SetConfig("CaptureVideoCodecDisplayName", "");
break;
}
default:
break;
}
return __super::OnCommand(hWnd, id, code);
}
-51
View File
@@ -1,51 +0,0 @@
/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2021 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "GSDialog.h"
#include "GS/resource.h"
#include <streams.h>
#include <wil/com.h>
class GSCaptureDlg : public GSDialog
{
struct Codec
{
wil::com_ptr_nothrow<IMoniker> moniker;
wil::com_ptr_nothrow<IBaseFilter> filter;
std::wstring FriendlyName;
std::wstring DisplayName;
};
std::list<Codec> m_codecs;
int GetSelCodec(Codec& c);
void UpdateConfigureButton();
protected:
void OnInit();
bool OnCommand(HWND hWnd, UINT id, UINT code);
public:
GSCaptureDlg();
void InvalidFile();
int m_width;
int m_height;
std::wstring m_filename;
INT_PTR m_colorspace;
wil::com_ptr_nothrow<IBaseFilter> m_enc;
};

Some files were not shown because too many files have changed in this diff Show More