This commit is contained in:
Connor McLaughlin 2020-10-13 02:14:12 +10:00
parent eab8022be7
commit 424be088a1
15 changed files with 734 additions and 5 deletions

View File

@ -53,6 +53,8 @@
<ClInclude Include="event.h" /> <ClInclude Include="event.h" />
<ClInclude Include="fifo_queue.h" /> <ClInclude Include="fifo_queue.h" />
<ClInclude Include="file_system.h" /> <ClInclude Include="file_system.h" />
<ClInclude Include="frame_dumper.h" />
<ClInclude Include="frame_dumper_wmf.h" />
<ClInclude Include="gl\context.h" /> <ClInclude Include="gl\context.h" />
<ClInclude Include="gl\context_wgl.h" /> <ClInclude Include="gl\context_wgl.h" />
<ClInclude Include="gl\program.h" /> <ClInclude Include="gl\program.h" />
@ -112,6 +114,7 @@
<ClCompile Include="d3d11\texture.cpp" /> <ClCompile Include="d3d11\texture.cpp" />
<ClCompile Include="event.cpp" /> <ClCompile Include="event.cpp" />
<ClCompile Include="file_system.cpp" /> <ClCompile Include="file_system.cpp" />
<ClCompile Include="frame_dumper_wmf.cpp" />
<ClCompile Include="gl\context.cpp" /> <ClCompile Include="gl\context.cpp" />
<ClCompile Include="gl\context_wgl.cpp" /> <ClCompile Include="gl\context_wgl.cpp" />
<ClCompile Include="gl\program.cpp" /> <ClCompile Include="gl\program.cpp" />

View File

@ -102,6 +102,8 @@
<ClInclude Include="win32_progress_callback.h" /> <ClInclude Include="win32_progress_callback.h" />
<ClInclude Include="make_array.h" /> <ClInclude Include="make_array.h" />
<ClInclude Include="shiftjis.h" /> <ClInclude Include="shiftjis.h" />
<ClInclude Include="frame_dumper_wmf.h" />
<ClInclude Include="frame_dumper.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="jit_code_buffer.cpp" /> <ClCompile Include="jit_code_buffer.cpp" />
@ -196,6 +198,7 @@
<ClCompile Include="minizip_helpers.cpp" /> <ClCompile Include="minizip_helpers.cpp" />
<ClCompile Include="win32_progress_callback.cpp" /> <ClCompile Include="win32_progress_callback.cpp" />
<ClCompile Include="shiftjis.cpp" /> <ClCompile Include="shiftjis.cpp" />
<ClCompile Include="frame_dumper_wmf.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Natvis Include="bitfield.natvis" /> <Natvis Include="bitfield.natvis" />

35
src/common/frame_dumper.h Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include "types.h"
#include <memory>
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<FrameDumper> 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;
};

View File

