Skip to content

Commit 80a4915

Browse files
committed
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.
1 parent e63016e commit 80a4915

1 file changed

Lines changed: 102 additions & 8 deletions

File tree

Source/effects.cpp

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
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;
4349
std::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

217304
void 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

Comments
 (0)