GS: Add video dumping via ffmpeg

This commit is contained in:
Connor McLaughlin 2022-12-18 23:05:00 +10:00 committed by refractionpcsx2
parent e5009398e0
commit 699c3d80b7
29 changed files with 1038 additions and 1588 deletions

195
cmake/FindFFMPEG.cmake Normal file
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)

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)

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)

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

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"/>

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

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

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;

View File

@ -44,6 +44,7 @@ private Q_SLOTS:
void onGpuPaletteConversionChanged(int state);
void onFullscreenModeChanged(int index);
void onShadeBoostChanged();
void onVideoCaptureContainerChanged();
private:
GSRendererType getEffectiveRenderer() const;

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>

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)

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

View File

@ -1,350 +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 <Shlwapi.h>
#include <CommCtrl.h>
#include <commdlg.h>
#include "GS.h"
#include "GSDialog.h"
#include "GS/GSVector.h"
GSDialog::GSDialog(UINT id)
: m_id(id)
, m_hWnd(NULL)
{
}
INT_PTR GSDialog::DoModal()
{
return DialogBoxParam(GetModuleHandle(nullptr), MAKEINTRESOURCE(m_id), GetActiveWindow(), DialogProc, (LPARAM)this);
}
INT_PTR CALLBACK GSDialog::DialogProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
GSDialog* dlg = NULL;
if (message == WM_INITDIALOG)
{
dlg = (GSDialog*)lParam;
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)dlg);
dlg->m_hWnd = hWnd;
MONITORINFO mi;
mi.cbSize = sizeof(mi);
GetMonitorInfo(MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), &mi);
GSVector4i r;
GetWindowRect(hWnd, reinterpret_cast<LPRECT>(&r));
int x = (mi.rcWork.left + mi.rcWork.right - r.width()) / 2;
int y = (mi.rcWork.top + mi.rcWork.bottom - r.height()) / 2;
SetWindowPos(hWnd, NULL, x, y, -1, -1, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
dlg->OnInit();
return true;
}
dlg = (GSDialog*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
if (message == WM_NOTIFY)
{
if (((LPNMHDR)lParam)->code == TTN_GETDISPINFO)
{
LPNMTTDISPINFO pInfo = (LPNMTTDISPINFO)lParam;
const UINT id = (UINT)GetWindowLongPtr((HWND)pInfo->hdr.idFrom, GWL_ID);
// lpszText is used only if hinst is NULL. Seems to be NULL already,
// but it can't hurt to explicitly set it.
pInfo->hinst = NULL;
pInfo->lpszText = (LPTSTR)dialog_message(id);
SendMessage(pInfo->hdr.hwndFrom, TTM_SETMAXTIPWIDTH, 0, 500);
return true;
}
}
return dlg != NULL ? dlg->OnMessage(message, wParam, lParam) : FALSE;
}
// Tooltips will only show if the TOOLINFO cbSize <= the struct size. If it's
// smaller some functionality might be disabled. So let's try and use the
// correct size.
UINT GSDialog::GetTooltipStructSize()
{
DLLGETVERSIONPROC dllGetVersion = (DLLGETVERSIONPROC)GetProcAddress(GetModuleHandle(L"ComCtl32.dll"), "DllGetVersion");
if (dllGetVersion)
{
DLLVERSIONINFO2 dllversion = {0};
dllversion.info1.cbSize = sizeof(DLLVERSIONINFO2);
if (dllGetVersion((DLLVERSIONINFO*)&dllversion) == S_OK)
{
// Minor, then major version.
DWORD version = MAKELONG(dllversion.info1.dwMinorVersion, dllversion.info1.dwMajorVersion);
DWORD tooltip_v3 = MAKELONG(0, 6);
if (version >= tooltip_v3)
return TTTOOLINFOA_V3_SIZE;
}
}
// Should be fine for XP and onwards, comctl versions >= 4.7 should at least
// be this size.
return TTTOOLINFOA_V2_SIZE;
}
bool GSDialog::OnMessage(UINT message, WPARAM wParam, LPARAM lParam)
{
return message == WM_COMMAND ? OnCommand((HWND)lParam, LOWORD(wParam), HIWORD(wParam)) : false;
}
bool GSDialog::OnCommand(HWND hWnd, UINT id, UINT code)
{
if (id == IDOK || id == IDCANCEL)
{
EndDialog(m_hWnd, id);
return true;
}
return false;
}
std::wstring GSDialog::GetText(UINT id)
{
std::wstring s;
wchar_t* buff = NULL;
for (int size = 256, limit = 65536; size < limit; size <<= 1)
{
buff = new wchar_t[size];
if (GetDlgItemText(m_hWnd, id, buff, size))
{
s = buff;
size = limit;
}
delete[] buff;
}
return s;
}
int GSDialog::GetTextAsInt(UINT id)
{
return _wtoi(GetText(id).c_str());
}
void GSDialog::SetText(UINT id, const wchar_t* str)
{
SetDlgItemText(m_hWnd, id, str);
}
void GSDialog::SetTextAsInt(UINT id, int i)
{
wchar_t buff[32] = {0};
_itow(i, buff, 10);
SetText(id, buff);
}
void GSDialog::ComboBoxInit(UINT id, const std::vector<GSSetting>& settings, int32_t selectionValue, int32_t maxValue)
{
if (settings.empty())
return;
HWND hWnd = GetDlgItem(m_hWnd, id);
SendMessage(hWnd, CB_RESETCONTENT, 0, 0);
const auto is_present = [=](const GSSetting& x) { return selectionValue == x.value; };
if (std::none_of(settings.begin(), settings.end(), is_present))
selectionValue = settings.front().value;
for (size_t i = 0; i < settings.size(); i++)
{
const GSSetting& s = settings[i];
if (s.value <= maxValue)
{
std::string str(s.name);
if (!s.note.empty())
{
str = str + " (" + s.note + ")";
}
ComboBoxAppend(id, str.c_str(), (LPARAM)s.value, s.value == selectionValue);
}
}
ComboBoxFixDroppedWidth(id);
}
int GSDialog::ComboBoxAppend(UINT id, const char* str, LPARAM data, bool select)
{
HWND hWnd = GetDlgItem(m_hWnd, id);
int item = (int)SendMessageA(hWnd, CB_ADDSTRING, 0, (LPARAM)str);
return BoxAppend(hWnd, item, data, select);
}
int GSDialog::ComboBoxAppend(UINT id, const wchar_t* str, LPARAM data, bool select)
{
HWND hWnd = GetDlgItem(m_hWnd, id);
int item = (int)SendMessageW(hWnd, CB_ADDSTRING, 0, (LPARAM)str);
return BoxAppend(hWnd, item, data, select);
}
int GSDialog::BoxAppend(HWND& hWnd, int item, LPARAM data, bool select)
{
SendMessage(hWnd, CB_SETITEMDATA, item, (LPARAM)data);
if (select)
{
SendMessage(hWnd, CB_SETCURSEL, item, 0);
}
return item;
}
bool GSDialog::ComboBoxGetSelData(UINT id, INT_PTR& data)
{
HWND hWnd = GetDlgItem(m_hWnd, id);
const int item = (int)SendMessage(hWnd, CB_GETCURSEL, 0, 0);
if (item >= 0)
{
data = SendMessage(hWnd, CB_GETITEMDATA, item, 0);
return true;
}
return false;
}
void GSDialog::ComboBoxFixDroppedWidth(UINT id)
{
HWND hWnd = GetDlgItem(m_hWnd, id);
int count = (int)SendMessage(hWnd, CB_GETCOUNT, 0, 0);
if (count > 0)
{
HDC hDC = GetDC(hWnd);
SelectObject(hDC, (HFONT)SendMessage(hWnd, WM_GETFONT, 0, 0));
int width = (int)SendMessage(hWnd, CB_GETDROPPEDWIDTH, 0, 0);
for (int i = 0; i < count; i++)
{
int len = (int)SendMessage(hWnd, CB_GETLBTEXTLEN, i, 0);
if (len > 0)
{
wchar_t* buff = new wchar_t[len + 1];
SendMessage(hWnd, CB_GETLBTEXT, i, (LPARAM)buff);
SIZE size;
if (GetTextExtentPoint32(hDC, buff, wcslen(buff), &size))
{
size.cx += 10;
if (size.cx > width)
width = size.cx;
}
delete[] buff;
}
}
ReleaseDC(hWnd, hDC);
if (width > 0)
{
SendMessage(hWnd, CB_SETDROPPEDWIDTH, width, 0);
}
}
}
void GSDialog::OpenFileDialog(UINT id, const wchar_t* title)
{
wchar_t filename[512];
OPENFILENAME ofn = {0};
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = m_hWnd;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST;
ofn.lpstrFile = filename;
ofn.lpstrFile[0] = 0;
ofn.nMaxFile = 512;
ofn.lpstrTitle = title;
// GetOpenFileName changes the current directory, so we need to save and
// restore the current directory or everything using relative paths will
// break.
wchar_t current_directory[512];
GetCurrentDirectory(512, current_directory);
if (GetOpenFileName(&ofn))
SendMessage(GetDlgItem(m_hWnd, id), WM_SETTEXT, 0, (LPARAM)filename);
SetCurrentDirectory(current_directory);
}
void GSDialog::AddTooltip(UINT id)
{
static UINT tooltipStructSize = GetTooltipStructSize();
bool hasTooltip;
dialog_message(id, &hasTooltip);
if (!hasTooltip)
return;
HWND hWnd = GetDlgItem(m_hWnd, id);
if (hWnd == NULL)
return;
// TTS_NOPREFIX allows tabs and '&' to be used.
HWND hwndTip = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
TTS_ALWAYSTIP | TTS_NOPREFIX,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
m_hWnd, NULL, GetModuleHandle(nullptr), NULL);
if (hwndTip == NULL)
return;
TOOLINFO toolInfo = {0};
toolInfo.cbSize = tooltipStructSize;
toolInfo.hwnd = m_hWnd;
toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
toolInfo.uId = (UINT_PTR)hWnd;
// Can't directly add the tooltip string - it doesn't work for long messages
toolInfo.lpszText = LPSTR_TEXTCALLBACK;
SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);
// 32.767s is the max show time.
SendMessage(hwndTip, TTM_SETDELAYTIME, TTDT_AUTOPOP, 32767);
}
void GSDialog::InitCommonControls()
{
INITCOMMONCONTROLSEX icex;
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_TAB_CLASSES;
InitCommonControlsEx(&icex);
}

