wip
This commit is contained in:
parent
eab8022be7
commit
424be088a1
|
@ -53,6 +53,8 @@
|
|||
<ClInclude Include="event.h" />
|
||||
<ClInclude Include="fifo_queue.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_wgl.h" />
|
||||
<ClInclude Include="gl\program.h" />
|
||||
|
@ -112,6 +114,7 @@
|
|||
<ClCompile Include="d3d11\texture.cpp" />
|
||||
<ClCompile Include="event.cpp" />
|
||||
<ClCompile Include="file_system.cpp" />
|
||||
<ClCompile Include="frame_dumper_wmf.cpp" />
|
||||
<ClCompile Include="gl\context.cpp" />
|
||||
<ClCompile Include="gl\context_wgl.cpp" />
|
||||
<ClCompile Include="gl\program.cpp" />
|
||||
|
|
|
@ -102,6 +102,8 @@
|
|||
<ClInclude Include="win32_progress_callback.h" />
|
||||
<ClInclude Include="make_array.h" />
|
||||
<ClInclude Include="shiftjis.h" />
|
||||
<ClInclude Include="frame_dumper_wmf.h" />
|
||||
<ClInclude Include="frame_dumper.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="jit_code_buffer.cpp" />
|
||||
|
@ -196,6 +198,7 @@
|
|||
<ClCompile Include="minizip_helpers.cpp" />
|
||||
<ClCompile Include="win32_progress_callback.cpp" />
|
||||
<ClCompile Include="shiftjis.cpp" />
|
||||
<ClCompile Include="frame_dumper_wmf.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="bitfield.natvis" />
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -11,7 +11,7 @@
|
|||
#ifdef _WIN32_WINNT
|
||||
#undef _WIN32_WINNT
|
||||
#endif
|
||||
#define _WIN32_WINNT _WIN32_WINNT_VISTA
|
||||
#define _WIN32_WINNT _WIN32_WINNT_WIN7
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
|
|
|
@ -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<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()
|
||||
{
|
||||
#ifdef WITH_IMGUI
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<FrameDumper> 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<CheatList> 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
|
|
@ -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<FrameDumper> 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<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
|
||||
|
|
|
@ -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(); });
|
||||
|
||||
|
|
|
@ -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 */)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue