diff --git a/Common/GPU/OpenGL/GLRenderManager.h b/Common/GPU/OpenGL/GLRenderManager.h index 8a7ddf0a9c..707aa94e58 100644 --- a/Common/GPU/OpenGL/GLRenderManager.h +++ b/Common/GPU/OpenGL/GLRenderManager.h @@ -249,6 +249,11 @@ public: void ThreadStart(Draw::DrawContext *draw); void ThreadEnd(); bool ThreadFrame(); // False means it's time to exit. + // True if ThreadFrame() has queued work and will not block on an empty queue. + bool HasQueuedThreadWork() { + std::lock_guard lock(pushMutex_); + return !renderThreadQueue_.empty(); + } void NotifyEmuThreadExit(); diff --git a/libretro/LibretroGLContext.h b/libretro/LibretroGLContext.h index 0ae24937d0..508ec266ef 100644 --- a/libretro/LibretroGLContext.h +++ b/libretro/LibretroGLContext.h @@ -28,6 +28,7 @@ public: void ThreadStart() override { renderManager_->ThreadStart(draw_); } bool ThreadFrame() override { return renderManager_->ThreadFrame(); } + bool ThreadFramePending() override { return renderManager_->HasQueuedThreadWork(); } void ThreadEnd() override { renderManager_->ThreadEnd(); } GPUCore GetGPUCore() override { return GPUCORE_GLES; } diff --git a/libretro/LibretroGLCoreContext.h b/libretro/LibretroGLCoreContext.h index d91c20164d..5fb4da3ce4 100644 --- a/libretro/LibretroGLCoreContext.h +++ b/libretro/LibretroGLCoreContext.h @@ -22,6 +22,7 @@ public: void ThreadStart() override { renderManager_->ThreadStart(draw_); } bool ThreadFrame() override { return renderManager_->ThreadFrame(); } + bool ThreadFramePending() override { return renderManager_->HasQueuedThreadWork(); } void ThreadEnd() override { renderManager_->ThreadEnd(); } // Call from emu thread diff --git a/libretro/LibretroGraphicsContext.h b/libretro/LibretroGraphicsContext.h index 99d61a6405..8a5c318d50 100644 --- a/libretro/LibretroGraphicsContext.h +++ b/libretro/LibretroGraphicsContext.h @@ -31,6 +31,8 @@ public: } virtual void SwapBuffers() = 0; void Resize() override {} + // True if ThreadFrame() has queued work to run (emu thread backends only). + virtual bool ThreadFramePending() { return false; } virtual void GotBackbuffer(); virtual void LostBackbuffer(); diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index 901f6b1044..dfa6eea672 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -132,15 +132,24 @@ namespace Libretro static s64 runTicksLast = 0; // Must be called with output_audio_buffer_mutex held. - static void ensure_output_audio_buffer_capacity(int32_t capacity) + static bool ensure_output_audio_buffer_capacity(int32_t capacity) { if (capacity <= output_audio_buffer.capacity) { - return; + return true; } - output_audio_buffer.data = (int16_t*)realloc(output_audio_buffer.data, capacity * sizeof(*output_audio_buffer.data)); + int16_t *data = (int16_t*)realloc(output_audio_buffer.data, capacity * sizeof(*output_audio_buffer.data)); + if (!data) { + // Keep the old buffer, drop the new samples. + if (log_cb) + log_cb(RETRO_LOG_ERROR, "Failed to grow output audio buffer to %d samples\n", capacity); + return false; + } + output_audio_buffer.data = data; output_audio_buffer.capacity = capacity; - log_cb(RETRO_LOG_DEBUG, "Output audio buffer capacity set to %d\n", capacity); + if (log_cb) + log_cb(RETRO_LOG_DEBUG, "Output audio buffer capacity set to %d\n", capacity); + return true; } static void init_output_audio_buffer(int32_t capacity) @@ -164,7 +173,8 @@ namespace Libretro static void upload_output_audio_buffer() { std::lock_guard lock(output_audio_buffer_mutex); - audio_batch_cb(output_audio_buffer.data, output_audio_buffer.size / 2); + if (output_audio_buffer.data && output_audio_buffer.size > 0) + audio_batch_cb(output_audio_buffer.data, output_audio_buffer.size / 2); output_audio_buffer.size = 0; } @@ -1388,14 +1398,16 @@ namespace Libretro { SetCurrentThreadName("EmuThread"); for (;;) { - switch ((EmuThreadState)emuThreadState) + EmuThreadState state = emuThreadState; + switch (state) { case EmuThreadState::RUNNING: EmuFrame(); break; case EmuThreadState::PAUSE_REQUESTED: - emuThreadState = EmuThreadState::PAUSED; - [[fallthrough]]; + // CAS so a concurrent QUIT_REQUESTED is not clobbered; the loop re-reads the state. + emuThreadState.compare_exchange_strong(state, EmuThreadState::PAUSED); + break; case EmuThreadState::PAUSED: sleep_ms(1, "libretro-paused"); break; @@ -1428,16 +1440,19 @@ namespace Libretro { } void EmuThreadStop() { - if (emuThreadState != EmuThreadState::RUNNING) + // Stop and join from any live state, not just RUNNING: unloading while + // PAUSED (the state retro_serialize_size() leaves behind) must not leave + // a live thread over freed engine state or a joinable std::thread. + if (!emuThread.joinable()) return; - emuThreadState = EmuThreadState::QUIT_REQUESTED; + if (emuThreadState != EmuThreadState::STOPPED) + emuThreadState = EmuThreadState::QUIT_REQUESTED; // Eat remaining frames. while (ctx->ThreadFrame()) {} emuThread.join(); - emuThread = std::thread(); ctx->ThreadEnd(); } @@ -1447,11 +1462,16 @@ namespace Libretro { emuThreadState = EmuThreadState::PAUSE_REQUESTED; - // Is this safe? - ctx->ThreadFrame(); // Eat 1 frame - - while (emuThreadState != EmuThreadState::PAUSED) - sleep_ms(1, "libretro-pause-poll"); + // Drain queued frames until the emu thread parks at a frame boundary. + // A single frame is not enough when the emu thread is blocked on a + // readback sync in the following frame, and ThreadFrame() blocks on an + // empty queue, so only pump it when work is queued. + while (emuThreadState != EmuThreadState::PAUSED) { + if (ctx->ThreadFramePending()) + ctx->ThreadFrame(); + else + sleep_ms(1, "libretro-pause-poll"); + } } } // namespace Libretro @@ -1563,6 +1583,11 @@ void retro_unload_game(void) { } void retro_reset(void) { + // EmuFrame() must not run while the core is torn down; retro_run() + // restarts the thread. + if (Libretro::useEmuThread) + Libretro::EmuThreadStop(); + PSP_Shutdown(true); if (BootState::Complete != PSP_Init(PSP_CoreParameter(), &g_bootErrorString)) { @@ -1709,10 +1734,13 @@ void retro_run(void) { } // Update setting if any have changed. - bool updated; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated) + bool updated = false; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated) { + // The emu thread reads g_Config mid-frame; it is resumed below. + if (useEmuThread) + EmuThreadPause(); check_variables(PSP_CoreParameter()); - else + } else check_dynamic_variables(PSP_CoreParameter()); // Process input. @@ -1720,13 +1748,8 @@ void retro_run(void) { // Handle thread pumping. if (useEmuThread) { - if (emuThreadState == EmuThreadState::PAUSED || - emuThreadState == EmuThreadState::PAUSE_REQUESTED) { - VsyncSwapIntervalDetect(); - ctx->SwapBuffers(); - return; - } - + // Also resumes from PAUSED, so a retro_serialize_size() without a + // following retro_serialize() cannot freeze emulation. if (emuThreadState != EmuThreadState::RUNNING) { EmuThreadStart(); } @@ -1780,16 +1803,24 @@ bool retro_serialize(void *data, size_t size) { EmuThreadPause(); // Does nothing if already paused } - size_t measuredSize; SaveState::SaveStart state; - auto err = CChunkFileReader::MeasureAndSavePtr(state, (u8 **)&data, &measuredSize); - bool retVal = err == CChunkFileReader::ERROR_NONE; + bool retVal = false; - if (useEmuThread) { - EmuThreadStart(); - sleep_ms(4, "libretro-serialize"); + // The buffer was sized by an earlier retro_serialize_size(); the state + // may have grown since, so never write past 'size'. + size_t measuredSize = CChunkFileReader::MeasurePtr(state); + if (measuredSize <= size) { + u8 *buffer = (u8 *)data; + auto err = CChunkFileReader::MeasureAndSavePtr(state, &buffer, &measuredSize); + retVal = err == CChunkFileReader::ERROR_NONE; + } else { + ERROR_LOG(Log::SaveState, "Savestate (%d bytes) does not fit in frontend buffer (%d bytes)", + (int)measuredSize, (int)size); } + if (useEmuThread) + EmuThreadStart(); + return retVal; } @@ -1797,9 +1828,13 @@ bool retro_unserialize(const void *data, size_t size) { // The HW renderer isn't ready on first pass. // So we save the data until we are ready to use it. if (!gpu) { - unserialize_data = malloc(size); - memcpy(unserialize_data, data, size); + void *buffer = malloc(size); + if (!buffer) + return false; + free(unserialize_data); + unserialize_data = buffer; unserialize_size = size; + memcpy(unserialize_data, data, size); return true; } @@ -1812,31 +1847,32 @@ bool retro_unserialize(const void *data, size_t size) { bool retVal = CChunkFileReader::LoadPtr((u8 *)data, size, state, &errorString) == CChunkFileReader::ERROR_NONE; - if (useEmuThread) { + if (useEmuThread) EmuThreadStart(); - sleep_ms(4, "libretro-unserialize"); - } return retVal; } void *retro_get_memory_data(unsigned id) { - if ( id == RETRO_MEMORY_SYSTEM_RAM ) - return Memory::GetPointerWriteUnchecked(PSP_GetKernelMemoryBase()) ; + // Frontends query this right after retro_load_game(), before the + // deferred boot in retro_run() has mapped PSP memory. + if (id == RETRO_MEMORY_SYSTEM_RAM && Memory::IsActive()) + return Memory::GetPointerWriteUnchecked(PSP_GetKernelMemoryBase()); return NULL; } size_t retro_get_memory_size(unsigned id) { - if ( id == RETRO_MEMORY_SYSTEM_RAM ) - return Memory::g_MemorySize ; + if (id == RETRO_MEMORY_SYSTEM_RAM && Memory::IsActive()) + return Memory::g_MemorySize; return 0; } void retro_cheat_reset(void) { // Init Cheat Engine - CWCheatEngine *cheatEngine = new CWCheatEngine(g_paramSFO.GetDiscID()); + CWCheatEngine cheatEngineObj(g_paramSFO.GetDiscID()); + CWCheatEngine *cheatEngine = &cheatEngineObj; Path file=cheatEngine->CheatFilename(); // Output cheats to cheat file @@ -1858,7 +1894,8 @@ void retro_cheat_reset(void) { void retro_cheat_set(unsigned index, bool enabled, const char *code) { // Initialize Cheat Engine - CWCheatEngine *cheatEngine = new CWCheatEngine(g_paramSFO.GetDiscID()); + CWCheatEngine cheatEngineObj(g_paramSFO.GetDiscID()); + CWCheatEngine *cheatEngine = &cheatEngineObj; cheatEngine->CreateCheatFile(); Path file=cheatEngine->CheatFilename(); @@ -2003,26 +2040,22 @@ inline int16_t Clamp16(int32_t sample) { void System_AudioPushSamples(const int32_t *audio, int numSamples, float volume) { // We ignore volume here, because it's handled by libretro presumably. + if (!audio || numSamples <= 0) + return; + std::lock_guard lock(output_audio_buffer_mutex); - // Convert to 16-bit audio for further processing. - int16_t buffer[1024 * 2]; - int origSamples = numSamples * 2; - - while (numSamples > 0) { - int blockSize = std::min(1024, numSamples); - for (int i = 0; i < blockSize; i++) { - buffer[i * 2] = Clamp16(audio[i * 2]); - buffer[i * 2 + 1] = Clamp16(audio[i * 2 + 1]); - } - - numSamples -= blockSize; + int32_t values = numSamples * 2; // stereo + if (output_audio_buffer.capacity - output_audio_buffer.size < values) { + if (!ensure_output_audio_buffer_capacity((int32_t)((output_audio_buffer.capacity + values) * 1.5f))) + return; } - if (output_audio_buffer.capacity - output_audio_buffer.size < origSamples) - ensure_output_audio_buffer_capacity((output_audio_buffer.capacity + origSamples) * 1.5); - memcpy(output_audio_buffer.data + output_audio_buffer.size, buffer, origSamples * sizeof(*output_audio_buffer.data)); - output_audio_buffer.size += origSamples; + // Convert straight into the output buffer. + int16_t *dst = output_audio_buffer.data + output_audio_buffer.size; + for (int32_t i = 0; i < values; i++) + dst[i] = Clamp16(audio[i]); + output_audio_buffer.size += values; } void System_AudioGetDebugStats(char *buf, size_t bufSize) { if (buf) buf[0] = '\0'; }