View File

@ -1,63 +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 "GSSetting.h"
#include "common/RedtapeWindows.h"
class GSDialog
{
int m_id;
static INT_PTR CALLBACK DialogProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
static UINT GetTooltipStructSize();
protected:
HWND m_hWnd;
virtual void OnInit() {}
virtual bool OnMessage(UINT message, WPARAM wParam, LPARAM lParam);
virtual bool OnCommand(HWND hWnd, UINT id, UINT code);
public:
GSDialog(UINT id);
virtual ~GSDialog() {}
int GetId() const { return m_id; }
INT_PTR DoModal();
std::wstring GetText(UINT id);
int GetTextAsInt(UINT id);
void SetText(UINT id, const wchar_t* str);
void SetTextAsInt(UINT id, int i);
void ComboBoxInit(UINT id, const std::vector<GSSetting>& settings, int32_t selectionValue, int32_t maxValue = INT32_MAX);
int ComboBoxAppend(UINT id, const char* str, LPARAM data = 0, bool select = false);
int ComboBoxAppend(UINT id, const wchar_t* str, LPARAM data = 0, bool select = false);
bool ComboBoxGetSelData(UINT id, INT_PTR& data);
void ComboBoxFixDroppedWidth(UINT id);
void OpenFileDialog(UINT id, const wchar_t* title);
void AddTooltip(UINT id);
static void InitCommonControls();
private:
int BoxAppend(HWND& hWnd, int item, LPARAM data = 0, bool select = false);
};

