wip
This commit is contained in:
parent
eab8022be7
commit
424be088a1
|
@ -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" />
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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
|
#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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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(); });
|
||||||
|
|
||||||
|
|
|
@ -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 */)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue