55 */
66#include " effects.h"
77
8+ #include < array>
89#include < cstdint>
910#include < string_view>
1011
1112#include < expected.hpp>
1213#include < magic_enum/magic_enum.hpp>
14+ #ifdef USE_SDL3
15+ #include < SDL3/SDL_timer.h>
16+ #else
17+ #include < SDL.h>
18+ #endif
1319
1420#include " data/file.hpp"
1521#include " data/iterators.hpp"
@@ -43,6 +49,73 @@ TSFX *sgpStreamSFX = nullptr;
4349std::vector<TSFX> sgSFX;
4450
4551#ifdef __DREAMCAST__
52+ constexpr uint32_t DreamcastMissingLoadRetryMs = 2000 ;
53+ constexpr uint32_t DreamcastDeferredLoadRetryMs = 250 ;
54+ constexpr uint32_t DreamcastLateLoadThresholdMs = 20 ;
55+ constexpr uint32_t DreamcastRealtimeLoadIntervalMs = 500 ;
56+
57+ std::array<uint32_t , static_cast <size_t >(SfxID::LAST) + 1 > SfxLoadRetryAfterMs {};
58+ uint32_t NextDreamcastRealtimeLoadAtMs = 0 ;
59+
60+ size_t GetSfxIndex (const TSFX *sfx)
61+ {
62+ return static_cast <size_t >(sfx - sgSFX.data ());
63+ }
64+
65+ bool ShouldAttemptSfxLoadNow (const TSFX *sfx)
66+ {
67+ const size_t index = GetSfxIndex (sfx);
68+ return SDL_GetTicks () >= SfxLoadRetryAfterMs[index];
69+ }
70+
71+ void DeferSfxLoad (const TSFX *sfx, uint32_t delayMs)
72+ {
73+ const size_t index = GetSfxIndex (sfx);
74+ SfxLoadRetryAfterMs[index] = SDL_GetTicks () + delayMs;
75+ }
76+
77+ bool TryLoadSfxForPlayback (TSFX *sfx, bool stream, bool allowBlockingLoad, bool *loadedLate)
78+ {
79+ if (loadedLate != nullptr )
80+ *loadedLate = false ;
81+ if (sfx->pSnd != nullptr )
82+ return true ;
83+ if (!ShouldAttemptSfxLoadNow (sfx))
84+ return false ;
85+ if (!allowBlockingLoad) {
86+ DeferSfxLoad (sfx, DreamcastDeferredLoadRetryMs);
87+ return false ;
88+ }
89+
90+ const uint32_t startedAt = SDL_GetTicks ();
91+ sfx->pSnd = sound_file_load (sfx->pszName .c_str (), stream);
92+ if (sfx->pSnd == nullptr ) {
93+ DeferSfxLoad (sfx, DreamcastMissingLoadRetryMs);
94+ return false ;
95+ }
96+
97+ if (loadedLate != nullptr && SDL_GetTicks () - startedAt > DreamcastLateLoadThresholdMs)
98+ *loadedLate = true ;
99+ return true ;
100+ }
101+
102+ void PreloadDreamcastSfx (SfxID id)
103+ {
104+ const size_t index = static_cast <size_t >(id);
105+ if (index >= sgSFX.size ())
106+ return ;
107+
108+ TSFX &sfx = sgSFX[index];
109+ if (sfx.pSnd != nullptr || (sfx.bFlags & sfx_STREAM) != 0 )
110+ return ;
111+ if (!ShouldAttemptSfxLoadNow (&sfx))
112+ return ;
113+
114+ sfx.pSnd = sound_file_load (sfx.pszName .c_str (), /* stream=*/ false );
115+ if (sfx.pSnd == nullptr )
116+ DeferSfxLoad (&sfx, DreamcastMissingLoadRetryMs);
117+ }
118+
46119/* *
47120 * Evict non-playing sounds to free memory for new sound loading.
48121 * @param exclude Sound to skip during eviction (the one being loaded)
@@ -87,8 +160,11 @@ void StreamPlay(TSFX *pSFX, int lVolume, int lPan)
87160 if (pSFX->pSnd == nullptr ) {
88161 music_mute ();
89162 EvictSoundsIfNeeded (pSFX, /* streamOnly=*/ true , /* maxLoaded=*/ 8 , /* targetLoaded=*/ 4 );
90- pSFX->pSnd = sound_file_load (pSFX->pszName .c_str (), AllowStreaming);
163+ bool loadedLate = false ;
164+ const bool loaded = TryLoadSfxForPlayback (pSFX, AllowStreaming, /* allowBlockingLoad=*/ true , &loadedLate);
91165 music_unmute ();
166+ if (!loaded || loadedLate)
167+ return ;
92168 }
93169 if (pSFX->pSnd != nullptr && pSFX->pSnd ->DSB .IsLoaded ())
94170 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)
137213 if (pSFX->pSnd == nullptr ) {
138214 music_mute ();
139215 EvictSoundsIfNeeded (pSFX, /* streamOnly=*/ false , /* maxLoaded=*/ 20 , /* targetLoaded=*/ 15 );
140- pSFX->pSnd = sound_file_load (pSFX->pszName .c_str ());
141- // If loading failed (OOM), evict ALL non-playing sounds and retry once.
142- if (pSFX->pSnd == nullptr ) {
216+ bool loadedLate = false ;
217+ const uint32_t now = SDL_GetTicks ();
218+ const bool canDoRealtimeLoad = !loc || now >= NextDreamcastRealtimeLoadAtMs;
219+ bool loaded = TryLoadSfxForPlayback (pSFX, /* stream=*/ false , /* allowBlockingLoad=*/ canDoRealtimeLoad, &loadedLate);
220+ if (loc && canDoRealtimeLoad)
221+ NextDreamcastRealtimeLoadAtMs = SDL_GetTicks () + DreamcastRealtimeLoadIntervalMs;
222+ // For non-positional (menu/UI) sounds, one eviction+retry is acceptable.
223+ if (!loaded && !loc) {
143224 EvictSoundsIfNeeded (nullptr , /* streamOnly=*/ false , /* maxLoaded=*/ 0 , /* targetLoaded=*/ 0 );
144225 ClearDuplicateSounds ();
145- pSFX->pSnd = sound_file_load (pSFX->pszName .c_str ());
226+ DeferSfxLoad (pSFX, 0 );
227+ loaded = TryLoadSfxForPlayback (pSFX, /* stream=*/ false , /* allowBlockingLoad=*/ true , &loadedLate);
146228 }
147229 music_unmute ();
230+ if (!loaded || loadedLate)
231+ return ;
148232 }
149233#else
150234 if (pSFX->pSnd == nullptr )
@@ -212,6 +296,9 @@ void LoadEffectsData()
212296 reader.readString (" path" , item.pszName );
213297 }
214298 sgSFX.shrink_to_fit ();
299+ #ifdef __DREAMCAST__
300+ SfxLoadRetryAfterMs.fill (0 );
301+ #endif
215302}
216303
217304void PrivSoundInit (uint8_t bLoadMask)
@@ -223,14 +310,18 @@ void PrivSoundInit(uint8_t bLoadMask)
223310 if (sgSFX.empty ()) LoadEffectsData ();
224311
225312#ifdef __DREAMCAST__
226- // On Dreamcast (16MB RAM), skip preloading sounds to avoid OOM.
227- // Sounds load on-demand in PlaySfxPriv/StreamPlay when first played.
228- // Free all non-playing sounds to reclaim memory during level transitions.
313+ // Free non-playing sounds during level transitions to reclaim RAM.
229314 for (auto &sfx : sgSFX) {
230315 if (sfx.pSnd != nullptr && !sfx.pSnd ->isPlaying ()) {
231316 sfx.pSnd = nullptr ;
232317 }
233318 }
319+ // Keep high-frequency sounds resident to avoid CD reads in combat.
320+ for (const SfxID id : { SfxID::Walk, SfxID::Swing, SfxID::Swing2, SfxID::ShootBow, SfxID::CastSpell,
321+ SfxID::CastFire, SfxID::SpellFireHit, SfxID::ItemPotion, SfxID::ItemGold, SfxID::GrabItem,
322+ SfxID::DoorOpen, SfxID::DoorClose, SfxID::ChestOpen, SfxID::MenuMove, SfxID::MenuSelect }) {
323+ PreloadDreamcastSfx (id);
324+ }
234325 (void )bLoadMask;
235326 return ;
236327#endif
@@ -329,6 +420,9 @@ void effects_cleanup_sfx(bool fullUnload)
329420
330421 if (fullUnload) {
331422 sgSFX.clear ();
423+ #ifdef __DREAMCAST__
424+ SfxLoadRetryAfterMs.fill (0 );
425+ #endif
332426 return ;
333427 }
334428
0 commit comments