View File

@ -337,6 +337,13 @@ const char* Pcsx2Config::GSOptions::FMVAspectRatioSwitchNames[] = {
"16:9",
nullptr};
const char* Pcsx2Config::GSOptions::VideoCaptureContainers[] = {
"mp4",
"mkv",
"avi",
nullptr};
const char* Pcsx2Config::GSOptions::DEFAULT_VIDEO_CAPTURE_CONTAINER = "mp4";
const char* Pcsx2Config::GSOptions::GetRendererName(GSRendererType type)
{
switch (type)
@ -489,6 +496,10 @@ bool Pcsx2Config::GSOptions::OptionsAreEqual(const GSOptions& right) const
OpEqu(ScreenshotFormat) &&
OpEqu(ScreenshotQuality) &&
OpEqu(VideoCaptureContainer) &&
OpEqu(VideoCaptureCodec) &&
OpEqu(VideoCaptureBitrate) &&
OpEqu(Adapter));
}
@ -685,6 +696,10 @@ void Pcsx2Config::GSOptions::ReloadIniSettings()
GSSettingIntEx(SaveN, "saven");
GSSettingIntEx(SaveL, "savel");
GSSettingStringEx(VideoCaptureContainer, "VideoCaptureContainer");
GSSettingStringEx(VideoCaptureCodec, "VideoCaptureCodec");
GSSettingIntEx(VideoCaptureBitrate, "VideoCaptureBitrate");
GSSettingString(Adapter);
#undef GSSettingInt

