diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake new file mode 100644 index 0000000000..f727ef2ddd --- /dev/null +++ b/cmake/FindFFMPEG.cmake @@ -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__FOUND`: Libraries for the component. + * `FFMPEG__INCLUDE_DIRS`: Include directories for + the component. + * `FFMPEG__LIBRARIES`: Libraries for the component. + * `FFMPEG::`: 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.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) diff --git a/cmake/SearchForStuff.cmake b/cmake/SearchForStuff.cmake index 4a62944e47..5d472b8dfd 100644 --- a/cmake/SearchForStuff.cmake +++ b/cmake/SearchForStuff.cmake @@ -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) diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 9ed1d3b444..7f34f28c77 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -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) diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 8d75adccc2..a36995c73a 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -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(); diff --git a/pcsx2-qt/MainWindow.ui b/pcsx2-qt/MainWindow.ui index 96d35f0dc0..022271cda1 100644 --- a/pcsx2-qt/MainWindow.ui +++ b/pcsx2-qt/MainWindow.ui @@ -188,6 +188,7 @@ + @@ -882,6 +883,14 @@ Recording Viewer + + + true + + + Video Capture + + diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index 10b3e84bc7..cb5c3ae713 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -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"); diff --git a/pcsx2-qt/QtHost.h b/pcsx2-qt/QtHost.h index 65b2885af6..c239e0882a 100644 --- a/pcsx2-qt/QtHost.h +++ b/pcsx2-qt/QtHost.h @@ -112,6 +112,8 @@ public Q_SLOTS: void enumerateVibrationMotors(); void runOnCPUThread(const std::function& func); void queueSnapshot(quint32 gsdump_frames); + void beginCapture(const QString& path); + void endCapture(); Q_SIGNALS: bool messageConfirmed(const QString& title, const QString& message); diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp index 31ac58ecb4..3be549ee44 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp @@ -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> 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; diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.h b/pcsx2-qt/Settings/GraphicsSettingsWidget.h index 409c5163b7..ec11784c88 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.h +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.h @@ -44,6 +44,7 @@ private Q_SLOTS: void onGpuPaletteConversionChanged(int state); void onFullscreenModeChanged(int index); void onShadeBoostChanged(); + void onVideoCaptureContainerChanged(); private: GSRendererType getEffectiveRenderer() const; diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui index 1bc2eb4448..40b1aefab6 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui @@ -1629,11 +1629,135 @@ - + - Debug Options + Advanced Options - + + + + + Hardware Download Mode: + + + + + + + + Accurate (Recommended) + + + + + Disable Readbacks (Synchronize GS Thread) + + + + + Unsynchronized (Non-Deterministic) + + + + + Disabled (Ignore Transfers) + + + + + + + + GS Dump Compression: + + + + + + + + Uncompressed + + + + + LZMA (xz) + + + + + Zstandard (zst) + + + + + + + + + + Use Blit Swap Chain + + + + + + + Skip Presenting Duplicate Frames + + + + + + + + + Video Capture Codec: + + + + + + + + + + + + + + + Bitrate: + + + + + + + kbps + + + 100 + + + 100000 + + + 100 + + + + + + + + + + + + Debugging Options + + @@ -1686,102 +1810,31 @@ - + - - - - Skip Presenting Duplicate Frames - - - - - - - Use Blit Swap Chain - - - - - - - Disable Framebuffer Fetch - - - - + Use Debug Device - + Disable Dual Source Blending + + + + Disable Framebuffer Fetch + + + - - - - GS Dump Compression: - - - - - - - - Uncompressed - - - - - LZMA (xz) - - - - - Zstandard (zst) - - - - - - - - Hardware Download Mode: - - - - - - - - Accurate (Recommended) - - - - - Disable Readbacks (Synchronize GS Thread) - - - - - Unsynchronized (Non-Deterministic) - - - - - Disabled (Ignore Transfers) - - - - diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index e1bafa9110..de1bdde480 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -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) diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 1cf171145b..4a0739dd58 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -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(); diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp index 374dc2945d..4d5ee3e2ec 100644 --- a/pcsx2/GS/GS.cpp +++ b/pcsx2/GS/GS.cpp @@ -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) { diff --git a/pcsx2/GS/GS.h b/pcsx2/GS/GS.h index f6ad418af7..3f499637d4 100644 --- a/pcsx2/GS/GS.h +++ b/pcsx2/GS/GS.h @@ -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); diff --git a/pcsx2/GS/GSCapture.cpp b/pcsx2/GS/GSCapture.cpp index 320d53fae4..25dbd2088b 100644 --- a/pcsx2/GS/GSCapture.cpp +++ b/pcsx2/GS/GSCapture.cpp @@ -1,5 +1,5 @@ /* PCSX2 - PS2 Emulator for PCs - * Copyright (C) 2002-2021 PCSX2 Dev Team + * Copyright (C) 2002-2022 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- @@ -18,604 +18,539 @@ #include "GSPng.h" #include "GSUtil.h" #include "GSExtra.h" +#include "Host.h" +#include "IconsFontAwesome5.h" +#include "common/Assertions.h" +#include "common/Align.h" +#include "common/DynamicLibrary.h" +#include "common/Path.h" #include "common/StringUtil.h" -#ifdef _WIN32 - -static void __stdcall ClosePinInfo(_Inout_ PIN_INFO* info) WI_NOEXCEPT -{ - if (info->pFilter) - { - info->pFilter->Release(); - } +extern "C" { +#include "libavcodec/avcodec.h" +#include "libavcodec/version.h" +#include "libavformat/avformat.h" +#include "libavformat/version.h" +#include "libavutil/version.h" +#include "libswscale/swscale.h" +#include "libswscale/version.h" } -static void __stdcall CloseFilterInfo(_Inout_ FILTER_INFO* info) WI_NOEXCEPT +#include + +// Compatibility with both ffmpeg 4.x and 5.x. +#if (LIBAVFORMAT_VERSION_MAJOR < 59) +#define ff_const59 +#else +#define ff_const59 const +#endif + +#define VISIT_AVCODEC_IMPORTS(X) \ + X(avcodec_find_encoder_by_name) \ + X(avcodec_find_encoder) \ + X(avcodec_alloc_context3) \ + X(avcodec_open2) \ + X(avcodec_free_context) \ + X(avcodec_send_frame) \ + X(avcodec_receive_packet) \ + X(avcodec_parameters_from_context) \ + X(av_codec_iterate) \ + X(av_packet_alloc) \ + X(av_packet_free) \ + X(av_packet_rescale_ts) + +#define VISIT_AVFORMAT_IMPORTS(X) \ + X(avformat_alloc_output_context2) \ + X(avformat_new_stream) \ + X(avformat_write_header) \ + X(av_guess_format) \ + X(av_interleaved_write_frame) \ + X(av_write_trailer) \ + X(avformat_free_context) \ + X(avformat_query_codec) \ + X(avio_open) \ + X(avio_closep) + +#define VISIT_AVUTIL_IMPORTS(X) \ + X(av_frame_alloc) \ + X(av_frame_get_buffer) \ + X(av_frame_free) \ + X(av_strerror) \ + X(av_reduce) + +#define VISIT_SWSCALE_IMPORTS(X) \ + X(sws_getCachedContext) \ + X(sws_scale) \ + X(sws_freeContext) + +namespace GSCapture { - if (info->pGraph) - { - info->pGraph->Release(); - } -} + static void LogAVError(int errnum, const char* format, ...); + static bool LoadFFmpeg(bool report_errors); + static void UnloadFFmpeg(std::unique_lock& lock); + static void UnloadFFmpeg(); + static void ReceivePackets(); +} // namespace GSCapture -using unique_pin_info = wil::unique_struct; -using unique_filter_info = wil::unique_struct; +static std::recursive_mutex s_lock; +static GSVector2i s_size{}; +static std::string s_filename; +static bool s_capturing = false; -template -static void EnumFilters(IGraphBuilder* filterGraph, Func&& f) +static AVFormatContext* s_format_context = nullptr; +static AVCodecContext* s_codec_context = nullptr; +static AVStream* s_video_stream = nullptr; +static AVFrame* s_converted_frame = nullptr; // YUV +static AVPacket* s_video_packet = nullptr; +static s64 s_next_pts = 0; +static SwsContext* s_sws_context = nullptr; + +#define DECLARE_IMPORT(X) static decltype(X)* wrap_##X; +VISIT_AVCODEC_IMPORTS(DECLARE_IMPORT); +VISIT_AVFORMAT_IMPORTS(DECLARE_IMPORT); +VISIT_AVUTIL_IMPORTS(DECLARE_IMPORT); +VISIT_SWSCALE_IMPORTS(DECLARE_IMPORT); +#undef DECLARE_IMPORT + +// We could refcount this, but really, may as well just load it and pay the cost once. +// Not like we need to save a few megabytes of memory... +static Common::DynamicLibrary s_avcodec_library; +static Common::DynamicLibrary s_avformat_library; +static Common::DynamicLibrary s_avutil_library; +static Common::DynamicLibrary s_swscale_library; +static bool s_library_loaded = false; +static std::mutex s_load_mutex; + +bool GSCapture::LoadFFmpeg(bool report_errors) { - wil::com_ptr_nothrow enumFilters; - if (SUCCEEDED(filterGraph->EnumFilters(enumFilters.put()))) - { - wil::com_ptr_nothrow baseFilter; - while (enumFilters->Next(1, baseFilter.put(), nullptr) == S_OK) - { - std::forward(f)(baseFilter.get()); - } - } -} + std::unique_lock lock(s_load_mutex); + if (s_library_loaded) + return true; -template -static void EnumPins(IBaseFilter* baseFilter, Func&& f) -{ - wil::com_ptr_nothrow enumPins; - if (SUCCEEDED(baseFilter->EnumPins(enumPins.put()))) - { - wil::com_ptr_nothrow pin; - while (enumPins->Next(1, pin.put(), nullptr) == S_OK) - { - if (!std::forward(f)(pin.get())) - { - break; - } - } - } -} - -// -// GSSource -// -interface __declspec(uuid("59C193BB-C520-41F3-BC1D-E245B80A86FA")) -IGSSource : public IUnknown -{ - STDMETHOD(DeliverNewSegment)() PURE; - STDMETHOD(DeliverFrame)(const void* bits, int pitch, bool rgba) PURE; - STDMETHOD(DeliverEOS)() PURE; -}; - -class __declspec(uuid("F8BB6F4F-0965-4ED4-BA74-C6A01E6E6C77")) -GSSource : public CBaseFilter, private CCritSec, public IGSSource -{ - GSVector2i m_size; - REFERENCE_TIME m_atpf; - REFERENCE_TIME m_now; - - STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void** ppv) - { - return riid == __uuidof(IGSSource) - ? GetInterface((IGSSource*)this, ppv) - : __super::NonDelegatingQueryInterface(riid, ppv); - } - - class GSSourceOutputPin : public CBaseOutputPin - { - GSVector2i m_size; - std::vector m_mts; - - public: - GSSourceOutputPin(const GSVector2i& size, REFERENCE_TIME atpf, CBaseFilter* pFilter, CCritSec* pLock, HRESULT& hr, int colorspace) - : CBaseOutputPin("GSSourceOutputPin", pFilter, pLock, &hr, L"Output") - , m_size(size) - { - CMediaType mt; - mt.majortype = MEDIATYPE_Video; - mt.formattype = FORMAT_VideoInfo; - - VIDEOINFOHEADER vih; - memset(&vih, 0, sizeof(vih)); - vih.AvgTimePerFrame = atpf; - vih.bmiHeader.biSize = sizeof(vih.bmiHeader); - vih.bmiHeader.biWidth = m_size.x; - vih.bmiHeader.biHeight = m_size.y; - - // YUY2 - - mt.subtype = MEDIASUBTYPE_YUY2; - mt.lSampleSize = m_size.x * m_size.y * 2; - - vih.bmiHeader.biCompression = '2YUY'; - vih.bmiHeader.biPlanes = 1; - vih.bmiHeader.biBitCount = 16; - vih.bmiHeader.biSizeImage = m_size.x * m_size.y * 2; - mt.SetFormat((u8*)&vih, sizeof(vih)); - - m_mts.push_back(mt); - - // RGB32 - - mt.subtype = MEDIASUBTYPE_RGB32; - mt.lSampleSize = m_size.x * m_size.y * 4; - - vih.bmiHeader.biCompression = BI_RGB; - vih.bmiHeader.biPlanes = 1; - vih.bmiHeader.biBitCount = 32; - vih.bmiHeader.biSizeImage = m_size.x * m_size.y * 4; - mt.SetFormat((u8*)&vih, sizeof(vih)); - - if (colorspace == 1) - m_mts.insert(m_mts.begin(), mt); - else - m_mts.push_back(mt); - } - - HRESULT GSSourceOutputPin::DecideBufferSize(IMemAllocator* pAlloc, ALLOCATOR_PROPERTIES* pProperties) - { - ASSERT(pAlloc && pProperties); - - HRESULT hr; - - pProperties->cBuffers = 1; - pProperties->cbBuffer = m_mt.lSampleSize; - - ALLOCATOR_PROPERTIES Actual; - - if (FAILED(hr = pAlloc->SetProperties(pProperties, &Actual))) - { - return hr; - } - - if (Actual.cbBuffer < pProperties->cbBuffer) - { - return E_FAIL; - } - - ASSERT(Actual.cBuffers == pProperties->cBuffers); - - return S_OK; - } - - HRESULT CheckMediaType(const CMediaType* pmt) - { - for (const auto& mt : m_mts) - { - if (mt.majortype == pmt->majortype && mt.subtype == pmt->subtype) - { - return S_OK; - } - } - - return E_FAIL; - } - - HRESULT GetMediaType(int i, CMediaType* pmt) - { - CheckPointer(pmt, E_POINTER); - - if (i < 0) - return E_INVALIDARG; - if (i > 1) - return VFW_S_NO_MORE_ITEMS; - - *pmt = m_mts[i]; - - return S_OK; - } - - STDMETHODIMP Notify(IBaseFilter* pSender, Quality q) - { - return E_NOTIMPL; - } - - const CMediaType& CurrentMediaType() - { - return m_mt; - } + const auto open_dynlib = [](Common::DynamicLibrary& lib, const char* name, int major_version) { + std::string full_name(Common::DynamicLibrary::GetVersionedFilename(name, major_version)); + return lib.Open(full_name.c_str()); }; - GSSourceOutputPin* m_output; + bool result = true; -public: - GSSource(int w, int h, float fps, IUnknown* pUnk, HRESULT& hr, int colorspace) - : CBaseFilter("GSSource", pUnk, this, __uuidof(this), &hr) - , m_output(NULL) - , m_size(w, h) - , m_atpf((REFERENCE_TIME)(10000000.0f / fps)) - , m_now(0) + result = result && open_dynlib(s_avutil_library, "avutil", LIBAVUTIL_VERSION_MAJOR); + result = result && open_dynlib(s_avcodec_library, "avcodec", LIBAVCODEC_VERSION_MAJOR); + result = result && open_dynlib(s_avformat_library, "avformat", LIBAVFORMAT_VERSION_MAJOR); + result = result && open_dynlib(s_swscale_library, "swscale", LIBSWSCALE_VERSION_MAJOR); + +#define RESOLVE_IMPORT(X) result = result && s_avcodec_library.GetSymbol(#X, &wrap_##X); + VISIT_AVCODEC_IMPORTS(RESOLVE_IMPORT); +#undef RESOLVE_IMPORT + +#define RESOLVE_IMPORT(X) result = result && s_avformat_library.GetSymbol(#X, &wrap_##X); + VISIT_AVFORMAT_IMPORTS(RESOLVE_IMPORT); +#undef RESOLVE_IMPORT + +#define RESOLVE_IMPORT(X) result = result && s_avutil_library.GetSymbol(#X, &wrap_##X); + VISIT_AVUTIL_IMPORTS(RESOLVE_IMPORT); +#undef RESOLVE_IMPORT + +#define RESOLVE_IMPORT(X) result = result && s_swscale_library.GetSymbol(#X, &wrap_##X); + VISIT_SWSCALE_IMPORTS(RESOLVE_IMPORT); +#undef RESOLVE_IMPORT + + if (result) { - m_output = new GSSourceOutputPin(m_size, m_atpf, this, this, hr, colorspace); + s_library_loaded = true; + return true; } - virtual ~GSSource() + UnloadFFmpeg(lock); + lock.unlock(); + + if (report_errors) { - delete m_output; + Host::ReportErrorAsync("Failed to load FFmpeg", + fmt::format( + "You may be missing one or more files, or are using the incorrect version. This build of PCSX2 requires:\n" + " libavcodec: {}\n" + " libavformat: {}\n" + " libavutil: {}\n" + " libswscale: {}", + LIBAVCODEC_VERSION_MAJOR, LIBAVFORMAT_VERSION_MAJOR, LIBAVUTIL_VERSION_MAJOR, LIBSWSCALE_VERSION_MAJOR)); } - DECLARE_IUNKNOWN; - - int GetPinCount() - { - return 1; - } - - CBasePin* GetPin(int n) - { - return n == 0 ? m_output : NULL; - } - - // IGSSource - - STDMETHODIMP DeliverNewSegment() - { - m_now = 0; - - return m_output->DeliverNewSegment(0, _I64_MAX, 1.0); - } - - STDMETHODIMP DeliverFrame(const void* bits, int pitch, bool rgba) - { - if (!m_output || !m_output->IsConnected()) - { - return E_UNEXPECTED; - } - - wil::com_ptr_nothrow sample; - - if (FAILED(m_output->GetDeliveryBuffer(sample.put(), NULL, NULL, 0))) - { - return E_FAIL; - } - - REFERENCE_TIME start = m_now; - REFERENCE_TIME stop = m_now + m_atpf; - - sample->SetTime(&start, &stop); - sample->SetSyncPoint(TRUE); - - const CMediaType& mt = m_output->CurrentMediaType(); - - u8* src = (u8*)bits; - u8* dst = NULL; - - sample->GetPointer(&dst); - - int w = m_size.x; - int h = m_size.y; - int srcpitch = pitch; - - if (mt.subtype == MEDIASUBTYPE_YUY2) - { - int dstpitch = ((VIDEOINFOHEADER*)mt.Format())->bmiHeader.biWidth * 2; - - GSVector4 ys(0.257f, 0.504f, 0.098f, 0.0f); - GSVector4 us(-0.148f / 2, -0.291f / 2, 0.439f / 2, 0.0f); - GSVector4 vs(0.439f / 2, -0.368f / 2, -0.071f / 2, 0.0f); - - if (!rgba) - { - ys = ys.zyxw(); - us = us.zyxw(); - vs = vs.zyxw(); - } - - const GSVector4 offset(16, 128, 16, 128); - - for (int j = 0; j < h; j++, dst += dstpitch, src += srcpitch) - { - u32* s = (u32*)src; - u16* d = (u16*)dst; - - for (int i = 0; i < w; i += 2) - { - GSVector4 c0 = GSVector4::rgba32(s[i + 0]); - GSVector4 c1 = GSVector4::rgba32(s[i + 1]); - GSVector4 c2 = c0 + c1; - - GSVector4 lo = (c0 * ys).hadd(c2 * us); - GSVector4 hi = (c1 * ys).hadd(c2 * vs); - - GSVector4 c = lo.hadd(hi) + offset; - - *((u32*)&d[i]) = GSVector4i(c).rgba32(); - } - } - } - else if (mt.subtype == MEDIASUBTYPE_RGB32) - { - int dstpitch = ((VIDEOINFOHEADER*)mt.Format())->bmiHeader.biWidth * 4; - - dst += dstpitch * (h - 1); - dstpitch = -dstpitch; - - for (int j = 0; j < h; j++, dst += dstpitch, src += srcpitch) - { - if (rgba) - { - GSVector4i* s = (GSVector4i*)src; - GSVector4i* d = (GSVector4i*)dst; - - GSVector4i mask(2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15); - - for (int i = 0, w4 = w >> 2; i < w4; i++) - { - d[i] = s[i].shuffle8(mask); - } - } - else - { - memcpy(dst, src, w * 4); - } - } - } - else - { - return E_FAIL; - } - - if (FAILED(m_output->Deliver(sample.get()))) - { - return E_FAIL; - } - - m_now = stop; - - return S_OK; - } - - STDMETHODIMP DeliverEOS() - { - return m_output->DeliverEndOfStream(); - } -}; - -static wil::com_ptr_nothrow GetFirstPin(IBaseFilter* pBF, PIN_DIRECTION dir) -{ - wil::com_ptr_nothrow result; - if (pBF) - { - EnumPins(pBF, [&](IPin* pin) - { - PIN_DIRECTION dir2; - pin->QueryDirection(&dir2); - if (dir == dir2) - { - result = pin; - return false; - } - return true; - }); - } - - return result; + return false; } -#endif - -// -// GSCapture -// - -GSCapture::GSCapture() - : m_capturing(false) - , m_out_dir("/tmp/GS_Capture") // FIXME Later add an option -#if defined(__unix__) - , m_frame(0) -#endif +void GSCapture::UnloadFFmpeg(std::unique_lock& lock) { +#define CLEAR_IMPORT(X) wrap_##X = nullptr; + VISIT_AVCODEC_IMPORTS(CLEAR_IMPORT); + VISIT_AVFORMAT_IMPORTS(CLEAR_IMPORT); + VISIT_AVUTIL_IMPORTS(CLEAR_IMPORT); + VISIT_SWSCALE_IMPORTS(CLEAR_IMPORT); +#undef CLEAR_IMPORT + + s_swscale_library.Close(); + s_avutil_library.Close(); + s_avformat_library.Close(); + s_avcodec_library.Close(); } -GSCapture::~GSCapture() +void GSCapture::UnloadFFmpeg() { - EndCapture(); + std::unique_lock lock(s_load_mutex); + if (!s_library_loaded) + return; + + s_library_loaded = false; + UnloadFFmpeg(lock); } -bool GSCapture::BeginCapture(float fps, GSVector2i recommendedResolution, float aspect, std::string& filename) +void GSCapture::LogAVError(int errnum, const char* format, ...) { - printf("Recommended resolution: %d x %d, DAR for muxing: %.4f\n", recommendedResolution.x, recommendedResolution.y, aspect); - std::lock_guard lock(m_lock); + va_list ap; + va_start(ap, format); + std::string msg(StringUtil::StdStringFromFormatV(format, ap)); + va_end(ap); + + char errbuf[128]; + wrap_av_strerror(errnum, errbuf, sizeof(errbuf)); + + Host::AddIconOSDMessage("GSCaptureError", ICON_FA_CAMERA, fmt::format("{}{} ({})", msg, errbuf, errnum), Host::OSD_ERROR_DURATION); +} + +bool GSCapture::BeginCapture(float fps, GSVector2i recommendedResolution, float aspect, std::string filename) +{ + Console.WriteLn("Recommended resolution: %d x %d, DAR for muxing: %.4f", recommendedResolution.x, recommendedResolution.y, aspect); + if (filename.empty() || !LoadFFmpeg(true)) + return false; + + std::lock_guard lock(s_lock); ASSERT(fps != 0); EndCapture(); - // reload settings because they may have changed - m_out_dir = theApp.GetConfigS("capture_out_dir"); - m_threads = theApp.GetConfigI("capture_threads"); -#if defined(__unix__) - m_compression_level = theApp.GetConfigI("png_compression_level"); -#endif - -#ifdef _WIN32 - - GSCaptureDlg dlg; - - if (IDOK != dlg.DoModal()) - return false; + s_size = GSVector2i(Common::AlignUpPow2(recommendedResolution.x, 8), Common::AlignUpPow2(recommendedResolution.y, 8)); + s_filename = std::move(filename); + ff_const59 AVOutputFormat* output_format = wrap_av_guess_format(nullptr, s_filename.c_str(), nullptr); + if (!output_format) { - const int start = dlg.m_filename.length() - 4; - if (start > 0) - { - std::wstring test = dlg.m_filename.substr(start); - std::transform(test.begin(), test.end(), test.begin(), (char(_cdecl*)(int))tolower); - if (test.compare(L".avi") != 0) - dlg.m_filename += L".avi"; - } - else - dlg.m_filename += L".avi"; + Console.Error(fmt::format("Failed to get output format for '{}'", s_filename)); + EndCapture(); + return false; + } - FILE* test = _wfopen(dlg.m_filename.c_str(), L"w"); - if (test) - fclose(test); - else + // find the codec id + const AVCodec* codec = nullptr; + if (!GSConfig.VideoCaptureCodec.empty()) + { + codec = wrap_avcodec_find_encoder_by_name(GSConfig.VideoCaptureCodec.c_str()); + if (!codec) { - dlg.InvalidFile(); - return false; + Host::AddIconOSDMessage("GSCaptureCodecNotFound", ICON_FA_CAMERA, + fmt::format("Video codec {} not found, using default.", GSConfig.VideoCaptureCodec), + Host::OSD_ERROR_DURATION); } } - - m_size.x = (dlg.m_width + 7) & ~7; - m_size.y = (dlg.m_height + 7) & ~7; - // - - auto graph = wil::CoCreateInstanceNoThrow(CLSID_FilterGraph); - if (!graph) - { - return false; - } - auto cgb = wil::CoCreateInstanceNoThrow(CLSID_CaptureGraphBuilder2); - if (!cgb) + if (!codec) + codec = wrap_avcodec_find_encoder(output_format->video_codec); + if (!codec) { + Host::AddIconOSDMessage("GSCaptureError", ICON_FA_CAMERA, "Failed to find encoder.", Host::OSD_ERROR_DURATION); + EndCapture(); return false; } - wil::com_ptr_nothrow mux; - if (FAILED(cgb->SetFiltergraph(graph.get())) - || FAILED(cgb->SetOutputFileName(&MEDIASUBTYPE_Avi, std::wstring(dlg.m_filename.begin(), dlg.m_filename.end()).c_str(), mux.put(), nullptr))) + int res = wrap_avformat_alloc_output_context2(&s_format_context, output_format, nullptr, s_filename.c_str()); + if (res < 0) { + LogAVError(res, "avformat_alloc_output_context2() failed: "); + EndCapture(); return false; } - HRESULT source_hr = S_OK; - // WARNING: This increases the reference count! Right now it's fine, since GSSource inherits from CUnknown that - // starts the reference count from 0. Should this ever change and GSSource ends up with a refcount of 1 after constructing, - // change this to `.attach(new GSSource(...))`. - wil::com_ptr_nothrow src = new GSSource(m_size.x, m_size.y, fps, NULL, source_hr, dlg.m_colorspace); - - if (dlg.m_enc == 0) + s_codec_context = wrap_avcodec_alloc_context3(codec); + if (!s_codec_context) { - if (FAILED(graph->AddFilter(src.get(), L"Source"))) - return false; - if (FAILED(graph->ConnectDirect(GetFirstPin(src.get(), PINDIR_OUTPUT).get(), GetFirstPin(mux.get(), PINDIR_INPUT).get(), nullptr))) - return false; + Host::AddIconOSDMessage("GSCaptureError", ICON_FA_CAMERA, "Failed to allocate codec context.", Host::OSD_ERROR_DURATION); + EndCapture(); + return false; + } + + s_codec_context->codec_type = AVMEDIA_TYPE_VIDEO; + s_codec_context->bit_rate = GSConfig.VideoCaptureBitrate * 1000; + s_codec_context->width = s_size.x; + s_codec_context->height = s_size.y; + wrap_av_reduce(&s_codec_context->time_base.num, &s_codec_context->time_base.den, + 10000, static_cast(static_cast(fps) * 10000.0), std::numeric_limits::max()); + + // Default to YUV 4:2:0 if the codec doesn't specify a pixel format. + if (!codec->pix_fmts) + { + s_codec_context->pix_fmt = AV_PIX_FMT_YUV420P; } else { - if (FAILED(graph->AddFilter(src.get(), L"Source")) || FAILED(graph->AddFilter(dlg.m_enc.get(), L"Encoder"))) + // Prefer YUV420 given the choice, but otherwise fall back to whatever it supports. + s_codec_context->pix_fmt = codec->pix_fmts[0]; + for (u32 i = 0; codec->pix_fmts[i] != AV_PIX_FMT_NONE; i++) { - return false; - } - - if (FAILED(graph->ConnectDirect(GetFirstPin(src.get(), PINDIR_OUTPUT).get(), GetFirstPin(dlg.m_enc.get(), PINDIR_INPUT).get(), nullptr)) - || FAILED(graph->ConnectDirect(GetFirstPin(dlg.m_enc.get(), PINDIR_OUTPUT).get(), GetFirstPin(mux.get(), PINDIR_INPUT).get(), nullptr))) - { - return false; + if (codec->pix_fmts[i] == AV_PIX_FMT_YUV420P) + { + s_codec_context->pix_fmt = codec->pix_fmts[i]; + break; + } } } - EnumFilters(graph.get(), [](IBaseFilter* baseFilter) + if (output_format->flags & AVFMT_GLOBALHEADER) + s_codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + res = wrap_avcodec_open2(s_codec_context, codec, nullptr); + if (res < 0) { - unique_filter_info filter; - baseFilter->QueryFilterInfo(&filter); - printf("Filter [%p]: %ls\n", baseFilter, filter.achName); - - EnumPins(baseFilter, [](IPin* pin) - { - wil::com_ptr_nothrow pinTo; - pin->ConnectedTo(pinTo.put()); - - unique_pin_info pi; - pin->QueryPinInfo(&pi); - printf("- Pin [%p - %p]: %ls (%s)\n", pin, pinTo.get(), pi.achName, pi.dir ? "out" : "in"); - return true; - }); - }); - - // Moving forward, we want failfast semantics so "commit" these interfaces by persisting them in the class - m_graph = std::move(graph); - m_src = std::move(src); - - m_graph.query()->Run(); - m_src.query()->DeliverNewSegment(); - - m_capturing = true; - filename = StringUtil::WideStringToUTF8String(dlg.m_filename.erase(dlg.m_filename.length() - 3, 3) + L"wav"); - return true; -#elif defined(__unix__) - // Note I think it doesn't support multiple depth creation - GSmkdir(m_out_dir.c_str()); - - // Really cheap recording - m_frame = 0; - // Add option !!! - m_size.x = theApp.GetConfigI("CaptureWidth"); - m_size.y = theApp.GetConfigI("CaptureHeight"); - - for (int i = 0; i < m_threads; i++) - { - m_workers.push_back(std::unique_ptr(new GSPng::Worker({}, &GSPng::Process, {}))); + LogAVError(res, "avcodec_open2() failed: "); + EndCapture(); + return false; } - m_capturing = true; - filename = m_out_dir + "/audio_recording.wav"; + s_converted_frame = wrap_av_frame_alloc(); + if (!s_converted_frame) + { + Console.Error("Failed to allocate frame"); + EndCapture(); + return false; + } + + s_converted_frame->format = s_codec_context->pix_fmt; + s_converted_frame->width = s_codec_context->width; + s_converted_frame->height = s_codec_context->height; + res = wrap_av_frame_get_buffer(s_converted_frame, 0); + if (res < 0) + { + LogAVError(res, "av_frame_get_buffer() for converted frame failed: "); + EndCapture(); + return false; + } + + s_video_stream = wrap_avformat_new_stream(s_format_context, codec); + if (!s_video_stream) + { + Console.Error("avformat_new_stream() failed"); + EndCapture(); + return false; + } + + res = wrap_avcodec_parameters_from_context(s_video_stream->codecpar, s_codec_context); + if (res < 0) + { + LogAVError(res, "avcodec_parameters_from_context() failed: "); + EndCapture(); + return false; + } + + s_video_stream->time_base = s_codec_context->time_base; + res = wrap_avio_open(&s_format_context->pb, s_filename.c_str(), AVIO_FLAG_WRITE); + if (res < 0) + { + LogAVError(res, "avio_open() failed: "); + EndCapture(); + return false; + } + + res = wrap_avformat_write_header(s_format_context, nullptr); + if (res < 0) + { + LogAVError(res, "avformat_write_header() failed: "); + EndCapture(); + return false; + } + + s_video_packet = wrap_av_packet_alloc(); + if (!s_video_packet) + { + Console.Error("av_packet_alloc() failed"); + EndCapture(); + return false; + } + + Host::AddIconOSDMessage("GSCapture", ICON_FA_CAMERA, + fmt::format("Starting capturing video to '{}'.", Path::GetFileName(s_filename)), + Host::OSD_INFO_DURATION); + + s_next_pts = 0; + s_capturing = true; return true; -#else - // FIXME: MACOS - return false; -#endif } bool GSCapture::DeliverFrame(const void* bits, int pitch, bool rgba) { - std::lock_guard lock(m_lock); + std::lock_guard lock(s_lock); + pxAssert(bits && pitch > 0); - if (bits == NULL || pitch == 0) + const AVPixelFormat source_format = rgba ? AV_PIX_FMT_RGBA : AV_PIX_FMT_BGRA; + const int source_width = s_size.x; + const int source_height = s_size.y; + + s_sws_context = wrap_sws_getCachedContext(s_sws_context, source_width, source_height, source_format, + s_converted_frame->width, s_converted_frame->height, s_codec_context->pix_fmt, SWS_BICUBIC, + nullptr, nullptr, nullptr); + if (!s_sws_context) { - ASSERT(0); - + Console.Error("sws_getCachedContext() failed"); return false; } -#ifdef _WIN32 + wrap_sws_scale(s_sws_context, reinterpret_cast(&bits), &pitch, 0, source_height, + s_converted_frame->data, s_converted_frame->linesize); - if (m_src) + s_converted_frame->pts = s_next_pts++; + + int res = wrap_avcodec_send_frame(s_codec_context, s_converted_frame); + if (res < 0) { - m_src.query()->DeliverFrame(bits, pitch, rgba); - - return true; + LogAVError(res, "avcodec_send_frame() failed: "); + return false; } -#elif defined(__unix__) + ReceivePackets(); + return true; +} - std::string out_file = m_out_dir + StringUtil::StdStringFromFormat("/frame.%010d.png", m_frame); - //GSPng::Save(GSPng::RGB_PNG, out_file, (u8*)bits, m_size.x, m_size.y, pitch, m_compression_level); - m_workers[m_frame % m_threads]->Push(std::make_shared(GSPng::RGB_PNG, out_file, static_cast(bits), m_size.x, m_size.y, pitch, m_compression_level)); +void GSCapture::ReceivePackets() +{ + for (;;) + { + int res = wrap_avcodec_receive_packet(s_codec_context, s_video_packet); + if (res == AVERROR(EAGAIN) || res == AVERROR_EOF) + { + // no more data available + break; + } + else if (res < 0) + { + LogAVError(res, "avcodec_receive_packet() failed: "); + s_capturing = false; + EndCapture(); + break; + } - m_frame++; + s_video_packet->stream_index = s_video_stream->index; -#endif + // in case the frame rate changed... + wrap_av_packet_rescale_ts(s_video_packet, s_codec_context->time_base, s_video_stream->time_base); - return false; + res = wrap_av_interleaved_write_frame(s_format_context, s_video_packet); + if (res < 0) + { + LogAVError(res, "av_interleaved_write_frame() failed: "); + s_capturing = false; + EndCapture(); + break; + } + } } bool GSCapture::EndCapture() { - if (!m_capturing) - return false; + std::lock_guard lock(s_lock); + int res; - std::lock_guard lock(m_lock); + bool was_capturing = s_capturing; -#ifdef _WIN32 - - if (m_src) + if (was_capturing) { - m_src.query()->DeliverEOS(); - m_src.reset(); + Host::AddIconOSDMessage("GSCapture", ICON_FA_CAMERA, + fmt::format("Stopped capturing video to '{}'.", Path::GetFileName(s_filename)), + Host::OSD_INFO_DURATION); + + s_capturing = false; + s_filename = {}; + + // end of stream + res = wrap_avcodec_send_frame(s_codec_context, nullptr); + if (res < 0) + LogAVError(res, "avcodec_send_frame() for EOS failed: "); + else + ReceivePackets(); + + // end of file! + res = wrap_av_write_trailer(s_format_context); + if (res < 0) + LogAVError(res, "av_write_trailer() failed: "); } - if (m_graph) + if (s_format_context) { - m_graph.query()->Stop(); - m_graph.reset();; + res = wrap_avio_closep(&s_format_context->pb); + if (res < 0) + LogAVError(res, "avio_closep() failed: "); } -#elif defined(__unix__) - m_workers.clear(); + if (s_sws_context) + { + wrap_sws_freeContext(s_sws_context); + s_sws_context = nullptr; + } + if (s_video_packet) + wrap_av_packet_free(&s_video_packet); + if (s_converted_frame) + wrap_av_frame_free(&s_converted_frame); + if (s_codec_context) + wrap_avcodec_free_context(&s_codec_context); + s_video_stream = nullptr; + if (s_format_context) + { + wrap_avformat_free_context(s_format_context); + s_format_context = nullptr; + } - m_frame = 0; - -#endif - - m_capturing = false; + if (was_capturing) + UnloadFFmpeg(); return true; } + +bool GSCapture::IsCapturing() +{ + return s_capturing; +} + +GSVector2i GSCapture::GetSize() +{ + return s_size; +} + +std::vector> GSCapture::GetVideoCodecList(const char* container) +{ + std::vector> ret; + + if (!LoadFFmpeg(false)) + return ret; + + const AVOutputFormat* output_format = wrap_av_guess_format(nullptr, fmt::format("video.{}", container ? container : "mp4").c_str(), nullptr); + if (!output_format) + { + Console.Error("(GetVideoCodecList) av_guess_format() failed"); + return ret; + } + + void* iter = nullptr; + const AVCodec* codec; + while ((codec = wrap_av_codec_iterate(&iter)) != nullptr) + { + // only get audio codecs + if (codec->type != AVMEDIA_TYPE_VIDEO || !wrap_avcodec_find_encoder(codec->id) || !wrap_avcodec_find_encoder_by_name(codec->name)) + continue; + + if (!wrap_avformat_query_codec(output_format, codec->id, FF_COMPLIANCE_NORMAL)) + continue; + + if (std::find_if(ret.begin(), ret.end(), [codec](const auto& it) { return it.first == codec->name; }) != ret.end()) + continue; + + ret.emplace_back(codec->name, codec->long_name ? codec->long_name : codec->name); + } + + return ret; +} diff --git a/pcsx2/GS/GSCapture.h b/pcsx2/GS/GSCapture.h index 340e11457e..629aa1986e 100644 --- a/pcsx2/GS/GSCapture.h +++ b/pcsx2/GS/GSCapture.h @@ -14,44 +14,20 @@ */ #pragma once +#include +#include +#include #include "GSVector.h" -#include "GSPng.h" -#ifdef _WIN32 -#include "Window/GSCaptureDlg.h" -#include -#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 m_graph; - wil::com_ptr_failfast m_src; - -#elif defined(__unix__) - - u64 m_frame; - std::vector> 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> GetVideoCodecList(const char* container); +}; // namespace GSCapture diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.cpp b/pcsx2/GS/Renderers/Common/GSRenderer.cpp index 826c646b4b..be6d8675ee 100644 --- a/pcsx2/GS/Renderers/Common/GSRenderer.cpp +++ b/pcsx2/GS/Renderers/Common/GSRenderer.cpp @@ -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 diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.h b/pcsx2/GS/Renderers/Common/GSRenderer.h index d1d4f78714..656f6ce529 100644 --- a/pcsx2/GS/Renderers/Common/GSRenderer.h +++ b/pcsx2/GS/Renderers/Common/GSRenderer.h @@ -16,8 +16,8 @@ #pragma once #include "GS/GSState.h" -#include "GS/GSCapture.h" #include +#include #ifndef PCSX2_CORE #include @@ -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 }; diff --git a/pcsx2/GS/Window/GSCaptureDlg.cpp b/pcsx2/GS/Window/GSCaptureDlg.cpp deleted file mode 100644 index 88013437e8..0000000000 --- a/pcsx2/GS/Window/GSCaptureDlg.cpp +++ /dev/null @@ -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 . - */ - -#include "PrecompiledHeader.h" -#include "GS.h" -#include "GSCaptureDlg.h" -#include "GS/GSExtra.h" -#include "common/StringUtil.h" -#include - -// 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; -using unique_olestr = wil::unique_any; - -template -static void EnumSysDev(const GUID& clsid, Func&& f) -{ - if (auto devEnum = wil::CoCreateInstanceNoThrow(CLSID_SystemDeviceEnum)) - { - wil::com_ptr_nothrow classEnum; - if (SUCCEEDED(devEnum->CreateClassEnumerator(clsid, classEnum.put(), 0))) - { - wil::com_ptr_nothrow moniker; - while (classEnum->Next(1, moniker.put(), nullptr) == S_OK) - { - std::forward(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()) - { - unique_cauuid caGUID; - enable = SUCCEEDED(pSPP->GetPages(&caGUID)); - } - else if (auto pAMVfWCD = c.filter.try_query()) - { - 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 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()) - { - unique_cauuid caGUID; - if (SUCCEEDED(pSPP->GetPages(&caGUID))) - { - auto lpUnk = pSPP.try_query(); - 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()) - { - 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); -} diff --git a/pcsx2/GS/Window/GSCaptureDlg.h b/pcsx2/GS/Window/GSCaptureDlg.h deleted file mode 100644 index 1e2ada1d08..0000000000 --- a/pcsx2/GS/Window/GSCaptureDlg.h +++ /dev/null @@ -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 . - */ - -#pragma once - -#include "GSDialog.h" -#include "GS/resource.h" -#include -#include - -class GSCaptureDlg : public GSDialog -{ - struct Codec - { - wil::com_ptr_nothrow moniker; - wil::com_ptr_nothrow filter; - std::wstring FriendlyName; - std::wstring DisplayName; - }; - - std::list 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 m_enc; -}; diff --git a/pcsx2/GS/Window/GSDialog.cpp b/pcsx2/GS/Window/GSDialog.cpp deleted file mode 100644 index 27ee6c4570..0000000000 --- a/pcsx2/GS/Window/GSDialog.cpp +++ /dev/null @@ -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 . - */ - -#include "PrecompiledHeader.h" -#include -#include -#include -#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(&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& 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); -} diff --git a/pcsx2/GS/Window/GSDialog.h b/pcsx2/GS/Window/GSDialog.h deleted file mode 100644 index 98056fbd04..0000000000 --- a/pcsx2/GS/Window/GSDialog.h +++ /dev/null @@ -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 . - */ - -#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& 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); -}; diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index 5e8e7a370b..9cc09678a2 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -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 diff --git a/pcsx2/gui/GlobalCommands.cpp b/pcsx2/gui/GlobalCommands.cpp index 3824c5324e..56676e508f 100644 --- a/pcsx2/gui/GlobalCommands.cpp +++ b/pcsx2/gui/GlobalCommands.cpp @@ -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, diff --git a/pcsx2/gui/MainMenuClicks.cpp b/pcsx2/gui/MainMenuClicks.cpp index cd52c95443..528645e812 100644 --- a/pcsx2/gui/MainMenuClicks.cpp +++ b/pcsx2/gui/MainMenuClicks.cpp @@ -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) diff --git a/pcsx2/pcsx2.vcxproj b/pcsx2/pcsx2.vcxproj index 3aad85624c..a8f7056db8 100644 --- a/pcsx2/pcsx2.vcxproj +++ b/pcsx2/pcsx2.vcxproj @@ -47,6 +47,7 @@ $(SolutionDir)3rdparty\d3d12memalloc\include;%(AdditionalIncludeDirectories) %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\zstd\zstd\lib %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rapidyaml\rapidyaml\ext\c4core\src\c4\ext\fast_float\include + %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\ffmpeg\include Async Use PrecompiledHeader.h @@ -384,7 +385,6 @@ - @@ -392,7 +392,6 @@ - @@ -780,7 +779,6 @@ - @@ -788,7 +786,6 @@ - @@ -1068,4 +1065,4 @@ - + \ No newline at end of file diff --git a/pcsx2/pcsx2.vcxproj.filters b/pcsx2/pcsx2.vcxproj.filters index 3e5cfc0c48..87d517f3f7 100644 --- a/pcsx2/pcsx2.vcxproj.filters +++ b/pcsx2/pcsx2.vcxproj.filters @@ -1472,12 +1472,6 @@ System\Ps2\GS\Window - - System\Ps2\GS\Window - - - System\Ps2\GS\Window - System\Ps2\GS\Window @@ -2530,12 +2524,6 @@ System\Ps2\GS\Window - - System\Ps2\GS\Window - - - System\Ps2\GS\Window - System\Ps2\GS\Window @@ -2762,4 +2750,4 @@ AppHost\Resources - + \ No newline at end of file diff --git a/pcsx2/pcsx2core.vcxproj b/pcsx2/pcsx2core.vcxproj index 0fc5874dcf..ede9f90186 100644 --- a/pcsx2/pcsx2core.vcxproj +++ b/pcsx2/pcsx2core.vcxproj @@ -53,6 +53,7 @@ %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rcheevos\rcheevos\include;$(SolutionDir)3rdparty\rainterface %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\discord-rpc\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\zydis\include;$(SolutionDir)3rdparty\zydis\dependencies\zycore\include + %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\ffmpeg\include Async Use PrecompiledHeader.h @@ -282,7 +283,6 @@ - @@ -626,7 +626,6 @@ - diff --git a/pcsx2/pcsx2core.vcxproj.filters b/pcsx2/pcsx2core.vcxproj.filters index cf2dce5f16..d167a79ddc 100644 --- a/pcsx2/pcsx2core.vcxproj.filters +++ b/pcsx2/pcsx2core.vcxproj.filters @@ -1175,9 +1175,6 @@ System\Ps2\GS\Window - - System\Ps2\GS\Window - System\Ps2\GS\Window @@ -2126,9 +2123,6 @@ System\Ps2\GS\Window - - System\Ps2\GS\Window - System\Ps2\GS\Window