From 80a4915cc498380b9c3fd0fb5d7d47160e6e4b4e Mon Sep 17 00:00:00 2001 From: Panagiotis Georgiadis Date: Sun, 1 Mar 2026 23:42:48 +0100 Subject: [PATCH] Improve Dreamcast sound playback stability. Preload frequently used effects and rate-limit on-demand loads so missing or slow files are skipped instead of stalling gameplay frames. --- Source/effects.cpp | 110 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 8 deletions(-) diff --git a/Source/effects.cpp b/Source/effects.cpp index 525053f39..d98beb4de 100644 --- a/Source/effects.cpp +++ b/Source/effects.cpp @@ -5,11 +5,17 @@ */ #include "effects.h" +#include #include #include #include #include +#ifdef USE_SDL3 +#include +#else +#include +#endif #include "data/file.hpp" #include "data/iterators.hpp" @@ -43,6 +49,73 @@ TSFX *sgpStreamSFX = nullptr; std::vector sgSFX; #ifdef __DREAMCAST__ +constexpr uint32_t DreamcastMissingLoadRetryMs = 2000; +constexpr uint32_t DreamcastDeferredLoadRetryMs = 250; +constexpr uint32_t DreamcastLateLoadThresholdMs = 20; +constexpr uint32_t DreamcastRealtimeLoadIntervalMs = 500; + +std::array(SfxID::LAST) + 1> SfxLoadRetryAfterMs {}; +uint32_t NextDreamcastRealtimeLoadAtMs = 0; + +size_t GetSfxIndex(const TSFX *sfx) +{ + return static_cast(sfx - sgSFX.data()); +} + +bool ShouldAttemptSfxLoadNow(const TSFX *sfx) +{ + const size_t index = GetSfxIndex(sfx); + return SDL_GetTicks() >= SfxLoadRetryAfterMs[index]; +} + +void DeferSfxLoad(const TSFX *sfx, uint32_t delayMs) +{ + const size_t index = GetSfxIndex(sfx); + SfxLoadRetryAfterMs[index] = SDL_GetTicks() + delayMs; +} + +bool TryLoadSfxForPlayback(TSFX *sfx, bool stream, bool allowBlockingLoad, bool *loadedLate) +{ + if (loadedLate != nullptr) + *loadedLate = false; + if (sfx->pSnd != nullptr) + return true; + if (!ShouldAttemptSfxLoadNow(sfx)) + return false; + if (!allowBlockingLoad) { + DeferSfxLoad(sfx, DreamcastDeferredLoadRetryMs); + return false; + } + + const uint32_t startedAt = SDL_GetTicks(); + sfx->pSnd = sound_file_load(sfx->pszName.c_str(), stream); + if (sfx->pSnd == nullptr) { + DeferSfxLoad(sfx, DreamcastMissingLoadRetryMs); + return false; + } + + if (loadedLate != nullptr && SDL_GetTicks() - startedAt > DreamcastLateLoadThresholdMs) + *loadedLate = true; + return true; +} + +void PreloadDreamcastSfx(SfxID id) +{ + const size_t index = static_cast(id); + if (index >= sgSFX.size()) + return; + + TSFX &sfx = sgSFX[index]; + if (sfx.pSnd != nullptr || (sfx.bFlags & sfx_STREAM) != 0) + return; + if (!ShouldAttemptSfxLoadNow(&sfx)) + return; + + sfx.pSnd = sound_file_load(sfx.pszName.c_str(), /*stream=*/false); + if (sfx.pSnd == nullptr) + DeferSfxLoad(&sfx, DreamcastMissingLoadRetryMs); +} + /** * Evict non-playing sounds to free memory for new sound loading. * @param exclude Sound to skip during eviction (the one being loaded) @@ -87,8 +160,11 @@ void StreamPlay(TSFX *pSFX, int lVolume, int lPan) if (pSFX->pSnd == nullptr) { music_mute(); EvictSoundsIfNeeded(pSFX, /*streamOnly=*/true, /*maxLoaded=*/8, /*targetLoaded=*/4); - pSFX->pSnd = sound_file_load(pSFX->pszName.c_str(), AllowStreaming); + bool loadedLate = false; + const bool loaded = TryLoadSfxForPlayback(pSFX, AllowStreaming, /*allowBlockingLoad=*/true, &loadedLate); music_unmute(); + if (!loaded || loadedLate) + return; } if (pSFX->pSnd != nullptr && pSFX->pSnd->DSB.IsLoaded()) pSFX->pSnd->DSB.PlayWithVolumeAndPan(lVolume, sound_get_or_set_sound_volume(1), lPan); @@ -137,14 +213,22 @@ void PlaySfxPriv(TSFX *pSFX, bool loc, Point position) if (pSFX->pSnd == nullptr) { music_mute(); EvictSoundsIfNeeded(pSFX, /*streamOnly=*/false, /*maxLoaded=*/20, /*targetLoaded=*/15); - pSFX->pSnd = sound_file_load(pSFX->pszName.c_str()); - // If loading failed (OOM), evict ALL non-playing sounds and retry once. - if (pSFX->pSnd == nullptr) { + bool loadedLate = false; + const uint32_t now = SDL_GetTicks(); + const bool canDoRealtimeLoad = !loc || now >= NextDreamcastRealtimeLoadAtMs; + bool loaded = TryLoadSfxForPlayback(pSFX, /*stream=*/false, /*allowBlockingLoad=*/canDoRealtimeLoad, &loadedLate); + if (loc && canDoRealtimeLoad) + NextDreamcastRealtimeLoadAtMs = SDL_GetTicks() + DreamcastRealtimeLoadIntervalMs; + // For non-positional (menu/UI) sounds, one eviction+retry is acceptable. + if (!loaded && !loc) { EvictSoundsIfNeeded(nullptr, /*streamOnly=*/false, /*maxLoaded=*/0, /*targetLoaded=*/0); ClearDuplicateSounds(); - pSFX->pSnd = sound_file_load(pSFX->pszName.c_str()); + DeferSfxLoad(pSFX, 0); + loaded = TryLoadSfxForPlayback(pSFX, /*stream=*/false, /*allowBlockingLoad=*/true, &loadedLate); } music_unmute(); + if (!loaded || loadedLate) + return; } #else if (pSFX->pSnd == nullptr) @@ -212,6 +296,9 @@ void LoadEffectsData() reader.readString("path", item.pszName); } sgSFX.shrink_to_fit(); +#ifdef __DREAMCAST__ + SfxLoadRetryAfterMs.fill(0); +#endif } void PrivSoundInit(uint8_t bLoadMask) @@ -223,14 +310,18 @@ void PrivSoundInit(uint8_t bLoadMask) if (sgSFX.empty()) LoadEffectsData(); #ifdef __DREAMCAST__ - // On Dreamcast (16MB RAM), skip preloading sounds to avoid OOM. - // Sounds load on-demand in PlaySfxPriv/StreamPlay when first played. - // Free all non-playing sounds to reclaim memory during level transitions. + // Free non-playing sounds during level transitions to reclaim RAM. for (auto &sfx : sgSFX) { if (sfx.pSnd != nullptr && !sfx.pSnd->isPlaying()) { sfx.pSnd = nullptr; } } + // Keep high-frequency sounds resident to avoid CD reads in combat. + for (const SfxID id : { SfxID::Walk, SfxID::Swing, SfxID::Swing2, SfxID::ShootBow, SfxID::CastSpell, + SfxID::CastFire, SfxID::SpellFireHit, SfxID::ItemPotion, SfxID::ItemGold, SfxID::GrabItem, + SfxID::DoorOpen, SfxID::DoorClose, SfxID::ChestOpen, SfxID::MenuMove, SfxID::MenuSelect }) { + PreloadDreamcastSfx(id); + } (void)bLoadMask; return; #endif @@ -329,6 +420,9 @@ void effects_cleanup_sfx(bool fullUnload) if (fullUnload) { sgSFX.clear(); +#ifdef __DREAMCAST__ + SfxLoadRetryAfterMs.fill(0); +#endif return; }