Compare commits

...

7 Commits

Author SHA1 Message Date
Pokechu22
e3dd7bf12c
Merge 2fd4a3500f into 24b0bf01d5 2025-06-08 10:04:19 +02:00
JMC47
24b0bf01d5
Merge pull request #12836 from JosJuice/opensles-buffer-size
Android: Ask system for optimal audio buffer size and sample rate
2025-06-07 23:07:37 -04:00
Tilka
19fbbf0dba
Merge pull request #13727 from JoshuaVandaele/fmt-11.2.0-localtime-deprec
fmt: Replace deprecated `fmt::localtime` usage with `Common::LocalTime`
2025-06-08 04:04:37 +01:00
Pokechu22
2fd4a3500f Allow debugging apploader code 2025-06-07 15:24:49 -07:00
Joshua Vandaële
4b65cc9a4c
fmt: Replace deprecated fmt::localtime usage with Common::LocalTime 2025-06-04 13:32:12 +02:00
JosJuice
f99d3dbd5c Android: Ask system for optimal audio buffer size and sample rate
This can reduce audio latency according to
https://developer.android.com/ndk/guides/audio/opensl/opensl-prog-notes#perform.

Previously we were using the hardcoded values of 48000 Hz and 256 frames
per buffer. The sample rate we use with this change is 48000 Hz on all
devices I'm aware of, but the buffer size does vary across devices.

Terminology note: The old code used the term "sample" to refer to what
Android refers to as a "frame". "Frame" is a clearer term to use for
this, so I've changed OpenSLESStream's terminology. One frame consists
of one sample per channel.
2025-05-25 11:59:33 +02:00
JosJuice
34e8fb068f Android: Get rid of OpenSLESStream's global state
Not sure if we're ever going to want to have more than one of these at
the same time, but these global variables are a code smell nonetheless.

I'm also deleting the existing member variables because they were
unused.
2025-05-25 11:55:22 +02:00
20 changed files with 473 additions and 168 deletions

View File

@ -0,0 +1,28 @@
package org.dolphinemu.dolphinemu.utils
import android.content.Context
import android.media.AudioManager
import androidx.annotation.Keep
import org.dolphinemu.dolphinemu.DolphinApplication
object AudioUtils {
@JvmStatic @Keep
fun getSampleRate(): Int =
getAudioServiceProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE, 48000)
@JvmStatic @Keep
fun getFramesPerBuffer(): Int =
getAudioServiceProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER, 256)
private fun getAudioServiceProperty(property: String, fallback: Int): Int {
return try {
val context = DolphinApplication.getAppContext()
val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Integer.parseUnsignedInt(am.getProperty(property))
} catch (e: NullPointerException) {
fallback
} catch (e: NumberFormatException) {
fallback
}
}
}

View File

@ -122,6 +122,10 @@ static jmethodID s_permission_handler_request_record_audio_permission;
static jmethodID s_runnable_run;
static jclass s_audio_utils_class;
static jmethodID s_audio_utils_get_sample_rate;
static jmethodID s_audio_utils_get_frames_per_buffer;
namespace IDCache
{
JNIEnv* GetEnvForThread()
@ -562,6 +566,21 @@ jmethodID GetRunnableRun()
return s_runnable_run;
}
jclass GetAudioUtilsClass()
{
return s_audio_utils_class;
}
jmethodID GetAudioUtilsGetSampleRate()
{
return s_audio_utils_get_sample_rate;
}
jmethodID GetAudioUtilsGetFramesPerBuffer()
{
return s_audio_utils_get_frames_per_buffer;
}
} // namespace IDCache
extern "C" {
@ -798,6 +817,13 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_runnable_run = env->GetMethodID(runnable_class, "run", "()V");
env->DeleteLocalRef(runnable_class);
const jclass audio_utils_class = env->FindClass("org/dolphinemu/dolphinemu/utils/AudioUtils");
s_audio_utils_class = reinterpret_cast<jclass>(env->NewGlobalRef(audio_utils_class));
s_audio_utils_get_sample_rate = env->GetStaticMethodID(audio_utils_class, "getSampleRate", "()I");
s_audio_utils_get_frames_per_buffer =
env->GetStaticMethodID(audio_utils_class, "getFramesPerBuffer", "()I");
env->DeleteLocalRef(audio_utils_class);
return JNI_VERSION;
}
@ -834,5 +860,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
env->DeleteGlobalRef(s_core_device_control_class);
env->DeleteGlobalRef(s_input_detector_class);
env->DeleteGlobalRef(s_permission_handler_class);
env->DeleteGlobalRef(s_audio_utils_class);
}
}

View File

@ -121,4 +121,8 @@ jmethodID GetPermissionHandlerRequestRecordAudioPermission();
jmethodID GetRunnableRun();
jclass GetAudioUtilsClass();
jmethodID GetAudioUtilsGetSampleRate();
jmethodID GetAudioUtilsGetFramesPerBuffer();
} // namespace IDCache

View File

@ -16,6 +16,7 @@
#include "AudioCommon/WASAPIStream.h"
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/TimeUtil.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h"
#include "Core/System.h"
@ -219,8 +220,11 @@ void StartAudioDump(Core::System& system)
std::string path_prefix = File::GetUserPath(D_DUMPAUDIO_IDX) + SConfig::GetInstance().GetGameID();
std::string base_name =
fmt::format("{}_{:%Y-%m-%d_%H-%M-%S}", path_prefix, fmt::localtime(start_time));
const auto local_time = Common::LocalTime(start_time);
if (!local_time)
return;
std::string base_name = fmt::format("{}_{:%Y-%m-%d_%H-%M-%S}", path_prefix, *local_time);
const std::string audio_file_name_dtk = fmt::format("{}_dtkdump.wav", base_name);
const std::string audio_file_name_dsp = fmt::format("{}_dspdump.wav", base_name);

