diff --git a/CMakeLists.txt b/CMakeLists.txt index 64424f455534..12b7b7874c7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1190,6 +1190,14 @@ else() ext/disarm.cpp) endif() +if (NOT MOBILE_DEVICE) + set(CoreExtra ${CoreExtra} + Core/AVIDump.cpp + Core/AVIDump.h + Core/WaveFile.cpp + Core/WaveFile.h) +endif() + if(ARMV7) set(CORE_NEON Core/Util/AudioFormatNEON.cpp Core/Util/AudioFormatNEON.h) endif() diff --git a/Core/AVIDump.cpp b/Core/AVIDump.cpp new file mode 100644 index 000000000000..9e0680dc69f9 --- /dev/null +++ b/Core/AVIDump.cpp @@ -0,0 +1,261 @@ +// Copyright 2009 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#ifndef MOBILE_DEVICE +#if defined(__FreeBSD__) +#define __STDC_CONSTANT_MACROS 1 +#endif + +#include +#include + +extern "C" { +#include +#include +#include +#include +} + +#include "Common/FileUtil.h" +#include "Common/MsgHandler.h" +#include "Common/ColorConv.h" + +#include "Core/Config.h" +#include "Core/AVIDump.h" +#include "Core/System.h" +#include "Core/Screenshot.h" + +#include "GPU/Common/GPUDebugInterface.h" + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55, 28, 1) +#define av_frame_alloc avcodec_alloc_frame +#define av_frame_free avcodec_free_frame +#endif + +static AVFormatContext* s_format_context = nullptr; +static AVStream* s_stream = nullptr; +static AVFrame* s_src_frame = nullptr; +static AVFrame* s_scaled_frame = nullptr; +static int s_bytes_per_pixel; +static SwsContext* s_sws_context = nullptr; +static int s_width; +static int s_height; +static bool s_start_dumping = false; +static int s_current_width; +static int s_current_height; +static int s_file_index = 0; +static GPUDebugBuffer buf; + +static void InitAVCodec() +{ + static bool first_run = true; + if (first_run) + { + av_register_all(); + first_run = false; + } +} + +bool AVIDump::Start(int w, int h) +{ + s_width = w; + s_height = h; + s_current_width = w; + s_current_height = h; + + InitAVCodec(); + bool success = CreateAVI(); + if (!success) + CloseFile(); + return success; +} + +bool AVIDump::CreateAVI() +{ + AVCodec* codec = nullptr; + + s_format_context = avformat_alloc_context(); + std::stringstream s_file_index_str; + s_file_index_str << s_file_index; + snprintf(s_format_context->filename, sizeof(s_format_context->filename), "%s", (GetSysDirectory(DIRECTORY_VIDEO) + "framedump" + s_file_index_str.str() + ".avi").c_str()); + // Make sure that the path exists + if (!File::Exists(GetSysDirectory(DIRECTORY_VIDEO))) + File::CreateDir(GetSysDirectory(DIRECTORY_VIDEO)); + + if (File::Exists(s_format_context->filename)) + File::Delete(s_format_context->filename); + + if (!(s_format_context->oformat = av_guess_format("avi", nullptr, nullptr)) || !(s_stream = avformat_new_stream(s_format_context, codec))) + { + return false; + } + + s_stream->codec->codec_id = g_Config.bUseFFV1 ? AV_CODEC_ID_FFV1 : s_format_context->oformat->video_codec; + if (!g_Config.bUseFFV1) + s_stream->codec->codec_tag = MKTAG('X', 'V', 'I', 'D'); // Force XVID FourCC for better compatibility + s_stream->codec->codec_type = AVMEDIA_TYPE_VIDEO; + s_stream->codec->bit_rate = 400000; + s_stream->codec->width = s_width; + s_stream->codec->height = s_height; + s_stream->codec->time_base.num = 1001; + s_stream->codec->time_base.den = 60000; + s_stream->codec->gop_size = 12; + s_stream->codec->pix_fmt = g_Config.bUseFFV1 ? AV_PIX_FMT_BGRA : AV_PIX_FMT_YUV420P; + + if (!(codec = avcodec_find_encoder(s_stream->codec->codec_id)) || (avcodec_open2(s_stream->codec, codec, nullptr) < 0)) + { + return false; + } + + s_src_frame = av_frame_alloc(); + s_scaled_frame = av_frame_alloc(); + + s_scaled_frame->format = s_stream->codec->pix_fmt; + s_scaled_frame->width = s_width; + s_scaled_frame->height = s_height; + +#if LIBAVCODEC_VERSION_MAJOR >= 55 + if (av_frame_get_buffer(s_scaled_frame, 1)) + return false; +#else + if (avcodec_default_get_buffer(s_stream->codec, s_scaled_frame)) + return false; +#endif + + NOTICE_LOG(G3D, "Opening file %s for dumping", s_format_context->filename); + if (avio_open(&s_format_context->pb, s_format_context->filename, AVIO_FLAG_WRITE) < 0 || avformat_write_header(s_format_context, nullptr)) + { + WARN_LOG(G3D, "Could not open %s", s_format_context->filename); + return false; + } + + return true; +} + +static void PreparePacket(AVPacket* pkt) +{ + av_init_packet(pkt); + pkt->data = nullptr; + pkt->size = 0; + if (s_stream->codec->coded_frame->pts != AV_NOPTS_VALUE) + { + pkt->pts = av_rescale_q(s_stream->codec->coded_frame->pts, + s_stream->codec->time_base, s_stream->time_base); + } + if (s_stream->codec->coded_frame->key_frame) + pkt->flags |= AV_PKT_FLAG_KEY; + pkt->stream_index = s_stream->index; +} + +void AVIDump::AddFrame() +{ + gpuDebug->GetCurrentFramebuffer(buf); + u32 w = buf.GetStride(); + u32 h = buf.GetHeight(); + CheckResolution(w, h); + u8 *flipbuffer = nullptr; + const u8 *buffer = ConvertBufferTo888RGB(buf, flipbuffer, w, h); + s_src_frame->data[0] = const_cast(buffer); + s_src_frame->linesize[0] = w * 3; + s_src_frame->format = AV_PIX_FMT_RGB24; + s_src_frame->width = s_width; + s_src_frame->height = s_height; + + // Convert image from BGR24 to desired pixel format, and scale to initial width and height + if ((s_sws_context = sws_getCachedContext(s_sws_context, w, h, AV_PIX_FMT_RGB24, s_width, s_height, s_stream->codec->pix_fmt, SWS_BICUBIC, nullptr, nullptr, nullptr))) + { + sws_scale(s_sws_context, s_src_frame->data, s_src_frame->linesize, 0, h, s_scaled_frame->data, s_scaled_frame->linesize); + } + + s_scaled_frame->format = s_stream->codec->pix_fmt; + s_scaled_frame->width = s_width; + s_scaled_frame->height = s_height; + + // Encode and write the image. + AVPacket pkt; + PreparePacket(&pkt); + int got_packet; + int error = avcodec_encode_video2(s_stream->codec, &pkt, s_scaled_frame, &got_packet); + while (!error && got_packet) + { + // Write the compressed frame in the media file. + if (pkt.pts != (s64)AV_NOPTS_VALUE) + { + pkt.pts = av_rescale_q(pkt.pts, s_stream->codec->time_base, s_stream->time_base); + } + if (pkt.dts != (s64)AV_NOPTS_VALUE) + { + pkt.dts = av_rescale_q(pkt.dts, s_stream->codec->time_base, s_stream->time_base); + } +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(56, 60, 100) + if (s_stream->codec->coded_frame->key_frame) + pkt.flags |= AV_PKT_FLAG_KEY; +#endif + pkt.stream_index = s_stream->index; + av_interleaved_write_frame(s_format_context, &pkt); + + // Handle delayed frames. + PreparePacket(&pkt); + error = avcodec_encode_video2(s_stream->codec, &pkt, nullptr, &got_packet); + } + if (error) + ERROR_LOG(G3D, "Error while encoding video: %d", error); +} + +void AVIDump::Stop() +{ + av_write_trailer(s_format_context); + CloseFile(); + s_file_index = 0; + NOTICE_LOG(G3D, "Stopping frame dump"); +} + +void AVIDump::CloseFile() +{ + if (s_stream) + { + if (s_stream->codec) + { +#if LIBAVCODEC_VERSION_MAJOR < 55 + avcodec_default_release_buffer(s_stream->codec, s_src_frame); +#endif + avcodec_close(s_stream->codec); + } + av_freep(&s_stream); + } + + av_frame_free(&s_src_frame); + av_frame_free(&s_scaled_frame); + + if (s_format_context) + { + if (s_format_context->pb) + avio_close(s_format_context->pb); + av_freep(&s_format_context); + } + + if (s_sws_context) + { + sws_freeContext(s_sws_context); + s_sws_context = nullptr; + } +} + +void AVIDump::CheckResolution(int width, int height) +{ + // We check here to see if the requested width and height have changed since the last frame which + // was dumped, then create a new file accordingly. However, is it possible for the width and height + // to have a value of zero. If this is the case, simply keep the last known resolution of the video + // for the added frame. + if ((width != s_current_width || height != s_current_height) && (width > 0 && height > 0)) + { + int temp_file_index = s_file_index; + Stop(); + s_file_index = temp_file_index + 1; + Start(width, height); + s_current_width = width; + s_current_height = height; + } +} +#endif diff --git a/Core/AVIDump.h b/Core/AVIDump.h new file mode 100644 index 000000000000..07147d578057 --- /dev/null +++ b/Core/AVIDump.h @@ -0,0 +1,22 @@ +// Copyright 2008 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. +#ifndef MOBILE_DEVICE + +#pragma once + +#include "Common/CommonTypes.h" + +class AVIDump +{ +private: + static bool CreateAVI(); + static void CloseFile(); + static void CheckResolution(int width, int height); + +public: + static bool Start(int w, int h); + static void AddFrame(); + static void Stop(); +}; +#endif diff --git a/Core/Config.cpp b/Core/Config.cpp index e38a04c2d9a6..f32aa786cfb0 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -40,7 +40,7 @@ #include "HLE/sceUtility.h" #ifndef USING_QT_UI -extern const char *PPSSPP_GIT_VERSION; +extern const char *PPSSPP_GIT_VERSION; #endif // TODO: Find a better place for this. @@ -327,6 +327,9 @@ static ConfigSetting generalSettings[] = { ConfigSetting("CwCheatRefreshRate", &g_Config.iCwCheatRefreshRate, 77, true, true), ConfigSetting("ScreenshotsAsPNG", &g_Config.bScreenshotsAsPNG, false, true, true), + ConfigSetting("UseFFV1", &g_Config.bUseFFV1, false), + ConfigSetting("DumpFrames", &g_Config.bDumpFrames, false), + ConfigSetting("DumpAudio", &g_Config.bDumpAudio, false), ConfigSetting("StateSlot", &g_Config.iCurrentStateSlot, 0, true, true), ConfigSetting("RewindFlipFrequency", &g_Config.iRewindFlipFrequency, 0, true, true), @@ -926,7 +929,7 @@ void Config::Load(const char *iniFileName, const char *controllerIniFilename) { fcombo4X /= screen_width; fcombo4Y /= screen_height; } - + const char *gitVer = PPSSPP_GIT_VERSION; Version installed(gitVer); Version upgrade(upgradeVersion); @@ -955,7 +958,7 @@ void Config::Load(const char *iniFileName, const char *controllerIniFilename) { bSaveSettings = true; LoadStandardControllerIni(); - + //so this is all the way down here to overwrite the controller settings //sadly it won't benefit from all the "version conversion" going on up-above //but these configs shouldn't contain older versions anyhow @@ -987,7 +990,7 @@ void Config::Save() { g_Config.iCpuCore = CPU_CORE_JIT; } if (iniFilename_.size() && g_Config.bSaveSettings) { - + saveGameConfig(gameId_); CleanRecent(); diff --git a/Core/Config.h b/Core/Config.h index 7b7d58014e59..aa9dcd6e44fb 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -104,6 +104,9 @@ struct Config { // General int iNumWorkerThreads; bool bScreenshotsAsPNG; + bool bUseFFV1; + bool bDumpFrames; + bool bDumpAudio; bool bEnableLogging; bool bDumpDecryptedEboot; bool bFullscreenOnDoubleclick; @@ -236,7 +239,7 @@ struct Config { //considers this orientation to be equal to no movement of the analog stick. float fTiltBaseX, fTiltBaseY; //whether the x axes and y axes should invert directions (left becomes right, top becomes bottom.) - bool bInvertTiltX, bInvertTiltY; + bool bInvertTiltX, bInvertTiltY; //the sensitivity of the tilt in the x direction int iTiltSensitivityX; //the sensitivity of the tilt in the Y direction @@ -331,7 +334,7 @@ struct Config { bool bShowComboKey2; bool bShowComboKey3; bool bShowComboKey4; - + // Combo_key mapping. These are bitfields. int iCombokey0; int iCombokey1; @@ -360,9 +363,9 @@ struct Config { // proper options when good enough. // PrescaleUV: // * Applies UV scale/offset when decoding verts. Get rid of some work in the vertex shader, - // saves a uniform upload and is a prerequisite for future optimized hybrid + // saves a uniform upload and is a prerequisite for future optimized hybrid // (SW skinning, HW transform) skinning. - // * Still has major problems so off by default - need to store tex scale/offset per DeferredDrawCall, + // * Still has major problems so off by default - need to store tex scale/offset per DeferredDrawCall, // which currently isn't done so if texscale/offset isn't static (like in Tekken 6) things go wrong. bool bPrescaleUV; bool bDisableAlphaTest; // Helps PowerVR immensely, breaks some graphics @@ -425,7 +428,7 @@ struct Config { bool bShowFrameProfiler; std::string currentDirectory; - std::string externalDirectory; + std::string externalDirectory; std::string memStickDirectory; std::string flash0Directory; std::string internalDataDirectory; @@ -439,7 +442,7 @@ struct Config { void Load(const char *iniFileName = nullptr, const char *controllerIniFilename = nullptr); void Save(); void RestoreDefaults(); - + //per game config managment, should maybe be in it's own class void changeGameSpecific(const std::string &gameId = ""); bool createGameConfig(const std::string &game_id); @@ -473,7 +476,7 @@ struct Config { protected: void LoadStandardControllerIni(); - + private: std::string gameId_; std::string iniFilename_; diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 60d9c7c66c21..ef80700b1b32 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -181,6 +181,7 @@ + @@ -500,6 +501,7 @@ AnySuitable AnySuitable + @@ -519,6 +521,7 @@ + @@ -729,6 +732,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 600070d1e8d4..09949201de09 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -673,6 +673,10 @@ MIPS\IR + + Core + + @@ -1236,6 +1240,10 @@ MIPS\IR + + Core + + diff --git a/Core/CoreTiming.cpp b/Core/CoreTiming.cpp index 887e420dd146..7999cb18ad46 100644 --- a/Core/CoreTiming.cpp +++ b/Core/CoreTiming.cpp @@ -308,7 +308,7 @@ void AddEventToQueue(Event* ne) // This must be run ONLY from within the cpu thread // cyclesIntoFuture may be VERY inaccurate if called from anything else -// than Advance +// than Advance void ScheduleEvent(s64 cyclesIntoFuture, int event_type, u64 userdata) { Event *ne = GetNewEvent(); @@ -418,7 +418,7 @@ void RegisterMHzChangeCallback(MHzChangeCallback callback) { mhzChangeCallbacks.push_back(callback); } -bool IsScheduled(int event_type) +bool IsScheduled(int event_type) { if (!first) return false; @@ -498,7 +498,7 @@ void RemoveThreadsafeEvent(int event_type) while (ptr) { if (ptr->type == event_type) - { + { prev->next = ptr->next; if (ptr == tsLast) tsLast = prev; @@ -526,7 +526,7 @@ void ProcessFifoWaitEvents() { if (first->time <= (s64)GetTicks()) { -// LOG(TIMER, "[Scheduler] %s (%lld, %lld) ", +// LOG(TIMER, "[Scheduler] %s (%lld, %lld) ", // first->name ? first->name : "?", (u64)GetTicks(), (u64)first->time); Event* evt = first; first = first->next; diff --git a/Core/HLE/__sceAudio.cpp b/Core/HLE/__sceAudio.cpp index 02a0b604cec9..84483a035015 100644 --- a/Core/HLE/__sceAudio.cpp +++ b/Core/HLE/__sceAudio.cpp @@ -32,6 +32,10 @@ #include "Core/Host.h" #include "Core/MemMapHelpers.h" #include "Core/Reporting.h" +#include "Core/System.h" +#ifndef MOBILE_DEVICE +#include "Core/WaveFile.h" +#endif #include "Core/HLE/__sceAudio.h" #include "Core/HLE/sceAudio.h" #include "Core/HLE/sceKernel.h" @@ -54,7 +58,7 @@ enum latency { }; int eventAudioUpdate = -1; -int eventHostAudioUpdate = -1; +int eventHostAudioUpdate = -1; int mixFrequency = 44100; const int hwSampleRate = 44100; @@ -66,6 +70,11 @@ static int audioIntervalCycles; static int audioHostIntervalCycles; static s32 *mixBuffer; +static s16 *clampedMixBuffer; +#ifndef MOBILE_DEVICE +WaveFileWriter g_wave_writer; +static bool m_logAudio; +#endif // High and low watermarks, basically. For perfect emulation, the correct values are 0 and 1, respectively. // TODO: Tweak. Hm, there aren't actually even used currently... @@ -130,10 +139,21 @@ void __AudioInit() { chans[i].clear(); mixBuffer = new s32[hwBlockSize * 2]; + clampedMixBuffer = new s16[hwBlockSize * 2]; memset(mixBuffer, 0, hwBlockSize * 2 * sizeof(s32)); resampler.Clear(); CoreTiming::RegisterMHzChangeCallback(&__AudioCPUMHzChange); +#ifndef MOBILE_DEVICE + if (g_Config.bDumpAudio) + { + std::string audio_file_name = GetSysDirectory(DIRECTORY_AUDIO) + "audiodump.wav"; + // Create the path just in case it doesn't exist + File::CreateDir(GetSysDirectory(DIRECTORY_AUDIO)); + File::CreateEmptyFile(audio_file_name); + __StartLogAudio(audio_file_name); + } +#endif } void __AudioDoState(PointerWrap &p) { @@ -173,10 +193,18 @@ void __AudioDoState(PointerWrap &p) { void __AudioShutdown() { delete [] mixBuffer; + delete [] clampedMixBuffer; mixBuffer = 0; for (u32 i = 0; i < PSP_AUDIO_CHANNEL_MAX + 1; i++) chans[i].clear(); + +#ifndef MOBILE_DEVICE + if (g_Config.bDumpAudio) + { + __StopLogAudio(); + } +#endif } u32 __AudioEnqueue(AudioChannel &chan, int chanNum, bool blocking) { @@ -364,6 +392,15 @@ void __AudioUpdate() { if (g_Config.bEnableSound) { resampler.PushSamples(mixBuffer, hwBlockSize); +#ifndef MOBILE_DEVICE + if (m_logAudio) + { + for (int i = 0; i < hwBlockSize * 2; i++) { + clampedMixBuffer[i] = clamp_s16(mixBuffer[i]); + } + g_wave_writer.AddStereoSamples(clampedMixBuffer, hwBlockSize); + } +#endif } } @@ -385,3 +422,33 @@ void __PushExternalAudio(const s32 *audio, int numSamples) { resampler.Clear(); } } +#ifndef MOBILE_DEVICE +void __StartLogAudio(const std::string& filename) +{ + if (!m_logAudio) + { + m_logAudio = true; + g_wave_writer.Start(filename, 44100); + g_wave_writer.SetSkipSilence(false); + NOTICE_LOG(SCEAUDIO, "Starting Audio logging"); + } + else + { + WARN_LOG(SCEAUDIO, "Audio logging has already been started"); + } +} + +void __StopLogAudio() +{ + if (m_logAudio) + { + m_logAudio = false; + g_wave_writer.Stop(); + NOTICE_LOG(SCEAUDIO, "Stopping Audio logging"); + } + else + { + WARN_LOG(SCEAUDIO, "Audio logging has already been stopped"); + } +} +#endif diff --git a/Core/HLE/__sceAudio.h b/Core/HLE/__sceAudio.h index 83948414bd52..87365fb3612a 100644 --- a/Core/HLE/__sceAudio.h +++ b/Core/HLE/__sceAudio.h @@ -45,3 +45,7 @@ void __AudioWakeThreads(AudioChannel &chan, int result); int __AudioMix(short *outstereo, int numSamples, int sampleRate); const AudioDebugStats *__AudioGetDebugStats(); void __PushExternalAudio(const s32 *audio, int numSamples); // Should not be used in-game, only at the menu! + +// Audio Dumping stuff +void __StartLogAudio(const std::string& filename); +void __StopLogAudio(); diff --git a/Core/HLE/sceAudio.cpp b/Core/HLE/sceAudio.cpp index 93acd0a1f53b..cdbc811e4768 100644 --- a/Core/HLE/sceAudio.cpp +++ b/Core/HLE/sceAudio.cpp @@ -211,7 +211,7 @@ static u32 sceAudioChReserve(int chan, u32 sampleCount, u32 format) { ERROR_LOG(SCEAUDIO, "sceAudioChReserve - no channels remaining"); return SCE_ERROR_AUDIO_NO_CHANNELS_AVAILABLE; } - } + } if ((u32)chan >= PSP_AUDIO_CHANNEL_MAX) { ERROR_LOG(SCEAUDIO, "sceAudioChReserve(%08x, %08x, %08x) - bad channel", chan, sampleCount, format); return SCE_ERROR_AUDIO_INVALID_CHANNEL; @@ -457,7 +457,7 @@ static u32 sceAudioRoutingGetVolumeMode() { return defaultRoutingVolMode; } -const HLEFunction sceAudio[] = +const HLEFunction sceAudio[] = { // Newer simplified single channel audio output. Presumably for games that use Atrac3 // directly from Sas instead of playing it on a separate audio channel. diff --git a/Core/HW/StereoResampler.cpp b/Core/HW/StereoResampler.cpp index bdd616c7fbfe..2b01f84b743a 100644 --- a/Core/HW/StereoResampler.cpp +++ b/Core/HW/StereoResampler.cpp @@ -38,7 +38,7 @@ StereoResampler::StereoResampler() : m_dma_mixer(this, 44100) { - // Some Android devices are v-synced to non-60Hz framerates. We simply timestretch audio to fit. + // Some Android devices are v-synced to non-60Hz framerates. We simply timestretch audio to fit. // TODO: should only do this if auto frameskip is off? float refresh = System_GetPropertyInt(SYSPROP_DISPLAY_REFRESH_RATE) / 1000.0f; @@ -120,7 +120,7 @@ unsigned int StereoResampler::MixerFifo::Mix(short* samples, unsigned int numSam if (offset < -MAX_FREQ_SHIFT) offset = -MAX_FREQ_SHIFT; aid_sample_rate_ = m_input_sample_rate + offset; - + /* Hm? u32 framelimit = SConfig::GetInstance().m_Framelimit; if (consider_framelimit && framelimit > 1) { diff --git a/Core/Screenshot.cpp b/Core/Screenshot.cpp index e2175d3bd4fc..35cd517c9f5a 100644 --- a/Core/Screenshot.cpp +++ b/Core/Screenshot.cpp @@ -125,7 +125,7 @@ static bool WriteScreenshotToPNG(png_imagep image, const char *filename, int con } #endif -static const u8 *ConvertBufferTo888RGB(const GPUDebugBuffer &buf, u8 *&temp, u32 &w, u32 &h) { +const u8 *ConvertBufferTo888RGB(const GPUDebugBuffer &buf, u8 *&temp, u32 &w, u32 &h) { // The temp buffer will be freed by the caller if set, and can be the return value. temp = nullptr; diff --git a/Core/Screenshot.h b/Core/Screenshot.h index 3cecc4547e4a..1f4bdd6d285b 100644 --- a/Core/Screenshot.h +++ b/Core/Screenshot.h @@ -17,6 +17,8 @@ #pragma once +class GPUDebugBuffer; + enum ScreenshotFormat { SCREENSHOT_PNG, SCREENSHOT_JPG, @@ -30,4 +32,6 @@ enum ScreenshotType { SCREENSHOT_RENDER, }; +const u8 * ConvertBufferTo888RGB(const GPUDebugBuffer & buf, u8 *& temp, u32 & w, u32 & h); + bool TakeGameScreenshot(const char *filename, ScreenshotFormat fmt, ScreenshotType type, int *width = nullptr, int *height = nullptr, int maxRes = -1); diff --git a/Core/System.cpp b/Core/System.cpp index 5a78541596d7..03976499c9c6 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -182,7 +182,7 @@ void CPU_Shutdown(); void CPU_Init() { coreState = CORE_POWERUP; currentMIPS = &mipsr4k; - + g_symbolMap = new SymbolMap(); // Default memory settings @@ -386,7 +386,7 @@ bool PSP_InitStart(const CoreParameter &coreParam, std::string *error_string) { #else INFO_LOG(BOOT, "PPSSPP %s", PPSSPP_GIT_VERSION); #endif - + GraphicsContext *temp = coreParameter.graphicsContext; coreParameter = coreParam; if (coreParameter.graphicsContext == nullptr) { @@ -593,6 +593,10 @@ std::string GetSysDirectory(PSPDirectories directoryType) { return g_Config.appCacheDirectory; } return g_Config.memStickDirectory + "PSP/SYSTEM/CACHE/"; + case DIRECTORY_VIDEO: + return g_Config.memStickDirectory + "PSP/VIDEO/"; + case DIRECTORY_AUDIO: + return g_Config.memStickDirectory + "PSP/AUDIO/"; // Just return the memory stick root if we run into some sort of problem. default: ERROR_LOG(FILESYS, "Unknown directory type."); diff --git a/Core/System.h b/Core/System.h index 7815b60accc1..b9c5feadf6c2 100644 --- a/Core/System.h +++ b/Core/System.h @@ -47,6 +47,8 @@ enum PSPDirectories { DIRECTORY_CACHE, DIRECTORY_TEXTURES, DIRECTORY_APP_CACHE, // Use the OS app cache if available + DIRECTORY_VIDEO, + DIRECTORY_AUDIO }; class GraphicsContext; diff --git a/Core/WaveFile.cpp b/Core/WaveFile.cpp new file mode 100644 index 000000000000..76df969f61ee --- /dev/null +++ b/Core/WaveFile.cpp @@ -0,0 +1,117 @@ +// Copyright 2008 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. +#ifndef MOBILE_DEVICE +#include + +#include "Core/WaveFile.h" +#include "Common/CommonTypes.h" +#include "Common/MsgHandler.h" +#include "Core/Config.h" + +constexpr size_t WaveFileWriter::BUFFER_SIZE; + +WaveFileWriter::WaveFileWriter() +{ +} + +WaveFileWriter::~WaveFileWriter() +{ + Stop(); +} + +bool WaveFileWriter::Start(const std::string& filename, unsigned int HLESampleRate) +{ + // Check if the file is already open + if (file) + { + PanicAlert("The file %s was already open, the file header will not be written.", + filename.c_str()); + return false; + } + + file.Open(filename, "wb"); + if (!file) + { + PanicAlert("The file %s could not be opened for writing. Please check if it's already opened " + "by another program.", + filename.c_str()); + return false; + } + + audio_size = 0; + + // ----------------- + // Write file header + // ----------------- + Write4("RIFF"); + Write(100 * 1000 * 1000); // write big value in case the file gets truncated + Write4("WAVE"); + Write4("fmt "); + + Write(16); // size of fmt block + Write(0x00020001); // two channels, uncompressed + + const u32 sample_rate = HLESampleRate; + Write(sample_rate); + Write(sample_rate * 2 * 2); // two channels, 16bit + + Write(0x00100004); + Write4("data"); + Write(100 * 1000 * 1000 - 32); + + // We are now at offset 44 + if (file.Tell() != 44) + PanicAlert("Wrong offset: %lld", (long long)file.Tell()); + + return true; +} + +void WaveFileWriter::Stop() +{ + // u32 file_size = (u32)ftello(file); + file.Seek(4, SEEK_SET); + Write(audio_size + 36); + + file.Seek(40, SEEK_SET); + Write(audio_size); + + file.Close(); +} + +void WaveFileWriter::Write(u32 value) +{ + file.WriteArray(&value, 1); +} + +void WaveFileWriter::Write4(const char* ptr) +{ + file.WriteBytes(ptr, 4); +} + +void WaveFileWriter::AddStereoSamples(const short* sample_data, u32 count) +{ + if (!file) + PanicAlert("WaveFileWriter - file not open."); + + if (count > BUFFER_SIZE * 2) + PanicAlert("WaveFileWriter - buffer too small (count = %u).", count); + + if (skip_silence) + { + bool all_zero = true; + + for (u32 i = 0; i < count * 2; i++) + { + if (sample_data[i]) + all_zero = false; + } + + if (all_zero) + return; + } + + file.WriteBytes(sample_data, count * 4); + audio_size += count * 4; +} +#endif diff --git a/Core/WaveFile.h b/Core/WaveFile.h new file mode 100644 index 000000000000..39bcf483a475 --- /dev/null +++ b/Core/WaveFile.h @@ -0,0 +1,45 @@ +// Copyright 2008 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +// --------------------------------------------------------------------------------- +// Class: WaveFileWriter +// Description: Simple utility class to make it easy to write long 16-bit stereo +// audio streams to disk. +// Use Start() to start recording to a file, and AddStereoSamples to add wave data. +// If Stop is not called when it destructs, the destructor will call Stop(). +// --------------------------------------------------------------------------------- + +#pragma once +#ifndef MOBILE_DEVICE + +#include +#include +#include "Common/CommonTypes.h" +#include "Common/FileUtil.h" + +class WaveFileWriter +{ +public: + WaveFileWriter(); + ~WaveFileWriter(); + + bool Start(const std::string& filename, unsigned int HLESampleRate); + void Stop(); + + void SetSkipSilence(bool skip) { skip_silence = skip; } + void AddStereoSamples(const short* sample_data, u32 count); + u32 GetAudioSize() const { return audio_size; } +private: + static constexpr size_t BUFFER_SIZE = 32 * 1024; + + File::IOFile file; + bool skip_silence = false; + u32 audio_size = 0; + std::array conv_buffer{}; + void Write(u32 value); + void Write4(const char* ptr); +}; + +#endif + diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 0e697f5cc68a..28ea79d11b19 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -32,6 +32,9 @@ #include "Common/KeyMap.h" +#ifndef MOBILE_DEVICE +#include "Core/AVIDump.h" +#endif #include "Core/Config.h" #include "Core/CoreTiming.h" #include "Core/CoreParameter.h" @@ -71,8 +74,13 @@ #include "Windows/MainWindow.h" #endif +#ifndef MOBILE_DEVICE +AVIDump avi; +#endif + static bool frameStep_; static int lastNumFlips; +static bool startDumping; static void __EmuScreenVblank() { @@ -82,6 +90,24 @@ static void __EmuScreenVblank() Core_EnableStepping(true); lastNumFlips = gpuStats.numFlips; } +#ifndef MOBILE_DEVICE + if (g_Config.bDumpFrames && !startDumping) + { + avi.Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight); + osm.Show("AVI Dump started.", 3.0f); + startDumping = true; + } + if (g_Config.bDumpFrames && startDumping) + { + avi.AddFrame(); + } + else if (!g_Config.bDumpFrames && startDumping) + { + avi.Stop(); + osm.Show("AVI Dump stopped.", 3.0f); + startDumping = false; + } +#endif } EmuScreen::EmuScreen(const std::string &filename) @@ -91,6 +117,7 @@ EmuScreen::EmuScreen(const std::string &filename) __DisplayListenVblank(__EmuScreenVblank); frameStep_ = false; lastNumFlips = gpuStats.numFlips; + startDumping = false; } void EmuScreen::bootGame(const std::string &filename) { @@ -230,6 +257,14 @@ EmuScreen::~EmuScreen() { // If we were invalid, it would already be shutdown. PSP_Shutdown(); } +#ifndef MOBILE_DEVICE + if (g_Config.bDumpFrames && startDumping) + { + avi.Stop(); + osm.Show("AVI Dump stopped.", 3.0f); + startDumping = false; + } +#endif } void EmuScreen::dialogFinished(const Screen *dialog, DialogResult result) { diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index fea7d947efdf..a241fcb465b7 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -273,7 +273,7 @@ void GameSettingsScreen::CreateViews() { static const char *quality[] = { "Low", "Medium", "High"}; PopupMultiChoice *beziersChoice = graphicsSettings->Add(new PopupMultiChoice(&g_Config.iSplineBezierQuality, gr->T("LowCurves", "Spline/Bezier curves quality"), quality, 0, ARRAY_SIZE(quality), gr->GetName(), screenManager())); beziersChoice->SetDisabledPtr(&g_Config.bSoftwareRendering); - + // In case we're going to add few other antialiasing option like MSAA in the future. // graphicsSettings->Add(new CheckBox(&g_Config.bFXAA, gr->T("FXAA"))); graphicsSettings->Add(new ItemHeader(gr->T("Texture Scaling"))); @@ -676,6 +676,9 @@ void GameSettingsScreen::CreateViews() { #if defined(_WIN32) || (defined(USING_QT_UI) && !defined(MOBILE_DEVICE)) // Screenshot functionality is not yet available on non-Windows/non-Qt systemSettings->Add(new CheckBox(&g_Config.bScreenshotsAsPNG, sy->T("Screenshots as PNG"))); + systemSettings->Add(new CheckBox(&g_Config.bDumpFrames, sy->T("Record Display"))); + systemSettings->Add(new CheckBox(&g_Config.bUseFFV1, sy->T("Use Lossless Video Codec (FFV1)"))); + systemSettings->Add(new CheckBox(&g_Config.bDumpAudio, sy->T("Record Audio"))); #endif systemSettings->Add(new CheckBox(&g_Config.bDayLightSavings, sy->T("Day Light Saving"))); static const char *dateFormat[] = { "YYYYMMDD", "MMDDYYYY", "DDMMYYYY"}; @@ -801,7 +804,7 @@ UI::EventReturn GameSettingsScreen::OnSavePathMydoc(UI::EventParams &e) { } UI::EventReturn GameSettingsScreen::OnSavePathOther(UI::EventParams &e) { - const std::string PPSSPPpath = File::GetExeDirectory(); + const std::string PPSSPPpath = File::GetExeDirectory(); if (otherinstalled_) { I18NCategory *di = GetI18NCategory("Dialog"); std::string folder = W32Util::BrowseForFolder(MainWindow::GetHWND(), di->T("Choose PPSSPP save folder")); @@ -983,8 +986,8 @@ UI::EventReturn GameSettingsScreen::OnChangeNickname(UI::EventParams &e) { return UI::EVENT_DONE; } -UI::EventReturn GameSettingsScreen::OnChangeproAdhocServerAddress(UI::EventParams &e) { -#if defined(_WIN32) || defined(USING_QT_UI) +UI::EventReturn GameSettingsScreen::OnChangeproAdhocServerAddress(UI::EventParams &e) { +#if defined(_WIN32) || defined(USING_QT_UI) if (!g_Config.bFullScreen) { const size_t name_len = 256; @@ -1002,7 +1005,7 @@ UI::EventReturn GameSettingsScreen::OnChangeproAdhocServerAddress(UI::EventParam #else screenManager()->push(new ProAdhocServerScreen); #endif - + return UI::EVENT_DONE; } @@ -1246,14 +1249,14 @@ UI::EventReturn DeveloperToolsScreen::OnJitAffectingSetting(UI::EventParams &e) } void ProAdhocServerScreen::CreateViews() { - using namespace UI; + using namespace UI; I18NCategory *sy = GetI18NCategory("System"); I18NCategory *di = GetI18NCategory("Dialog"); - + tempProAdhocServer = g_Config.proAdhocServer; root_ = new AnchorLayout(new LayoutParams(FILL_PARENT, FILL_PARENT)); LinearLayout *leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); - + leftColumn->Add(new ItemHeader(sy->T("proAdhocServer Address:"))); addrView_ = new TextView(tempProAdhocServer, ALIGN_LEFT, false); leftColumn->Add(addrView_); diff --git a/Windows/MainWindowMenu.cpp b/Windows/MainWindowMenu.cpp index 3a6de2fc42b7..c9916aefef70 100644 --- a/Windows/MainWindowMenu.cpp +++ b/Windows/MainWindowMenu.cpp @@ -79,6 +79,7 @@ namespace MainWindow { // File submenus SUBMENU_FILE_SAVESTATE_SLOT = 6, + SUBMENU_FILE_RECORD = 11, // Emulation submenus SUBMENU_DISPLAY_ROTATION = 4, @@ -232,6 +233,7 @@ namespace MainWindow { TranslateMenuItem(menu, ID_FILE_QUICKSAVESTATE, L"\tF2"); TranslateMenuItem(menu, ID_FILE_LOADSTATEFILE); TranslateMenuItem(menu, ID_FILE_SAVESTATEFILE); + TranslateSubMenu(menu, "Record", MENU_FILE, SUBMENU_FILE_RECORD); TranslateMenuItem(menu, ID_FILE_EXIT, L"\tAlt+F4"); // Emulation menu @@ -271,6 +273,11 @@ namespace MainWindow { TranslateMenuItem(menu, ID_OPTIONS_CONTROLS); TranslateMenuItem(menu, ID_OPTIONS_DISPLAY_LAYOUT); + // Movie menu + TranslateMenuItem(menu, ID_FILE_DUMPFRAMES); + TranslateMenuItem(menu, ID_FILE_USEFFV1); + TranslateMenuItem(menu, ID_FILE_DUMPAUDIO); + // Skip display multipliers x1-x10 TranslateMenuItem(menu, ID_OPTIONS_FULLSCREEN, L"\tAlt+Return, F11"); TranslateMenuItem(menu, ID_OPTIONS_VSYNC); @@ -928,6 +935,18 @@ namespace MainWindow { g_TakeScreenshot = true; break; + case ID_FILE_DUMPFRAMES: + g_Config.bDumpFrames = !g_Config.bDumpFrames; + break; + + case ID_FILE_USEFFV1: + g_Config.bUseFFV1 = !g_Config.bUseFFV1; + break; + + case ID_FILE_DUMPAUDIO: + g_Config.bDumpAudio = !g_Config.bDumpAudio; + break; + default: { // Handle the dynamic shader switching here. @@ -966,6 +985,9 @@ namespace MainWindow { CHECKITEM(ID_TEXTURESCALING_DEPOSTERIZE, g_Config.bTexDeposterize); CHECKITEM(ID_EMULATION_CHEATS, g_Config.bEnableCheats); CHECKITEM(ID_OPTIONS_IGNOREWINKEY, g_Config.bIgnoreWindowsKey); + CHECKITEM(ID_FILE_DUMPFRAMES, g_Config.bDumpFrames); + CHECKITEM(ID_FILE_USEFFV1, g_Config.bUseFFV1); + CHECKITEM(ID_FILE_DUMPAUDIO, g_Config.bDumpAudio); static const int displayrotationitems[] = { ID_EMULATION_ROTATION_H, diff --git a/Windows/ppsspp.rc b/Windows/ppsspp.rc index 5135556f6fd1..233dfd8105df 100644 --- a/Windows/ppsspp.rc +++ b/Windows/ppsspp.rc @@ -19,17 +19,17 @@ LANGUAGE LANG_NEUTRAL, SUBLANG_DEFAULT // TEXTINCLUDE // -1 TEXTINCLUDE +1 TEXTINCLUDE BEGIN "resource.h\0" END -2 TEXTINCLUDE +2 TEXTINCLUDE BEGIN "\0" END -3 TEXTINCLUDE +3 TEXTINCLUDE BEGIN "\r\n" "\0" @@ -401,9 +401,17 @@ BEGIN MENUITEM "&5", ID_FILE_SAVESTATE_SLOT_5 END MENUITEM "Load State", ID_FILE_QUICKLOADSTATE - MENUITEM "Save State", ID_FILE_QUICKSAVESTATE + MENUITEM "Save State", ID_FILE_QUICKSAVESTATE MENUITEM "Load State File...", ID_FILE_LOADSTATEFILE MENUITEM "Save State File...", ID_FILE_SAVESTATEFILE + + POPUP "Record" + BEGIN + MENUITEM "Record Display", ID_FILE_DUMPFRAMES + MENUITEM "Use Lossless Video Codec (FFV1)", ID_FILE_USEFFV1 + MENUITEM SEPARATOR + MENUITEM "Record Audio", ID_FILE_DUMPAUDIO + END MENUITEM SEPARATOR MENUITEM "Exit", ID_FILE_EXIT END diff --git a/Windows/resource.h b/Windows/resource.h index 66e88a362068..37e054bcaa2d 100644 --- a/Windows/resource.h +++ b/Windows/resource.h @@ -328,6 +328,9 @@ #define IDC_GEDBG_BREAKTARGET 40162 #define ID_GEDBG_COPYALL 40163 #define ID_GEDBG_WATCH 40164 +#define ID_FILE_DUMPFRAMES 40165 +#define ID_FILE_USEFFV1 40166 +#define ID_FILE_DUMPAUDIO 40167 // Dummy option to let the buffered rendering hotkey cycle through all the options. #define ID_OPTIONS_BUFFEREDRENDERINGDUMMY 40500