System: Add video capture feature

This commit is contained in:
Stenzek
2024-08-11 20:45:14 +10:00
parent 5f8082734e
commit af47eb6956
27 changed files with 2809 additions and 241 deletions
+54 -24
View File
@@ -14,6 +14,7 @@
#include "util/gpu_device.h"
#include "util/image.h"
#include "util/imgui_manager.h"
#include "util/media_capture.h"
#include "util/postprocessing.h"
#include "util/shadergen.h"
#include "util/state_wrapper.h"
@@ -2116,6 +2117,26 @@ bool GPU::RenderDisplay(GPUTexture* target, const GSVector4i display_rect, const
return true;
}
bool GPU::SendDisplayToMediaCapture(MediaCapture* cap)
{
GPUTexture* target = cap->GetRenderTexture();
if (!target)
return false;
const bool apply_aspect_ratio =
(g_settings.display_screenshot_mode != DisplayScreenshotMode::UncorrectedInternalResolution);
const bool postfx = (g_settings.display_screenshot_mode != DisplayScreenshotMode::InternalResolution);
GSVector4i display_rect, draw_rect;
CalculateDrawRect(target->GetWidth(), target->GetHeight(), !g_settings.debugging.show_vram, apply_aspect_ratio,
&display_rect, &draw_rect);
if (!RenderDisplay(target, display_rect, draw_rect, postfx))
return false;
// TODO: Check for frame rate change
return cap->DeliverVideoFrame(target);
}
void GPU::DestroyDeinterlaceTextures()
{
for (std::unique_ptr<GPUTexture>& tex : m_deinterlace_buffers)
@@ -2676,21 +2697,20 @@ bool GPU::RenderScreenshotToBuffer(u32 width, u32 height, const GSVector4i displ
return true;
}
bool GPU::RenderScreenshotToFile(std::string filename, DisplayScreenshotMode mode, u8 quality, bool compress_on_thread,
bool show_osd_message)
void GPU::CalculateScreenshotSize(DisplayScreenshotMode mode, u32* width, u32* height, GSVector4i* display_rect,
GSVector4i* draw_rect) const
{
u32 width = g_gpu_device->GetWindowWidth();
u32 height = g_gpu_device->GetWindowHeight();
GSVector4i display_rect, draw_rect;
CalculateDrawRect(width, height, true, !g_settings.debugging.show_vram, &display_rect, &draw_rect);
*width = g_gpu_device->GetWindowWidth();
*height = g_gpu_device->GetWindowHeight();
CalculateDrawRect(*width, *height, true, !g_settings.debugging.show_vram, display_rect, draw_rect);
const bool internal_resolution = (mode != DisplayScreenshotMode::ScreenResolution || g_settings.debugging.show_vram);
if (internal_resolution && m_display_texture_view_width != 0 && m_display_texture_view_height != 0)
{
if (mode == DisplayScreenshotMode::InternalResolution)
{
const u32 draw_width = static_cast<u32>(draw_rect.width());
const u32 draw_height = static_cast<u32>(draw_rect.height());
const u32 draw_width = static_cast<u32>(draw_rect->width());
const u32 draw_height = static_cast<u32>(draw_rect->height());
// If internal res, scale the computed draw rectangle to the internal res.
// We re-use the draw rect because it's already been AR corrected.
@@ -2701,42 +2721,52 @@ bool GPU::RenderScreenshotToFile(std::string filename, DisplayScreenshotMode mod
{
// stretch height, preserve width
const float scale = static_cast<float>(m_display_texture_view_width) / static_cast<float>(draw_width);
width = m_display_texture_view_width;
height = static_cast<u32>(std::round(static_cast<float>(draw_height) * scale));
*width = m_display_texture_view_width;
*height = static_cast<u32>(std::round(static_cast<float>(draw_height) * scale));
}
else
{
// stretch width, preserve height
const float scale = static_cast<float>(m_display_texture_view_height) / static_cast<float>(draw_height);
width = static_cast<u32>(std::round(static_cast<float>(draw_width) * scale));
height = m_display_texture_view_height;
*width = static_cast<u32>(std::round(static_cast<float>(draw_width) * scale));
*height = m_display_texture_view_height;
}
// DX11 won't go past 16K texture size.
const u32 max_texture_size = g_gpu_device->GetMaxTextureSize();
if (width > max_texture_size)
if (*width > max_texture_size)
{
height = static_cast<u32>(static_cast<float>(height) /
(static_cast<float>(width) / static_cast<float>(max_texture_size)));
width = max_texture_size;
*height = static_cast<u32>(static_cast<float>(*height) /
(static_cast<float>(*width) / static_cast<float>(max_texture_size)));
*width = max_texture_size;
}
if (height > max_texture_size)
if (*height > max_texture_size)
{
height = max_texture_size;
width = static_cast<u32>(static_cast<float>(width) /
(static_cast<float>(height) / static_cast<float>(max_texture_size)));
*height = max_texture_size;
*width = static_cast<u32>(static_cast<float>(*width) /
(static_cast<float>(*height) / static_cast<float>(max_texture_size)));
}
}
else // if (mode == DisplayScreenshotMode::UncorrectedInternalResolution)
{
width = m_display_texture_view_width;
height = m_display_texture_view_height;
*width = m_display_texture_view_width;
*height = m_display_texture_view_height;
}
// Remove padding, it's not part of the framebuffer.
draw_rect = GSVector4i(0, 0, static_cast<s32>(width), static_cast<s32>(height));
display_rect = draw_rect;
*draw_rect = GSVector4i(0, 0, static_cast<s32>(*width), static_cast<s32>(*height));
*display_rect = *draw_rect;
}
}
bool GPU::RenderScreenshotToFile(std::string filename, DisplayScreenshotMode mode, u8 quality, bool compress_on_thread,
bool show_osd_message)
{
u32 width, height;
GSVector4i display_rect, draw_rect;
CalculateScreenshotSize(mode, &width, &height, &display_rect, &draw_rect);
const bool internal_resolution = (mode != DisplayScreenshotMode::ScreenResolution);
if (width == 0 || height == 0)
return false;
+8
View File
@@ -28,6 +28,7 @@ class StateWrapper;
class GPUDevice;
class GPUTexture;
class GPUPipeline;
class MediaCapture;
struct Settings;
@@ -210,6 +211,10 @@ public:
void CalculateDrawRect(s32 window_width, s32 window_height, bool apply_rotation, bool apply_aspect_ratio,
GSVector4i* display_rect, GSVector4i* draw_rect) const;
/// Helper function for computing screenshot bounds.
void CalculateScreenshotSize(DisplayScreenshotMode mode, u32* width, u32* height, GSVector4i* display_rect,
GSVector4i* draw_rect) const;
/// Helper function to save current display texture to PNG.
bool WriteDisplayTextureToFile(std::string filename, bool compress_on_thread = false);
@@ -225,6 +230,9 @@ public:
/// Draws the current display texture, with any post-processing.
bool PresentDisplay();
/// Sends the current frame to media capture.
bool SendDisplayToMediaCapture(MediaCapture* cap);
/// Reads the CLUT from the specified coordinates, accounting for wrap-around.
static void ReadCLUT(u16* dest, GPUTexturePaletteReg reg, bool clut_is_8bit);
+11
View File
@@ -358,6 +358,17 @@ DEFINE_HOTKEY("ResetEmulationSpeed", TRANSLATE_NOOP("Hotkeys", "System"),
}
})
DEFINE_HOTKEY("ToggleMediaCapture", TRANSLATE_NOOP("Hotkeys", "System"),
TRANSLATE_NOOP("Hotkeys", "Toggle Media Capture"), [](s32 pressed) {
if (!pressed)
{
if (System::GetMediaCapture())
System::StopMediaCapture();
else
System::StartMediaCapture();
}
})
DEFINE_HOTKEY("ToggleSoftwareRendering", TRANSLATE_NOOP("Hotkeys", "Graphics"),
TRANSLATE_NOOP("Hotkeys", "Toggle Software Rendering"), [](s32 pressed) {
if (!pressed && System::IsValid())
+127 -69
View File
@@ -21,6 +21,7 @@
#include "util/imgui_fullscreen.h"
#include "util/imgui_manager.h"
#include "util/input_manager.h"
#include "util/media_capture.h"
#include "common/align.h"
#include "common/error.h"
@@ -48,7 +49,9 @@ Log_SetChannel(ImGuiManager);
namespace ImGuiManager {
static void FormatProcessorStat(SmallStringBase& text, double usage, double time);
static void DrawPerformanceOverlay();
static void DrawPerformanceOverlay(float& position_y, float scale, float margin, float spacing);
static void DrawMediaCaptureOverlay(float& position_y, float scale, float margin, float spacing);
static void DrawFrameTimeOverlay(float& position_y, float scale, float margin, float spacing);
static void DrawEnhancementsOverlay();
static void DrawInputsOverlay();
} // namespace ImGuiManager
@@ -191,7 +194,13 @@ void ImGuiManager::RenderTextOverlays()
const System::State state = System::GetState();
if (state != System::State::Shutdown)
{
DrawPerformanceOverlay();
const float scale = ImGuiManager::GetGlobalScale();
const float margin = std::ceil(10.0f * scale);
const float spacing = std::ceil(5.0f * scale);
float position_y = margin;
DrawPerformanceOverlay(position_y, scale, margin, spacing);
DrawFrameTimeOverlay(position_y, scale, margin, spacing);
DrawMediaCaptureOverlay(position_y, scale, margin, spacing);
if (g_settings.display_show_enhancements && state != System::State::Paused)
DrawEnhancementsOverlay();
@@ -212,7 +221,7 @@ void ImGuiManager::FormatProcessorStat(SmallStringBase& text, double usage, doub
text.append_format("{:.1f}% ({:.2f}ms)", usage, time);
}
void ImGuiManager::DrawPerformanceOverlay()
void ImGuiManager::DrawPerformanceOverlay(float& position_y, float scale, float margin, float spacing)
{
if (!(g_settings.display_show_fps || g_settings.display_show_speed || g_settings.display_show_gpu_stats ||
g_settings.display_show_resolution || g_settings.display_show_cpu_usage ||
@@ -222,14 +231,9 @@ void ImGuiManager::DrawPerformanceOverlay()
return;
}
const float scale = ImGuiManager::GetGlobalScale();
const float shadow_offset = std::ceil(1.0f * scale);
const float margin = std::ceil(10.0f * scale);
const float spacing = std::ceil(5.0f * scale);
ImFont* fixed_font = ImGuiManager::GetFixedFont();
ImFont* standard_font = ImGuiManager::GetStandardFont();
float position_y = margin;
ImDrawList* dl = ImGui::GetBackgroundDrawList();
SmallString text;
ImVec2 text_size;
@@ -364,6 +368,13 @@ void ImGuiManager::DrawPerformanceOverlay()
FormatProcessorStat(text, System::GetSWThreadUsage(), System::GetSWThreadAverageTime());
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
}
if (MediaCapture* cap = System::GetMediaCapture())
{
text.assign("CAP: ");
FormatProcessorStat(text, cap->GetCaptureThreadUsage(), cap->GetCaptureThreadTime());
DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255));
}
}
if (g_settings.display_show_gpu_usage && g_gpu_device->IsGPUTimingEnabled())
@@ -382,67 +393,6 @@ void ImGuiManager::DrawPerformanceOverlay()
DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255));
}
}
if (g_settings.display_show_frame_times)
{
const ImVec2 history_size(200.0f * scale, 50.0f * scale);
ImGui::SetNextWindowSize(ImVec2(history_size.x, history_size.y));
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - margin - history_size.x, position_y));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.25f));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
if (ImGui::Begin("##frame_times", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs))
{
ImGui::PushFont(fixed_font);
auto [min, max] = GetMinMax(System::GetFrameTimeHistory());
// add a little bit of space either side, so we're not constantly resizing
if ((max - min) < 4.0f)
{
min = min - std::fmod(min, 1.0f);
max = max - std::fmod(max, 1.0f) + 1.0f;
min = std::max(min - 2.0f, 0.0f);
max += 2.0f;
}
ImGui::PlotEx(
ImGuiPlotType_Lines, "##frame_times",
[](void*, int idx) -> float {
return System::GetFrameTimeHistory()[((System::GetFrameTimeHistoryPos() + idx) %
System::NUM_FRAME_TIME_SAMPLES)];
},
nullptr, System::NUM_FRAME_TIME_SAMPLES, 0, nullptr, min, max, history_size);
ImDrawList* win_dl = ImGui::GetCurrentWindow()->DrawList;
const ImVec2 wpos(ImGui::GetCurrentWindow()->Pos);
text.format("{:.1f} ms", max);
text_size = fixed_font->CalcTextSizeA(fixed_font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.end_ptr());
win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing + shadow_offset, wpos.y + shadow_offset),
IM_COL32(0, 0, 0, 100), text.c_str(), text.end_ptr());
win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing, wpos.y), IM_COL32(255, 255, 255, 255),
text.c_str(), text.end_ptr());
text.format("{:.1f} ms", min);
text_size = fixed_font->CalcTextSizeA(fixed_font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.end_ptr());
win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing + shadow_offset,
wpos.y + history_size.y - fixed_font->FontSize + shadow_offset),
IM_COL32(0, 0, 0, 100), text.c_str(), text.end_ptr());
win_dl->AddText(
ImVec2(wpos.x + history_size.x - text_size.x - spacing, wpos.y + history_size.y - fixed_font->FontSize),
IM_COL32(255, 255, 255, 255), text.c_str(), text.end_ptr());
ImGui::PopFont();
}
ImGui::End();
ImGui::PopStyleVar(5);
ImGui::PopStyleColor(3);
}
}
else if (g_settings.display_show_status_indicators && state == System::State::Paused &&
!FullscreenUI::HasActiveWindow())
@@ -547,6 +497,114 @@ void ImGuiManager::DrawEnhancementsOverlay()
IM_COL32(255, 255, 255, 255), text.c_str(), text.end_ptr());
}
void ImGuiManager::DrawMediaCaptureOverlay(float& position_y, float scale, float margin, float spacing)
{
MediaCapture* const cap = System::GetMediaCapture();
if (!cap || FullscreenUI::HasActiveWindow())
return;
const float shadow_offset = std::ceil(scale);
ImFont* const standard_font = ImGuiManager::GetStandardFont();
ImDrawList* dl = ImGui::GetBackgroundDrawList();
static constexpr const char* ICON = ICON_FA_VIDEO;
const time_t elapsed_time = cap->GetElapsedTime();
const TinyString text_msg = TinyString::from_format(" {:02d}:{:02d}:{:02d}", elapsed_time / 3600,
(elapsed_time % 3600) / 60, (elapsed_time % 3600) % 60);
const ImVec2 icon_size = standard_font->CalcTextSizeA(standard_font->FontSize, std::numeric_limits<float>::max(),
-1.0f, ICON, nullptr, nullptr);
const ImVec2 text_size = standard_font->CalcTextSizeA(standard_font->FontSize, std::numeric_limits<float>::max(),
-1.0f, text_msg.c_str(), text_msg.end_ptr(), nullptr);
const float box_margin = 2.0f * scale;
const ImVec2 box_size = ImVec2(icon_size.x + shadow_offset + text_size.x + box_margin * 2.0f,
std::max(icon_size.x, text_size.y) + box_margin * 2.0f);
const ImVec2 box_pos = ImVec2(ImGui::GetIO().DisplaySize.x - margin - box_size.x, position_y);
dl->AddRectFilled(box_pos, box_pos + box_size, IM_COL32(0, 0, 0, 64), box_margin);
const ImVec2 text_start = ImVec2(box_pos.x + box_margin, box_pos.y + box_margin);
dl->AddText(standard_font, standard_font->FontSize,
ImVec2(text_start.x + shadow_offset, text_start.y + shadow_offset), IM_COL32(0, 0, 0, 100), ICON);
dl->AddText(standard_font, standard_font->FontSize,
ImVec2(text_start.x + icon_size.x + shadow_offset, text_start.y + shadow_offset), IM_COL32(0, 0, 0, 100),
text_msg.c_str(), text_msg.end_ptr());
dl->AddText(standard_font, standard_font->FontSize, text_start, IM_COL32(255, 0, 0, 255), ICON);
dl->AddText(standard_font, standard_font->FontSize, ImVec2(text_start.x + icon_size.x, text_start.y),
IM_COL32(255, 255, 255, 255), text_msg.c_str(), text_msg.end_ptr());
position_y += box_size.y + spacing;
}
void ImGuiManager::DrawFrameTimeOverlay(float& position_y, float scale, float margin, float spacing)
{
if (!g_settings.display_show_frame_times || System::IsPaused())
return;
const float shadow_offset = std::ceil(1.0f * scale);
ImFont* fixed_font = ImGuiManager::GetFixedFont();
const ImVec2 history_size(200.0f * scale, 50.0f * scale);
ImGui::SetNextWindowSize(ImVec2(history_size.x, history_size.y));
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - margin - history_size.x, position_y));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.25f));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
if (ImGui::Begin("##frame_times", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs))
{
ImGui::PushFont(fixed_font);
auto [min, max] = GetMinMax(System::GetFrameTimeHistory());
// add a little bit of space either side, so we're not constantly resizing
if ((max - min) < 4.0f)
{
min = min - std::fmod(min, 1.0f);
max = max - std::fmod(max, 1.0f) + 1.0f;
min = std::max(min - 2.0f, 0.0f);
max += 2.0f;
}
ImGui::PlotEx(
ImGuiPlotType_Lines, "##frame_times",
[](void*, int idx) -> float {
return System::GetFrameTimeHistory()[((System::GetFrameTimeHistoryPos() + idx) %
System::NUM_FRAME_TIME_SAMPLES)];
},
nullptr, System::NUM_FRAME_TIME_SAMPLES, 0, nullptr, min, max, history_size);
ImDrawList* win_dl = ImGui::GetCurrentWindow()->DrawList;
const ImVec2 wpos(ImGui::GetCurrentWindow()->Pos);
TinyString text;
text.format("{:.1f} ms", max);
ImVec2 text_size = fixed_font->CalcTextSizeA(fixed_font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.end_ptr());
win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing + shadow_offset, wpos.y + shadow_offset),
IM_COL32(0, 0, 0, 100), text.c_str(), text.end_ptr());
win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing, wpos.y), IM_COL32(255, 255, 255, 255),
text.c_str(), text.end_ptr());
text.format("{:.1f} ms", min);
text_size = fixed_font->CalcTextSizeA(fixed_font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.end_ptr());
win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing + shadow_offset,
wpos.y + history_size.y - fixed_font->FontSize + shadow_offset),
IM_COL32(0, 0, 0, 100), text.c_str(), text.end_ptr());
win_dl->AddText(
ImVec2(wpos.x + history_size.x - text_size.x - spacing, wpos.y + history_size.y - fixed_font->FontSize),
IM_COL32(255, 255, 255, 255), text.c_str(), text.end_ptr());
ImGui::PopFont();
}
ImGui::End();
ImGui::PopStyleVar(5);
ImGui::PopStyleColor(3);
position_y += history_size.y + spacing;
}
void ImGuiManager::DrawInputsOverlay()
{
const float scale = ImGuiManager::GetGlobalScale();
+52
View File
@@ -10,6 +10,7 @@
#include "util/gpu_device.h"
#include "util/imgui_manager.h"
#include "util/input_manager.h"
#include "util/media_capture.h"
#include "common/assert.h"
#include "common/file_system.h"
@@ -82,6 +83,12 @@ float SettingInfo::FloatStepValue() const
return step_value ? StringUtil::FromChars<float>(step_value).value_or(fallback_value) : fallback_value;
}
#if defined(_WIN32)
const MediaCaptureBackend Settings::DEFAULT_MEDIA_CAPTURE_BACKEND = MediaCaptureBackend::MediaFoundation;
#elif !defined(__ANDROID__)
const MediaCaptureBackend Settings::DEFAULT_MEDIA_CAPTURE_BACKEND = MediaCaptureBackend::FFMPEG;
#endif
Settings::Settings()
{
controller_types[0] = DEFAULT_CONTROLLER_1_TYPE;
@@ -405,6 +412,27 @@ void Settings::Load(SettingsInterface& si)
achievements_leaderboard_duration =
si.GetIntValue("Cheevos", "LeaderboardsDuration", DEFAULT_LEADERBOARD_NOTIFICATION_TIME);
#ifndef __ANDROID__
media_capture_backend =
MediaCapture::ParseBackendName(
si.GetStringValue("MediaCapture", "Backend", MediaCapture::GetBackendName(DEFAULT_MEDIA_CAPTURE_BACKEND)).c_str())
.value_or(DEFAULT_MEDIA_CAPTURE_BACKEND);
media_capture_container = si.GetStringValue("MediaCapture", "Container", "mp4");
media_capture_video = si.GetBoolValue("MediaCapture", "VideoCapture", true);
media_capture_video_width = si.GetUIntValue("MediaCapture", "VideoWidth", 640);
media_capture_video_height = si.GetUIntValue("MediaCapture", "VideoHeight", 480);
media_capture_video_auto_size = si.GetBoolValue("MediaCapture", "VideoAutoSize", false);
media_capture_video_bitrate = si.GetUIntValue("MediaCapture", "VideoBitrate", 6000);
media_capture_video_codec = si.GetStringValue("MediaCapture", "VideoCodec");
media_capture_video_codec_use_args = si.GetBoolValue("MediaCapture", "VideoCodecUseArgs", false);
media_capture_video_codec_args = si.GetStringValue("MediaCapture", "AudioCodecArgs");
media_capture_audio = si.GetBoolValue("MediaCapture", "AudioCapture", true);
media_capture_audio_bitrate = si.GetUIntValue("MediaCapture", "AudioBitrate", 128);
media_capture_audio_codec = si.GetStringValue("MediaCapture", "AudioCodec");
media_capture_audio_codec_use_args = si.GetBoolValue("MediaCapture", "AudioCodecUseArgs", false);
media_capture_audio_codec_args = si.GetStringValue("MediaCapture", "AudioCodecArgs");
#endif
log_level = ParseLogLevelName(si.GetStringValue("Logging", "LogLevel", GetLogLevelName(DEFAULT_LOG_LEVEL)).c_str())
.value_or(DEFAULT_LOG_LEVEL);
log_filter = si.GetStringValue("Logging", "LogFilter", "");
@@ -657,6 +685,24 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
si.SetIntValue("Cheevos", "NotificationsDuration", achievements_notification_duration);
si.SetIntValue("Cheevos", "LeaderboardsDuration", achievements_leaderboard_duration);
#ifndef __ANDROID__
si.SetStringValue("MediaCapture", "Backend", MediaCapture::GetBackendName(media_capture_backend));
si.SetStringValue("MediaCapture", "Container", media_capture_container.c_str());
si.SetBoolValue("MediaCapture", "VideoCapture", media_capture_video);
si.SetUIntValue("MediaCapture", "VideoWidth", media_capture_video_width);
si.SetUIntValue("MediaCapture", "VideoHeight", media_capture_video_height);
si.SetBoolValue("MediaCapture", "VideoAutoSize", media_capture_video_auto_size);
si.SetUIntValue("MediaCapture", "VideoBitrate", media_capture_video_bitrate);
si.SetStringValue("MediaCapture", "VideoCodec", media_capture_video_codec.c_str());
si.SetBoolValue("MediaCapture", "VideoCodecUseArgs", media_capture_video_codec_use_args);
si.SetStringValue("MediaCapture", "AudioCodecArgs", media_capture_video_codec_args.c_str());
si.SetBoolValue("MediaCapture", "AudioCapture", media_capture_audio);
si.SetUIntValue("MediaCapture", "AudioBitrate", media_capture_audio_bitrate);
si.SetStringValue("MediaCapture", "AudioCodec", media_capture_audio_codec.c_str());
si.SetBoolValue("MediaCapture", "AudioCodecUseArgs", media_capture_audio_codec_use_args);
si.SetStringValue("MediaCapture", "AudioCodecArgs", media_capture_audio_codec_args.c_str());
#endif
if (!ignore_base)
{
si.SetStringValue("Logging", "LogLevel", GetLogLevelName(log_level));
@@ -1823,6 +1869,7 @@ std::string EmuFolders::Screenshots;
std::string EmuFolders::Shaders;
std::string EmuFolders::Textures;
std::string EmuFolders::UserResources;
std::string EmuFolders::Videos;
void EmuFolders::SetDefaults()
{
@@ -1840,6 +1887,7 @@ void EmuFolders::SetDefaults()
Shaders = Path::Combine(DataRoot, "shaders");
Textures = Path::Combine(DataRoot, "textures");
UserResources = Path::Combine(DataRoot, "resources");
Videos = Path::Combine(DataRoot, "videos");
}
static std::string LoadPathFromSettings(SettingsInterface& si, const std::string& root, const char* section,
@@ -1870,6 +1918,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
Shaders = LoadPathFromSettings(si, DataRoot, "Folders", "Shaders", "shaders");
Textures = LoadPathFromSettings(si, DataRoot, "Folders", "Textures", "textures");
UserResources = LoadPathFromSettings(si, DataRoot, "Folders", "UserResources", "resources");
Videos = LoadPathFromSettings(si, DataRoot, "Folders", "Videos", "videos");
DEV_LOG("BIOS Directory: {}", Bios);
DEV_LOG("Cache Directory: {}", Cache);
@@ -1886,6 +1935,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
DEV_LOG("Shaders Directory: {}", Shaders);
DEV_LOG("Textures Directory: {}", Textures);
DEV_LOG("User Resources Directory: {}", UserResources);
DEV_LOG("Videos Directory: {}", Videos);
}
void EmuFolders::Save(SettingsInterface& si)
@@ -1905,6 +1955,7 @@ void EmuFolders::Save(SettingsInterface& si)
si.SetStringValue("Folders", "Shaders", Path::MakeRelative(Shaders, DataRoot).c_str());
si.SetStringValue("Folders", "Textures", Path::MakeRelative(Textures, DataRoot).c_str());
si.SetStringValue("Folders", "UserResources", Path::MakeRelative(UserResources, DataRoot).c_str());
si.SetStringValue("Folders", "Videos", Path::MakeRelative(UserResources, Videos).c_str());
}
void EmuFolders::Update()
@@ -1954,6 +2005,7 @@ bool EmuFolders::EnsureFoldersExist()
result;
result = FileSystem::EnsureDirectoryExists(Textures.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(UserResources.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Videos.c_str(), false) && result;
return result;
}
+26
View File
@@ -19,6 +19,7 @@
#include <vector>
enum class RenderAPI : u32;
enum class MediaCaptureBackend : u8;
struct SettingInfo
{
@@ -223,6 +224,25 @@ struct Settings
s32 achievements_notification_duration = DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME;
s32 achievements_leaderboard_duration = DEFAULT_LEADERBOARD_NOTIFICATION_TIME;
#ifndef __ANDROID__
// media capture
std::string media_capture_container;
std::string media_capture_audio_codec;
std::string media_capture_audio_codec_args;
std::string media_capture_video_codec;
std::string media_capture_video_codec_args;
u32 media_capture_video_width = 640;
u32 media_capture_video_height = 480;
u32 media_capture_video_bitrate = 6000;
u32 media_capture_audio_bitrate = 128;
MediaCaptureBackend media_capture_backend = DEFAULT_MEDIA_CAPTURE_BACKEND;
bool media_capture_video : 1 = true;
bool media_capture_video_codec_use_args : 1 = true;
bool media_capture_video_auto_size : 1 = false;
bool media_capture_audio : 1 = true;
bool media_capture_audio_codec_use_args : 1 = true;
#endif
struct DebugSettings
{
bool show_vram : 1 = false;
@@ -517,6 +537,11 @@ struct Settings
static constexpr SaveStateCompressionMode DEFAULT_SAVE_STATE_COMPRESSION_MODE = SaveStateCompressionMode::ZstDefault;
#ifndef __ANDROID__
static const MediaCaptureBackend DEFAULT_MEDIA_CAPTURE_BACKEND;
static constexpr const char* DEFAULT_MEDIA_CAPTURE_CONTAINER = "mp4";
#endif
// Enable console logging by default on Linux platforms.
#if defined(__linux__) && !defined(__ANDROID__)
static constexpr bool DEFAULT_LOG_TO_CONSOLE = true;
@@ -562,6 +587,7 @@ extern std::string Screenshots;
extern std::string Shaders;
extern std::string Textures;
extern std::string UserResources;
extern std::string Videos;
// Assumes that AppRoot and DataRoot have been initialized.
void SetDefaults();
+9 -8
View File
@@ -12,6 +12,7 @@
#include "util/audio_stream.h"
#include "util/imgui_manager.h"
#include "util/media_capture.h"
#include "util/state_wrapper.h"
#include "util/wav_writer.h"
@@ -482,7 +483,6 @@ void SPU::CPUClockChanged()
void SPU::Shutdown()
{
StopDumpingAudio();
s_state.tick_event.Deactivate();
s_state.transfer_event.Deactivate();
s_state.audio_stream.reset();
@@ -1508,11 +1508,8 @@ void SPU::InternalGeneratePendingSamples()
s_state.tick_event.InvokeEarly(force_exec);
}
bool SPU::IsDumpingAudio()
{
return static_cast<bool>(s_state.dump_writer);
}
#if 0
// TODO: FIXME
bool SPU::StartDumpingAudio(const char* filename)
{
s_state.dump_writer.reset();
@@ -1562,6 +1559,7 @@ bool SPU::StopDumpingAudio()
return true;
}
#endif
const std::array<u8, SPU::RAM_SIZE>& SPU::GetRAM()
{
@@ -2435,8 +2433,11 @@ void SPU::Execute(void* param, TickCount ticks, TickCount ticks_late)
}
}
if (s_state.dump_writer) [[unlikely]]
s_state.dump_writer->WriteFrames(output_frame_start, frames_in_this_batch);
if (MediaCapture* cap = System::GetMediaCapture()) [[unlikely]]
{
if (!cap->DeliverAudioFrames(output_frame_start, frames_in_this_batch))
System::StopMediaCapture();
}
output_stream->EndWrite(frames_in_this_batch);
remaining_frames -= frames_in_this_batch;
-9
View File
@@ -38,15 +38,6 @@ void DrawDebugStateWindow();
// Executes the SPU, generating any pending samples.
void GeneratePendingSamples();
/// Returns true if currently dumping audio.
bool IsDumpingAudio();
/// Starts dumping audio to file.
bool StartDumpingAudio(const char* filename);
/// Stops dumping audio to file, if started.
bool StopDumpingAudio();
/// Access to SPU RAM.
const std::array<u8, RAM_SIZE>& GetRAM();
std::array<u8, RAM_SIZE>& GetWritableRAM();
+146 -57
View File
@@ -41,6 +41,7 @@
#include "util/ini_settings_interface.h"
#include "util/input_manager.h"
#include "util/iso_reader.h"
#include "util/media_capture.h"
#include "util/platform_misc.h"
#include "util/postprocessing.h"
#include "util/sockets.h"
@@ -78,6 +79,7 @@ Log_SetChannel(System);
#ifdef _WIN32
#include "common/windows_headers.h"
#include <Objbase.h>
#include <mmsystem.h>
#include <objbase.h>
#endif
@@ -302,6 +304,7 @@ static Common::Timer s_frame_timer;
static Threading::ThreadHandle s_cpu_thread_handle;
static std::unique_ptr<CheatList> s_cheat_list;
static std::unique_ptr<MediaCapture> s_media_capture;
// temporary save state, created when loading, used to undo load state
static std::optional<System::SaveStateBuffer> s_undo_load_state;
@@ -445,6 +448,8 @@ void System::Internal::ProcessShutdown()
bool System::Internal::CPUThreadInitialize(Error* error)
{
Threading::SetNameOfCurrentThread("CPU Thread");
#ifdef _WIN32
// On Win32, we have a bunch of things which use COM (e.g. SDL, Cubeb, etc).
// We need to initialize COM first, before anything else does, because otherwise they might
@@ -1690,8 +1695,8 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (parameters.load_image_to_ram || g_settings.cdrom_load_image_to_ram)
CDROM::PrecacheMedia();
if (parameters.start_audio_dump)
StartDumpingAudio();
if (parameters.start_media_capture)
StartMediaCapture({});
if (g_settings.start_paused || parameters.override_start_paused.value_or(false))
PauseSystem(true);
@@ -1809,6 +1814,9 @@ void System::DestroySystem()
if (s_state == State::Shutdown)
return;
if (s_media_capture)
StopMediaCapture();
s_undo_load_state.reset();
#ifdef ENABLE_GDB_SERVER
@@ -2003,6 +2011,13 @@ void System::FrameDone()
SaveRunaheadState();
}
// Kick off media capture early, might take a while.
if (s_media_capture && s_media_capture->IsCapturingVideo()) [[unlikely]]
{
if (!g_gpu->SendDisplayToMediaCapture(s_media_capture.get())) [[unlikely]]
StopMediaCapture();
}
Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
// pre-frame sleep accounting (input lag reduction)
@@ -3134,6 +3149,9 @@ void System::UpdatePerformanceCounters()
s_sw_thread_usage = static_cast<float>(static_cast<double>(sw_delta) * pct_divider);
s_sw_thread_time = static_cast<float>(static_cast<double>(sw_delta) * time_divider);
if (s_media_capture)
s_media_capture->UpdateCaptureThreadUsage(pct_divider, time_divider);
s_fps_timer.ResetTo(now_ticks);
if (g_gpu_device->IsGPUTimingEnabled())
@@ -4896,61 +4914,6 @@ void System::UpdateVolume()
SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume());
}
bool System::IsDumpingAudio()
{
return SPU::IsDumpingAudio();
}
bool System::StartDumpingAudio(const char* filename)
{
if (System::IsShutdown())
return false;
std::string auto_filename;
if (!filename)
{
const auto& serial = System::GetGameSerial();
if (serial.empty())
{
auto_filename = Path::Combine(
EmuFolders::Dumps, fmt::format("audio" FS_OSPATH_SEPARATOR_STR "{}.wav", GetTimestampStringForFileName()));
}
else
{
auto_filename = Path::Combine(EmuFolders::Dumps, fmt::format("audio" FS_OSPATH_SEPARATOR_STR "{}_{}.wav", serial,
GetTimestampStringForFileName()));
}
filename = auto_filename.c_str();
}
if (SPU::StartDumpingAudio(filename))
{
Host::AddIconOSDMessage(
"audio_dumping", ICON_FA_VOLUME_UP,
fmt::format(TRANSLATE_FS("OSDMessage", "Started dumping audio to '{}'."), Path::GetFileName(filename)),
Host::OSD_INFO_DURATION);
return true;
}
else
{
Host::AddIconOSDMessage(
"audio_dumping", ICON_FA_VOLUME_UP,
fmt::format(TRANSLATE_FS("OSDMessage", "Failed to start dumping audio to '{}'."), Path::GetFileName(filename)),
Host::OSD_ERROR_DURATION);
return false;
}
}
void System::StopDumpingAudio()
{
if (System::IsShutdown() || !SPU::StopDumpingAudio())
return;
Host::AddIconOSDMessage("audio_dumping", ICON_FA_VOLUME_MUTE, TRANSLATE_STR("OSDMessage", "Stopped dumping audio."),
Host::OSD_INFO_DURATION);
}
bool System::SaveScreenshot(const char* filename, DisplayScreenshotMode mode, DisplayScreenshotFormat format,
u8 quality, bool compress_on_thread)
{
@@ -4985,6 +4948,132 @@ bool System::SaveScreenshot(const char* filename, DisplayScreenshotMode mode, Di
return g_gpu->RenderScreenshotToFile(filename, mode, quality, compress_on_thread, true);
}
static std::string_view GetCaptureTypeForMessage(bool capture_video, bool capture_audio)
{
return capture_video ? (capture_audio ? TRANSLATE_SV("System", "capturing audio and video") :
TRANSLATE_SV("System", "capturing video")) :
TRANSLATE_SV("System", "capturing audio");
}
MediaCapture* System::GetMediaCapture()
{
return s_media_capture.get();
}
std::string System::GetNewMediaCapturePath(const std::string_view title, const std::string_view container)
{
const std::string sanitized_name = Path::SanitizeFileName(title);
std::string path;
if (sanitized_name.empty())
{
path = Path::Combine(EmuFolders::Videos, fmt::format("{}.{}", GetTimestampStringForFileName(), container));
}
else
{
path = Path::Combine(EmuFolders::Videos,
fmt::format("{} {}.{}", sanitized_name, GetTimestampStringForFileName(), container));
}
return path;
}
bool System::StartMediaCapture(std::string path, bool capture_video, bool capture_audio)
{
if (!IsValid())
return false;
if (s_media_capture)
StopMediaCapture();
// Need to work out the size.
u32 capture_width = g_settings.media_capture_video_width;
u32 capture_height = g_settings.media_capture_video_height;
const GPUTexture::Format capture_format =
g_gpu_device->HasSurface() ? g_gpu_device->GetWindowFormat() : GPUTexture::Format::RGBA8;
const float fps = g_gpu->ComputeVerticalFrequency();
if (capture_video)
{
// TODO: This will be a mess with GPU thread.
if (g_settings.media_capture_video_auto_size)
{
GSVector4i unused_display_rect, unused_draw_rect;
g_gpu->CalculateScreenshotSize(DisplayScreenshotMode::InternalResolution, &capture_width, &capture_height,
&unused_display_rect, &unused_draw_rect);
}
MediaCapture::AdjustVideoSize(&capture_width, &capture_height);
}
// TODO: Render anamorphic capture instead?
constexpr float aspect = 1.0f;
if (path.empty())
path = GetNewMediaCapturePath(GetGameTitle(), g_settings.media_capture_container);
Error error;
s_media_capture = MediaCapture::Create(g_settings.media_capture_backend, &error);
if (!s_media_capture ||
!s_media_capture->BeginCapture(
fps, aspect, capture_width, capture_height, capture_format, SPU::SAMPLE_RATE, std::move(path), capture_video,
g_settings.media_capture_video_codec, g_settings.media_capture_video_bitrate,
g_settings.media_capture_video_codec_use_args ? std::string_view(g_settings.media_capture_video_codec_args) :
std::string_view(),
capture_audio, g_settings.media_capture_audio_codec, g_settings.media_capture_audio_bitrate,
g_settings.media_capture_audio_codec_use_args ? std::string_view(g_settings.media_capture_audio_codec_args) :
std::string_view(),
&error))
{
Host::AddIconOSDMessage(
"MediaCapture", ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Failed to create media capture: {0}"), error.GetDescription()),
Host::OSD_ERROR_DURATION);
s_media_capture.reset();
Host::OnMediaCaptureStopped();
return false;
}
Host::AddIconOSDMessage(
"MediaCapture", ICON_FA_CAMERA,
fmt::format(TRANSLATE_FS("System", "Starting {0} to '{1}'."),
GetCaptureTypeForMessage(s_media_capture->IsCapturingVideo(), s_media_capture->IsCapturingAudio()),
Path::GetFileName(s_media_capture->GetPath())),
Host::OSD_INFO_DURATION);
Host::OnMediaCaptureStarted();
return true;
}
void System::StopMediaCapture()
{
if (!s_media_capture)
return;
const bool was_capturing_audio = s_media_capture->IsCapturingAudio();
const bool was_capturing_video = s_media_capture->IsCapturingVideo();
Error error;
if (s_media_capture->EndCapture(&error))
{
Host::AddIconOSDMessage("MediaCapture", ICON_FA_CAMERA,
fmt::format(TRANSLATE_FS("System", "Stopped {0} to '{1}'."),
GetCaptureTypeForMessage(was_capturing_video, was_capturing_audio),
Path::GetFileName(s_media_capture->GetPath())),
Host::OSD_INFO_DURATION);
}
else
{
Host::AddIconOSDMessage(
"MediaCapture", ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Stopped {0}: {1}."),
GetCaptureTypeForMessage(s_media_capture->IsCapturingVideo(), s_media_capture->IsCapturingAudio()),
error.GetDescription()),
Host::OSD_INFO_DURATION);
}
s_media_capture.reset();
Host::OnMediaCaptureStopped();
}
std::string System::GetGameSaveStateFileName(std::string_view serial, s32 slot)
{
if (slot < 0)
+17 -10
View File
@@ -27,6 +27,7 @@ struct CheatCode;
class CheatList;
class GPUTexture;
class MediaCapture;
namespace BIOS {
struct ImageInfo;
@@ -54,7 +55,7 @@ struct SystemBootParameters
bool load_image_to_ram = false;
bool force_software_renderer = false;
bool disable_achievements_hardcore_mode = false;
bool start_audio_dump = false;
bool start_media_capture = false;
};
struct SaveStateInfo
@@ -382,20 +383,22 @@ std::string GetGameMemoryCardPath(std::string_view serial, std::string_view path
s32 GetAudioOutputVolume();
void UpdateVolume();
/// Returns true if currently dumping audio.
bool IsDumpingAudio();
/// Starts dumping audio to a file. If no file name is provided, one will be generated automatically.
bool StartDumpingAudio(const char* filename = nullptr);
/// Stops dumping audio to file if it has been started.
void StopDumpingAudio();
/// Saves a screenshot to the specified file. If no file name is provided, one will be generated automatically.
bool SaveScreenshot(const char* filename = nullptr, DisplayScreenshotMode mode = g_settings.display_screenshot_mode,
DisplayScreenshotFormat format = g_settings.display_screenshot_format,
u8 quality = g_settings.display_screenshot_quality, bool compress_on_thread = true);
/// Returns the path that a new media capture would be saved to by default. Safe to call from any thread.
std::string GetNewMediaCapturePath(const std::string_view title, const std::string_view container);
/// Current media capture (if active).
MediaCapture* GetMediaCapture();
/// Media capture (video and/or audio). If no path is provided, one will be generated automatically.
bool StartMediaCapture(std::string path = {}, bool capture_video = g_settings.media_capture_video,
bool capture_audio = g_settings.media_capture_audio);
void StopMediaCapture();
/// Loads the cheat list for the current game title from the user directory.
bool LoadCheatList();
@@ -508,6 +511,10 @@ void OnPerformanceCountersUpdated();
/// Provided by the host; called when the running executable changes.
void OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name);
/// Called when media capture starts/stops.
void OnMediaCaptureStarted();
void OnMediaCaptureStopped();
/// Provided by the host; called once per frame at guest vsync.
void PumpMessagesOnCPUThread();