View File

@ -8,40 +8,27 @@
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
#include <jni.h>
#include "Common/Assert.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
#include "Core/ConfigManager.h"
#include "jni/AndroidCommon/IDCache.h"
// engine interfaces
static SLObjectItf engineObject;
static SLEngineItf engineEngine;
static SLObjectItf outputMixObject;
// buffer queue player interfaces
static SLObjectItf bqPlayerObject = nullptr;
static SLPlayItf bqPlayerPlay;
static SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue;
static SLVolumeItf bqPlayerVolume;
static Mixer* g_mixer;
#define BUFFER_SIZE 512
#define BUFFER_SIZE_IN_SAMPLES (BUFFER_SIZE / 2)
// Double buffering.
static short buffer[2][BUFFER_SIZE];
static int curBuffer = 0;
static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context)
void OpenSLESStream::BQPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context)
{
ASSERT(bq == bqPlayerBufferQueue);
ASSERT(nullptr == context);
reinterpret_cast<OpenSLESStream*>(context)->PushSamples(bq);
}
void OpenSLESStream::PushSamples(SLAndroidSimpleBufferQueueItf bq)
{
ASSERT(bq == m_bq_player_buffer_queue);
// Render to the fresh buffer
g_mixer->Mix(reinterpret_cast<short*>(buffer[curBuffer]), BUFFER_SIZE_IN_SAMPLES);
SLresult result =
(*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, buffer[curBuffer], sizeof(buffer[0]));
curBuffer ^= 1; // Switch buffer
m_mixer->Mix(m_buffer[m_current_buffer].data(), m_frames_per_buffer);
SLresult result = (*bq)->Enqueue(bq, m_buffer[m_current_buffer].data(), m_bytes_per_buffer);
m_current_buffer ^= 1; // Switch buffer
// Comment from sample code:
// the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT,
@ -51,61 +38,78 @@ static void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context)
bool OpenSLESStream::Init()
{
JNIEnv* env = IDCache::GetEnvForThread();
jclass audio_utils = IDCache::GetAudioUtilsClass();
const SLuint32 sample_rate =
env->CallStaticIntMethod(audio_utils, IDCache::GetAudioUtilsGetSampleRate());
m_frames_per_buffer =
env->CallStaticIntMethod(audio_utils, IDCache::GetAudioUtilsGetFramesPerBuffer());
INFO_LOG_FMT(AUDIO, "OpenSLES configuration: {} Hz, {} frames per buffer", sample_rate,
m_frames_per_buffer);
constexpr SLuint32 channels = 2;
const SLuint32 samples_per_buffer = m_frames_per_buffer * channels;
m_bytes_per_buffer = m_frames_per_buffer * channels * sizeof(m_buffer[0][0]);
for (std::vector<short>& buffer : m_buffer)
buffer.resize(samples_per_buffer);
SLresult result;
// create engine
result = slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
result = slCreateEngine(&m_engine_object, 0, nullptr, 0, nullptr, nullptr);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
result = (*m_engine_object)->Realize(m_engine_object, SL_BOOLEAN_FALSE);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
result = (*m_engine_object)->GetInterface(m_engine_object, SL_IID_ENGINE, &m_engine_engine);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, 0, 0);
result = (*m_engine_engine)->CreateOutputMix(m_engine_engine, &m_output_mix_object, 0, 0, 0);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
result = (*m_output_mix_object)->Realize(m_output_mix_object, SL_BOOLEAN_FALSE);
ASSERT(SL_RESULT_SUCCESS == result);
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM,
2,
m_mixer->GetSampleRate() * 1000,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
SL_BYTEORDER_LITTLEENDIAN};
SLDataFormat_PCM format_pcm = {
SL_DATAFORMAT_PCM, channels,
sample_rate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
SL_BYTEORDER_LITTLEENDIAN};
SLDataSource audioSrc = {&loc_bufq, &format_pcm};
// configure audio sink
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, m_output_mix_object};
SLDataSink audioSnk = {&loc_outmix, nullptr};
// create audio player
const SLInterfaceID ids[2] = {SL_IID_BUFFERQUEUE, SL_IID_VOLUME};
const SLboolean req[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
result =
(*engineEngine)
->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk, 2, ids, req);
result = (*m_engine_engine)
->CreateAudioPlayer(m_engine_engine, &m_bq_player_object, &audioSrc, &audioSnk, 2,
ids, req);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
result = (*m_bq_player_object)->Realize(m_bq_player_object, SL_BOOLEAN_FALSE);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
result = (*m_bq_player_object)->GetInterface(m_bq_player_object, SL_IID_PLAY, &m_bq_player_play);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*m_bq_player_object)
->GetInterface(m_bq_player_object, SL_IID_BUFFERQUEUE, &m_bq_player_buffer_queue);
ASSERT(SL_RESULT_SUCCESS == result);
result =
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);
(*m_bq_player_object)->GetInterface(m_bq_player_object, SL_IID_VOLUME, &m_bq_player_volume);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
result = (*m_bq_player_buffer_queue)
->RegisterCallback(m_bq_player_buffer_queue, BQPlayerCallback, this);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, nullptr);
ASSERT(SL_RESULT_SUCCESS == result);
result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
result = (*m_bq_player_play)->SetPlayState(m_bq_player_play, SL_PLAYSTATE_PLAYING);
ASSERT(SL_RESULT_SUCCESS == result);
// Render and enqueue a first buffer.
curBuffer ^= 1;
g_mixer = m_mixer.get();
m_current_buffer ^= 1;
result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, buffer[0], sizeof(buffer[0]));
result = (*m_bq_player_buffer_queue)
->Enqueue(m_bq_player_buffer_queue, m_buffer[0].data(), m_bytes_per_buffer);
if (SL_RESULT_SUCCESS != result)
return false;
@ -114,39 +118,39 @@ bool OpenSLESStream::Init()
OpenSLESStream::~OpenSLESStream()
{
if (bqPlayerObject != nullptr)
if (m_bq_player_object != nullptr)
{
(*bqPlayerObject)->Destroy(bqPlayerObject);
bqPlayerObject = nullptr;
bqPlayerPlay = nullptr;
bqPlayerBufferQueue = nullptr;
bqPlayerVolume = nullptr;
(*m_bq_player_object)->Destroy(m_bq_player_object);
m_bq_player_object = nullptr;
m_bq_player_play = nullptr;
m_bq_player_buffer_queue = nullptr;
m_bq_player_volume = nullptr;
}
if (outputMixObject != nullptr)
if (m_output_mix_object != nullptr)
{
(*outputMixObject)->Destroy(outputMixObject);
outputMixObject = nullptr;
(*m_output_mix_object)->Destroy(m_output_mix_object);
m_output_mix_object = nullptr;
}
if (engineObject != nullptr)
if (m_engine_object != nullptr)
{
(*engineObject)->Destroy(engineObject);
engineObject = nullptr;
engineEngine = nullptr;
(*m_engine_object)->Destroy(m_engine_object);
m_engine_object = nullptr;
m_engine_engine = nullptr;
}
}
bool OpenSLESStream::SetRunning(bool running)
{
SLuint32 new_state = running ? SL_PLAYSTATE_PLAYING : SL_PLAYSTATE_PAUSED;
return (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, new_state) == SL_RESULT_SUCCESS;
return (*m_bq_player_play)->SetPlayState(m_bq_player_play, new_state) == SL_RESULT_SUCCESS;
}
void OpenSLESStream::SetVolume(int volume)
{
const SLmillibel attenuation =
volume <= 0 ? SL_MILLIBEL_MIN : static_cast<SLmillibel>(2000 * std::log10(volume / 100.0f));
(*bqPlayerVolume)->SetVolumeLevel(bqPlayerVolume, attenuation);
(*m_bq_player_volume)->SetVolumeLevel(m_bq_player_volume, attenuation);
}
#endif // HAVE_OPENSL_ES

