[Scummvm-git-logs] scummvm master -> edd14e08a65d198a65071519421299b21f57eb77
chkuendig
noreply at scummvm.org
Sun Jun 28 20:06:26 UTC 2026
This automated email contains information about 5 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
26fa61ad5e EMSCRIPTEN: TIMER: Add EmscriptenTimerManager to allow for I/O to happen in timers and limit calls to SDL_Delay to impro
1dccebfb0c EMSCRIPTEN: AUDIO: Add Emscripten-specific mixer manager that does not perform I/O in the SDL callback (SDL3 only)
1a02bc153f NETWORKING: Rewrite NetworkReadStreamEmscripten
9285b5e115 EMSCRIPTEN: Fix HEAPU8 access in downloadFile (broke in ade1c2fa60a)
edd14e08a6 NETWORKING: Cleanup CURL specific variables from the common class
Commit: 26fa61ad5ee53315b31865a87b93c066f670b96d
https://github.com/scummvm/scummvm/commit/26fa61ad5ee53315b31865a87b93c066f670b96d
Author: Christian Kündig (christian at kuendig.info)
Date: 2026-06-28T22:06:19+02:00
Commit Message:
EMSCRIPTEN: TIMER: Add EmscriptenTimerManager to allow for I/O to happen in timers and limit calls to SDL_Delay to improve performance
Changed paths:
A backends/timer/emscripten/emscripten-timer.cpp
A backends/timer/emscripten/emscripten-timer.h
backends/module.mk
backends/platform/sdl/emscripten/emscripten.cpp
backends/timer/default/default-timer.cpp
backends/timer/default/default-timer.h
diff --git a/backends/module.mk b/backends/module.mk
index f15e8947d81..6af4c276dce 100644
--- a/backends/module.mk
+++ b/backends/module.mk
@@ -104,7 +104,8 @@ MODULE_OBJS += \
fs/emscripten/emscripten-fs-factory.o \
fs/emscripten/emscripten-posix-fs.o \
fs/emscripten/http-fs.o \
- midi/webmidi.o
+ midi/webmidi.o \
+ timer/emscripten/emscripten-timer.o
ifdef USE_CLOUD
MODULE_OBJS += \
fs/emscripten/cloud-fs.o
diff --git a/backends/platform/sdl/emscripten/emscripten.cpp b/backends/platform/sdl/emscripten/emscripten.cpp
index 03ecd52c3ab..4d665eb86b5 100644
--- a/backends/platform/sdl/emscripten/emscripten.cpp
+++ b/backends/platform/sdl/emscripten/emscripten.cpp
@@ -30,7 +30,7 @@
#include "backends/mutex/null/null-mutex.h"
#include "backends/fs/emscripten/emscripten-fs-factory.h"
#include "backends/platform/sdl/emscripten/emscripten.h"
-#include "backends/timer/default/default-timer.h"
+#include "backends/timer/emscripten/emscripten-timer.h"
#include "common/file.h"
#ifdef USE_TTS
#include "backends/text-to-speech/emscripten/emscripten-text-to-speech.h"
@@ -108,10 +108,6 @@ void OSystem_Emscripten::initBackend() {
_textToSpeechManager = new EmscriptenTextToSpeechManager();
#endif
- // SDL Timers don't work in Emscripten unless threads are enabled or Asyncify is disabled.
- // We can do neither, so we use the DefaultTimerManager instead.
- _timerManager = new DefaultTimerManager();
-
// Event source
_eventSource = new EmscriptenSdlEventSource();
@@ -120,6 +116,13 @@ void OSystem_Emscripten::initBackend() {
}
void OSystem_Emscripten::init() {
+
+ // SDL Timers don't work in Emscripten unless threads are enabled or Asyncify is disabled.
+ // We can do neither, so we use the EmscriptenTimerManager instead.
+ // This has to be done before the filesystem is initialized so it's available for folders
+ // being loaded over HTTP.
+ _timerManager = new EmscriptenTimerManager();
+
// Initialze File System Factory
EmscriptenFilesystemFactory *fsFactory = new EmscriptenFilesystemFactory();
_fsFactory = fsFactory;
@@ -199,24 +202,6 @@ void OSystem_Emscripten::exportFile(const Common::Path &filename) {
delete[] bytes;
}
-void OSystem_Emscripten::updateTimers() {
- // avoid a recursion loop if a timer callback decides to call OSystem::delayMillis()
- static bool inTimer = false;
-
- if (!inTimer) {
- inTimer = true;
- ((DefaultTimerManager *)_timerManager)->checkTimers();
- inTimer = false;
- } else {
- const Common::ConfigManager::Domain *activeDomain = ConfMan.getActiveDomain();
- assert(activeDomain);
-
- warning("%s/%s calls update() from timer",
- activeDomain->getValOrDefault("engineid").c_str(),
- activeDomain->getValOrDefault("gameid").c_str());
- }
-}
-
Common::MutexInternal *OSystem_Emscripten::createMutex() {
return new NullMutexInternal();
}
@@ -231,22 +216,17 @@ void OSystem_Emscripten::addSysArchivesToSearchSet(Common::SearchSet &s, int pri
}
void OSystem_Emscripten::delayMillis(uint msecs) {
- static uint32 lastThreshold = 0;
- const uint32 threshold = getMillis() + msecs;
- if (msecs == 0 && threshold - lastThreshold < 10) {
+ static uint32 lastSleep = 0;
+ if (msecs == 0 && getMillis() - lastSleep < 20) {
return;
}
- uint32 pause = 0;
- do {
#ifdef ENABLE_EVENTRECORDER
- if (!g_eventRec.processDelayMillis())
+ if (!g_eventRec.processDelayMillis())
#endif
- SDL_Delay(pause);
- updateTimers();
- pause = getMillis() > threshold ? 0 : threshold - getMillis(); // Avoid negative values
- pause = pause > 10 ? 10 : pause; // ensure we don't pause for too long
- } while (pause > 0);
- lastThreshold = threshold;
+ SDL_Delay(msecs);
+
+ ((EmscriptenTimerManager *)_timerManager)->checkTimers();
+ lastSleep = getMillis();
}
#ifdef USE_CLOUD
diff --git a/backends/timer/default/default-timer.cpp b/backends/timer/default/default-timer.cpp
index 84981106d41..2212b1ca05d 100644
--- a/backends/timer/default/default-timer.cpp
+++ b/backends/timer/default/default-timer.cpp
@@ -24,19 +24,6 @@
#include "common/util.h"
#include "common/system.h"
-struct TimerSlot {
- Common::TimerManager::TimerProc callback;
- void *refCon;
- Common::String id;
- uint32 interval; // in microseconds
-
- uint32 nextFireTime; // in milliseconds
- uint32 nextFireTimeMicro; // microseconds part of nextFire
-
- TimerSlot *next;
-
- TimerSlot() : callback(nullptr), refCon(nullptr), interval(0), nextFireTime(0), nextFireTimeMicro(0), next(nullptr) {}
-};
void insertPrioQueue(TimerSlot *head, TimerSlot *newSlot) {
// The head points to a fake anchor TimerSlot; this common
diff --git a/backends/timer/default/default-timer.h b/backends/timer/default/default-timer.h
index 661360edeb6..cf59d5065ca 100644
--- a/backends/timer/default/default-timer.h
+++ b/backends/timer/default/default-timer.h
@@ -27,18 +27,35 @@
#include "common/timer.h"
#include "common/mutex.h"
-struct TimerSlot;
+
+struct TimerSlot {
+ Common::TimerManager::TimerProc callback;
+ void *refCon;
+ Common::String id;
+ uint32 interval; // in microseconds
+
+ uint32 nextFireTime; // in milliseconds
+ uint32 nextFireTimeMicro; // microseconds part of nextFire
+
+ TimerSlot *next;
+
+ TimerSlot() : callback(nullptr), refCon(nullptr), interval(0), nextFireTime(0), nextFireTimeMicro(0), next(nullptr) {}
+};
+
+void insertPrioQueue(TimerSlot *head, TimerSlot *newSlot);
class DefaultTimerManager : public Common::TimerManager {
private:
typedef Common::HashMap<Common::String, TimerProc, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> TimerSlotMap;
- Common::Mutex _mutex;
- TimerSlot *_head;
TimerSlotMap _callbacks;
uint32 _timerCallbackNext;
+protected:
+ Common::Mutex _mutex;
+ TimerSlot *_head;
+
public:
DefaultTimerManager();
virtual ~DefaultTimerManager();
@@ -48,7 +65,7 @@ public:
/**
* Timer callback, to be invoked at regular time intervals by the backend.
*/
- void handler();
+ virtual void handler();
/*
* Ensure that the callback is called at regular time intervals.
diff --git a/backends/timer/emscripten/emscripten-timer.cpp b/backends/timer/emscripten/emscripten-timer.cpp
new file mode 100644
index 00000000000..bcedfc429b4
--- /dev/null
+++ b/backends/timer/emscripten/emscripten-timer.cpp
@@ -0,0 +1,75 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "backends/timer/emscripten/emscripten-timer.h"
+#include "common/config-manager.h"
+#include "common/system.h"
+#include "common/util.h"
+
+EmscriptenTimerManager::EmscriptenTimerManager() : DefaultTimerManager() {
+}
+
+EmscriptenTimerManager::~EmscriptenTimerManager() {
+}
+
+void EmscriptenTimerManager::handler() {
+ Common::StackLock lock(_mutex);
+
+ uint32 curTime = g_system->getMillis(true);
+ TimerSlot *slot = _head->next;
+ TimerSlot *prev = _head;
+
+ // Repeat as long as there is a TimerSlot that is scheduled to fire.
+ while (slot && slot->nextFireTime < curTime) {
+ // avoid a recursion loop if a timer callback decides to call OSystem::delayMillis()
+ // timers should not be triggering timers, but many timers trigger I/O - which in case
+ // of virtual FS will trigger delayMillis for the network timer to keep running.
+ // We know the network timer itself does not have this recursion issue, so we let it pass.
+ static bool inTimer = false;
+ bool isConnManTimer = (slot->id == "Networking::ConnectionManager's Timer");
+ if (!inTimer || isConnManTimer) {
+ // Remove the slot from the priority queue
+ prev->next = slot->next;
+ // Update the fire time and reinsert the TimerSlot into the priority
+ // queue.
+ assert(slot->interval > 0);
+ slot->nextFireTime += (slot->interval / 1000);
+ slot->nextFireTimeMicro += (slot->interval % 1000);
+ if (slot->nextFireTimeMicro > 1000) {
+ slot->nextFireTime += slot->nextFireTimeMicro / 1000;
+ slot->nextFireTimeMicro %= 1000;
+ }
+ insertPrioQueue(_head, slot);
+
+ // Invoke the timer callback
+ assert(slot->callback);
+ if (!isConnManTimer)
+ inTimer = true;
+ slot->callback(slot->refCon);
+ if (!isConnManTimer)
+ inTimer = false;
+ } else {
+ // Skip to the next timer, leaving this one in place to be checked again later
+ prev = slot;
+ }
+ slot = prev->next;
+ }
+}
diff --git a/backends/timer/emscripten/emscripten-timer.h b/backends/timer/emscripten/emscripten-timer.h
new file mode 100644
index 00000000000..b26cf889d21
--- /dev/null
+++ b/backends/timer/emscripten/emscripten-timer.h
@@ -0,0 +1,36 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef BACKENDS_TIMER_EMSCRIPTEN_H
+#define BACKENDS_TIMER_EMSCRIPTEN_H
+
+#include "backends/timer/default/default-timer.h"
+
+class EmscriptenTimerManager : public DefaultTimerManager {
+public:
+ EmscriptenTimerManager();
+ virtual ~EmscriptenTimerManager();
+
+protected:
+ void handler() override;
+};
+
+#endif
Commit: 1dccebfb0c244934a6dfdaae13ada148cbd86bff
https://github.com/scummvm/scummvm/commit/1dccebfb0c244934a6dfdaae13ada148cbd86bff
Author: Christian Kündig (christian at kuendig.info)
Date: 2026-06-28T22:06:19+02:00
Commit Message:
EMSCRIPTEN: AUDIO: Add Emscripten-specific mixer manager that does not perform I/O in the SDL callback (SDL3 only)
Changed paths:
A backends/mixer/emscriptensdl/emscriptensdl-mixer.cpp
A backends/mixer/emscriptensdl/emscriptensdl-mixer.h
backends/module.mk
backends/platform/sdl/emscripten/emscripten.cpp
diff --git a/backends/mixer/emscriptensdl/emscriptensdl-mixer.cpp b/backends/mixer/emscriptensdl/emscriptensdl-mixer.cpp
new file mode 100644
index 00000000000..9737b26d691
--- /dev/null
+++ b/backends/mixer/emscriptensdl/emscriptensdl-mixer.cpp
@@ -0,0 +1,160 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/scummsys.h"
+
+#if defined(SDL_BACKEND) && defined(__EMSCRIPTEN__)
+
+#include "backends/mixer/emscriptensdl/emscriptensdl-mixer.h"
+#include "common/debug.h"
+#include "common/system.h"
+#include "common/timer.h"
+
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+EmscriptenSdlMixerManager::EmscriptenSdlMixerManager() : SdlMixerManager() {
+ _dataBuffers = new Common::Queue<Common::Pair<int, Uint8 *> >();
+}
+
+EmscriptenSdlMixerManager::~EmscriptenSdlMixerManager() {
+ // Clean up any remaining buffers
+ while (_dataBuffers && !_dataBuffers->empty()) {
+ Common::Pair<int, Uint8 *> dataBuffer = _dataBuffers->pop();
+ SDL_free(dataBuffer.second);
+ }
+ delete _dataBuffers;
+}
+#endif
+
+void EmscriptenSdlMixerManager::startAudio() {
+ // Start the sound system
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+ _stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &_obtained, emscriptenSdl3Callback, this);
+ if (_stream)
+ SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(_stream));
+ if (!_timerStarted)
+ startTimer();
+#else
+ SDL_PauseAudio(0);
+#endif
+}
+
+void EmscriptenSdlMixerManager::suspendAudio() {
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+ SDL_CloseAudioDevice(SDL_GetAudioStreamDevice(_stream));
+ SDL_DestroyAudioStream(_stream);
+ if (_timerStarted)
+ stopTimer();
+#else
+ SDL_CloseAudio();
+#endif
+ _audioSuspended = true;
+}
+
+int EmscriptenSdlMixerManager::resumeAudio() {
+ if (!_audioSuspended)
+ return -2;
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+ _stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &_obtained, emscriptenSdl3Callback, this);
+ if (!_stream)
+ return -1;
+ if (SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(_stream)))
+ return -1;
+ if (!_timerStarted)
+ startTimer();
+#else
+ if (SDL_OpenAudio(&_obtained, nullptr) < 0) {
+ return -1;
+ }
+ SDL_PauseAudio(0);
+#endif
+ _audioSuspended = false;
+ return 0;
+}
+
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+void EmscriptenSdlMixerManager::startTimer(int interval) {
+ Common::TimerManager *manager = g_system->getTimerManager();
+ if (manager->installTimerProc(updateMixer, interval, this, "EmscriptenSdlMixerManager's Timer")) {
+ _timerStarted = true;
+ } else {
+ warning("Failed to install Networking::ConnectionManager's timer");
+ }
+}
+
+void EmscriptenSdlMixerManager::stopTimer() {
+ debug(9, "timer stopped");
+ Common::TimerManager *manager = g_system->getTimerManager();
+ manager->removeTimerProc(updateMixer);
+ _timerStarted = false;
+}
+
+void EmscriptenSdlMixerManager::emscriptenSdl3Callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) {
+ static int emptyBufferCount = 0;
+ static int lastEmptyBufferPrint = 0;
+ EmscriptenSdlMixerManager *manager = (EmscriptenSdlMixerManager *)userdata;
+
+ if (manager->_dataBuffers->size() > 0) {
+ // Put the data into the stream
+ Common::Pair<int, Uint8 *> dataBuffer = manager->_dataBuffers->pop();
+ SDL_PutAudioStreamData(manager->_stream, dataBuffer.second, dataBuffer.first);
+ SDL_free(dataBuffer.second);
+ } else {
+ emptyBufferCount++;
+ }
+ if (g_system->getMillis() - 5000 > lastEmptyBufferPrint && emptyBufferCount > 0) {
+ debug(5, "EmscriptenSdlMixerManager::emscriptenSdl3Callback called %d times in last 5 second with empty buffer", emptyBufferCount);
+ emptyBufferCount = 0;
+ lastEmptyBufferPrint = g_system->getMillis();
+ }
+ // Just store the requested amount, updateMixer will process it
+ manager->_lastRequestedAmount = additional_amount;
+}
+#endif
+
+void EmscriptenSdlMixerManager::updateMixer(void *refCon) {
+ EmscriptenSdlMixerManager *manager = (EmscriptenSdlMixerManager *)refCon;
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+ static int fullBufferCount = 0;
+ static uint lastFullBufferPrint = 0;
+ if (manager->_dataBuffers->size() < 2) {
+ int amount = manager->_lastRequestedAmount > 0 ? manager->_lastRequestedAmount : 8 * 1024;
+ if (manager->_stream) {
+ Uint8 *dataBuffer = (Uint8 *)SDL_malloc(sizeof(Uint8) * (amount));
+ if (dataBuffer) {
+ // Generate the audio data
+ manager->callbackHandler(dataBuffer, amount);
+ manager->_dataBuffers->push({amount, dataBuffer});
+ }
+ }
+ manager->_lastRequestedAmount = -1; // Reset to -1 after reading
+ } else {
+ fullBufferCount++;
+ }
+ if (g_system->getMillis() - 5000 > lastFullBufferPrint && fullBufferCount > 0) {
+ debug(5, "EmscriptenSdlMixerManager::updateMixer called %d times in last 5 second with full buffer", fullBufferCount);
+ fullBufferCount = 0;
+ lastFullBufferPrint = g_system->getMillis();
+ }
+#endif
+ // For non-SDL3 versions, this method does nothing
+}
+
+#endif
diff --git a/backends/mixer/emscriptensdl/emscriptensdl-mixer.h b/backends/mixer/emscriptensdl/emscriptensdl-mixer.h
new file mode 100644
index 00000000000..a38bf918436
--- /dev/null
+++ b/backends/mixer/emscriptensdl/emscriptensdl-mixer.h
@@ -0,0 +1,75 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef BACKENDS_MIXER_EMSCRIPTENSDL_H
+#define BACKENDS_MIXER_EMSCRIPTENSDL_H
+
+#include "backends/mixer/sdl/sdl-mixer.h"
+#include "common/queue.h"
+
+/**
+ * Emscripten SDL mixer manager. Extends the SDL mixer to decouple
+ * I/O operations from the audio callback.
+ */
+class EmscriptenSdlMixerManager : public SdlMixerManager {
+public:
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+ EmscriptenSdlMixerManager();
+ virtual ~EmscriptenSdlMixerManager();
+#endif
+
+protected:
+ /**
+ * Starts SDL audio
+ */
+ void startAudio() override;
+
+ /**
+ * Pauses the audio system
+ */
+ void suspendAudio() override;
+
+ /**
+ * Resumes the audio system
+ */
+ int resumeAudio() override;
+
+#if SDL_VERSION_ATLEAST(3, 0, 0)
+ void startTimer(int interval = 10 * 1000);
+ void stopTimer();
+ /**
+ * Fills the data buffers with new audio data for playback
+ */
+ static void updateMixer(void *refCon);
+
+ /**
+ * Emscripten-specific SDL3 callback with buffering support
+ */
+ static void emscriptenSdl3Callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount);
+
+ int _lastRequestedAmount = -1;
+ uint32 _requestTimestamp = 0;
+ bool _timerStarted = false;
+ Common::Queue<Common::Pair<int, Uint8 *> > *_dataBuffers;
+#endif
+};
+
+#endif
diff --git a/backends/module.mk b/backends/module.mk
index 6af4c276dce..9837ccee8b8 100644
--- a/backends/module.mk
+++ b/backends/module.mk
@@ -105,6 +105,7 @@ MODULE_OBJS += \
fs/emscripten/emscripten-posix-fs.o \
fs/emscripten/http-fs.o \
midi/webmidi.o \
+ mixer/emscriptensdl/emscriptensdl-mixer.o \
timer/emscripten/emscripten-timer.o
ifdef USE_CLOUD
MODULE_OBJS += \
diff --git a/backends/platform/sdl/emscripten/emscripten.cpp b/backends/platform/sdl/emscripten/emscripten.cpp
index 4d665eb86b5..aac9a98da3a 100644
--- a/backends/platform/sdl/emscripten/emscripten.cpp
+++ b/backends/platform/sdl/emscripten/emscripten.cpp
@@ -27,6 +27,7 @@
#include "backends/events/emscriptensdl/emscriptensdl-events.h"
#include "backends/fs/emscripten/emscripten-fs-factory.h"
+#include "backends/mixer/emscriptensdl/emscriptensdl-mixer.h"
#include "backends/mutex/null/null-mutex.h"
#include "backends/fs/emscripten/emscripten-fs-factory.h"
#include "backends/platform/sdl/emscripten/emscripten.h"
@@ -111,6 +112,10 @@ void OSystem_Emscripten::initBackend() {
// Event source
_eventSource = new EmscriptenSdlEventSource();
+ // Emscripten-specific mixer manager
+ _mixerManager = new EmscriptenSdlMixerManager();
+ _mixerManager->init();
+
// Invoke parent implementation of this method
OSystem_POSIX::initBackend();
}
Commit: 1a02bc153f8f96bf9899f05f554139c46077ae2a
https://github.com/scummvm/scummvm/commit/1a02bc153f8f96bf9899f05f554139c46077ae2a
Author: Christian Kündig (christian at kuendig.info)
Date: 2026-06-28T22:06:19+02:00
Commit Message:
NETWORKING: Rewrite NetworkReadStreamEmscripten
- Using (asyncronous) Fetch API instead of (synchronous) emscripten_fetch/ XMLHttpRequest
- Remove extra buffer
- Using timer instead of callbacks (lower re-entracy risk)
Changed paths:
A backends/networking/http/emscripten/networkreadstream-emscripten.js
backends/networking/http/android/networkreadstream-android.cpp
backends/networking/http/android/networkreadstream-android.h
backends/networking/http/curl/networkreadstream-curl.cpp
backends/networking/http/curl/networkreadstream-curl.h
backends/networking/http/emscripten/networkreadstream-emscripten.cpp
backends/networking/http/emscripten/networkreadstream-emscripten.h
backends/networking/http/networkreadstream.cpp
backends/networking/http/networkreadstream.h
configure
diff --git a/backends/networking/http/android/networkreadstream-android.cpp b/backends/networking/http/android/networkreadstream-android.cpp
index eec8ce12586..7ebac2f7d74 100644
--- a/backends/networking/http/android/networkreadstream-android.cpp
+++ b/backends/networking/http/android/networkreadstream-android.cpp
@@ -216,7 +216,8 @@ static jobjectArray getFormFiles(JNIEnv *env, const Common::HashMap<Common::Stri
NetworkReadStreamAndroid::NetworkReadStreamAndroid(const char *url, RequestHeaders *headersList, const Common::String &postFields,
bool uploading, bool usingPatch, bool keepAlive, long keepAliveIdle, long keepAliveInterval) :
- NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _request(nullptr) {
+ NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _request(nullptr), _backingStream(DisposeAfterUse::YES),
+ _downloaded(0), _progressDownloaded(0), _progressTotal(0), _errorCode(0) {
if (!reuse(url, headersList, postFields, uploading, usingPatch)) {
abort();
}
@@ -224,7 +225,8 @@ NetworkReadStreamAndroid::NetworkReadStreamAndroid(const char *url, RequestHeade
NetworkReadStreamAndroid::NetworkReadStreamAndroid(const char *url, RequestHeaders *headersList, const Common::HashMap<Common::String, Common::String> &formFields,
const Common::HashMap<Common::String, Common::Path> &formFiles, bool keepAlive, long keepAliveIdle, long keepAliveInterval) :
- NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _request(nullptr) {
+ NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _request(nullptr), _backingStream(DisposeAfterUse::YES),
+ _downloaded(0), _progressDownloaded(0), _progressTotal(0), _errorCode(0) {
if (!reuse(url, headersList, formFields, formFiles)) {
abort();
}
@@ -232,7 +234,8 @@ NetworkReadStreamAndroid::NetworkReadStreamAndroid(const char *url, RequestHeade
NetworkReadStreamAndroid::NetworkReadStreamAndroid(const char *url, RequestHeaders *headersList, const byte *buffer, uint32 bufferSize,
bool uploading, bool usingPatch, bool post, bool keepAlive, long keepAliveIdle, long keepAliveInterval) :
- NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _request(nullptr) {
+ NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _request(nullptr), _backingStream(DisposeAfterUse::YES),
+ _downloaded(0), _progressDownloaded(0), _progressTotal(0), _errorCode(0) {
if (!reuse(url, headersList, buffer, bufferSize, uploading, usingPatch, post)) {
abort();
}
@@ -354,6 +357,7 @@ void NetworkReadStreamAndroid::finished(int errorCode, const Common::String &err
void NetworkReadStreamAndroid::resetStream(JNIEnv *env) {
_eos = _requestComplete = false;
_progressDownloaded = _progressTotal = 0;
+ _backingStream = Common::MemoryReadWriteStream(DisposeAfterUse::YES);
if (_request) {
env->CallVoidMethod(_request, NetJNI::_MID_request_cancel);
@@ -372,6 +376,29 @@ void NetworkReadStreamAndroid::resetStream(JNIEnv *env) {
_errorMsg.clear();
}
+void NetworkReadStreamAndroid::setProgress(uint64 downloaded, uint64 total) {
+ _progressDownloaded = downloaded;
+ _progressTotal = total;
+}
+
+double NetworkReadStreamAndroid::getProgress() const {
+ if (_progressTotal < 1)
+ return 0;
+ return (double)_progressDownloaded / (double)_progressTotal;
+}
+
+uint32 NetworkReadStreamAndroid::read(void *dataPtr, uint32 dataSize) {
+ uint32 actuallyRead = _backingStream.read(dataPtr, dataSize);
+
+ if (actuallyRead == 0) {
+ if (_requestComplete)
+ _eos = true;
+ return 0;
+ }
+
+ return actuallyRead;
+}
+
Common::String NetworkReadStreamAndroid::currentLocation() const {
if (!_request) {
return Common::String();
diff --git a/backends/networking/http/android/networkreadstream-android.h b/backends/networking/http/android/networkreadstream-android.h
index 9b8db3f2f61..99f582de902 100644
--- a/backends/networking/http/android/networkreadstream-android.h
+++ b/backends/networking/http/android/networkreadstream-android.h
@@ -25,6 +25,7 @@
#include <jni.h>
#include "backends/networking/http/networkreadstream.h"
+#include "common/memstream.h"
namespace Networking {
@@ -37,11 +38,14 @@ private:
void resetStream(JNIEnv *env);
void finished(int errorCode, const Common::String &errorMsg);
+ void setProgress(uint64 downloaded, uint64 total);
jobject _request;
+ Common::MemoryReadWriteStream _backingStream;
Common::HashMap<Common::String, Common::String> _responseHeadersMap;
uint64 _downloaded;
+ uint64 _progressDownloaded, _progressTotal;
int _errorCode;
Common::String _errorMsg;
public:
@@ -75,6 +79,8 @@ public:
bool hasError() const override { return _errorCode < 200 || _errorCode >= 300; }
const char *getError() const override { return _errorMsg.c_str(); }
+ double getProgress() const override;
+ uint32 read(void *dataPtr, uint32 dataSize) override;
};
} // End of namespace Networking
diff --git a/backends/networking/http/curl/networkreadstream-curl.cpp b/backends/networking/http/curl/networkreadstream-curl.cpp
index 75e66289c48..d8cb9a92c80 100644
--- a/backends/networking/http/curl/networkreadstream-curl.cpp
+++ b/backends/networking/http/curl/networkreadstream-curl.cpp
@@ -216,21 +216,21 @@ void NetworkReadStreamCurl::setupFormMultipart(const Common::HashMap<Common::Str
}
NetworkReadStreamCurl::NetworkReadStreamCurl(const char *url, RequestHeaders *headersList, const Common::String &postFields, bool uploading, bool usingPatch, bool keepAlive, long keepAliveIdle, long keepAliveInterval)
- : NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval),
+ : NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _backingStream(DisposeAfterUse::YES),
_errorBuffer(nullptr), _headersSlist(nullptr) {
initCurl(url, headersList);
setupBufferContents((const byte *)postFields.c_str(), postFields.size(), uploading, usingPatch, false);
}
NetworkReadStreamCurl::NetworkReadStreamCurl(const char *url, RequestHeaders *headersList, const Common::HashMap<Common::String, Common::String> &formFields, const Common::HashMap<Common::String, Common::Path> &formFiles, bool keepAlive, long keepAliveIdle, long keepAliveInterval)
- : NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval),
+ : NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _backingStream(DisposeAfterUse::YES),
_errorBuffer(nullptr), _headersSlist(nullptr) {
initCurl(url, headersList);
setupFormMultipart(formFields, formFiles);
}
NetworkReadStreamCurl::NetworkReadStreamCurl(const char *url, RequestHeaders *headersList, const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post, bool keepAlive, long keepAliveIdle, long keepAliveInterval)
- : NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval),
+ : NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval), _backingStream(DisposeAfterUse::YES),
_errorBuffer(nullptr), _headersSlist(nullptr) {
initCurl(url, headersList);
setupBufferContents(buffer, bufferSize, uploading, usingPatch, post);
@@ -302,6 +302,29 @@ bool NetworkReadStreamCurl::hasError() const {
return _errorCode != CURLE_OK;
}
+double NetworkReadStreamCurl::getProgress() const {
+ if (_progressTotal < 1)
+ return 0;
+ return (double)_progressDownloaded / (double)_progressTotal;
+}
+
+void NetworkReadStreamCurl::setProgress(uint64 downloaded, uint64 total) {
+ _progressDownloaded = downloaded;
+ _progressTotal = total;
+}
+
+uint32 NetworkReadStreamCurl::read(void *dataPtr, uint32 dataSize) {
+ uint32 actuallyRead = _backingStream.read(dataPtr, dataSize);
+
+ if (actuallyRead == 0) {
+ if (_requestComplete)
+ _eos = true;
+ return 0;
+ }
+
+ return actuallyRead;
+}
+
const char *NetworkReadStreamCurl::getError() const {
return strlen(_errorBuffer) ? _errorBuffer : curl_easy_strerror(_errorCode);
}
diff --git a/backends/networking/http/curl/networkreadstream-curl.h b/backends/networking/http/curl/networkreadstream-curl.h
index ec280092f0a..34e9d78c6ed 100644
--- a/backends/networking/http/curl/networkreadstream-curl.h
+++ b/backends/networking/http/curl/networkreadstream-curl.h
@@ -38,11 +38,13 @@ namespace Networking {
class NetworkReadStreamCurl : public NetworkReadStream {
private:
+ Common::MemoryReadWriteStream _backingStream;
CURL *_easy;
struct curl_slist *_headersSlist;
char *_errorBuffer;
CURLcode _errorCode;
byte *_bufferCopy; // To use with old curl version where CURLOPT_COPYPOSTFIELDS is not available
+ uint64 _progressDownloaded, _progressTotal;
void initCurl(const char *url, RequestHeaders *headersList);
bool reuseCurl(const char *url, RequestHeaders *headersList);
static struct curl_slist *requestHeadersToSlist(const RequestHeaders *headersList);
@@ -78,6 +80,11 @@ public:
/** Send <buffer>, using POST by default. */
bool reuse(const char *url, RequestHeaders *headersList, const byte *buffer, uint32 bufferSize, bool uploading = false, bool usingPatch = false, bool post = true) override;
+ /** Used in curl progress callback to pass current downloaded/total values. */
+ void setProgress(uint64 downloaded, uint64 total);
+ double getProgress() const override;
+ uint32 read(void *dataPtr, uint32 dataSize) override;
+
long httpResponseCode() const override;
Common::String currentLocation() const override;
/**
diff --git a/backends/networking/http/emscripten/networkreadstream-emscripten.cpp b/backends/networking/http/emscripten/networkreadstream-emscripten.cpp
index f67113545bb..f012a325447 100644
--- a/backends/networking/http/emscripten/networkreadstream-emscripten.cpp
+++ b/backends/networking/http/emscripten/networkreadstream-emscripten.cpp
@@ -19,21 +19,6 @@
*
*/
-#define FORBIDDEN_SYMBOL_EXCEPTION_asctime
-#define FORBIDDEN_SYMBOL_EXCEPTION_clock
-#define FORBIDDEN_SYMBOL_EXCEPTION_ctime
-#define FORBIDDEN_SYMBOL_EXCEPTION_difftime
-#define FORBIDDEN_SYMBOL_EXCEPTION_FILE
-#define FORBIDDEN_SYMBOL_EXCEPTION_getdate
-#define FORBIDDEN_SYMBOL_EXCEPTION_gmtime
-#define FORBIDDEN_SYMBOL_EXCEPTION_localtime
-#define FORBIDDEN_SYMBOL_EXCEPTION_mktime
-#define FORBIDDEN_SYMBOL_EXCEPTION_strcpy
-#define FORBIDDEN_SYMBOL_EXCEPTION_strdup
-#define FORBIDDEN_SYMBOL_EXCEPTION_time
-#include <emscripten.h>
-#include <emscripten/fetch.h>
-
#include "backends/networking/http/emscripten/networkreadstream-emscripten.h"
#include "backends/networking/http/networkreadstream.h"
#include "base/version.h"
@@ -53,254 +38,275 @@ NetworkReadStream *NetworkReadStream::make(const char *url, RequestHeaders *head
return new NetworkReadStreamEmscripten(url, headersList, buffer, bufferSize, uploading, usingPatch, post, keepAlive, keepAliveIdle, keepAliveInterval);
}
-void NetworkReadStreamEmscripten::emscriptenOnReadyStateChange(emscripten_fetch_t *fetch) {
- if (fetch->readyState != 2)
- return;
-
- size_t headersLengthBytes = emscripten_fetch_get_response_headers_length(fetch) + 1;
- char *headerString = (char *)malloc(headersLengthBytes);
-
- assert(headerString);
- emscripten_fetch_get_response_headers(fetch, headerString, headersLengthBytes);
- NetworkReadStreamEmscripten *stream = (NetworkReadStreamEmscripten *)fetch->userData;
- stream->addResponseHeaders(headerString, headersLengthBytes);
- free(headerString);
+void NetworkReadStreamEmscripten::resetStream() {
+ _eos = false;
+ _sendingContentsSize = _sendingContentsPos = 0;
+ _readPos = 0;
+ _fetchId = 0;
}
-void NetworkReadStreamEmscripten::emscriptenOnProgress(emscripten_fetch_t *fetch) {
- /*
- if (fetch->totalBytes) {
- debug(5,"Downloading %s.. %.2f percent complete.", fetch->url, fetch->dataOffset * 100.0 / fetch->totalBytes);
- } else {
- debug(5,"Downloading %s.. %lld bytes complete.", fetch->url, fetch->dataOffset + fetch->numBytes);
- }
- debug(5,"Downloading %s.. %.2f %s complete. HTTP readyState: %hu. HTTP status: %hu - "
- "HTTP statusText: %s. Received chunk [%llu, %llu]",
- fetch->url,
- fetch->totalBytes > 0 ? (fetch->dataOffset + fetch->numBytes) * 100.0 / fetch->totalBytes : (fetch->dataOffset + fetch->numBytes),
- fetch->totalBytes > 0 ? "percent" : " bytes",
- fetch->readyState,
- fetch->status,
- fetch->statusText,
- fetch->dataOffset,
- fetch->dataOffset + fetch->numBytes);
- */
- NetworkReadStreamEmscripten *stream = (NetworkReadStreamEmscripten *)fetch->userData;
- if (stream) {
- stream->setProgress(fetch->dataOffset, fetch->totalBytes);
+void NetworkReadStreamEmscripten::initFetch() {
+ static bool initialized = false;
+ if (!initialized) {
+ NetworkReadStreamEmscripten_init();
+ initialized = true;
}
}
-void NetworkReadStreamEmscripten::emscriptenOnSuccess(emscripten_fetch_t *fetch) {
- NetworkReadStreamEmscripten *stream = (NetworkReadStreamEmscripten *)fetch->userData;
- stream->emscriptenDownloadFinished(true);
-}
+char **NetworkReadStreamEmscripten::buildHeadersArray(const RequestHeaders *headersList) {
+ if (!headersList || headersList->empty())
+ return nullptr;
-void NetworkReadStreamEmscripten::emscriptenOnError(emscripten_fetch_t *fetch) {
- NetworkReadStreamEmscripten *stream = (NetworkReadStreamEmscripten *)fetch->userData;
- stream->emscriptenDownloadFinished(false);
-}
+ const int maxEntries = headersList->size() * 2;
+ char **headers = new char *[maxEntries + 1];
-void NetworkReadStreamEmscripten::emscriptenDownloadFinished(bool success) {
- _requestComplete = true;
- if (_emscripten_fetch->numBytes > 0) {
- // TODO: This could be done continuously during emscriptenOnProgress?
- this->_backingStream.write(_emscripten_fetch->data, _emscripten_fetch->numBytes);
- }
- this->setProgress(_emscripten_fetch->numBytes, _emscripten_fetch->numBytes);
-
- if (success) {
- debug(5, "NetworkReadStreamEmscripten::emscriptenHandleDownload Finished downloading %llu bytes from URL %s. HTTP status code: %d", _emscripten_fetch->numBytes, _emscripten_fetch->url, _emscripten_fetch->status);
- _success = true; // TODO: actually pass the result code from emscripten_fetch
- } else {
- debug(5, "NetworkReadStreamEmscripten::emscriptenHandleDownload Downloading %s failed, HTTP failure status code: %d, status text: %s", _emscripten_fetch->url, _emscripten_fetch->status, _emscripten_fetch->statusText);
-
- // Make a copy of the error message since _emscripten_fetch might be cleaned up
- if (_emscripten_fetch && _emscripten_fetch->statusText) {
- _errorBuffer = strdup(_emscripten_fetch->statusText);
- } else {
- _errorBuffer = strdup("Unknown error");
+ int idx = 0;
+ for (const Common::String &header : *headersList) {
+ uint colonPos = header.findFirstOf(':');
+ if (colonPos == Common::String::npos) {
+ warning("NetworkReadStreamEmscripten: Malformed header (no colon): %s", header.c_str());
+ continue;
}
- warning("NetworkReadStreamEmscripten::finished %s - Request failed (%s)", _emscripten_fetch_url, getError());
+
+ Common::String key = header.substr(0, colonPos);
+ Common::String value = header.substr(colonPos + 1);
+ key.trim();
+ value.trim();
+ headers[idx++] = scumm_strdup(key.c_str());
+ headers[idx++] = scumm_strdup(value.c_str());
+ debug(5, "Header: '%s' = '%s'", key.c_str(), value.c_str());
}
+
+ headers[idx] = nullptr;
+
+ return headers;
}
-void NetworkReadStreamEmscripten::resetStream() {
- _eos = _requestComplete = false;
- _sendingContentsSize = _sendingContentsPos = 0;
- _progressDownloaded = _progressTotal = 0;
- _emscripten_fetch = nullptr;
- _emscripten_request_headers = nullptr;
- free(_errorBuffer);
- _errorBuffer = nullptr;
+void NetworkReadStreamEmscripten::cleanupStringArray(char **array) {
+ if (!array)
+ return;
+
+ for (int i = 0; array[i] != nullptr; ++i) {
+ free(array[i]);
+ }
+
+ delete[] array;
}
-void NetworkReadStreamEmscripten::initEmscripten(const char *url, RequestHeaders *headersList) {
+char **NetworkReadStreamEmscripten::buildFormFieldsArray(const Common::HashMap<Common::String, Common::String> &formFields) {
+ if (formFields.empty())
+ return nullptr;
- resetStream();
- emscripten_fetch_attr_init(_emscripten_fetch_attr);
+ // Array contains alternating name/value pairs, plus null terminator
+ int count = formFields.size() * 2;
+ char **fields = new char *[count + 1];
- // convert header list
- // first get size of list
- int size = 0;
- if (headersList) {
- size = headersList->size();
- debug(5, "_emscripten_request_headers count: %d", size);
- }
- _emscripten_request_headers = new char *[size * 2 + 1];
- _emscripten_request_headers[size * 2] = 0; // header array needs to be null-terminated.
-
- int i = 0;
- if (headersList) {
- for (const Common::String &header : *headersList) {
- // Find the colon separator
- uint colonPos = header.findFirstOf(':');
- if (colonPos == Common::String::npos) {
- warning("NetworkReadStreamEmscripten: Malformed header (no colon): %s", header.c_str());
- continue;
- }
-
- // Split into key and value parts
- Common::String key = header.substr(0, colonPos);
- Common::String value = header.substr(colonPos + 1);
-
- // Trim whitespace from key and value
- key.trim();
- value.trim();
-
- // Store key and value as separate strings
- _emscripten_request_headers[i++] = strdup(key.c_str());
- _emscripten_request_headers[i++] = strdup(value.c_str());
- debug(9, "_emscripten_request_headers key='%s' value='%s'", key.c_str(), value.c_str());
- }
+ int idx = 0;
+ for (Common::HashMap<Common::String, Common::String>::const_iterator i = formFields.begin(); i != formFields.end(); ++i) {
+ fields[idx++] = scumm_strdup(i->_key.c_str());
+ fields[idx++] = scumm_strdup(i->_value.c_str());
}
+ fields[idx] = nullptr;
- _emscripten_fetch_attr->requestHeaders = _emscripten_request_headers;
- strcpy(_emscripten_fetch_attr->requestMethod, "GET"); // todo: move down to setup buffer contents
- _emscripten_fetch_attr->attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
- _emscripten_fetch_attr->onerror = emscriptenOnError;
- _emscripten_fetch_attr->onprogress = emscriptenOnProgress;
- _emscripten_fetch_attr->onreadystatechange = emscriptenOnReadyStateChange;
- _emscripten_fetch_attr->onsuccess = emscriptenOnSuccess;
- _emscripten_fetch_attr->userData = this;
+ return fields;
}
-void NetworkReadStreamEmscripten::setupBufferContents(const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post) {
- if (uploading) {
- strcpy(_emscripten_fetch_attr->requestMethod, "PUT");
- _emscripten_fetch_attr->requestDataSize = bufferSize;
- _emscripten_fetch_attr->requestData = (const char *)buffer;
- } else if (usingPatch) {
- strcpy(_emscripten_fetch_attr->requestMethod, "PATCH");
- } else {
- if (post || bufferSize != 0) {
- strcpy(_emscripten_fetch_attr->requestMethod, "POST");
- _emscripten_fetch_attr->requestDataSize = bufferSize;
- _emscripten_fetch_attr->requestData = (const char *)buffer;
- }
+
+char **NetworkReadStreamEmscripten::buildFormFilesArray(const Common::HashMap<Common::String, Common::Path> &formFiles) {
+ if (formFiles.empty())
+ return nullptr;
+
+ // Array contains alternating name/path pairs, plus null terminator
+ int count = formFiles.size() * 2;
+ char **files = new char *[count + 1];
+
+ int idx = 0;
+ for (Common::HashMap<Common::String, Common::Path>::const_iterator i = formFiles.begin(); i != formFiles.end(); ++i) {
+ files[idx++] = scumm_strdup(i->_key.c_str());
+ files[idx++] = scumm_strdup(i->_value.toString(Common::Path::kNativeSeparator).c_str());
}
- debug(5, "NetworkReadStreamEmscripten::setupBufferContents uploading %s usingPatch %s post %s ->method %s", uploading ? "true" : "false", usingPatch ? "true" : "false", post ? "true" : "false", _emscripten_fetch_attr->requestMethod);
- _emscripten_fetch = emscripten_fetch(_emscripten_fetch_attr, _emscripten_fetch_url);
+ files[idx] = nullptr;
+
+ return files;
}
-void NetworkReadStreamEmscripten::setupFormMultipart(const Common::HashMap<Common::String, Common::String> &formFields, const Common::HashMap<Common::String, Common::Path> &formFiles) {
- // set POST multipart upload form fields/files
- error("NetworkReadStreamEmscripten::setupFormMultipart not implemented");
+double NetworkReadStreamEmscripten::getProgress() const {
+ uint64 numBytes = NetworkReadStreamEmscripten_getNumBytes(_fetchId);
+ uint64 totalBytes = NetworkReadStreamEmscripten_totalBytes(_fetchId);
+ if (numBytes == 0 || totalBytes == 0) {
+ return 0.0; // avoid division by zero or infinite if either is zero
+ }
+ debug(5, "NetworkReadStreamEmscripten::getProgress - Progress: %llu / %llu for %s", numBytes, totalBytes, _url.c_str());
+ return (double)numBytes / (double)totalBytes;
}
/** Send <postFields>, using POST by default. */
NetworkReadStreamEmscripten::NetworkReadStreamEmscripten(const char *url, RequestHeaders *headersList, const Common::String &postFields,
- bool uploading, bool usingPatch, bool keepAlive, long keepAliveIdle, long keepAliveInterval) :
- _emscripten_fetch_attr(new emscripten_fetch_attr_t()), _emscripten_fetch_url(url), _errorBuffer(nullptr),
- NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval) {
- initEmscripten(url, headersList);
- setupBufferContents((const byte *)postFields.c_str(), postFields.size(), uploading, usingPatch, false);
+ bool uploading, bool usingPatch, bool keepAlive, long keepAliveIdle, long keepAliveInterval) :
+ NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval),
+ _fetchId(0), _url(url), _headersList(headersList), _readPos(0) {
+
+ initFetch();
+
+ // Determine HTTP method
+ const char *method = "GET";
+ if (uploading) {
+ method = "PUT";
+ } else if (usingPatch) {
+ method = "PATCH";
+ } else if (postFields.size() != 0) {
+ method = "POST";
+ }
+
+ // Build headers
+ char **headers = buildHeadersArray(_headersList);
+
+ debug(5, "Starting fetch: %s %s (body size: %u)", method, _url.c_str(), postFields.size());
+
+ // Start the fetch
+ _fetchId = NetworkReadStreamEmscripten_fetch(method, _url.c_str(), headers, postFields.c_str(), postFields.size());
+
+ // Clean up headers
+ cleanupStringArray(headers);
}
+
/** Send <formFields>, <formFiles>, using POST multipart/form. */
-NetworkReadStreamEmscripten::NetworkReadStreamEmscripten(const char *url, RequestHeaders *headersList, const Common::HashMap<Common::String,
- Common::String> &formFields, const Common::HashMap<Common::String, Common::Path> &formFiles, bool keepAlive, long keepAliveIdle,
- long keepAliveInterval) : _emscripten_fetch_attr(new emscripten_fetch_attr_t()), _emscripten_fetch_url(url), _errorBuffer(nullptr),
- NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval) {
- initEmscripten(url, headersList);
- setupFormMultipart(formFields, formFiles);
-}
+NetworkReadStreamEmscripten::NetworkReadStreamEmscripten(const char *url, RequestHeaders *headersList, const Common::HashMap<Common::String, Common::String> &formFields,
+ const Common::HashMap<Common::String, Common::Path> &formFiles, bool keepAlive, long keepAliveIdle, long keepAliveInterval) :
+ NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval) ,
+ _fetchId(0), _url(url), _headersList(headersList), _readPos(0){
+
+ initFetch();
+
+ // Build all three arrays
+ char **formFieldsArray = buildFormFieldsArray(formFields);
+ char **formFilesArray = buildFormFilesArray(formFiles);
+ char **headers = buildHeadersArray(_headersList);
+
+ debug(5, "Starting FormData fetch: POST %s with %d fields and %d files", _url.c_str(), formFields.size(), formFiles.size());
+
+ // Call NetworkReadStreamEmscripten_fetch with FormData parameters (method is always POST for FormData)
+ _fetchId = NetworkReadStreamEmscripten_fetch("POST", _url.c_str(), headers, nullptr, 0, // no regular request body
+ formFieldsArray, formFilesArray);
+ // Clean up allocated strings
+ cleanupStringArray(formFieldsArray);
+ cleanupStringArray(formFilesArray);
+ cleanupStringArray(headers);
+}
/** Send <buffer>, using POST by default. */
NetworkReadStreamEmscripten::NetworkReadStreamEmscripten(const char *url, RequestHeaders *headersList, const byte *buffer, uint32 bufferSize,
- bool uploading, bool usingPatch, bool post, bool keepAlive, long keepAliveIdle, long keepAliveInterval) :
- _emscripten_fetch_attr(new emscripten_fetch_attr_t()), _emscripten_fetch_url(url), _errorBuffer(nullptr),
- NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval) {
- initEmscripten(url, headersList);
- setupBufferContents(buffer, bufferSize, uploading, usingPatch, post);
-}
+ bool uploading, bool usingPatch, bool post, bool keepAlive, long keepAliveIdle, long keepAliveInterval) :
+ NetworkReadStream(keepAlive, keepAliveIdle, keepAliveInterval),
+ _fetchId(0), _url(url), _headersList(headersList), _readPos(0) {
-NetworkReadStreamEmscripten::~NetworkReadStreamEmscripten() {
- if (_emscripten_fetch) {
- debug(5, "~NetworkReadStreamEmscripten: emscripten_fetch_close");
- emscripten_fetch_close(_emscripten_fetch);
+ initFetch();
+
+ // Determine HTTP method
+ const char *method = "GET";
+ if (uploading) {
+ method = "PUT";
+ } else if (usingPatch) {
+ method = "PATCH";
+ } else if (post || bufferSize != 0) {
+ method = "POST";
}
- // Free the headers array and its contents
- if (_emscripten_request_headers) {
- for (int i = 0; _emscripten_request_headers[i] != nullptr; ++i) {
- free(_emscripten_request_headers[i]); // Free each strdup'd string
- }
- delete[] _emscripten_request_headers;
+ // Build headers
+ char **headers = buildHeadersArray(_headersList);
+
+ debug(5, "Starting fetch: %s %s (buffer size: %u)", method, _url.c_str(), bufferSize);
+
+ // Start the fetch
+ _fetchId = NetworkReadStreamEmscripten_fetch(method, _url.c_str(), headers, (const char *)buffer, bufferSize);
+
+ // Clean up headers
+ cleanupStringArray(headers);
+}
+
+NetworkReadStreamEmscripten::~NetworkReadStreamEmscripten() {
+ debug(5, "NetworkReadStreamEmscripten::~NetworkReadStreamEmscripten %s", _url.c_str());
+ if (_fetchId) {
+ debug(5, "~NetworkReadStreamEmscripten: NetworkReadStreamEmscripten_close");
+ NetworkReadStreamEmscripten_close(_fetchId);
+ _fetchId = 0;
}
}
uint32 NetworkReadStreamEmscripten::read(void *dataPtr, uint32 dataSize) {
- uint32 actuallyRead = _backingStream.read(dataPtr, dataSize);
+ if (!_fetchId || _eos || dataSize == 0) {
+ warning("NetworkReadStreamEmscripten::read - Invalid state");
+ return 0;
+ }
+
+ // Get direct pointer to JS buffer
+ char *jsBuffer = NetworkReadStreamEmscripten_getDataPtr(_fetchId);
+ if (!jsBuffer) {
+ return 0;
+ }
- // Only access _emscripten_fetch->url if _emscripten_fetch is valid
- // debug(5,"NetworkReadStreamEmscripten::read %u %s %s %s", actuallyRead, _eos ? "_eos" : "not _eos", _requestComplete ? "_requestComplete" : "_request not Complete", _emscripten_fetch ? _emscripten_fetch->url : "no-url");
- if (actuallyRead == 0) {
- if (_requestComplete)
+ // Get current number of bytes available
+ uint32 numBytes = NetworkReadStreamEmscripten_getNumBytes(_fetchId);
+
+ // Calculate how many bytes we can actually read
+ uint32 availableBytes = numBytes - _readPos;
+ uint32 bytesToRead = (dataSize < availableBytes) ? dataSize : availableBytes;
+ uint32 totalBytes = NetworkReadStreamEmscripten_totalBytes(_fetchId);
+ debug(5, "NetworkReadStreamEmscripten::read - Progress: %u / %u for %s, currently at %u Trying to read %u bytes", numBytes, totalBytes, _url.c_str(), _readPos, bytesToRead);
+
+ if (bytesToRead == 0) {
+ // Check if transfer is complete
+ if (NetworkReadStreamEmscripten_completed(_fetchId))
_eos = true;
return 0;
}
- return actuallyRead;
+ // Copy data directly from JS buffer
+ memcpy(dataPtr, jsBuffer + _readPos, bytesToRead);
+ _readPos += bytesToRead;
+
+ return bytesToRead;
}
bool NetworkReadStreamEmscripten::hasError() const {
- return !_success;
+ return NetworkReadStreamEmscripten_hasError(_fetchId) && NetworkReadStreamEmscripten_completed(_fetchId);
}
const char *NetworkReadStreamEmscripten::getError() const {
- return _errorBuffer;
+ if (!hasError() || _fetchId == 0) {
+ return "No error";
+ }
+
+ return NetworkReadStreamEmscripten_getErr(_fetchId);
+}
+
+bool NetworkReadStreamEmscripten::eos() const {
+ return _eos;
}
long NetworkReadStreamEmscripten::httpResponseCode() const {
- // return 200;
- unsigned short responseCode = 0;
- if (_emscripten_fetch)
- responseCode = _emscripten_fetch->status;
- debug(5, "NetworkReadStreamEmscripten::httpResponseCode %hu", responseCode);
- return responseCode;
+ return _fetchId ? NetworkReadStreamEmscripten_status(_fetchId) : 0;
}
Common::String NetworkReadStreamEmscripten::currentLocation() const {
- debug(5, "NetworkReadStreamEmscripten::currentLocation %s", _emscripten_fetch_url);
- return Common::String(_emscripten_fetch_url);
+ return Common::String(_url);
}
Common::HashMap<Common::String, Common::String> NetworkReadStreamEmscripten::responseHeadersMap() const {
Common::HashMap<Common::String, Common::String> headers;
- const char *headerString = _responseHeaders.c_str();
- char **responseHeaders = emscripten_fetch_unpack_response_headers(headerString);
- assert(responseHeaders);
+ if (!_fetchId)
+ return headers;
- int numHeaders = 0;
- for (; responseHeaders[numHeaders * 2]; ++numHeaders) {
- // Check both the header and its value are present.
- assert(responseHeaders[(numHeaders * 2) + 1]);
- headers[responseHeaders[numHeaders * 2]] = responseHeaders[(numHeaders * 2) + 1];
- }
+ char **responseHeaders = NetworkReadStreamEmscripten_responseHeadersArray(_fetchId);
+ if (!responseHeaders)
+ return headers;
- emscripten_fetch_free_unpacked_response_headers(responseHeaders);
+ for (int i = 0; responseHeaders[i * 2]; ++i) {
+ headers[responseHeaders[i * 2]] = responseHeaders[(i * 2) + 1];
+ }
+ // Note: No need to free responseHeaders - it's managed by JavaScript side
return headers;
}
diff --git a/backends/networking/http/emscripten/networkreadstream-emscripten.h b/backends/networking/http/emscripten/networkreadstream-emscripten.h
index 7f52a98be6c..05650c5a3d8 100644
--- a/backends/networking/http/emscripten/networkreadstream-emscripten.h
+++ b/backends/networking/http/emscripten/networkreadstream-emscripten.h
@@ -25,24 +25,47 @@
#ifdef EMSCRIPTEN
#include "backends/networking/http/networkreadstream.h"
-#include <emscripten/fetch.h>
-namespace Networking {
+extern "C" {
+// External JS functions for HTTP fetching
+extern void NetworkReadStreamEmscripten_init(void);
+extern int NetworkReadStreamEmscripten_fetch(const char *method, const char *url,
+ char **requestHeaders,
+ const char *requestData, size_t requestDataSize,
+ char **formFields = nullptr,
+ char **formFiles = nullptr);
+extern void NetworkReadStreamEmscripten_close(int fetchId);
+
+// Getter functions for fetch properties
+extern char *NetworkReadStreamEmscripten_getDataPtr(int fetchId);
+extern char *NetworkReadStreamEmscripten_getErr(int fetchId);
+extern uint32 NetworkReadStreamEmscripten_getNumBytes(int fetchId);
+extern char **NetworkReadStreamEmscripten_responseHeadersArray(int fetchId);
+extern unsigned short NetworkReadStreamEmscripten_status(int fetchId);
+extern uint32 NetworkReadStreamEmscripten_totalBytes(int fetchId);
+extern bool NetworkReadStreamEmscripten_completed(int fetchId);
+extern bool NetworkReadStreamEmscripten_hasError(int fetchId);
+}
-class NetworkReadStream; // Forward declaration
+namespace Networking {
class NetworkReadStreamEmscripten : public NetworkReadStream {
private:
- emscripten_fetch_attr_t *_emscripten_fetch_attr;
- emscripten_fetch_t *_emscripten_fetch;
- const char *_emscripten_fetch_url = nullptr;
- char **_emscripten_request_headers;
- bool _success;
- char *_errorBuffer;
+ int _fetchId; // Fetch ID instead of pointer
+ Common::String _url;
+ RequestHeaders *_headersList;
+ uint32 _readPos; // Current read position in the JS buffer
+
+ // Helper methods
+ static char **buildHeadersArray(const RequestHeaders *headersList);
+ static char **buildFormFieldsArray(const Common::HashMap<Common::String, Common::String> &formFields);
+ static char **buildFormFilesArray(const Common::HashMap<Common::String, Common::Path> &formFiles);
+ static void cleanupStringArray(char **array);
void resetStream();
void setupBufferContents(const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post);
void setupFormMultipart(const Common::HashMap<Common::String, Common::String> &formFields, const Common::HashMap<Common::String, Common::Path> &formFiles);
+
public:
NetworkReadStreamEmscripten(const char *url, RequestHeaders *headersList, const Common::String &postFields, bool uploading, bool usingPatch, bool keepAlive, long keepAliveIdle, long keepAliveInterval);
@@ -51,7 +74,7 @@ public:
NetworkReadStreamEmscripten(const char *url, RequestHeaders *headersList, const byte *buffer, uint32 bufferSize, bool uploading, bool usingPatch, bool post, bool keepAlive, long keepAliveIdle, long keepAliveInterval);
~NetworkReadStreamEmscripten() override;
- void initEmscripten(const char *url, RequestHeaders *headersList);
+ void initFetch();
// NetworkReadStream interface
bool reuse(const char *url, RequestHeaders *headersList, const Common::String &postFields, bool uploading = false, bool usingPatch = false) override { return false; } // no reuse for Emscripten
@@ -60,19 +83,13 @@ public:
bool hasError() const override;
const char *getError() const override;
-
+ double getProgress() const override;
long httpResponseCode() const override;
Common::String currentLocation() const override;
Common::HashMap<Common::String, Common::String> responseHeadersMap() const override;
uint32 read(void *dataPtr, uint32 dataSize) override;
-
- // Static callback functions
- static void emscriptenOnSuccess(emscripten_fetch_t *fetch);
- static void emscriptenOnError(emscripten_fetch_t *fetch);
- static void emscriptenOnProgress(emscripten_fetch_t *fetch);
- static void emscriptenOnReadyStateChange(emscripten_fetch_t *fetch);
- void emscriptenDownloadFinished(bool success);
+ bool eos() const override;
};
} // End of namespace Networking
diff --git a/backends/networking/http/emscripten/networkreadstream-emscripten.js b/backends/networking/http/emscripten/networkreadstream-emscripten.js
new file mode 100644
index 00000000000..ae2460242f7
--- /dev/null
+++ b/backends/networking/http/emscripten/networkreadstream-emscripten.js
@@ -0,0 +1,359 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+mergeInto(LibraryManager.library, {
+
+ NetworkReadStreamEmscripten_init: function () {
+ // Global storage for all fetch operations
+ if (!window.scummvmFetches) {
+ window.scummvmFetches = {};
+ window.scummvmNextFetchId = 1;
+ }
+
+ },
+
+ NetworkReadStreamEmscripten_fetch: function (methodPtr, urlPtr, headersPtr, requestDataPtr, requestDataSize, formFieldsPtr, formFilesPtr) {
+ const method = UTF8ToString(methodPtr);
+ const url = UTF8ToString(urlPtr);
+ const fetchId = window.scummvmNextFetchId++;
+ console.debug("Starting fetch #" + fetchId + " for URL: " + url + " with method: " + method);
+
+ // Initialize fetch object
+ const fetch = {
+ id: fetchId,
+ status: 0,
+ statusText: 0,
+ url: urlPtr,
+ numBytes: 0,
+ totalBytes: 0,
+ responseHeaders: 0,
+ active: true,
+ completed: false,
+ success: false,
+ reader: null,
+ buffer: null,
+ bufferSize: 0
+ };
+
+ // Store in global registry
+ window.scummvmFetches[fetchId] = fetch;
+
+ // Create headers object from the headers array
+ let headers = {};
+ if (headersPtr) {
+ const headersObj = new Headers();
+ let i = 0;
+ while (HEAP32[(headersPtr >> 2) + (i * 2)]) {
+ const keyPtr = HEAP32[(headersPtr >> 2) + (i * 2)];
+ const valuePtr = HEAP32[(headersPtr >> 2) + (i * 2 + 1)];
+ const key = UTF8ToString(keyPtr);
+ const value = UTF8ToString(valuePtr);
+ // Don't set Content-Type for FormData - browser will set it with boundary
+ if (!formFieldsPtr && !formFilesPtr || key.toLowerCase() !== 'content-type') {
+ console.debug(`Adding header: ${key} = ${value}`);
+ headersObj.append(key, value);
+ }
+ i++;
+ }
+ headers = headersObj;
+ }
+
+ const options = {
+ method: method,
+ headers: headers
+ };
+
+ // Add body if needed
+ if (requestDataPtr && requestDataSize > 0) {
+ const bodyData = new Uint8Array(HEAPU8.buffer, requestDataPtr, requestDataSize);
+ options.body = bodyData;
+ console.debug("Added request body, size:", requestDataSize);
+ }
+
+ // Handle FormData request
+ if (formFieldsPtr || formFilesPtr) {
+ const formData = new FormData();
+
+ // Add form fields (array contains alternating name/value pairs)
+ if (formFieldsPtr) {
+ let i = 0;
+ while (HEAP32[(formFieldsPtr >> 2) + i]) {
+ const namePtr = HEAP32[(formFieldsPtr >> 2) + i];
+ const valuePtr = HEAP32[(formFieldsPtr >> 2) + i + 1];
+ if (namePtr && valuePtr) {
+ const name = UTF8ToString(namePtr);
+ const value = UTF8ToString(valuePtr);
+ formData.append(name, value);
+ console.debug(`FormData field: ${name} = ${value}`);
+ }
+ i += 2; // Skip both name and value
+ }
+ }
+
+ // Add file fields (array contains alternating name/path pairs)
+ if (formFilesPtr) {
+ let i = 0;
+ while (HEAP32[(formFilesPtr >> 2) + i]) {
+ const namePtr = HEAP32[(formFilesPtr >> 2) + i];
+ const pathPtr = HEAP32[(formFilesPtr >> 2) + i + 1];
+ if (namePtr && pathPtr) {
+ const name = UTF8ToString(namePtr);
+ const path = UTF8ToString(pathPtr);
+ console.debug(`FormData file field: ${name} from path ${path}`);
+
+ // Read file from Emscripten filesystem
+ try {
+ const fileData = FS.readFile(path);
+ const fileName = path.split('/').pop();
+ const blob = new Blob([fileData], { type: 'application/octet-stream' });
+ formData.append(name, blob, fileName);
+ console.debug(`Added file ${fileName} (${fileData.length} bytes)`);
+ } catch (e) {
+ console.error(`Failed to read file ${path}:`, e);
+ }
+ }
+ i += 2; // Skip both name and path
+ }
+ }
+
+ options.body = formData;
+ }
+
+ // Start the fetch
+ window.fetch(url, options)
+ .then(response => {
+ fetch.status = response.status;
+ fetch.statusText = response.statusText;
+
+ // Store headers array for efficient access
+ let responseHeadersArray = 0;
+ if (response.headers) {
+ const headerPairs = [];
+ response.headers.forEach((value, key) => {
+ headerPairs.push(key, value);
+ });
+
+ // Allocate memory for the array of string pointers (pairs + null terminator)
+ const arraySize = (headerPairs.length + 1) * 4; // 4 bytes per pointer
+ const arrayPtr = _malloc(arraySize);
+
+ // Fill the array with pointers to the header strings
+ for (let i = 0; i < headerPairs.length; i++) {
+ const str = headerPairs[i];
+ const strLen = lengthBytesUTF8(str) + 1;
+ const strPtr = _malloc(strLen);
+ stringToUTF8(str, strPtr, strLen);
+ HEAP32[(arrayPtr >> 2) + i] = strPtr;
+ }
+
+ // Null terminate the array
+ HEAP32[(arrayPtr >> 2) + headerPairs.length] = 0;
+
+ responseHeadersArray = arrayPtr;
+ }
+ fetch.responseHeadersArray = responseHeadersArray;
+
+ // Get total size if available
+ const contentLength = response.headers.get('Content-Length');
+ if (contentLength) {
+ fetch.totalBytes = parseInt(contentLength, 10);
+ fetch.bufferSize = fetch.totalBytes * 1.2; // Allocate 20% extra space for gzipped content
+ } else {
+ fetch.bufferSize = 1024 * 1024; // 1MB
+ }
+ // Update fetch object
+ fetch.buffer = _malloc(fetch.bufferSize);
+
+ // Check for error status codes (4xx, 5xx)
+ if (!response.ok) {
+ console.debug(`Fetch #${fetchId} received error status: ${response.status} ${response.statusText}`);
+ // For error responses, we still want to read the body for potential error details
+ // but mark the request as not successful
+ fetch.success = false;
+ } else {
+ fetch.success = true;
+ }
+ console.debug("Fetch #" + fetchId + " headers received, status: " + response.status);
+
+ // Start reading the data
+ const reader = response.body.getReader();
+ fetch.reader = reader; // Store reader for possible cancellation
+
+ // Function to read all chunks (async)
+ const readAllChunks = async () => {
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ // Finished reading
+ fetch.completed = true;
+ fetch.active = false;
+ const date = new Date();
+ console.debug(`Fetch #${fetchId} complete: Read ${fetch.numBytes} bytes total at ${date.getMinutes()}:${date.getSeconds()}`);
+ break;
+ }
+
+ // Ensure we have enough buffer space
+ if (fetch.numBytes + value.length > fetch.bufferSize) { // totalBytes is based on content-length header which is off for gzipped content
+ console.debug(`Fetch #${fetchId} expanding buffer from ${fetch.bufferSize} to ${(fetch.numBytes + value.length) * 1.2}`);
+ fetch.bufferSize = (fetch.numBytes + value.length) * 1.2;
+ const newBuffer = _malloc(fetch.bufferSize);
+ if (fetch.numBytes > 0) {
+ // Copy existing data if needed
+ HEAPU8.set(HEAPU8.subarray(fetch.buffer, fetch.buffer + fetch.numBytes), newBuffer);
+ }
+ _free(fetch.buffer);
+ fetch.buffer = newBuffer;
+ }
+ if (fetch.totalBytes && fetch.numBytes + value.length > fetch.totalBytes) {
+ fetch.totalBytes = fetch.numBytes + value.length; // Adjust totalBytes if we underestimated due to gzip
+ }
+
+ // Copy data to the buffer
+ HEAPU8.set(value, fetch.buffer + fetch.numBytes);
+ fetch.numBytes += value.length;
+ }
+ } catch (error) {
+ console.error(`Error reading from fetch #${fetchId}:`, error);
+ fetch.completed = true;
+ fetch.active = false;
+ fetch.success = false;
+ }
+ };
+
+ // Start reading all chunks
+ readAllChunks();
+ })
+ .catch(error => {
+ console.error("Fetch #" + fetchId + " failed:", error);
+ // this is a bit of a hack, but emscripten malloc returns a valid pointer for size 0
+ fetch.buffer = _malloc(fetch.bufferSize);
+ fetch.bufferSize = 0;
+ fetch.completed = true;
+ fetch.active = false;
+ fetch.success = false;
+
+ if (fetch.statusText) _free(fetch.statusText);
+ const errorMsg = "Network error: " + error.message;
+ const errorLen = lengthBytesUTF8(errorMsg) + 1;
+ fetch.statusText = _malloc(errorLen);
+ stringToUTF8(errorMsg, fetch.statusText, errorLen);
+ });
+
+ return fetchId;
+ },
+
+ NetworkReadStreamEmscripten_close: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ if (!fetch) return;
+
+ console.debug("Closing fetch #" + fetchId);
+
+ // Cancel reader if active
+ if (fetch.reader) {
+ try {
+ fetch.reader.cancel();
+ } catch (e) { }
+ }
+
+ // Free allocated memory
+ if (fetch.statusText) _free(fetch.statusText);
+ if (fetch.responseHeadersArray) {
+ // Free the header strings in the array first
+ let i = 0;
+ while (HEAP32[(fetch.responseHeadersArray >> 2) + i] !== 0) {
+ _free(HEAP32[(fetch.responseHeadersArray >> 2) + i]);
+ i++;
+ }
+ // Free the array itself
+ _free(fetch.responseHeadersArray);
+ }
+ if (fetch.buffer) _free(fetch.buffer);
+
+ // Remove from registry
+ delete window.scummvmFetches[fetchId];
+ },
+
+ // Getter functions properties
+
+ NetworkReadStreamEmscripten_status: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ if (!fetch) return 0;
+ return fetch.status;
+ },
+
+
+
+ NetworkReadStreamEmscripten_getNumBytes: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ if (!fetch) return 0;
+ return fetch.numBytes;
+ },
+
+ NetworkReadStreamEmscripten_totalBytes: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ if (!fetch) return 0;
+ return fetch.totalBytes;
+ },
+
+ NetworkReadStreamEmscripten_responseHeadersArray: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ if (!fetch) return 0;
+ return fetch.responseHeadersArray;
+ },
+
+ NetworkReadStreamEmscripten_getDataPtr: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ //console.debug("Fetch #" + fetchId + " data pointer requested:", fetch ? fetch.buffer : "not found");
+ if (!fetch) return 0;
+ return fetch.buffer;
+ },
+
+ NetworkReadStreamEmscripten_completed: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ if (!fetch) return true;
+ return fetch.completed;
+ },
+
+ NetworkReadStreamEmscripten_hasError: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ if (!fetch) return true;
+ return !fetch.success;
+ },
+
+ NetworkReadStreamEmscripten_getErr: function (fetchId) {
+ const fetch = window.scummvmFetches[fetchId];
+ if (!fetch) return 0; // Return null pointer
+
+ if (fetch.statusText) {
+ // Return the statusText if available
+ const ptr = _malloc(lengthBytesUTF8(fetch.statusText) + 1);
+ stringToUTF8(fetch.statusText, ptr, lengthBytesUTF8(fetch.statusText) + 1);
+ return ptr;
+ }
+
+ // Return a generic error if no specific message is available
+ const errorMsg = "Unknown error";
+ const ptr = _malloc(lengthBytesUTF8(errorMsg) + 1);
+ stringToUTF8(errorMsg, ptr, lengthBytesUTF8(errorMsg) + 1);
+ return ptr;
+ }
+});
diff --git a/backends/networking/http/networkreadstream.cpp b/backends/networking/http/networkreadstream.cpp
index 59497271a8a..8aedf9d2e01 100644
--- a/backends/networking/http/networkreadstream.cpp
+++ b/backends/networking/http/networkreadstream.cpp
@@ -44,27 +44,4 @@ uint32 NetworkReadStream::addResponseHeaders(char *buffer, uint32 bufferSize) {
return bufferSize;
}
-double NetworkReadStream::getProgress() const {
- if (_progressTotal < 1)
- return 0;
- return (double)_progressDownloaded / (double)_progressTotal;
-}
-
-void NetworkReadStream::setProgress(uint64 downloaded, uint64 total) {
- _progressDownloaded = downloaded;
- _progressTotal = total;
-}
-
-uint32 NetworkReadStream::read(void *dataPtr, uint32 dataSize) {
- uint32 actuallyRead = _backingStream.read(dataPtr, dataSize);
-
- if (actuallyRead == 0) {
- if (_requestComplete)
- _eos = true;
- return 0;
- }
-
- return actuallyRead;
-}
-
} // End of namespace Networking
diff --git a/backends/networking/http/networkreadstream.h b/backends/networking/http/networkreadstream.h
index 3ae028cbde4..bbc3fdce668 100644
--- a/backends/networking/http/networkreadstream.h
+++ b/backends/networking/http/networkreadstream.h
@@ -36,7 +36,6 @@ typedef Common::Array<Common::String> RequestHeaders;
// Simple interface for platform-specific NetworkReadStream implementations
class NetworkReadStream : public Common::ReadStream {
protected:
- Common::MemoryReadWriteStream _backingStream;
bool _keepAlive;
long _keepAliveIdle, _keepAliveInterval;
bool _eos, _requestComplete;
@@ -44,7 +43,6 @@ protected:
uint32 _sendingContentsSize;
uint32 _sendingContentsPos;
Common::String _responseHeaders;
- uint64 _progressDownloaded, _progressTotal;
/**
* Fills the passed buffer with _sendingContentsBuffer contents.
@@ -63,8 +61,8 @@ protected:
uint32 addResponseHeaders(char *buffer, uint32 bufferSize);
NetworkReadStream(bool keepAlive, long keepAliveIdle, long keepAliveInterval)
- : _backingStream(DisposeAfterUse::YES), _eos(false), _requestComplete(false), _sendingContentsBuffer(nullptr),
- _sendingContentsSize(0), _sendingContentsPos(0), _progressDownloaded(0), _progressTotal(0),
+ : _eos(false), _requestComplete(false), _sendingContentsBuffer(nullptr),
+ _sendingContentsSize(0), _sendingContentsPos(0),
_keepAlive(keepAlive), _keepAliveIdle(keepAliveIdle), _keepAliveInterval(keepAliveInterval) {
}
@@ -119,7 +117,7 @@ public:
* @param dataSize number of bytes to be read
* @return the number of bytes which were actually read.
*/
- uint32 read(void *dataPtr, uint32 dataSize) override;
+ uint32 read(void *dataPtr, uint32 dataSize) override = 0;
/**
* Returns HTTP response code from inner CURL handle.
@@ -146,10 +144,7 @@ public:
virtual Common::HashMap<Common::String, Common::String> responseHeadersMap() const = 0;
/** Returns a number in range [0, 1], where 1 is "complete". */
- double getProgress() const;
-
- /** Used in curl progress callback to pass current downloaded/total values. */
- void setProgress(uint64 downloaded, uint64 total);
+ virtual double getProgress() const = 0;
bool keepAlive() const { return _keepAlive; }
diff --git a/configure b/configure
index 9f0f58efe0c..3c9e0ed8d92 100755
--- a/configure
+++ b/configure
@@ -3375,6 +3375,7 @@ EOF
append_var LDFLAGS "-s EXPORTED_FUNCTIONS=[_main,_malloc,_free,_raise]"
append_var LDFLAGS "-s EXPORTED_RUNTIME_METHODS=[ccall,lengthBytesUTF8,setValue,writeArrayToMemory]"
+ append_var LDFLAGS "--js-library backends/networking/http/emscripten/networkreadstream-emscripten.js"
append_var LDFLAGS "-O2"
_optimization_level=-O2
Commit: 9285b5e115f579fea793921c322495d39b49c950
https://github.com/scummvm/scummvm/commit/9285b5e115f579fea793921c322495d39b49c950
Author: Christian Kündig (christian at kuendig.info)
Date: 2026-06-28T22:06:19+02:00
Commit Message:
EMSCRIPTEN: Fix HEAPU8 access in downloadFile (broke in ade1c2fa60a)
Changed paths:
backends/platform/sdl/emscripten/emscripten.cpp
diff --git a/backends/platform/sdl/emscripten/emscripten.cpp b/backends/platform/sdl/emscripten/emscripten.cpp
index aac9a98da3a..b6794c84e4a 100644
--- a/backends/platform/sdl/emscripten/emscripten.cpp
+++ b/backends/platform/sdl/emscripten/emscripten.cpp
@@ -53,7 +53,7 @@ EM_JS(void, toggleFullscreen, (bool enable), {
});
EM_JS(void, downloadFile, (const char *filenamePtr, char *dataPtr, int dataSize), {
- const view = new Uint8Array(Module.HEAPU8.buffer, dataPtr, dataSize);
+ const view = new Uint8Array(HEAPU8.buffer, dataPtr, dataSize);
const blob = new Blob([view], {
type:
'octet/stream'
Commit: edd14e08a65d198a65071519421299b21f57eb77
https://github.com/scummvm/scummvm/commit/edd14e08a65d198a65071519421299b21f57eb77
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-06-28T22:06:19+02:00
Commit Message:
NETWORKING: Cleanup CURL specific variables from the common class
Changed paths:
R backends/networking/http/networkreadstream.cpp
backends/module.mk
backends/networking/http/android/networkreadstream-android.h
backends/networking/http/curl/networkreadstream-curl.cpp
backends/networking/http/curl/networkreadstream-curl.h
backends/networking/http/emscripten/networkreadstream-emscripten.cpp
backends/networking/http/networkreadstream.h
diff --git a/backends/module.mk b/backends/module.mk
index 9837ccee8b8..8effe44930b 100644
--- a/backends/module.mk
+++ b/backends/module.mk
@@ -30,7 +30,6 @@ MODULE_OBJS := \
ifdef USE_HTTP
MODULE_OBJS += \
networking/http/connectionmanager.o \
- networking/http/networkreadstream.o \
networking/http/httpjsonrequest.o \
networking/http/httprequest.o \
networking/http/postrequest.o \
diff --git a/backends/networking/http/android/networkreadstream-android.h b/backends/networking/http/android/networkreadstream-android.h
index 99f582de902..41c7c9fdecd 100644
--- a/backends/networking/http/android/networkreadstream-android.h
+++ b/backends/networking/http/android/networkreadstream-android.h
@@ -46,6 +46,7 @@ private:
Common::HashMap<Common::String, Common::String> _responseHeadersMap;
uint64 _downloaded;
uint64 _progressDownloaded, _progressTotal;
+ bool _requestComplete;
int _errorCode;
Common::String _errorMsg;
public:
diff --git a/backends/networking/http/curl/networkreadstream-curl.cpp b/backends/networking/http/curl/networkreadstream-curl.cpp
index d8cb9a92c80..262a2463bf1 100644
--- a/backends/networking/http/curl/networkreadstream-curl.cpp
+++ b/backends/networking/http/curl/networkreadstream-curl.cpp
@@ -55,16 +55,31 @@ size_t NetworkReadStreamCurl::curlDataCallback(char *d, size_t n, size_t l, void
size_t NetworkReadStreamCurl::curlReadDataCallback(char *d, size_t n, size_t l, void *p) {
NetworkReadStreamCurl *stream = (NetworkReadStreamCurl *)p;
- if (stream)
- return stream->fillWithSendingContents(d, n * l);
- return 0;
+ if (!stream) {
+ return 0;
+ }
+
+ const uint32 maxSize = n * l;
+
+ uint32 sendSize = stream->_sendingContentsSize - stream->_sendingContentsPos;
+ if (sendSize > maxSize)
+ sendSize = maxSize;
+
+ memcpy(d, &stream->_sendingContentsBuffer[stream->_sendingContentsPos], sendSize * sizeof(byte));
+
+ stream->_sendingContentsPos += sendSize;
+ return sendSize;
}
size_t NetworkReadStreamCurl::curlHeadersCallback(char *d, size_t n, size_t l, void *p) {
NetworkReadStreamCurl *stream = (NetworkReadStreamCurl *)p;
- if (stream)
- return stream->addResponseHeaders(d, n * l);
- return 0;
+ if (!stream) {
+ return 0;
+ }
+
+ const size_t bufferSize = n * l;
+ stream->_responseHeaders += Common::String(d, bufferSize);
+ return bufferSize;
}
static int curlProgressCallback(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
diff --git a/backends/networking/http/curl/networkreadstream-curl.h b/backends/networking/http/curl/networkreadstream-curl.h
index 34e9d78c6ed..6bf7a9dcaa8 100644
--- a/backends/networking/http/curl/networkreadstream-curl.h
+++ b/backends/networking/http/curl/networkreadstream-curl.h
@@ -45,6 +45,13 @@ private:
CURLcode _errorCode;
byte *_bufferCopy; // To use with old curl version where CURLOPT_COPYPOSTFIELDS is not available
uint64 _progressDownloaded, _progressTotal;
+ bool _requestComplete;
+ Common::String _responseHeaders;
+
+ const byte *_sendingContentsBuffer;
+ uint32 _sendingContentsSize;
+ uint32 _sendingContentsPos;
+
void initCurl(const char *url, RequestHeaders *headersList);
bool reuseCurl(const char *url, RequestHeaders *headersList);
static struct curl_slist *requestHeadersToSlist(const RequestHeaders *headersList);
diff --git a/backends/networking/http/emscripten/networkreadstream-emscripten.cpp b/backends/networking/http/emscripten/networkreadstream-emscripten.cpp
index f012a325447..4b92e41d5e7 100644
--- a/backends/networking/http/emscripten/networkreadstream-emscripten.cpp
+++ b/backends/networking/http/emscripten/networkreadstream-emscripten.cpp
@@ -40,7 +40,6 @@ NetworkReadStream *NetworkReadStream::make(const char *url, RequestHeaders *head
void NetworkReadStreamEmscripten::resetStream() {
_eos = false;
- _sendingContentsSize = _sendingContentsPos = 0;
_readPos = 0;
_fetchId = 0;
}
diff --git a/backends/networking/http/networkreadstream.cpp b/backends/networking/http/networkreadstream.cpp
deleted file mode 100644
index 8aedf9d2e01..00000000000
--- a/backends/networking/http/networkreadstream.cpp
+++ /dev/null
@@ -1,47 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#include "networkreadstream.h"
-#include "common/tokenizer.h"
-
-namespace Networking {
-
-/*
- * The make static functions are defined in the implementation-specific subclass
- */
-
-uint32 NetworkReadStream::fillWithSendingContents(char *bufferToFill, uint32 maxSize) {
- uint32 sendSize = _sendingContentsSize - _sendingContentsPos;
- if (sendSize > maxSize)
- sendSize = maxSize;
- for (uint32 i = 0; i < sendSize; ++i) {
- bufferToFill[i] = _sendingContentsBuffer[_sendingContentsPos + i];
- }
- _sendingContentsPos += sendSize;
- return sendSize;
-}
-
-uint32 NetworkReadStream::addResponseHeaders(char *buffer, uint32 bufferSize) {
- _responseHeaders += Common::String(buffer, bufferSize);
- return bufferSize;
-}
-
-} // End of namespace Networking
diff --git a/backends/networking/http/networkreadstream.h b/backends/networking/http/networkreadstream.h
index bbc3fdce668..3d3c11c8e21 100644
--- a/backends/networking/http/networkreadstream.h
+++ b/backends/networking/http/networkreadstream.h
@@ -38,32 +38,11 @@ class NetworkReadStream : public Common::ReadStream {
protected:
bool _keepAlive;
long _keepAliveIdle, _keepAliveInterval;
- bool _eos, _requestComplete;
- const byte *_sendingContentsBuffer;
- uint32 _sendingContentsSize;
- uint32 _sendingContentsPos;
- Common::String _responseHeaders;
-
- /**
- * Fills the passed buffer with _sendingContentsBuffer contents.
- * It works similarly to read(), expect it's not for reading
- * Stream's contents, but for sending our own data to the server.
- *
- * @returns how many bytes were actually read (filled in)
- */
- uint32 fillWithSendingContents(char *bufferToFill, uint32 maxSize);
-
- /**
- * Remembers headers returned to CURL in server's response.
- *
- * @returns how many bytes were actually read
- */
- uint32 addResponseHeaders(char *buffer, uint32 bufferSize);
+ bool _eos;
NetworkReadStream(bool keepAlive, long keepAliveIdle, long keepAliveInterval)
- : _eos(false), _requestComplete(false), _sendingContentsBuffer(nullptr),
- _sendingContentsSize(0), _sendingContentsPos(0),
- _keepAlive(keepAlive), _keepAliveIdle(keepAliveIdle), _keepAliveInterval(keepAliveInterval) {
+ : _eos(false), _keepAlive(keepAlive),
+ _keepAliveIdle(keepAliveIdle), _keepAliveInterval(keepAliveInterval) {
}
public:
More information about the Scummvm-git-logs
mailing list