View File

@ -278,50 +278,6 @@ namespace Implementations
#endif
}
void Sys_RecordingToggle()
{
ScopedCoreThreadPause paused_core;
paused_core.AllowResume();
if (wxGetApp().HasGUI())
{
sMainFrame.VideoCaptureToggle();
return;
}
GetMTGS().WaitGS(); // make sure GS is in sync with the audio stream when we start.
g_Pcsx2Recording = !g_Pcsx2Recording;
if (g_Pcsx2Recording)
{
// start recording
// make the recording setup dialog[s] pseudo-modal also for the main PCSX2 window
// (the GS dialog is already properly modal for the GS window)
if (GetMainFramePtr() && GetMainFramePtr()->IsEnabled())
GetMainFramePtr()->Disable();
// GSsetupRecording can be aborted/canceled by the user. Don't go on to record the audio if that happens.
std::string filename;
if (GSsetupRecording(filename))
{
if (g_Conf->AudioCapture.EnableAudio && !SPU2setupRecording(&filename))
{
GSendRecording();
g_Pcsx2Recording = false;
}
}
else // recording dialog canceled by the user. align our state
g_Pcsx2Recording = false;
}
else
{
// stop recording
GSendRecording();
if (g_Conf->AudioCapture.EnableAudio)
SPU2endRecording();
}
}
void Cpu_DumpRegisters()
{
#ifdef PCSX2_DEVBUILD
@ -567,14 +523,6 @@ static const GlobalCommandDescriptor CommandDeclarations[] =
false,
},
{
"Sys_RecordingToggle",
Implementations::Sys_RecordingToggle,
NULL,
NULL,
false,
},
{
"FullscreenToggle",
Implementations::FullscreenToggle,

View File

@ -857,53 +857,6 @@ void MainEmuFrame::Menu_Capture_Video_IncludeAudio_Click(wxCommandEvent& event)
void MainEmuFrame::VideoCaptureToggle()
{
GetMTGS().WaitGS(); // make sure GS is in sync with the audio stream when we start.
m_capturingVideo = !m_capturingVideo;
if (m_capturingVideo)
{
// start recording
// make the recording setup dialog[s] pseudo-modal also for the main PCSX2 window
// (the GS dialog is already properly modal for the GS window)
bool needsMainFrameEnable = false;
if (IsEnabled())
{
needsMainFrameEnable = true;
Disable();
}
// GSsetupRecording can be aborted/canceled by the user. Don't go on to record the audio if that happens
std::string filename;
if (GSsetupRecording(filename))
{
if (!g_Conf->AudioCapture.EnableAudio || SPU2setupRecording(&filename))
{
m_submenuVideoCapture.Enable(MenuId_Capture_Video_Record, false);
m_submenuVideoCapture.Enable(MenuId_Capture_Video_Stop, true);
m_submenuVideoCapture.Enable(MenuId_Capture_Video_IncludeAudio, false);
}
else
{
GSendRecording();
m_capturingVideo = false;
}
}
else // recording dialog canceled by the user. align our state
m_capturingVideo = false;
if (needsMainFrameEnable)
Enable();
}
else
{
// stop recording
GSendRecording();
if (g_Conf->AudioCapture.EnableAudio)
SPU2endRecording();
m_submenuVideoCapture.Enable(MenuId_Capture_Video_Record, true);
m_submenuVideoCapture.Enable(MenuId_Capture_Video_Stop, false);
m_submenuVideoCapture.Enable(MenuId_Capture_Video_IncludeAudio, true);
}
}
void MainEmuFrame::Menu_Capture_Screenshot_Screenshot_Click(wxCommandEvent& event)

View File

@ -47,6 +47,7 @@
<AdditionalIncludeDirectories>$(SolutionDir)3rdparty\d3d12memalloc\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\zstd\zstd\lib</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rapidyaml\rapidyaml\ext\c4core\src\c4\ext\fast_float\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\ffmpeg\include</AdditionalIncludeDirectories>
<ExceptionHandling>Async</ExceptionHandling>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>PrecompiledHeader.h</PrecompiledHeaderFile>
@ -384,7 +385,6 @@
<ClCompile Include="GS\GSAlignedClass.cpp" />
<ClCompile Include="GS\GSBlock.cpp" />
<ClCompile Include="GS\GSCapture.cpp" />
<ClCompile Include="GS\Window\GSCaptureDlg.cpp" />
<ClCompile Include="GS\GSClut.cpp" />
<ClCompile Include="GS\GSCodeBuffer.cpp" />
<ClCompile Include="GS\GSCrc.cpp" />
@ -392,7 +392,6 @@
<ClCompile Include="GS\Renderers\DX11\GSDevice11.cpp" />
<ClCompile Include="GS\Renderers\Null\GSDeviceNull.cpp" />
<ClCompile Include="GS\Renderers\OpenGL\GSDeviceOGL.cpp" />
<ClCompile Include="GS\Window\GSDialog.cpp" />
<ClCompile Include="GS\Renderers\Common\GSDirtyRect.cpp" />
<ClCompile Include="GS\GSDrawingContext.cpp" />
<ClCompile Include="GS\Renderers\SW\GSDrawScanline.cpp" />
@ -780,7 +779,6 @@
<ClInclude Include="GS\GSAlignedClass.h" />
<ClInclude Include="GS\GSBlock.h" />
<ClInclude Include="GS\GSCapture.h" />
<ClInclude Include="GS\Window\GSCaptureDlg.h" />
<ClInclude Include="GS\GSClut.h" />
<ClInclude Include="GS\GSCodeBuffer.h" />
<ClInclude Include="GS\GSCrc.h" />
@ -788,7 +786,6 @@
<ClInclude Include="GS\Renderers\DX11\GSDevice11.h" />
<ClInclude Include="GS\Renderers\Null\GSDeviceNull.h" />
<ClInclude Include="GS\Renderers\OpenGL\GSDeviceOGL.h" />
<ClInclude Include="GS\Window\GSDialog.h" />
<ClInclude Include="GS\Renderers\Common\GSDirtyRect.h" />
<ClInclude Include="GS\GSDrawingContext.h" />
<ClInclude Include="GS\GSDrawingEnvironment.h" />
@ -1068,4 +1065,4 @@
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>
</Project>

View File

@ -1472,12 +1472,6 @@
<ClCompile Include="GS\GSCapture.cpp">
<Filter>System\Ps2\GS\Window</Filter>
</ClCompile>
<ClCompile Include="GS\Window\GSCaptureDlg.cpp">
<Filter>System\Ps2\GS\Window</Filter>
</ClCompile>
<ClCompile Include="GS\Window\GSDialog.cpp">
<Filter>System\Ps2\GS\Window</Filter>
</ClCompile>
<ClCompile Include="GS\Window\GSSetting.cpp">
<Filter>System\Ps2\GS\Window</Filter>
</ClCompile>
@ -2530,12 +2524,6 @@
<ClInclude Include="GS\GSCapture.h">
<Filter>System\Ps2\GS\Window</Filter>
</ClInclude>
<ClInclude Include="GS\Window\GSCaptureDlg.h">
<Filter>System\Ps2\GS\Window</Filter>
</ClInclude>
<ClInclude Include="GS\Window\GSDialog.h">
<Filter>System\Ps2\GS\Window</Filter>
</ClInclude>
<ClInclude Include="GS\Window\GSSetting.h">
<Filter>System\Ps2\GS\Window</Filter>
</ClInclude>
@ -2762,4 +2750,4 @@
<Filter>AppHost\Resources</Filter>
</Manifest>
</ItemGroup>
</Project>
</Project>

View File

@ -53,6 +53,7 @@
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rcheevos\rcheevos\include;$(SolutionDir)3rdparty\rainterface</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\discord-rpc\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\zydis\include;$(SolutionDir)3rdparty\zydis\dependencies\zycore\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\ffmpeg\include</AdditionalIncludeDirectories>
<ExceptionHandling>Async</ExceptionHandling>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>PrecompiledHeader.h</PrecompiledHeaderFile>
@ -282,7 +283,6 @@
<ClCompile Include="GS\GSAlignedClass.cpp" />
<ClCompile Include="GS\GSBlock.cpp" />
<ClCompile Include="GS\GSCapture.cpp" />
<ClCompile Include="GS\Window\GSCaptureDlg.cpp" />
<ClCompile Include="GS\GSClut.cpp" />
<ClCompile Include="GS\GSCodeBuffer.cpp" />
<ClCompile Include="GS\GSCrc.cpp" />
@ -626,7 +626,6 @@
<ClInclude Include="GS\GSAlignedClass.h" />
<ClInclude Include="GS\GSBlock.h" />
<ClInclude Include="GS\GSCapture.h" />
<ClInclude Include="GS\Window\GSCaptureDlg.h" />
<ClInclude Include="GS\GSClut.h" />
<ClInclude Include="GS\GSCodeBuffer.h" />
<ClInclude Include="GS\GSCrc.h" />

View File

@ -1175,9 +1175,6 @@
<ClCompile Include="GS\GSCapture.cpp">
<Filter>System\Ps2\GS\Window</Filter>
</ClCompile>
<ClCompile Include="GS\Window\GSCaptureDlg.cpp">
<Filter>System\Ps2\GS\Window</Filter>
</ClCompile>
<ClCompile Include="GS\Window\GSSetting.cpp">
<Filter>System\Ps2\GS\Window</Filter>
</ClCompile>
@ -2126,9 +2123,6 @@
<ClInclude Include="GS\GSCapture.h">
<Filter>System\Ps2\GS\Window</Filter>
</ClInclude>
<ClInclude Include="GS\Window\GSCaptureDlg.h">
<Filter>System\Ps2\GS\Window</Filter>
</ClInclude>
<ClInclude Include="GS\Window\GSSetting.h">
<Filter>System\Ps2\GS\Window</Filter>
</ClInclude>