View File

@ -3,10 +3,15 @@
#pragma once
#include <thread>
#ifdef HAVE_OPENSL_ES
#include <array>
#include <vector>
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
#endif // HAVE_OPENSL_ES
#include "AudioCommon/SoundStream.h"
#include "Common/Event.h"
class OpenSLESStream final : public SoundStream
{
@ -19,7 +24,25 @@ public:
static bool IsValid() { return true; }
private:
std::thread thread;
Common::Event soundSyncEvent;
static void BQPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void* context);
void PushSamples(SLAndroidSimpleBufferQueueItf bq);
// engine interfaces
SLObjectItf m_engine_object;
SLEngineItf m_engine_engine;
SLObjectItf m_output_mix_object;
// buffer queue player interfaces
SLObjectItf m_bq_player_object = nullptr;
SLPlayItf m_bq_player_play;
SLAndroidSimpleBufferQueueItf m_bq_player_buffer_queue;
SLVolumeItf m_bq_player_volume;
SLuint32 m_frames_per_buffer;
SLuint32 m_bytes_per_buffer;
// Double buffering.
std::array<std::vector<short>, 2> m_buffer;
int m_current_buffer = 0;
#endif // HAVE_OPENSL_ES
};

View File

@ -25,6 +25,7 @@
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "Common/StringUtil.h"
#include "Common/TimeUtil.h"
#include "Core/Config/MainSettings.h"
@ -95,12 +96,7 @@ int SDCardDiskIOCtl(File::IOFile* image, u8 pdrv, u8 cmd, void* buff)
u32 GetSystemTimeFAT()
{
const std::time_t time = std::time(nullptr);
std::tm tm;
#ifdef _WIN32
localtime_s(&tm, &time);
#else
localtime_r(&time, &tm);
#endif
std::tm tm = *Common::LocalTime(time);
DWORD fattime = 0;
fattime |= (tm.tm_year - 80) << 25;

View File

@ -122,7 +122,6 @@ std::string SettingsWriter::GenerateSerialNumber()
// Must be 9 characters at most; otherwise the serial number will be rejected by SDK libraries,
// as there is a check to ensure the string length is strictly lower than 10.
// 3 for %j, 2 for %H, 2 for %M, 2 for %S.
return fmt::format("{:%j%H%M%S}", fmt::localtime(t));
return fmt::format("{:09}", t % 1000000000);
}
} // namespace Common

View File

@ -2,23 +2,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Common/TimeUtil.h"
#include "Common/Logging/Log.h"
#include <ctime>
#include <optional>
namespace Common
{
std::optional<std::tm> Localtime(std::time_t time)
std::optional<std::tm> LocalTime(std::time_t time)
{
std::tm local_time;
#ifdef _MSC_VER
if (localtime_s(&local_time, &time) != 0)
return std::nullopt;
#else
std::tm* result = localtime_r(&time, &local_time);
if (result != &local_time)
return std::nullopt;
if (localtime_r(&time, &local_time) == NULL)
#endif
{
ERROR_LOG_FMT(COMMON, "Failed to convert time to local time: {}", std::strerror(errno));
return std::nullopt;
}
return local_time;
}
} // Namespace Common

View File

@ -9,5 +9,5 @@
namespace Common
{
// Threadsafe and error-checking variant of std::localtime()
std::optional<std::tm> Localtime(std::time_t time);
std::optional<std::tm> LocalTime(std::time_t time);
} // Namespace Common

View File

