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