@ -0,0 +1,473 @@
#include "frame_dumper_wmf.h"
#include "log.h"
#include "string_util.h"
#include <atomic>
#include <wmcodecdsp.h>
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> FrameDumper::CreateWMFFrameDumper()
{
return std::make_unique<FrameDumperWMF>();
}
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<IMFMediaType> 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<IMFMediaType> 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<IMFMediaType> 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<IMFMediaType> 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<u32>(audio_sample_rate) * audio_channels * output_audio_bitrate) /
8);
}
if (FAILED(hr))
{
LogHR("Setting up output audio type", hr);
return false;
}
ComPtr<IMFMediaType> 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<IMFTransform> 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<IMFMediaSink> 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<u32>(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<IMFMediaBuffer> AllocAndCopy(u32 data_size, const void* data)
{
Microsoft::WRL::ComPtr<IMFMediaBuffer> 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<IMFSample> AllocAndCopySample(u32 data_size, const void* data, LONGLONG start_time,
LONGLONG duration)
{
Microsoft::WRL::ComPtr<IMFMediaBuffer> buffer = AllocAndCopy(data_size, data);
if (!buffer)
return {};
Microsoft::WRL::ComPtr<IMFSample> 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<IMFSample> 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<u32>(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<IMFSample> 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;
}

View File

@ -0,0 +1,44 @@
#include "frame_dumper.h"
#include "windows_headers.h"
#include <Mferror.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <vector>
#include <wrl/client.h>
class FrameDumperWMF final : public FrameDumper
{
public:
template<typename T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
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<IMFByteStream> m_byte_stream;
ComPtr<IMFSinkWriter> m_sink_writer;
ComPtr<IMFTransform> m_rgb_to_yuv_transform;
DWORD m_video_stream_index = 0;
DWORD m_audio_stream_index = 0;
std::vector<u32> m_last_frame_video;
std::vector<s16> m_last_frame_audio;
Timestamp m_last_frame_video_timestamp = 0;
Timestamp m_last_frame_audio_timestamp = 0;
};

View File

@ -11,7 +11,7 @@
#ifdef _WIN32_WINNT #ifdef _WIN32_WINNT
#undef _WIN32_WINNT #undef _WIN32_WINNT
#endif #endif
#define _WIN32_WINNT _WIN32_WINNT_VISTA #define _WIN32_WINNT _WIN32_WINNT_WIN7
#include <windows.h> #include <windows.h>

View File

@ -1,5 +1,6 @@
#include "gpu.h" #include "gpu.h"
#include "common/file_system.h" #include "common/file_system.h"
#include "common/frame_dumper.h"
#include "common/heap_array.h" #include "common/heap_array.h"
#include "common/log.h" #include "common/log.h"
#include "common/state_wrapper.h" #include "common/state_wrapper.h"
@ -762,6 +763,9 @@ void GPU::CRTCTickEvent(TickCount ticks)
// flush any pending draws and "scan out" the image // flush any pending draws and "scan out" the image
FlushRender(); FlushRender();
UpdateDisplay(); UpdateDisplay();
if (System::IsDumpingFrames())
DumpCurrentFrame(TimingEvents::GetGlobalTickCounter());
System::FrameDone(); System::FrameDone();
// switch fields early. this is needed so we draw to the correct one. // 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); 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<u32[]>(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() void GPU::DrawDebugStateWindow()
{ {
#ifdef WITH_IMGUI #ifdef WITH_IMGUI

View File

@ -206,6 +206,11 @@ public:
// Returns the video clock frequency. // Returns the video clock frequency.
TickCount GetCRTCFrequency() const; TickCount GetCRTCFrequency() const;
// Frame dumping.
virtual u32 GetFrameDumpWidth() const;
virtual u32 GetFrameDumpHeight() const;
virtual void DumpCurrentFrame(u64 timestamp);
protected: protected:
TickCount CRTCTicksToSystemTicks(TickCount crtc_ticks, TickCount fractional_ticks) const; TickCount CRTCTicksToSystemTicks(TickCount crtc_ticks, TickCount fractional_ticks) const;
TickCount SystemTicksToCRTCTicks(TickCount sysclk_ticks, TickCount* fractional_ticks) const; TickCount SystemTicksToCRTCTicks(TickCount sysclk_ticks, TickCount* fractional_ticks) const;

View File

@ -1,6 +1,7 @@
#include "spu.h" #include "spu.h"
#include "cdrom.h" #include "cdrom.h"
#include "common/audio_stream.h" #include "common/audio_stream.h"
#include "common/frame_dumper.h"
#include "common/log.h" #include "common/log.h"
#include "common/state_wrapper.h" #include "common/state_wrapper.h"
#include "common/wav_writer.h" #include "common/wav_writer.h"
@ -804,6 +805,8 @@ void SPU::Execute(TickCount ticks)
if (m_dump_writer) if (m_dump_writer)
m_dump_writer->WriteFrames(output_frame_start, frames_in_this_batch); 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); output_stream->EndWrite(frames_in_this_batch);
remaining_frames -= frames_in_this_batch; remaining_frames -= frames_in_this_batch;

View File

@ -17,6 +17,8 @@ class TimingEvent;
class SPU class SPU
{ {
public: public:
static constexpr u32 SAMPLE_RATE = 44100;
SPU(); SPU();
~SPU(); ~SPU();
@ -55,7 +57,6 @@ private:
static constexpr u32 NUM_VOICE_REGISTERS = 8; static constexpr u32 NUM_VOICE_REGISTERS = 8;
static constexpr u32 VOICE_ADDRESS_SHIFT = 3; static constexpr u32 VOICE_ADDRESS_SHIFT = 3;
static constexpr u32 NUM_SAMPLES_PER_ADPCM_BLOCK = 28; 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 u32 SYSCLK_TICKS_PER_SPU_TICK = System::MASTER_CLOCK / SAMPLE_RATE; // 0x300
static constexpr s16 ENVELOPE_MIN_VOLUME = 0; static constexpr s16 ENVELOPE_MIN_VOLUME = 0;
static constexpr s16 ENVELOPE_MAX_VOLUME = 0x7FFF; static constexpr s16 ENVELOPE_MAX_VOLUME = 0x7FFF;

View File

@ -5,6 +5,7 @@
#include "cheats.h" #include "cheats.h"
#include "common/audio_stream.h" #include "common/audio_stream.h"
#include "common/file_system.h" #include "common/file_system.h"
#include "common/frame_dumper.h"
#include "common/iso_reader.h" #include "common/iso_reader.h"
#include "common/log.h" #include "common/log.h"
#include "common/state_wrapper.h" #include "common/state_wrapper.h"
@ -69,6 +70,7 @@ static State s_state = State::Shutdown;
static ConsoleRegion s_region = ConsoleRegion::NTSC_U; static ConsoleRegion s_region = ConsoleRegion::NTSC_U;
TickCount g_ticks_per_second = MASTER_CLOCK; TickCount g_ticks_per_second = MASTER_CLOCK;
std::unique_ptr<FrameDumper> g_frame_dumper;
static TickCount s_max_slice_ticks = MASTER_CLOCK / 10; static TickCount s_max_slice_ticks = MASTER_CLOCK / 10;
static u32 s_frame_number = 1; static u32 s_frame_number = 1;
static u32 s_internal_frame_number = 1; static u32 s_internal_frame_number = 1;
@ -763,6 +765,7 @@ void Shutdown()
if (s_state == State::Shutdown) if (s_state == State::Shutdown)
return; return;
StopDumpingFrames();
g_sio.Shutdown(); g_sio.Shutdown();
g_mdec.Shutdown(); g_mdec.Shutdown();
g_spu.Shutdown(); g_spu.Shutdown();
@ -1721,4 +1724,55 @@ void SetCheatList(std::unique_ptr<CheatList> cheats)
s_cheat_list = std::move(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<FrameDumper::Timestamp>(ScaleTicksToOverclock(static_cast<TickCount>(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 } // namespace System

View File

@ -10,6 +10,7 @@
class ByteStream; class ByteStream;
class CDImage; class CDImage;
class FrameDumper;
class StateWrapper; class StateWrapper;
class Controller; class Controller;
@ -55,6 +56,7 @@ enum class State
}; };
extern TickCount g_ticks_per_second; extern TickCount g_ticks_per_second;
extern std::unique_ptr<FrameDumper> g_frame_dumper;
/// Returns true if the filename is a PlayStation executable we can inject. /// Returns true if the filename is a PlayStation executable we can inject.
bool IsExeFileName(const char* path); bool IsExeFileName(const char* path);
@ -209,4 +211,15 @@ void ApplyCheatCode(const CheatCode& code);
/// Sets or clears the provided cheat list, applying every frame. /// Sets or clears the provided cheat list, applying every frame.
void SetCheatList(std::unique_ptr<CheatList> cheats); void SetCheatList(std::unique_ptr<CheatList> cheats);
// Frame dumping.
ALWAYS_INLINE bool IsDumpingFrames()
{
return static_cast<bool>(g_frame_dumper);
}
std::string GenerateFrameDumpFilename();
bool StartDumpingFrames(const char* output_filename);
bool CheckFrameDumpVideoSize(u32 expected_width, u32 expected_height);
void StopDumpingFrames();
} // namespace System } // namespace System

View File

@ -941,9 +941,8 @@ void SDLHostInterface::DrawQuickSettingsMenu()
m_settings_copy.gpu_pgxp_enable); m_settings_copy.gpu_pgxp_enable);
settings_changed |= settings_changed |=
ImGui::MenuItem("PGXP CPU Instructions", nullptr, &m_settings_copy.gpu_pgxp_cpu, m_settings_copy.gpu_pgxp_enable); ImGui::MenuItem("PGXP CPU Instructions", nullptr, &m_settings_copy.gpu_pgxp_cpu, m_settings_copy.gpu_pgxp_enable);
settings_changed |= settings_changed |= ImGui::MenuItem("PGXP Preserve Projection Precision", nullptr,
ImGui::MenuItem("PGXP Preserve Projection Precision", nullptr, &m_settings_copy.gpu_pgxp_preserve_proj_fp, &m_settings_copy.gpu_pgxp_preserve_proj_fp, m_settings_copy.gpu_pgxp_enable);
m_settings_copy.gpu_pgxp_enable);
ImGui::EndMenu(); ImGui::EndMenu();
} }
@ -1019,6 +1018,14 @@ void SDLHostInterface::DrawQuickSettingsMenu()
StopDumpingAudio(); StopDumpingAudio();
} }
if (ImGui::MenuItem("Dump Frames", nullptr, IsDumpingFrames(), System::IsValid()))
{
if (!IsDumpingFrames())
StartDumpingFrames();
else
StopDumpingFrames();
}
if (ImGui::MenuItem("Save Screenshot")) if (ImGui::MenuItem("Save Screenshot"))
RunLater([this]() { SaveScreenshot(); }); RunLater([this]() { SaveScreenshot(); });

View File

@ -2254,6 +2254,46 @@ void CommonHostInterface::StopDumpingAudio()
AddOSDMessage("Stopped dumping audio.", 5.0f); 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 CommonHostInterface::SaveScreenshot(const char* filename /* = nullptr */, bool full_resolution /* = true */,
bool apply_aspect_ratio /* = true */) bool apply_aspect_ratio /* = true */)
{ {

View File

@ -150,6 +150,15 @@ public:
/// Stops dumping audio to file if it has been started. /// Stops dumping audio to file if it has been started.
void StopDumpingAudio(); 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. /// 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); bool SaveScreenshot(const char* filename = nullptr, bool full_resolution = true, bool apply_aspect_ratio = true);