@ -554,7 +554,7 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
if (!EmulatedBS2(system, guard, system.IsWii(), *volume, riivolution_patches))
return false;
SConfig::OnTitleDirectlyBooted(guard);
// SConfig::OnTitleDirectlyBooted(guard) is called by apploader runner code
return true;
}

View File

@ -8,6 +8,7 @@
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
@ -172,12 +173,17 @@ public:
static bool FindMapFile(std::string* existing_map_file, std::string* writable_map_file);
static bool LoadMapFromFilename(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_symbol_db);
static void HLEAppLoaderAfterEntry(const Core::CPUThreadGuard& guard);
static void HLEAppLoaderAfterInit(const Core::CPUThreadGuard& guard);
static void HLEAppLoaderAfterMain(const Core::CPUThreadGuard& guard);
static void HLEAppLoaderAfterClose(const Core::CPUThreadGuard& guard);
static void HLEAppLoaderReport(std::string_view message);
private:
static bool DVDRead(Core::System& system, const DiscIO::VolumeDisc& disc, u64 dvd_offset,
u32 output_address, u32 length, const DiscIO::Partition& partition);
static bool DVDReadDiscID(Core::System& system, const DiscIO::VolumeDisc& disc,
u32 output_address);
static void RunFunction(Core::System& system, u32 address);
static bool Boot_WiiWAD(Core::System& system, const DiscIO::VolumeWAD& wad);
static bool BootNANDTitle(Core::System& system, u64 title_id);

View File

