[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