summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThorbjørn Lindeijer <bjorn@lindeijer.nl>2025-02-20 15:41:35 +0000
committerThorbjørn Lindeijer <bjorn@lindeijer.nl>2025-02-20 16:52:22 +0100
commit51b0c3239265ddee2d1bf445f873299cc8193ab9 (patch)
tree358a2881ce101cd0f96c31cb21694d3451d96046
parent5c12ec8153cd401a99f505e39389f8450a625eab (diff)
downloadmana-51b0c3239265ddee2d1bf445f873299cc8193ab9.tar.gz
mana-51b0c3239265ddee2d1bf445f873299cc8193ab9.tar.bz2
mana-51b0c3239265ddee2d1bf445f873299cc8193ab9.tar.xz
mana-51b0c3239265ddee2d1bf445f873299cc8193ab9.zip
Fixed stutter when new music starts playing
This is a workaround for a performance issue when SDL_mixer is using stb_vorbis. Since stb_vorbis will request the file one byte at a time, the overhead of using SDL_RWops to call PHYSFS_readBytes is too high. Solved by introducing a buffered SDL_RWops wrapper with a fixed 2048 byte buffer. This is a more optimal solution than loading the entire OGG file in memory. Now starting a new song takes less than 1ms, when the OGG file isn't compressed (almost down to 0.2ms when SDL_mixer uses libvorbisfile). See https://github.com/libsdl-org/SDL_mixer/issues/670 Closes https://git.themanaworld.org/mana/mana/-/issues/93
-rw-r--r--NEWS1
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/resources/resourcemanager.cpp11
-rw-r--r--src/utils/bufferedrwops.c202
-rw-r--r--src/utils/bufferedrwops.h30
-rw-r--r--src/utils/filesystem.h9
6 files changed, 254 insertions, 1 deletions
diff --git a/NEWS b/NEWS
index a2cc3091..7c1437dd 100644
--- a/NEWS
+++ b/NEWS
@@ -59,6 +59,7 @@
- Fixed choosing default world when using -D command-line parameter
- Fixed storing of player relations
- Fixed handling of custom port in update URL
+- Fixed stutter when new music starts playing
- Updated to tmwAthena protocol changes
- Updated to Manaserv protocol changes (specials, guilds, debug mode, skills, text particles)
- CMake: Use GNUInstallDirs and made PKG_DATADIR / PKG_BINDIR paths modifiable
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d5131529..521d240d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -366,6 +366,8 @@ set(SRCS
resources/wallpaper.h
utils/base64.cpp
utils/base64.h
+ utils/bufferedrwops.c
+ utils/bufferedrwops.h
utils/copynpaste.cpp
utils/copynpaste.h
utils/dtor.h
diff --git a/src/resources/resourcemanager.cpp b/src/resources/resourcemanager.cpp
index fa7ac0e3..c64786ab 100644
--- a/src/resources/resourcemanager.cpp
+++ b/src/resources/resourcemanager.cpp
@@ -231,8 +231,17 @@ Resource *ResourceManager::get(const std::string &idPath,
Resource *ResourceManager::get(const std::string &path, loader fun)
{
return get(path, [&] () -> Resource * {
- if (SDL_RWops *rw = FS::openRWops(path))
+ //
+ // We use a buffered SDL_RWops to workaround a performance issue when
+ // SDL_mixer is using stb_vorbis. The overhead of calling
+ // PHYSFS_readBytes each time is too high because stb_vorbis requests
+ // the file one byte at a time.
+ //
+ // See https://github.com/libsdl-org/SDL_mixer/issues/670
+ //
+ if (SDL_RWops *rw = FS::openBufferedRWops(path))
return fun(rw);
+
return nullptr;
});
}
diff --git a/src/utils/bufferedrwops.c b/src/utils/bufferedrwops.c
new file mode 100644
index 00000000..441b477c
--- /dev/null
+++ b/src/utils/bufferedrwops.c
@@ -0,0 +1,202 @@
+/*
+ * Buffered SDL_RWops implementation
+ * Copyright (C) 2025 Thorbjørn Lindeijer
+ *
+ * Written as part of a conversation between a human and an AI assistant
+ * on GitHub Copilot Chat.
+ *
+ * This code is released into the public domain. You may use it freely
+ * for any purpose, including commercial applications.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "bufferedrwops.h"
+
+#include <string.h>
+
+#define BUFFER_SIZE 2048
+
+typedef struct BufferedRWops {
+ SDL_RWops* source; // The underlying RWops we're wrapping
+ size_t bufferPos; // Current position within buffer
+ size_t bufferFill; // How much valid data is in buffer
+ Sint64 virtualPos; // Current virtual stream position
+ Uint8 buffer[BUFFER_SIZE]; // Our read-ahead buffer
+} BufferedRWops;
+
+static void fillBuffer(BufferedRWops* br) {
+ // If there's still data in the buffer, move it to the start
+ if (br->bufferPos < br->bufferFill) {
+ size_t remaining = br->bufferFill - br->bufferPos;
+ memmove(br->buffer, br->buffer + br->bufferPos, remaining);
+ br->bufferFill = remaining;
+ } else {
+ br->bufferFill = 0;
+ }
+ br->bufferPos = 0;
+
+ // Fill the rest of the buffer
+ size_t space = BUFFER_SIZE - br->bufferFill;
+ if (space > 0) {
+ size_t read = SDL_RWread(br->source,
+ br->buffer + br->bufferFill,
+ 1, space);
+ br->bufferFill += read;
+ }
+}
+
+static Sint64 SDLCALL buffered_size(SDL_RWops* context) {
+ BufferedRWops* br = (BufferedRWops*)context->hidden.unknown.data1;
+ return SDL_RWsize(br->source);
+}
+
+static Sint64 SDLCALL buffered_seek(SDL_RWops* context, Sint64 offset, int whence) {
+ BufferedRWops* br = (BufferedRWops*)context->hidden.unknown.data1;
+ Sint64 newPos;
+
+ // Calculate new position
+ switch (whence) {
+ case RW_SEEK_SET:
+ newPos = offset;
+ break;
+ case RW_SEEK_CUR:
+ newPos = br->virtualPos + offset;
+ break;
+ case RW_SEEK_END: {
+ Sint64 size = SDL_RWsize(br->source);
+ if (size < 0) return -1;
+ newPos = size + offset;
+ break;
+ }
+ default:
+ SDL_SetError("Invalid whence value");
+ return -1;
+ }
+
+ if (newPos < 0) {
+ SDL_SetError("Attempt to seek before start of file");
+ return -1;
+ }
+
+ // Check if new position is within our buffer
+ Sint64 bufferStart = br->virtualPos - br->bufferPos;
+ Sint64 bufferEnd = bufferStart + br->bufferFill;
+
+ if (newPos >= bufferStart && newPos < bufferEnd) {
+ // Position is within buffer - just update buffer position
+ br->bufferPos = (size_t)(newPos - bufferStart);
+ } else {
+ // Position is outside buffer - need to seek in source
+ if (SDL_RWseek(br->source, newPos, RW_SEEK_SET) < 0) {
+ return -1;
+ }
+ br->bufferPos = 0;
+ br->bufferFill = 0;
+ }
+
+ br->virtualPos = newPos;
+ return newPos;
+}
+
+static size_t SDLCALL buffered_read(SDL_RWops* context, void* dst, size_t size, size_t maxnum) {
+ BufferedRWops* br = (BufferedRWops*)context->hidden.unknown.data1;
+ size_t total = size * maxnum;
+ size_t copied = 0;
+
+ // Handle single byte reads separately since it is a common case when
+ // SDL_mixer uses stb_vorbis.
+ if (total == 1 && br->bufferPos < br->bufferFill) {
+ *(Uint8*)dst = br->buffer[br->bufferPos];
+ ++br->bufferPos;
+ ++br->virtualPos;
+ return 1;
+ }
+
+ while (copied < total) {
+ size_t remaining = total - copied;
+
+ // If buffer is empty or exhausted, try to fill it
+ if (br->bufferPos >= br->bufferFill) {
+ // If we want to read more than the buffer size, bypass the buffer
+ if (remaining >= BUFFER_SIZE) {
+ size_t read = SDL_RWread(br->source, dst + copied, 1, remaining);
+ copied += read;
+ br->virtualPos += read;
+ break; // Nothing more to read
+ }
+
+ fillBuffer(br);
+ if (br->bufferFill == 0) break; // EOF or error
+ }
+
+ // Copy what we can from the buffer
+ size_t available = br->bufferFill - br->bufferPos;
+ size_t toCopy = remaining < available ? remaining : available;
+
+ memcpy(dst + copied, br->buffer + br->bufferPos, toCopy);
+ br->bufferPos += toCopy;
+ copied += toCopy;
+ br->virtualPos += toCopy;
+ }
+
+ return copied / size;
+}
+
+static size_t SDLCALL buffered_write(SDL_RWops* context, const void* ptr, size_t size, size_t num) {
+ SDL_SetError("Write operations not supported on buffered read-only RWops");
+ return 0;
+}
+
+static int SDLCALL buffered_close(SDL_RWops* context) {
+ if (context) {
+ BufferedRWops* br = (BufferedRWops*)context->hidden.unknown.data1;
+ if (br) {
+ if (br->source) {
+ SDL_RWclose(br->source);
+ }
+ SDL_free(br);
+ }
+ SDL_FreeRW(context);
+ }
+ return 0;
+}
+
+SDL_RWops* createBufferedRWops(SDL_RWops* source) {
+ if (!source) {
+ SDL_SetError("NULL source RWops");
+ return NULL;
+ }
+
+ BufferedRWops* br = (BufferedRWops*)SDL_malloc(sizeof(BufferedRWops));
+ if (!br) {
+ SDL_SetError("Out of memory");
+ return NULL;
+ }
+
+ br->source = source;
+ br->bufferPos = 0;
+ br->bufferFill = 0;
+ br->virtualPos = 0;
+
+ SDL_RWops* rw = SDL_AllocRW();
+ if (!rw) {
+ SDL_free(br);
+ return NULL;
+ }
+
+ rw->size = buffered_size;
+ rw->seek = buffered_seek;
+ rw->read = buffered_read;
+ rw->write = buffered_write;
+ rw->close = buffered_close;
+ rw->hidden.unknown.data1 = br;
+
+ return rw;
+}
diff --git a/src/utils/bufferedrwops.h b/src/utils/bufferedrwops.h
new file mode 100644
index 00000000..bba29467
--- /dev/null
+++ b/src/utils/bufferedrwops.h
@@ -0,0 +1,30 @@
+/*
+ * Buffered SDL_RWops implementation
+ * Copyright (C) 2025 Thorbjørn Lindeijer
+ *
+ * Written as part of a conversation between a human and an AI assistant
+ * on GitHub Copilot Chat.
+ *
+ * This code is released into the public domain. You may use it freely
+ * for any purpose, including commercial applications.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "SDL.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+SDL_RWops* createBufferedRWops(SDL_RWops* source);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/utils/filesystem.h b/src/utils/filesystem.h
index 796bc906..10c67202 100644
--- a/src/utils/filesystem.h
+++ b/src/utils/filesystem.h
@@ -23,6 +23,7 @@
// Suppress deprecation warnings for PHYSFS_getUserDir
#define PHYSFS_DEPRECATED
+#include "utils/bufferedrwops.h"
#include "utils/physfsrwops.h"
#include <optional>
@@ -263,6 +264,14 @@ inline SDL_RWops *openRWops(const std::string &path)
return PHYSFSRWOPS_openRead(path.c_str());
}
+inline SDL_RWops *openBufferedRWops(const std::string &path)
+{
+ auto rw = PHYSFSRWOPS_openRead(path.c_str());
+ if (auto buffered = createBufferedRWops(rw))
+ return buffered;
+ return rw;
+}
+
inline void *loadFile(const std::string &path, size_t &datasize)
{
auto file = openRWops(path);