@ -24,6 +24,7 @@
#include "Core/HW/DVD/DVDInterface.h"
#include "Core/HW/EXI/EXI_DeviceIPL.h"
#include "Core/HW/Memmap.h"
#include "Core/Host.h"
#include "Core/IOS/DI/DI.h"
#include "Core/IOS/ES/ES.h"
#include "Core/IOS/ES/Formats.h"
@ -32,6 +33,7 @@
#include "Core/IOS/IOSC.h"
#include "Core/IOS/Uids.h"
#include "Core/PowerPC/MMU.h"
#include "Core/PowerPC/PPCSymbolDB.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
@ -56,18 +58,6 @@ void PresetTimeBaseTicks(Core::System& system, const Core::CPUThreadGuard& guard
}
} // Anonymous namespace
void CBoot::RunFunction(Core::System& system, u32 address)
{
auto& power_pc = system.GetPowerPC();
auto& ppc_state = power_pc.GetPPCState();
ppc_state.pc = address;
LR(ppc_state) = 0x00;
while (ppc_state.pc != 0x00)
power_pc.SingleStep();
}
void CBoot::SetupMSR(PowerPC::PowerPCState& ppc_state)
{
// 0x0002032
@ -136,6 +126,135 @@ void CBoot::SetupBAT(Core::System& system, bool is_wii)
mmu.IBATUpdated();
}
/*
#include <ogc/machine/asm.h>
.globl AppLoaderRunnerBase
AppLoaderRunnerBase:
b RunAppLoader
// Variables that iAppLoaderMain stores into
AppLoaderMainRamAddress:
.4byte 0x00dead04
AppLoaderMainLength:
.4byte 0x00dead08
AppLoaderMainDiscOffset:
.4byte 0x00dead0c
// Function pointers returned by iAppLoaderEntry
AppLoaderInitPointer: // pointer to: void iAppLoaderInit(void (*OSReport)(char *fmt, ...))
.4byte 0x00dead10
AppLoaderMainPointer: // pointer to: bool iAppLoaderMain(u32* ram_addr, u32* len, u32* disc_offset)
.4byte 0x00dead14
AppLoaderClosePointer: // pointer to: void* iAppLoaderClose(void);
.4byte 0x00dead18
HLEAppLoaderAfterEntry: // void HLEAppLoaderAfterEntry(void), logging
blr
HLEAppLoaderAfterInit: // void HLEAppLoaderAfterInit(void), logging
blr
HLEAppLoaderAfterMain: // void HLEAppLoaderAfterMain(void), reads disc at AppLoaderMain* variables
blr
HLEAppLoaderAfterClose: // void HLEAppLoaderAfterClose(void), cleans up HLE state
blr
HLEAppLoaderReport: // void HLEAppLoaderReport(char *fmt, ...), passed to iAppLoaderInit
blr
RunAppLoader:
// Entry point; call iAppLoaderEntry
// r31 is AppLoaderRunnerBase
// r30 is iAppLoaderEntry (later used as a scratch register for other mtctr + bctrl sequences)
// Note that the above registers are specific to Dolphin's apploader HLE.
addi r3, r31, (AppLoaderInitPointer - AppLoaderRunnerBase)
addi r4, r31, (AppLoaderMainPointer - AppLoaderRunnerBase)
addi r5, r31, (AppLoaderClosePointer - AppLoaderRunnerBase)
mtctr r30
bctrl // Call iAppLoaderEntry, which looks like this:
// void iAppLoaderEntry(void** init, void** main, void** close)
// {
// *init = &iAppLoaderInit;
// *main = &iAppLoaderMain;
// *close = &iAppLoaderClose;
// }
bl HLEAppLoaderAfterEntry // Logging only
// Call iAppLoaderInit, which sets up logging and any internal state.
addi r3, r31, (HLEAppLoaderReport - AppLoaderRunnerBase)
lwz r30, (AppLoaderInitPointer - AppLoaderRunnerBase)(r31)
mtctr r30
bctrl // Call iAppLoaderInit
bl HLEAppLoaderAfterInit // Logging only
// iAppLoaderMain - Here we load the apploader, the DOL (the exe) and the FST (filesystem).
// To give you an idea about where the stuff is located on the disc take a look at yagcd
// ch 13.
// iAppLoaderMain returns 1 if the pointers in R3/R4/R5 were filled with values for DVD copy
// Typical behaviour is doing it once for each section defined in the DOL header. Some unlicensed
// titles don't have a properly constructed DOL and maintain a table of these values in apploader.
// iAppLoaderMain returns 0 when there are no more sections to copy.
LoopMain:
addi r3, r31, (AppLoaderMainRamAddress - AppLoaderRunnerBase)
addi r4, r31, (AppLoaderMainLength - AppLoaderRunnerBase)
addi r5, r31, (AppLoaderMainDiscOffset - AppLoaderRunnerBase)
lwz r30, (AppLoaderMainPointer - AppLoaderRunnerBase)(r31)
mtctr r30
bctrl // Call iAppLoaderMain
cmpwi r3,0
beq LoopMainDone
bl HLEAppLoaderAfterMain // Reads the disc (and logs)
b LoopMain
// Call iAppLoaderClose, which returns (in r3) the game's entry point
LoopMainDone:
lwz r30, (AppLoaderClosePointer - AppLoaderRunnerBase)(r31)
mtctr r30
bctrl // Call iAppLoaderClose
mr r30, r3
bl HLEAppLoaderAfterClose // Unpatches the HLE functions
mtlr r30
blr // "Return" to the game's entry point; this allows using "step out" to skip the apploader
*/
static constexpr u32 APPLOADER_RUNNER_BASE = 0x81300000;
static constexpr u32 APPLOADER_RUNNER_MAIN_RAM_ADDR = APPLOADER_RUNNER_BASE + 0x04;
static constexpr u32 APPLOADER_RUNNER_MAIN_LENGTH = APPLOADER_RUNNER_BASE + 0x08;
static constexpr u32 APPLOADER_RUNNER_MAIN_DISC_OFFSET = APPLOADER_RUNNER_BASE + 0x0c;
static constexpr u32 APPLOADER_RUNNER_INIT_POINTER = APPLOADER_RUNNER_BASE + 0x10;
static constexpr u32 APPLOADER_RUNNER_MAIN_POINTER = APPLOADER_RUNNER_BASE + 0x14;
static constexpr u32 APPLOADER_RUNNER_CLOSE_POINTER = APPLOADER_RUNNER_BASE + 0x18;
static constexpr u32 APPLOADER_RUNNER_HLE_AFTER_ENTRY = APPLOADER_RUNNER_BASE + 0x1c;
static constexpr u32 APPLOADER_RUNNER_HLE_AFTER_INIT = APPLOADER_RUNNER_BASE + 0x20;
static constexpr u32 APPLOADER_RUNNER_HLE_AFTER_MAIN = APPLOADER_RUNNER_BASE + 0x24;
static constexpr u32 APPLOADER_RUNNER_HLE_AFTER_CLOSE = APPLOADER_RUNNER_BASE + 0x28;
static constexpr u32 APPLOADER_RUNNER_HLE_REPORT = APPLOADER_RUNNER_BASE + 0x2c;
static constexpr u32 APPLOADER_RUNNER_RUN_ADDR = APPLOADER_RUNNER_BASE + 0x30;
static constexpr std::array<u32, 40> APPLOADER_RUNNER_ASM{
0x48000030, // Starting thunk
// Variables
0x00dead04, 0x00dead08, 0x00dead0c, 0x00dead10, 0x00dead14, 0x00dead18,
// HLE'd functions
0x4e800020, 0x4e800020, 0x4e800020, 0x4e800020, 0x4e800020,
// Actual code
0x387f0010, 0x389f0014, 0x38bf0018, 0x7fc903a6, 0x4e800421, 0x4bffffd9, 0x387f002c, 0x83df0010,
0x7fc903a6, 0x4e800421, 0x4bffffc9, 0x387f0004, 0x389f0008, 0x38bf000c, 0x83df0014, 0x7fc903a6,
0x4e800421, 0x2c030000, 0x4182000c, 0x4bffffa9, 0x4bffffdc, 0x83df0018, 0x7fc903a6, 0x4e800421,
0x7c7e1b78, 0x4bffff95, 0x7fc803a6, 0x4e800020};
struct ApploaderRunnerState
{
constexpr ApploaderRunnerState(
bool is_wii_, const DiscIO::VolumeDisc& volume_,
const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches_)
: is_wii(is_wii_), volume(volume_), riivolution_patches(riivolution_patches_)
{
}
bool is_wii;
const DiscIO::VolumeDisc& volume;
const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches;
};
static std::unique_ptr<ApploaderRunnerState> s_apploader_runner_state;
bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard, bool is_wii,
const DiscIO::VolumeDisc& volume,
const std::vector<DiscIO::Riivolution::Patch>& riivolution_patches)
@ -153,11 +272,13 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard
INFO_LOG_FMT(BOOT, "Invalid apploader. Your disc image is probably corrupted.");
return false;
}
INFO_LOG_FMT(BOOT,
"Loading apploader ({}) from disc offset {:08x} to {:08x}, "
"size {:#x} ({:#x} + {:#x}), entry point {:08x}",
volume.GetApploaderDate(partition), offset + 0x20, 0x81200000, *size + *trailer,
*size, *trailer, *entry);
DVDRead(system, volume, offset + 0x20, 0x01200000, *size + *trailer, partition);
// TODO - Make Apploader(or just RunFunction()) debuggable!!!
auto& ppc_state = system.GetPPCState();
auto& mmu = system.GetMMU();
auto& branch_watch = system.GetPowerPC().GetBranchWatch();
@ -165,66 +286,66 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard
if (system.IsBranchWatchIgnoreApploader())
branch_watch.SetRecordingActive(guard, false);
// Call iAppLoaderEntry.
DEBUG_LOG_FMT(BOOT, "Call iAppLoaderEntry");
const u32 iAppLoaderFuncAddr = is_wii ? 0x80004000 : 0x80003100;
ppc_state.gpr[3] = iAppLoaderFuncAddr + 0;
ppc_state.gpr[4] = iAppLoaderFuncAddr + 4;
ppc_state.gpr[5] = iAppLoaderFuncAddr + 8;
RunFunction(system, *entry);
const u32 iAppLoaderInit = mmu.Read_U32(iAppLoaderFuncAddr + 0);
const u32 iAppLoaderMain = mmu.Read_U32(iAppLoaderFuncAddr + 4);
const u32 iAppLoaderClose = mmu.Read_U32(iAppLoaderFuncAddr + 8);
// iAppLoaderInit
DEBUG_LOG_FMT(BOOT, "Call iAppLoaderInit");
PowerPC::MMU::HostWrite_U32(guard, 0x4E800020, 0x81300000); // Write BLR
HLE::Patch(system, 0x81300000, "AppLoaderReport"); // HLE OSReport for Apploader
ppc_state.gpr[3] = 0x81300000;
RunFunction(system, iAppLoaderInit);
// iAppLoaderMain - Here we load the apploader, the DOL (the exe) and the FST (filesystem).
// To give you an idea about where the stuff is located on the disc take a look at yagcd
// ch 13.
DEBUG_LOG_FMT(BOOT, "Call iAppLoaderMain");
ppc_state.gpr[3] = 0x81300004;
ppc_state.gpr[4] = 0x81300008;
ppc_state.gpr[5] = 0x8130000c;
RunFunction(system, iAppLoaderMain);
// iAppLoaderMain returns 1 if the pointers in R3/R4/R5 were filled with values for DVD copy
// Typical behaviour is doing it once for each section defined in the DOL header. Some unlicensed
// titles don't have a properly constructed DOL and maintain a table of these values in apploader.
// iAppLoaderMain returns 0 when there are no more sections to copy.
while (ppc_state.gpr[3] != 0x00)
for (u32 i = 0; i < APPLOADER_RUNNER_ASM.size(); i++)
{
const u32 ram_address = mmu.Read_U32(0x81300004);
const u32 length = mmu.Read_U32(0x81300008);
const u32 dvd_offset = mmu.Read_U32(0x8130000c) << (is_wii ? 2 : 0);
INFO_LOG_FMT(BOOT, "DVDRead: offset: {:08x} memOffset: {:08x} length: {}", dvd_offset,
ram_address, length);
DVDRead(system, volume, dvd_offset, ram_address, length, partition);
DiscIO::Riivolution::ApplyApploaderMemoryPatches(guard, riivolution_patches, ram_address,
length);
ppc_state.gpr[3] = 0x81300004;
ppc_state.gpr[4] = 0x81300008;
ppc_state.gpr[5] = 0x8130000c;
RunFunction(system, iAppLoaderMain);
mmu.HostWrite_U32(guard, APPLOADER_RUNNER_ASM[i], APPLOADER_RUNNER_BASE + i * sizeof(u32));
}
// iAppLoaderClose
DEBUG_LOG_FMT(BOOT, "call iAppLoaderClose");
RunFunction(system, iAppLoaderClose);
HLE::UnPatch(system, "AppLoaderReport");
HLE::Patch(system, APPLOADER_RUNNER_HLE_AFTER_ENTRY, "HLEAppLoaderAfterEntry");
HLE::Patch(system, APPLOADER_RUNNER_HLE_AFTER_INIT, "HLEAppLoaderAfterInit");
HLE::Patch(system, APPLOADER_RUNNER_HLE_AFTER_MAIN, "HLEAppLoaderAfterMain");
HLE::Patch(system, APPLOADER_RUNNER_HLE_AFTER_CLOSE, "HLEAppLoaderAfterClose");
HLE::Patch(system, APPLOADER_RUNNER_HLE_REPORT, "HLEAppLoaderReport");
s_apploader_runner_state =
std::make_unique<ApploaderRunnerState>(is_wii, volume, riivolution_patches);
// return
ppc_state.pc = ppc_state.gpr[3];
system.GetPPCSymbolDB().AddKnownSymbol(guard, *entry, 0, "iAppLoaderEntry", "Apploader");
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_MAIN_RAM_ADDR, 4,
"AppLoaderMainRamAddress", "Apploader",
Common::Symbol::Type::Data);
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_MAIN_LENGTH, 4,
"AppLoaderMainLength", "Apploader",
Common::Symbol::Type::Data);
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_MAIN_DISC_OFFSET, 4,
"AppLoaderMainDiscOffset", "Apploader",
Common::Symbol::Type::Data);
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_INIT_POINTER, 4,
"AppLoaderInitPointer", "Apploader",
Common::Symbol::Type::Data);
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_MAIN_POINTER, 4,
"AppLoaderMainPointer", "Apploader",
Common::Symbol::Type::Data);
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_CLOSE_POINTER, 4,
"AppLoaderClosePointer", "Apploader",
Common::Symbol::Type::Data);
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_HLE_AFTER_ENTRY, 4,
"HLEAppLoaderAfterEntry", "Dolphin BS2 HLE");
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_HLE_AFTER_INIT, 4,
"HLEAppLoaderAfterInit", "Dolphin BS2 HLE");
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_HLE_AFTER_MAIN, 4,
"HLEAppLoaderAfterMain", "Dolphin BS2 HLE");
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_HLE_AFTER_CLOSE, 4,
"HLEAppLoaderAfterClose", "Dolphin BS2 HLE");
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_HLE_REPORT, 4,
"HLEAppLoaderReport", "Dolphin BS2 HLE");
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_BASE, 4, "DolphinApploaderRunner",
"Dolphin BS2 HLE");
const u32 remainder_size = u32(APPLOADER_RUNNER_ASM.size() * sizeof(u32)) -
(APPLOADER_RUNNER_RUN_ADDR - APPLOADER_RUNNER_BASE);
system.GetPPCSymbolDB().AddKnownSymbol(guard, APPLOADER_RUNNER_RUN_ADDR, remainder_size,
"DolphinApploaderRunner", "Dolphin BS2 HLE");
system.GetPPCSymbolDB().Index();
Host_PPCSymbolsChanged();
auto& ppc_state = system.GetPPCState();
ppc_state.gpr[31] = APPLOADER_RUNNER_BASE;
ppc_state.gpr[30] = *entry;
// Run our apploader runner code
ppc_state.pc = APPLOADER_RUNNER_BASE;
branch_watch.SetRecordingActive(guard, resume_branch_watch);
// Blank out session key (https://debugmo.de/2008/05/part-2-dumping-the-media-board/)
@ -238,6 +359,69 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard
return true;
}
void CBoot::HLEAppLoaderAfterEntry(const Core::CPUThreadGuard& guard)
{
auto& system = Core::System::GetInstance();
auto& mmu = system.GetMMU();
const u32 init = mmu.HostRead_U32(guard, APPLOADER_RUNNER_INIT_POINTER);
const u32 main = mmu.HostRead_U32(guard, APPLOADER_RUNNER_MAIN_POINTER);
const u32 close = mmu.HostRead_U32(guard, APPLOADER_RUNNER_CLOSE_POINTER);
system.GetPPCSymbolDB().AddKnownSymbol(guard, init, 0, "iAppLoaderInit", "Apploader");
system.GetPPCSymbolDB().AddKnownSymbol(guard, main, 0, "iAppLoaderMain", "Apploader");
system.GetPPCSymbolDB().AddKnownSymbol(guard, close, 0, "iAppLoaderClose", "Apploader");
system.GetPPCSymbolDB().Index();
Host_PPCSymbolsChanged();
INFO_LOG_FMT(BOOT, "Called iAppLoaderEntry; init at {:08x}, main at {:08x}, close at {:08x}",
init, main, close);
}
void CBoot::HLEAppLoaderAfterInit(const Core::CPUThreadGuard& guard)
{
INFO_LOG_FMT(BOOT, "Called iAppLoaderInit");
}
void CBoot::HLEAppLoaderAfterMain(const Core::CPUThreadGuard& guard)
{
ASSERT(s_apploader_runner_state);
if (!s_apploader_runner_state)
return;
auto& system = Core::System::GetInstance();
auto& mmu = system.GetMMU();
const u32 ram_address = mmu.HostRead_U32(guard, APPLOADER_RUNNER_MAIN_RAM_ADDR);
const u32 length = mmu.HostRead_U32(guard, APPLOADER_RUNNER_MAIN_LENGTH);
const u32 dvd_shift = s_apploader_runner_state->is_wii ? 2 : 0;
const u32 dvd_offset = mmu.HostRead_U32(guard, APPLOADER_RUNNER_MAIN_DISC_OFFSET) << dvd_shift;
INFO_LOG_FMT(BOOT, "Called iAppLoaderMain; reading {:#x} bytes from disc offset {:08x} to {:08x}",
length, dvd_offset, ram_address);
DVDRead(system, s_apploader_runner_state->volume, dvd_offset, ram_address, length,
s_apploader_runner_state->volume.GetGamePartition());
DiscIO::Riivolution::ApplyApploaderMemoryPatches(
guard, s_apploader_runner_state->riivolution_patches, ram_address, length);
}
void CBoot::HLEAppLoaderAfterClose(const Core::CPUThreadGuard& guard)
{
auto& system = Core::System::GetInstance();
auto& ppc_state = system.GetPPCState();
INFO_LOG_FMT(BOOT, "Called iAppLoaderClose; game entry point is {:08x}", ppc_state.gpr[30]);
SConfig::OnTitleDirectlyBooted(guard); // Clears the temporary patches and symbols
}
void CBoot::HLEAppLoaderReport(std::string_view message)
{
auto& system = Core::System::GetInstance();
auto& ppc_state = system.GetPPCState();
INFO_LOG_FMT(BOOT, "AppLoader {:08x}->{:08x}| {}", LR(ppc_state), ppc_state.pc, message);
}
void CBoot::SetupGCMemory(Core::System& system, const Core::CPUThreadGuard& guard)
{
auto& memory = system.GetMemory();

View File

@ -8,6 +8,7 @@
#include <cstring>
#include <functional>
#include <mutex>
#include <optional>
#include <queue>
#include <utility>
#include <variant>
@ -34,6 +35,7 @@
#include "Common/ScopeGuard.h"
#include "Common/StringUtil.h"
#include "Common/Thread.h"
#include "Common/TimeUtil.h"
#include "Common/Version.h"
#include "Core/AchievementManager.h"
@ -733,15 +735,17 @@ static std::string GenerateScreenshotFolderPath()
return path;
}
static std::string GenerateScreenshotName()
static std::optional<std::string> GenerateScreenshotName()
{
// append gameId, path only contains the folder here.
const std::string path_prefix =
GenerateScreenshotFolderPath() + SConfig::GetInstance().GetGameID();
const std::time_t cur_time = std::time(nullptr);
const std::string base_name =
fmt::format("{}_{:%Y-%m-%d_%H-%M-%S}", path_prefix, fmt::localtime(cur_time));
const auto local_time = Common::LocalTime(cur_time);
if (!local_time)
return std::nullopt;
const std::string base_name = fmt::format("{}_{:%Y-%m-%d_%H-%M-%S}", path_prefix, *local_time);
// First try a filename without any suffixes, if already exists then append increasing numbers
std::string name = fmt::format("{}.png", base_name);
@ -757,7 +761,9 @@ static std::string GenerateScreenshotName()
void SaveScreenShot()
{
const Core::CPUThreadGuard guard(Core::System::GetInstance());
g_frame_dumper->SaveScreenshot(GenerateScreenshotName());
std::optional<std::string> name = GenerateScreenshotName();
if (name)
g_frame_dumper->SaveScreenshot(*name);
}
void SaveScreenShot(std::string_view name)

View File

@ -10,6 +10,7 @@
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
#include "Core/Boot/Boot.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
#include "Core/GeckoCode.h"
@ -28,7 +29,7 @@ namespace HLE
static std::map<u32, u32> s_hooked_addresses;
// clang-format off
constexpr std::array<Hook, 23> os_patches{{
constexpr std::array<Hook, 27> os_patches{{
// Placeholder, os_patches[0] is the "non-existent function" index
{"FAKE_TO_SKIP_0", HLE_Misc::UnimplementedFunction, HookType::Replace, HookFlag::Generic},
@ -60,7 +61,12 @@ constexpr std::array<Hook, 23> os_patches{{
{"GeckoCodehandler", HLE_Misc::GeckoCodeHandlerICacheFlush, HookType::Start, HookFlag::Fixed},
{"GeckoHandlerReturnTrampoline", HLE_Misc::GeckoReturnTrampoline, HookType::Replace, HookFlag::Fixed},
{"AppLoaderReport", HLE_OS::HLE_GeneralDebugPrint, HookType::Start, HookFlag::Fixed} // apploader needs OSReport-like function
{"HLEAppLoaderAfterEntry", CBoot::HLEAppLoaderAfterEntry, HookType::Start, HookFlag::Fixed},
{"HLEAppLoaderAfterInit", CBoot::HLEAppLoaderAfterInit, HookType::Start, HookFlag::Fixed},
{"HLEAppLoaderAfterMain", CBoot::HLEAppLoaderAfterMain, HookType::Start, HookFlag::Fixed},
{"HLEAppLoaderAfterClose", CBoot::HLEAppLoaderAfterClose, HookType::Start, HookFlag::Fixed},
{"HLEAppLoaderReport", HLE_OS::HLE_AppLoaderReport, HookType::Start, HookFlag::Fixed},
}};
// clang-format on

View File

@ -16,6 +16,7 @@
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/StringUtil.h"
#include "Core/Boot/Boot.h"
#include "Core/Core.h"
#include "Core/HLE/HLE_VarArgs.h"
#include "Core/PowerPC/MMU.h"
@ -208,6 +209,14 @@ void HLE_LogVFPrint(const Core::CPUThreadGuard& guard)
HLE_LogFPrint(guard, ParameterType::VariableArgumentList);
}
// Version of OSReport used by Dolphin for running apploaders from BS2 HLE
void HLE_AppLoaderReport(const Core::CPUThreadGuard& guard)
{
auto& system = Core::System::GetInstance();
CBoot::HLEAppLoaderReport(
SHIFTJISToUTF8(GetStringVA(system, guard, 3, ParameterType::ParameterList)));
}
namespace
{
class HLEPrintArgsVAList final : public HLEPrintArgs

View File

@ -35,4 +35,5 @@ void HLE_LogDPrint(const Core::CPUThreadGuard& guard);
void HLE_LogVDPrint(const Core::CPUThreadGuard& guard);
void HLE_LogFPrint(const Core::CPUThreadGuard& guard);
void HLE_LogVFPrint(const Core::CPUThreadGuard& guard);
void HLE_AppLoaderReport(const Core::CPUThreadGuard& guard);
} // namespace HLE_OS

View File

@ -16,6 +16,7 @@
#include "Common/Network.h"
#include "Common/PcapFile.h"
#include "Common/ScopeGuard.h"
#include "Common/TimeUtil.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h"
@ -82,7 +83,7 @@ PCAPSSLCaptureLogger::PCAPSSLCaptureLogger()
{
const std::string filepath =
fmt::format("{}{} {:%Y-%m-%d %Hh%Mm%Ss}.pcap", File::GetUserPath(D_DUMPSSL_IDX),
SConfig::GetInstance().GetGameID(), fmt::localtime(std::time(nullptr)));
SConfig::GetInstance().GetGameID(), *Common::LocalTime(std::time(nullptr)));
m_file = std::make_unique<Common::PCAP>(
new File::IOFile(filepath, "wb", File::SharedAccess::Read), Common::PCAP::LinkType::Ethernet);
}

View File

@ -281,7 +281,7 @@ static std::string SystemTimeAsDoubleToString(double time)
{
// revert adjustments from GetSystemTimeAsDouble() to get a normal Unix timestamp again
const time_t seconds = static_cast<time_t>(time) + DOUBLE_TIME_OFFSET;
const auto local_time = Common::Localtime(seconds);
const auto local_time = Common::LocalTime(seconds);
if (!local_time)
return "";

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/FrameDumpFFMpeg.h"
#include "Common/TimeUtil.h"
#if defined(__FreeBSD__)
#define __STDC_CONSTANT_MACROS 1
@ -124,11 +125,15 @@ std::string GetDumpPath(const std::string& extension, std::time_t time, u32 inde
if (!dump_path.empty())
return dump_path;
const auto local_time = Common::LocalTime(time);
if (!local_time)
return "";
const std::string path_prefix =
File::GetUserPath(D_DUMPFRAMES_IDX) + SConfig::GetInstance().GetGameID();
const std::string base_name =
fmt::format("{}_{:%Y-%m-%d_%H-%M-%S}_{}", path_prefix, fmt::localtime(time), index);
fmt::format("{}_{:%Y-%m-%d_%H-%M-%S}_{}", path_prefix, *local_time, index);
const std::string path = fmt::format("{}.{}", base_name, extension);