diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj
index d384e98e4..bdc7199ba 100644
--- a/src/common/common.vcxproj
+++ b/src/common/common.vcxproj
@@ -53,6 +53,8 @@
+
+
@@ -112,6 +114,7 @@
+
diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters
index 6d3229cc3..0a412e430 100644
--- a/src/common/common.vcxproj.filters
+++ b/src/common/common.vcxproj.filters
@@ -102,6 +102,8 @@
+
+
@@ -196,6 +198,7 @@
+
diff --git a/src/common/frame_dumper.h b/src/common/frame_dumper.h
new file mode 100644
index 000000000..7f089319a
--- /dev/null
+++ b/src/common/frame_dumper.h
@@ -0,0 +1,35 @@
+#pragma once
+#include "types.h"
+#include
+
+class FrameDumper
+{
+public:
+ using AudioSample = s16;
+ using Timestamp = u64;
+
+ virtual ~FrameDumper() = default;
+
+ ALWAYS_INLINE Timestamp GetTimestampFrequency() const { return m_timestamp_frequency; }
+ ALWAYS_INLINE Timestamp GetStartTimestamp() const { return m_start_timestamp; }
+ ALWAYS_INLINE u32 GetVideoWidth() const { return m_video_width; }
+ ALWAYS_INLINE u32 GetVideoHeight() const { return m_video_height; }
+ ALWAYS_INLINE u32 GetAudioChannels() const { return m_audio_channels; }
+
+ virtual bool Open(const char* output_file, u32 output_video_bitrate, u32 output_audio_bitrate, u32 video_width,
+ u32 video_height, float video_fps, u32 audio_sample_rate, u32 audio_channels,
+ Timestamp timestamp_frequency, Timestamp start_timestamp) = 0;
+ virtual void Close(Timestamp final_timestamp) = 0;
+
+ virtual void AddVideoFrame(const void* pixels, Timestamp timestamp) = 0;
+ virtual void AddAudioFrames(const AudioSample* frames, u32 num_frames, Timestamp timestamp) = 0;
+
+ static std::unique_ptr CreateWMFFrameDumper();
+
+protected:
+ Timestamp m_timestamp_frequency = 0;
+ Timestamp m_start_timestamp = 0;
+ u32 m_video_width = 0;
+ u32 m_video_height = 0;
+ u32 m_audio_channels = 0;
+};
diff --git a/src/common/frame_dumper_wmf.cpp b/src/common/frame_dumper_wmf.cpp
new file mode 100644
index 000000000..4889368de
--- /dev/null
+++ b/src/common/frame_dumper_wmf.cpp
@@ -0,0 +1,473 @@
+#include "frame_dumper_wmf.h"
+#include "log.h"
+#include "string_util.h"
+#include
+#include
+Log_SetChannel(FrameDumperWMF);
+
+#pragma comment(lib, "mfreadwrite")
+#pragma comment(lib, "mfplat")
+#pragma comment(lib, "mfuuid")
+#pragma comment(lib, "mf")
+
+static std::atomic_uint32_t s_mf_refcount{0};
+static bool s_com_initialized_by_us = false;
+
+static bool InitializeMF()
+{
+ if (s_mf_refcount.fetch_add(1) > 0)
+ return true;
+
+ HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ s_com_initialized_by_us = SUCCEEDED(hr);
+ if (FAILED(hr) && hr != RPC_E_CHANGED_MODE && hr != S_FALSE)
+ {
+ Log_ErrorPrintf("Failed to initialize COM");
+ s_mf_refcount.fetch_sub(1);
+ return false;
+ }
+
+ hr = MFStartup(MF_API_VERSION);
+ if (FAILED(hr))
+ {
+ Log_ErrorPrintf("MFStartup() failed: %08X", hr);
+ if (s_com_initialized_by_us)
+ {
+ s_com_initialized_by_us = false;
+ CoUninitialize();
+ }
+
+ s_mf_refcount.fetch_sub(1);
+ return false;
+ }
+
+ return true;
+}
+
+static void ShutdownMF()
+{
+ if (s_mf_refcount.fetch_sub(1) > 1)
+ return;
+
+ MFShutdown();
+
+ if (s_com_initialized_by_us)
+ {
+ CoUninitialize();
+ s_com_initialized_by_us = false;
+ }
+}
+
+static void LogHR(const char* reason, HRESULT hr)
+{
+ Log_ErrorPrintf("%s failed: %08X", reason, hr);
+}
+
+FrameDumperWMF::FrameDumperWMF()
+{
+ InitializeMF();
+}
+
+FrameDumperWMF::~FrameDumperWMF()
+{
+ if (m_sink_writer)
+ Close(std::max(m_last_frame_audio_timestamp, m_last_frame_video_timestamp) + 1);
+
+ ShutdownMF();
+}
+
+std::unique_ptr FrameDumper::CreateWMFFrameDumper()
+{
+ return std::make_unique();
+}
+
+bool FrameDumperWMF::Open(const char* output_file, u32 output_video_bitrate, u32 output_audio_bitrate, u32 video_width,
+ u32 video_height, float video_fps, u32 audio_sample_rate, u32 audio_channels,
+ Timestamp timestamp_frequency, Timestamp start_timestamp)
+{
+ ComPtr video_out_media_type;
+ HRESULT hr = MFCreateMediaType(video_out_media_type.GetAddressOf());
+ if (SUCCEEDED(hr))
+ hr = video_out_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
+ if (SUCCEEDED(hr))
+ hr = video_out_media_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264);
+ if (SUCCEEDED(hr))
+ hr = video_out_media_type->SetUINT32(MF_MT_AVG_BITRATE, output_video_bitrate);
+ if (SUCCEEDED(hr))
+ hr = video_out_media_type->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeSize(video_out_media_type.Get(), MF_MT_FRAME_SIZE, video_width, video_height);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeRatio(video_out_media_type.Get(), MF_MT_FRAME_RATE, 60, 1);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeRatio(video_out_media_type.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
+ // if (SUCCEEDED(hr))
+ // hr = video_out_media_type->SetBlob(MF_MT_MPEG4_SAMPLE_DESCRIPTION, nullptr, 0); // TODO: Is this needed?
+ if (FAILED(hr))
+ {
+ LogHR("Setting up output video type", hr);
+ return false;
+ }
+
+ ComPtr video_in_media_type;
+ hr = MFCreateMediaType(video_in_media_type.GetAddressOf());
+ if (SUCCEEDED(hr))
+ hr = video_in_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
+ if (SUCCEEDED(hr))
+ hr = video_in_media_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
+ if (SUCCEEDED(hr))
+ hr = video_in_media_type->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeSize(video_in_media_type.Get(), MF_MT_FRAME_SIZE, video_width, video_height);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeRatio(video_in_media_type.Get(), MF_MT_FRAME_RATE, 60, 1);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeRatio(video_in_media_type.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
+ if (FAILED(hr))
+ {
+ LogHR("Setting up input video type", hr);
+ return false;
+ }
+
+ ComPtr video_in_yuv_media_type;
+ hr = MFCreateMediaType(video_in_yuv_media_type.GetAddressOf());
+ if (SUCCEEDED(hr))
+ hr = video_in_yuv_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
+ if (SUCCEEDED(hr))
+ hr = video_in_yuv_media_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_YUY2);
+ if (SUCCEEDED(hr))
+ hr = video_in_yuv_media_type->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeSize(video_in_yuv_media_type.Get(), MF_MT_FRAME_SIZE, video_width, video_height);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeRatio(video_in_yuv_media_type.Get(), MF_MT_FRAME_RATE, 60, 1);
+ if (SUCCEEDED(hr))
+ hr = MFSetAttributeRatio(video_in_yuv_media_type.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1);
+ if (FAILED(hr))
+ {
+ LogHR("Setting up input yuv video type", hr);
+ return false;
+ }
+
+ ComPtr audio_out_media_type;
+ hr = MFCreateMediaType(audio_out_media_type.GetAddressOf());
+ if (SUCCEEDED(hr))
+ hr = audio_out_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
+ if (SUCCEEDED(hr))
+ hr = audio_out_media_type->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_AAC);
+ if (SUCCEEDED(hr))
+ hr = audio_out_media_type->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, sizeof(AudioSample) * 8);
+ if (SUCCEEDED(hr))
+ hr = audio_out_media_type->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, audio_sample_rate);
+ if (SUCCEEDED(hr))
+ hr = audio_out_media_type->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, audio_channels);
+ if (SUCCEEDED(hr))
+ {
+ hr = audio_out_media_type->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND,
+ (static_cast(audio_sample_rate) * audio_channels * output_audio_bitrate) /
+ 8);
+ }
+ if (FAILED(hr))
+ {
+ LogHR("Setting up output audio type", hr);
+ return false;
+ }
+
+ ComPtr audio_in_media_type;
+ hr = MFCreateMediaType(audio_in_media_type.GetAddressOf());
+ if (SUCCEEDED(hr))
+ hr = audio_in_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
+ if (SUCCEEDED(hr))
+ hr = audio_in_media_type->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
+ if (SUCCEEDED(hr))
+ hr = audio_in_media_type->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, sizeof(AudioSample) * 8);
+ if (SUCCEEDED(hr))
+ hr = audio_in_media_type->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, audio_sample_rate);
+ if (SUCCEEDED(hr))
+ hr = audio_in_media_type->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, audio_channels);
+ if (FAILED(hr))
+ {
+ LogHR("Setting up output audio type", hr);
+ return false;
+ }
+
+ hr = CoCreateInstance(__uuidof(CColorConvertDMO), NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(m_rgb_to_yuv_transform.ReleaseAndGetAddressOf()));
+ if (FAILED(hr))
+ {
+ LogHR("CoCreateInstance(CLSID_CColorConvertDMO)", hr);
+ return false;
+ }
+
+ hr = m_rgb_to_yuv_transform->SetInputType(0, video_in_media_type.Get(), 0);
+ if (SUCCEEDED(hr))
+ m_rgb_to_yuv_transform->SetOutputType(0, video_in_yuv_media_type.Get(), 0);
+ if (FAILED(hr))
+ {
+ LogHR("Set up YUV transform", hr);
+ return false;
+ }
+
+ ComPtr h264;
+ hr = CoCreateInstance(__uuidof(CMSH264EncoderMFT), NULL, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(h264.ReleaseAndGetAddressOf()));
+ if (FAILED(hr))
+ {
+ LogHR("blah", hr);
+ return false;
+ }
+
+ hr = h264->SetOutputType(0, video_out_media_type.Get(), 0);
+ if (SUCCEEDED(hr))
+ hr = h264->SetInputType(0, video_in_yuv_media_type.Get(), 0);
+
+ if (FAILED(hr))
+ return false;
+
+ const std::wstring wfilename(StringUtil::UTF8StringToWideString(output_file));
+
+ hr = MFCreateFile(MF_ACCESSMODE_WRITE, MF_OPENMODE_DELETE_IF_EXIST, MF_FILEFLAGS_NONE, wfilename.c_str(),
+ m_byte_stream.ReleaseAndGetAddressOf());
+ if (FAILED(hr))
+ {
+ LogHR("MFCreateFile", hr);
+ return false;
+ }
+
+ ComPtr sink;
+ hr = MFCreateMPEG4MediaSink(m_byte_stream.Get(), video_out_media_type.Get(), audio_out_media_type.Get(),
+ sink.GetAddressOf());
+ if (FAILED(hr))
+ {
+ LogHR("MFCreateMPEG4MediaSink", hr);
+ return false;
+ }
+
+ hr = MFCreateSinkWriterFromMediaSink(sink.Get(), nullptr, m_sink_writer.ReleaseAndGetAddressOf());
+ if (FAILED(hr))
+ {
+ LogHR("MFCreateSinkWriterFromURL", hr);
+ return false;
+ }
+
+ m_video_stream_index = 0;
+ m_audio_stream_index = 1;
+
+ // hr = m_sink_writer->AddStream(video_out_media_type.Get(), &m_video_stream_index);
+ // if (FAILED(hr))
+ // {
+ // LogHR("AddStream(Video)", hr);
+ // return false;
+ // }
+
+ hr = m_sink_writer->SetInputMediaType(m_video_stream_index, video_in_yuv_media_type.Get(), nullptr);
+ if (FAILED(hr))
+ {
+ LogHR("SetInputMediaType(Video)", hr);
+ return false;
+ }
+
+ // hr = m_sink_writer->AddStream(audio_out_media_type.Get(), &m_audio_stream_index);
+ // if (FAILED(hr))
+ // {
+ // LogHR("AddStream(Audio)", hr);
+ // return false;
+ // }
+
+ hr = m_sink_writer->SetInputMediaType(m_audio_stream_index, audio_in_media_type.Get(), nullptr);
+ if (FAILED(hr))
+ {
+ LogHR("SetInputMediaType(Audio)", hr);
+ return false;
+ }
+
+ hr = m_sink_writer->BeginWriting();
+ if (FAILED(hr))
+ {
+ LogHR("BeginWriting", hr);
+ return false;
+ }
+
+ m_timestamp_frequency = timestamp_frequency;
+ m_start_timestamp = start_timestamp;
+ m_video_width = video_width;
+ m_video_height = video_height;
+ m_audio_channels = audio_channels;
+ return true;
+}
+
+void FrameDumperWMF::Close(Timestamp final_timestamp)
+{
+ WriteLastVideoFrame(final_timestamp);
+ WriteLastAudioFrames(final_timestamp);
+
+ HRESULT hr = m_sink_writer->Finalize();
+ if (FAILED(hr))
+ LogHR("Finalize", hr);
+
+ m_sink_writer.Reset();
+}
+
+void FrameDumperWMF::AddVideoFrame(const void* pixels, Timestamp timestamp)
+{
+ WriteLastVideoFrame(timestamp);
+
+ m_last_frame_video_timestamp = timestamp;
+ m_last_frame_video.resize(m_video_width * m_video_height);
+ std::memcpy(m_last_frame_video.data(), pixels, m_video_width * m_video_height * sizeof(u32));
+}
+
+void FrameDumperWMF::AddAudioFrames(const AudioSample* frames, u32 num_frames, Timestamp timestamp)
+{
+ if (timestamp == m_last_frame_audio_timestamp)
+ {
+ const u32 start = static_cast(m_last_frame_audio.size());
+ m_last_frame_audio.resize(start + (num_frames * m_audio_channels));
+ std::memcpy(m_last_frame_audio.data() + start, frames, num_frames * m_audio_channels * sizeof(AudioSample));
+ }
+ else
+ {
+ WriteLastAudioFrames(timestamp);
+
+ m_last_frame_audio_timestamp = timestamp;
+ m_last_frame_audio.resize(num_frames * m_audio_channels);
+ std::memcpy(m_last_frame_audio.data(), frames, num_frames * m_audio_channels * sizeof(AudioSample));
+ }
+}
+
+LONGLONG FrameDumperWMF::TimestampToMFSampleTime(Timestamp timestamp) const
+{
+ // return in 100-nanosecond units
+ const Timestamp ticks_since_start = timestamp - m_start_timestamp;
+ return (ticks_since_start * 10000000) / m_timestamp_frequency;
+}
+
+LONGLONG FrameDumperWMF::TimestampToMFDuration(Timestamp timestamp) const
+{
+ // return in 100-nanosecond units
+ const Timestamp num = (timestamp * 10000000);
+ return (num /*+ (m_timestamp_frequency - 1)*/) / m_timestamp_frequency;
+}
+
+static Microsoft::WRL::ComPtr AllocAndCopy(u32 data_size, const void* data)
+{
+ Microsoft::WRL::ComPtr buffer;
+ HRESULT hr = MFCreateMemoryBuffer(data_size, buffer.GetAddressOf());
+ if (FAILED(hr))
+ {
+ LogHR("MFCreateMemoryBuffer", hr);
+ return {};
+ }
+
+ BYTE* mapped_ptr = nullptr;
+ hr = buffer->SetCurrentLength(data_size);
+ if (SUCCEEDED(hr))
+ hr = buffer->Lock(&mapped_ptr, nullptr, nullptr);
+ if (SUCCEEDED(hr))
+ {
+ std::memcpy(mapped_ptr, data, data_size);
+ hr = buffer->Unlock();
+ }
+ if (FAILED(hr))
+ {
+ LogHR("Buffer lock and upload", hr);
+ return {};
+ }
+
+ return buffer;
+}
+
+static Microsoft::WRL::ComPtr AllocAndCopySample(u32 data_size, const void* data, LONGLONG start_time,
+ LONGLONG duration)
+{
+ Microsoft::WRL::ComPtr buffer = AllocAndCopy(data_size, data);
+ if (!buffer)
+ return {};
+
+ Microsoft::WRL::ComPtr sample;
+ HRESULT hr = MFCreateSample(sample.GetAddressOf());
+ if (FAILED(hr))
+ {
+ LogHR("MFCreateSample", hr);
+ return {};
+ }
+
+ hr = sample->AddBuffer(buffer.Get());
+ if (FAILED(hr))
+ {
+ LogHR("AddBuffer", hr);
+ return {};
+ }
+
+ hr = sample->SetSampleTime(start_time);
+ if (SUCCEEDED(hr))
+ hr = sample->SetSampleDuration(duration);
+ if (FAILED(hr))
+ {
+ LogHR("SetSample{Time,Duration}", hr);
+ return {};
+ }
+
+ return sample;
+}
+
+void FrameDumperWMF::WriteLastVideoFrame(Timestamp next_timestamp)
+{
+ if (m_last_frame_video.empty())
+ return;
+
+ const u32 data_size = m_video_width * m_video_height * sizeof(u32);
+ const LONGLONG start_time = TimestampToMFSampleTime(m_last_frame_video_timestamp);
+ const LONGLONG duration = TimestampToMFDuration(next_timestamp - m_last_frame_video_timestamp);
+ Log_InfoPrintf("Write video frame @ %llu for %llu", start_time, duration);
+ ComPtr sample = AllocAndCopySample(data_size, m_last_frame_video.data(), start_time, duration);
+ if (sample)
+ {
+ HRESULT hr = m_rgb_to_yuv_transform->ProcessInput(0, sample.Get(), 0);
+ if (SUCCEEDED(hr))
+ {
+ MFT_OUTPUT_DATA_BUFFER dbuf = {};
+ DWORD status = 0;
+ hr = m_rgb_to_yuv_transform->ProcessOutput(0, 1, &dbuf, &status);
+ if (SUCCEEDED(hr))
+ {
+ HRESULT hr = m_sink_writer->WriteSample(m_video_stream_index, dbuf.pSample);
+ if (FAILED(hr))
+ LogHR("WriteSample(Video)", hr);
+
+ dbuf.pSample->Release();
+ }
+ else
+ {
+ LogHR("ProcessOutput", hr);
+ }
+ }
+ }
+
+ m_last_frame_video.clear();
+ m_last_frame_video_timestamp = 0;
+}
+
+void FrameDumperWMF::WriteLastAudioFrames(Timestamp next_timestamp)
+{
+ if (m_last_frame_audio.empty())
+ return;
+
+#if 1
+ const u32 data_size = static_cast(m_last_frame_audio.size()) * sizeof(AudioSample);
+ const LONGLONG start_time = TimestampToMFSampleTime(m_last_frame_audio_timestamp);
+ const LONGLONG duration = TimestampToMFDuration(next_timestamp - m_last_frame_audio_timestamp);
+ Log_InfoPrintf("Write %u audio frames @ %llu for %llu", u32(m_last_frame_audio.size()) / m_audio_channels, start_time,
+ duration);
+ ComPtr sample = AllocAndCopySample(data_size, m_last_frame_audio.data(), start_time, duration);
+ if (sample)
+ {
+ HRESULT hr = m_sink_writer->WriteSample(m_audio_stream_index, sample.Get());
+ if (FAILED(hr))
+ LogHR("WriteSample(Audio)", hr);
+ }
+#endif
+
+ m_last_frame_audio.clear();
+ m_last_frame_audio_timestamp = 0;
+}
diff --git a/src/common/frame_dumper_wmf.h b/src/common/frame_dumper_wmf.h
new file mode 100644
index 000000000..4391ced4c
--- /dev/null
+++ b/src/common/frame_dumper_wmf.h
@@ -0,0 +1,44 @@
+#include "frame_dumper.h"
+#include "windows_headers.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+class FrameDumperWMF final : public FrameDumper
+{
+public:
+ template
+ using ComPtr = Microsoft::WRL::ComPtr;
+
+ FrameDumperWMF();
+ ~FrameDumperWMF() override;
+
+ bool Open(const char* output_file, u32 output_video_bitrate, u32 output_audio_bitrate, u32 video_width,
+ u32 video_height, float video_fps, u32 audio_sample_rate, u32 audio_channels, Timestamp timestamp_frequency,
+ Timestamp start_timestamp) override;
+
+ void Close(Timestamp final_timestamp) override;
+
+ void AddVideoFrame(const void* pixels, Timestamp timestamp) override;
+ void AddAudioFrames(const AudioSample* frames, u32 num_frames, Timestamp timestamp) override;
+
+private:
+ LONGLONG TimestampToMFSampleTime(Timestamp timestamp) const;
+ LONGLONG TimestampToMFDuration(Timestamp timestamp) const;
+ void WriteLastVideoFrame(Timestamp next_timestamp);
+ void WriteLastAudioFrames(Timestamp next_timestamp);
+
+ ComPtr m_byte_stream;
+ ComPtr m_sink_writer;
+ ComPtr m_rgb_to_yuv_transform;
+ DWORD m_video_stream_index = 0;
+ DWORD m_audio_stream_index = 0;
+
+ std::vector m_last_frame_video;
+ std::vector m_last_frame_audio;
+ Timestamp m_last_frame_video_timestamp = 0;
+ Timestamp m_last_frame_audio_timestamp = 0;
+};
\ No newline at end of file
diff --git a/src/common/windows_headers.h b/src/common/windows_headers.h
index 178a8160e..00944251c 100644
--- a/src/common/windows_headers.h
+++ b/src/common/windows_headers.h
@@ -11,7 +11,7 @@
#ifdef _WIN32_WINNT
#undef _WIN32_WINNT
#endif
-#define _WIN32_WINNT _WIN32_WINNT_VISTA
+#define _WIN32_WINNT _WIN32_WINNT_WIN7
#include
diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp
index ebfd0559d..a160e0a47 100644
--- a/src/core/gpu.cpp
+++ b/src/core/gpu.cpp
@@ -1,5 +1,6 @@
#include "gpu.h"
#include "common/file_system.h"
+#include "common/frame_dumper.h"
#include "common/heap_array.h"
#include "common/log.h"
#include "common/state_wrapper.h"
@@ -762,6 +763,9 @@ void GPU::CRTCTickEvent(TickCount ticks)
// flush any pending draws and "scan out" the image
FlushRender();
UpdateDisplay();
+ if (System::IsDumpingFrames())
+ DumpCurrentFrame(TimingEvents::GetGlobalTickCounter());
+
System::FrameDone();
// switch fields early. this is needed so we draw to the correct one.
@@ -1378,6 +1382,41 @@ bool GPU::DumpVRAMToFile(const char* filename, u32 width, u32 height, u32 stride
return (stbi_write_png_to_func(write_func, fp.get(), width, height, 4, rgba8_buf.get(), sizeof(u32) * width) != 0);
}
+u32 GPU::GetFrameDumpWidth() const
+{
+ return m_crtc_state.display_vram_width;
+}
+
+u32 GPU::GetFrameDumpHeight() const
+{
+ return m_crtc_state.display_vram_height;
+}
+
+void GPU::DumpCurrentFrame(u64 timestamp)
+{
+ const u32 width = m_crtc_state.display_vram_width;
+ const u32 height = m_crtc_state.display_vram_height;
+ if (!System::CheckFrameDumpVideoSize(width, height))
+ return;
+
+ ReadVRAM(m_crtc_state.display_vram_left, m_crtc_state.display_vram_top, width, height);
+
+ auto rgba8_buf = std::make_unique(width * height);
+ const u16* ptr_in = &m_vram_ptr[m_crtc_state.display_vram_top * VRAM_WIDTH + m_crtc_state.display_vram_left];
+ u32* ptr_out = rgba8_buf.get();
+ for (u32 row = 0; row < height; row++)
+ {
+ const u16* row_ptr_in = ptr_in;
+
+ for (u32 col = 0; col < width; col++)
+ *(ptr_out++) = RGBA5551ToRGBA8888(*(row_ptr_in++) | u16(0x8000));
+
+ ptr_in += VRAM_WIDTH;
+ }
+
+ System::g_frame_dumper->AddVideoFrame(rgba8_buf.get(), timestamp);
+}
+
void GPU::DrawDebugStateWindow()
{
#ifdef WITH_IMGUI
diff --git a/src/core/gpu.h b/src/core/gpu.h
index ee217670e..3964b6042 100644
--- a/src/core/gpu.h
+++ b/src/core/gpu.h
@@ -206,6 +206,11 @@ public:
// Returns the video clock frequency.
TickCount GetCRTCFrequency() const;
+ // Frame dumping.
+ virtual u32 GetFrameDumpWidth() const;
+ virtual u32 GetFrameDumpHeight() const;
+ virtual void DumpCurrentFrame(u64 timestamp);
+
protected:
TickCount CRTCTicksToSystemTicks(TickCount crtc_ticks, TickCount fractional_ticks) const;
TickCount SystemTicksToCRTCTicks(TickCount sysclk_ticks, TickCount* fractional_ticks) const;
diff --git a/src/core/spu.cpp b/src/core/spu.cpp
index 0272144bc..5f09781db 100644
--- a/src/core/spu.cpp
+++ b/src/core/spu.cpp
@@ -1,6 +1,7 @@
#include "spu.h"
#include "cdrom.h"
#include "common/audio_stream.h"
+#include "common/frame_dumper.h"
#include "common/log.h"
#include "common/state_wrapper.h"
#include "common/wav_writer.h"
@@ -804,6 +805,8 @@ void SPU::Execute(TickCount ticks)
if (m_dump_writer)
m_dump_writer->WriteFrames(output_frame_start, frames_in_this_batch);
+ if (System::IsDumpingFrames())
+ System::g_frame_dumper->AddAudioFrames(output_frame_start, frames_in_this_batch, TimingEvents::GetGlobalTickCounter());
output_stream->EndWrite(frames_in_this_batch);
remaining_frames -= frames_in_this_batch;
diff --git a/src/core/spu.h b/src/core/spu.h
index 2b88c1062..c2aa82542 100644
--- a/src/core/spu.h
+++ b/src/core/spu.h
@@ -17,6 +17,8 @@ class TimingEvent;
class SPU
{
public:
+ static constexpr u32 SAMPLE_RATE = 44100;
+
SPU();
~SPU();
@@ -55,7 +57,6 @@ private:
static constexpr u32 NUM_VOICE_REGISTERS = 8;
static constexpr u32 VOICE_ADDRESS_SHIFT = 3;
static constexpr u32 NUM_SAMPLES_PER_ADPCM_BLOCK = 28;
- static constexpr u32 SAMPLE_RATE = 44100;
static constexpr u32 SYSCLK_TICKS_PER_SPU_TICK = System::MASTER_CLOCK / SAMPLE_RATE; // 0x300
static constexpr s16 ENVELOPE_MIN_VOLUME = 0;
static constexpr s16 ENVELOPE_MAX_VOLUME = 0x7FFF;
diff --git a/src/core/system.cpp b/src/core/system.cpp
index 04ce5618d..211d68e36 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -5,6 +5,7 @@
#include "cheats.h"
#include "common/audio_stream.h"
#include "common/file_system.h"
+#include "common/frame_dumper.h"
#include "common/iso_reader.h"
#include "common/log.h"
#include "common/state_wrapper.h"
@@ -69,6 +70,7 @@ static State s_state = State::Shutdown;
static ConsoleRegion s_region = ConsoleRegion::NTSC_U;
TickCount g_ticks_per_second = MASTER_CLOCK;
+std::unique_ptr g_frame_dumper;
static TickCount s_max_slice_ticks = MASTER_CLOCK / 10;
static u32 s_frame_number = 1;
static u32 s_internal_frame_number = 1;
@@ -763,6 +765,7 @@ void Shutdown()
if (s_state == State::Shutdown)
return;
+ StopDumpingFrames();
g_sio.Shutdown();
g_mdec.Shutdown();
g_spu.Shutdown();
@@ -1721,4 +1724,55 @@ void SetCheatList(std::unique_ptr cheats)
s_cheat_list = std::move(cheats);
}
+bool StartDumpingFrames(const char* output_filename)
+{
+ StopDumpingFrames();
+
+ const u32 video_bitrate = 5000 * 1000;
+ const u32 audio_bitrate = 128 * 1000;
+
+ const FrameDumper::Timestamp frequency =
+ static_cast(ScaleTicksToOverclock(static_cast(MASTER_CLOCK)));
+
+ g_frame_dumper = FrameDumper::CreateWMFFrameDumper();
+ if (!g_frame_dumper ||
+ !g_frame_dumper->Open(output_filename, video_bitrate, audio_bitrate, g_gpu->GetFrameDumpWidth(),
+ g_gpu->GetFrameDumpHeight(), s_throttle_frequency, SPU::SAMPLE_RATE, 2, frequency,
+ TimingEvents::GetGlobalTickCounter()))
+ {
+ g_host_interface->AddOSDMessage(
+ g_host_interface->TranslateStdString("OSDMessage", "Failed to start frame dumping."), 10.0f);
+ g_frame_dumper.reset();
+ return false;
+ }
+
+ g_host_interface->AddFormattedOSDMessage(
+ 10.0f, g_host_interface->TranslateString("OSDMessage", "Started dumping frames to '%s' (%ux%u, %ukbps)"),
+ output_filename, g_frame_dumper->GetVideoWidth(), g_frame_dumper->GetVideoHeight(),
+ (video_bitrate + audio_bitrate) / 1000);
+
+ return true;
+}
+
+bool CheckFrameDumpVideoSize(u32 expected_width, u32 expected_height)
+{
+ if (!g_frame_dumper)
+ return false;
+
+ if (g_frame_dumper->GetVideoWidth() != expected_width || g_frame_dumper->GetVideoHeight() != expected_height)
+ return false;
+
+ return true;
+}
+
+void StopDumpingFrames()
+{
+ if (!g_frame_dumper)
+ return;
+
+ g_host_interface->AddOSDMessage(g_host_interface->TranslateStdString("OSDMessage", "Stopped dumping frames."), 10.0f);
+ g_frame_dumper->Close(TimingEvents::GetGlobalTickCounter());
+ g_frame_dumper.reset();
+}
+
} // namespace System
\ No newline at end of file
diff --git a/src/core/system.h b/src/core/system.h
index 1971936a2..36e85a87b 100644
--- a/src/core/system.h
+++ b/src/core/system.h
@@ -10,6 +10,7 @@
class ByteStream;
class CDImage;
+class FrameDumper;
class StateWrapper;
class Controller;
@@ -55,6 +56,7 @@ enum class State
};
extern TickCount g_ticks_per_second;
+extern std::unique_ptr g_frame_dumper;
/// Returns true if the filename is a PlayStation executable we can inject.
bool IsExeFileName(const char* path);
@@ -209,4 +211,15 @@ void ApplyCheatCode(const CheatCode& code);
/// Sets or clears the provided cheat list, applying every frame.
void SetCheatList(std::unique_ptr cheats);
+// Frame dumping.
+ALWAYS_INLINE bool IsDumpingFrames()
+{
+ return static_cast(g_frame_dumper);
+}
+
+std::string GenerateFrameDumpFilename();
+bool StartDumpingFrames(const char* output_filename);
+bool CheckFrameDumpVideoSize(u32 expected_width, u32 expected_height);
+void StopDumpingFrames();
+
} // namespace System
diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp
index 8a95cf639..0de6b1f43 100644
--- a/src/duckstation-sdl/sdl_host_interface.cpp
+++ b/src/duckstation-sdl/sdl_host_interface.cpp
@@ -941,9 +941,8 @@ void SDLHostInterface::DrawQuickSettingsMenu()
m_settings_copy.gpu_pgxp_enable);
settings_changed |=
ImGui::MenuItem("PGXP CPU Instructions", nullptr, &m_settings_copy.gpu_pgxp_cpu, m_settings_copy.gpu_pgxp_enable);
- settings_changed |=
- ImGui::MenuItem("PGXP Preserve Projection Precision", nullptr, &m_settings_copy.gpu_pgxp_preserve_proj_fp,
- m_settings_copy.gpu_pgxp_enable);
+ settings_changed |= ImGui::MenuItem("PGXP Preserve Projection Precision", nullptr,
+ &m_settings_copy.gpu_pgxp_preserve_proj_fp, m_settings_copy.gpu_pgxp_enable);
ImGui::EndMenu();
}
@@ -1019,6 +1018,14 @@ void SDLHostInterface::DrawQuickSettingsMenu()
StopDumpingAudio();
}
+ if (ImGui::MenuItem("Dump Frames", nullptr, IsDumpingFrames(), System::IsValid()))
+ {
+ if (!IsDumpingFrames())
+ StartDumpingFrames();
+ else
+ StopDumpingFrames();
+ }
+
if (ImGui::MenuItem("Save Screenshot"))
RunLater([this]() { SaveScreenshot(); });
diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp
index 2c2c3dfe6..56688ebfb 100644
--- a/src/frontend-common/common_host_interface.cpp
+++ b/src/frontend-common/common_host_interface.cpp
@@ -2254,6 +2254,46 @@ void CommonHostInterface::StopDumpingAudio()
AddOSDMessage("Stopped dumping audio.", 5.0f);
}
+bool CommonHostInterface::IsDumpingFrames() const
+{
+ return System::IsDumpingFrames();
+}
+
+bool CommonHostInterface::StartDumpingFrames(const char* filename /* = nullptr */)
+{
+ if (!System::IsValid())
+ return false;
+
+ std::string auto_filename;
+ if (!filename)
+ {
+ const std::string& running_title = System::GetRunningTitle();
+ const char* extension = "mp4";
+ if (running_title.empty())
+ {
+ auto_filename = GetUserDirectoryRelativePath("dump" FS_OSPATH_SEPARATOR_STR "frames" FS_OSPATH_SEPARATOR_STR "%s.%s",
+ GetTimestampStringForFileName().GetCharArray(), extension);
+ }
+ else
+ {
+ auto_filename = GetUserDirectoryRelativePath("dump" FS_OSPATH_SEPARATOR_STR "frames" FS_OSPATH_SEPARATOR_STR "%s_%s.%s", running_title.c_str(),
+ GetTimestampStringForFileName().GetCharArray(), extension);
+ }
+
+ filename = auto_filename.c_str();
+ }
+
+ return System::StartDumpingFrames(filename);
+}
+
+void CommonHostInterface::StopDumpingFrames()
+{
+ if (!System::IsValid())
+ return;
+
+ System::StopDumpingFrames();
+}
+
bool CommonHostInterface::SaveScreenshot(const char* filename /* = nullptr */, bool full_resolution /* = true */,
bool apply_aspect_ratio /* = true */)
{
diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h
index 9d638466d..da4e015d3 100644
--- a/src/frontend-common/common_host_interface.h
+++ b/src/frontend-common/common_host_interface.h
@@ -150,6 +150,15 @@ public:
/// Stops dumping audio to file if it has been started.
void StopDumpingAudio();
+ /// Returns true if currently dumping frames.
+ bool IsDumpingFrames() const;
+
+ /// Starts dumping frames to a file. If no file name is provided, one will be generated automatically.
+ bool StartDumpingFrames(const char* filename = nullptr);
+
+ /// Stops dumping audio to file if it has been started.
+ void StopDumpingFrames();
+
/// Saves a screenshot to the specified file. IF no file name is provided, one will be generated automatically.
bool SaveScreenshot(const char* filename = nullptr, bool full_resolution = true, bool apply_aspect_ratio = true);