Forráskód Böngészése

Add THREADS_ENABLED macro in order to compile Godot to run on the main thread

Adam Scott 1 éve
szülő
commit
bd70b8e1f6

+ 26 - 4
.github/workflows/web_builds.yml

@@ -17,7 +17,24 @@ concurrency:
 jobs:
   web-template:
     runs-on: "ubuntu-22.04"
-    name: Template (target=template_release)
+    name: ${{ matrix.name }}
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - name: Template w/ threads (target=template_release, threads=yes)
+            cache-name: web-template
+            target: template_release
+            sconsflags: threads=yes
+            tests: false
+            artifact: true
+
+          - name: Template w/o threads (target=template_release, threads=no)
+            cache-name: web-nothreads-template
+            target: template_release
+            sconsflags: threads=no
+            tests: false
+            artifact: true
 
     steps:
       - uses: actions/checkout@v4
@@ -34,6 +51,8 @@ jobs:
 
       - name: Setup Godot build cache
         uses: ./.github/actions/godot-cache
+        with:
+          cache-name: ${{ matrix.cache-name }}
         continue-on-error: true
 
       - name: Setup python and scons
@@ -42,10 +61,13 @@ jobs:
       - name: Compilation
         uses: ./.github/actions/godot-build
         with:
-          sconsflags: ${{ env.SCONSFLAGS }}
+          sconsflags: ${{ env.SCONSFLAGS }} ${{ matrix.sconsflags }}
           platform: web
-          target: template_release
-          tests: false
+          target: ${{ matrix.target }}
+          tests: ${{ matrix.tests }}
 
       - name: Upload artifact
         uses: ./.github/actions/upload-artifact
+        if: ${{ matrix.artifact }}
+        with:
+          name: ${{ matrix.cache-name }}

+ 8 - 0
SConstruct

@@ -183,6 +183,7 @@ opts.Add(BoolVariable("separate_debug_symbols", "Extract debugging symbols to a
 opts.Add(EnumVariable("lto", "Link-time optimization (production builds)", "none", ("none", "auto", "thin", "full")))
 opts.Add(BoolVariable("production", "Set defaults to build Godot for use in production", False))
 opts.Add(BoolVariable("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False))
+opts.Add(BoolVariable("threads", "Enable threading support", True))
 
 # Components
 opts.Add(BoolVariable("deprecated", "Enable compatibility code for deprecated and removed features", True))
@@ -832,6 +833,10 @@ if selected_platform in platform_list:
         suffix += ".double"
 
     suffix += "." + env["arch"]
+
+    if not env["threads"]:
+        suffix += ".nothreads"
+
     suffix += env.extra_suffix
 
     sys.path.remove(tmppath)
@@ -972,6 +977,9 @@ if selected_platform in platform_list:
         env.Tool("compilation_db")
         env.Alias("compiledb", env.CompilationDatabase())
 
+    if env["threads"]:
+        env.Append(CPPDEFINES=["THREADS_ENABLED"])
+
     Export("env")
 
     # Build subdirs, the build order is dependent on link order.

+ 6 - 0
core/object/worker_thread_pool.cpp

@@ -47,6 +47,7 @@ WorkerThreadPool *WorkerThreadPool::singleton = nullptr;
 thread_local CommandQueueMT *WorkerThreadPool::flushing_cmd_queue = nullptr;
 
 void WorkerThreadPool::_process_task(Task *p_task) {
+#ifdef THREADS_ENABLED
 	int pool_thread_index = thread_ids[Thread::get_caller_id()];
 	ThreadData &curr_thread = threads[pool_thread_index];
 	Task *prev_task = nullptr; // In case this is recursively called.
@@ -69,6 +70,7 @@ void WorkerThreadPool::_process_task(Task *p_task) {
 		curr_thread.current_task = p_task;
 		task_mutex.unlock();
 	}
+#endif
 
 	if (p_task->group) {
 		// Handling a group
@@ -143,6 +145,7 @@ void WorkerThreadPool::_process_task(Task *p_task) {
 		}
 	}
 
+#ifdef THREADS_ENABLED
 	{
 		curr_thread.current_task = prev_task;
 		if (p_task->low_priority) {
@@ -159,6 +162,7 @@ void WorkerThreadPool::_process_task(Task *p_task) {
 	}
 
 	set_current_thread_safe_for_nodes(safe_for_nodes_backup);
+#endif
 }
 
 void WorkerThreadPool::_thread_function(void *p_user) {
@@ -542,6 +546,7 @@ bool WorkerThreadPool::is_group_task_completed(GroupID p_group) const {
 }
 
 void WorkerThreadPool::wait_for_group_task_completion(GroupID p_group) {
+#ifdef THREADS_ENABLED
 	task_mutex.lock();
 	Group **groupp = groups.getptr(p_group);
 	task_mutex.unlock();
@@ -574,6 +579,7 @@ void WorkerThreadPool::wait_for_group_task_completion(GroupID p_group) {
 	task_mutex.lock(); // This mutex is needed when Physics 2D and/or 3D is selected to run on a separate thread.
 	groups.erase(p_group);
 	task_mutex.unlock();
+#endif
 }
 
 int WorkerThreadPool::get_thread_index() {

+ 14 - 0
core/os/condition_variable.h

@@ -33,6 +33,8 @@
 
 #include "core/os/mutex.h"
 
+#ifdef THREADS_ENABLED
+
 #ifdef MINGW_ENABLED
 #define MINGW_STDTHREAD_REDUNDANCY_WARNING
 #include "thirdparty/mingw-std-threads/mingw.condition_variable.h"
@@ -66,4 +68,16 @@ public:
 	}
 };
 
+#else // No threads.
+
+class ConditionVariable {
+public:
+	template <class BinaryMutexT>
+	void wait(const MutexLock<BinaryMutexT> &p_lock) const {}
+	void notify_one() const {}
+	void notify_all() const {}
+};
+
+#endif // THREADS_ENABLED
+
 #endif // CONDITION_VARIABLE_H

+ 4 - 0
core/os/mutex.cpp

@@ -40,7 +40,11 @@ void _global_unlock() {
 	_global_mutex.unlock();
 }
 
+#ifdef THREADS_ENABLED
+
 template class MutexImpl<THREADING_NAMESPACE::recursive_mutex>;
 template class MutexImpl<THREADING_NAMESPACE::mutex>;
 template class MutexLock<MutexImpl<THREADING_NAMESPACE::recursive_mutex>>;
 template class MutexLock<MutexImpl<THREADING_NAMESPACE::mutex>>;
+
+#endif

+ 38 - 2
core/os/mutex.h

@@ -43,6 +43,8 @@
 #define THREADING_NAMESPACE std
 #endif
 
+#ifdef THREADS_ENABLED
+
 template <class MutexT>
 class MutexLock;
 
@@ -125,8 +127,8 @@ class MutexLock {
 	THREADING_NAMESPACE::unique_lock<typename MutexT::StdMutexType> lock;
 
 public:
-	_ALWAYS_INLINE_ explicit MutexLock(const MutexT &p_mutex) :
-			lock(p_mutex.mutex){};
+	explicit MutexLock(const MutexT &p_mutex) :
+			lock(p_mutex.mutex) {}
 };
 
 // This specialization is needed so manual locking and MutexLock can be used
@@ -155,4 +157,38 @@ extern template class MutexImpl<THREADING_NAMESPACE::mutex>;
 extern template class MutexLock<MutexImpl<THREADING_NAMESPACE::recursive_mutex>>;
 extern template class MutexLock<MutexImpl<THREADING_NAMESPACE::mutex>>;
 
+#else // No threads.
+
+class MutexImpl {
+	mutable THREADING_NAMESPACE::mutex mutex;
+
+public:
+	void lock() const {}
+	void unlock() const {}
+	bool try_lock() const { return true; }
+};
+
+template <int Tag>
+class SafeBinaryMutex : public MutexImpl {
+	static thread_local uint32_t count;
+};
+
+template <class MutexT>
+class MutexLock {
+public:
+	MutexLock(const MutexT &p_mutex) {}
+};
+
+template <int Tag>
+class MutexLock<SafeBinaryMutex<Tag>> {
+public:
+	MutexLock(const SafeBinaryMutex<Tag> &p_mutex) {}
+	~MutexLock() {}
+};
+
+using Mutex = MutexImpl;
+using BinaryMutex = MutexImpl;
+
+#endif // THREADS_ENABLED
+
 #endif // MUTEX_H

+ 6 - 0
core/os/os.cpp

@@ -504,6 +504,12 @@ bool OS::has_feature(const String &p_feature) {
 	}
 #endif
 
+#ifdef THREADS_ENABLED
+	if (p_feature == "threads") {
+		return true;
+	}
+#endif
+
 	if (_check_internal_feature_support(p_feature)) {
 		return true;
 	}

+ 17 - 0
core/os/semaphore.h

@@ -31,6 +31,10 @@
 #ifndef SEMAPHORE_H
 #define SEMAPHORE_H
 
+#include <cstdint>
+
+#ifdef THREADS_ENABLED
+
 #include "core/error/error_list.h"
 #include "core/typedefs.h"
 #ifdef DEBUG_ENABLED
@@ -132,4 +136,17 @@ public:
 #endif
 };
 
+#else // No threads.
+
+class Semaphore {
+public:
+	void post(uint32_t p_count = 1) const {}
+	void wait() const {}
+	bool try_wait() const {
+		return true;
+	}
+};
+
+#endif // THREADS_ENABLED
+
 #endif // SEMAPHORE_H

+ 7 - 2
core/os/thread.cpp

@@ -33,19 +33,22 @@
 
 #include "thread.h"
 
+#ifdef THREADS_ENABLED
 #include "core/object/script_language.h"
 #include "core/templates/safe_refcount.h"
 
-Thread::PlatformFunctions Thread::platform_functions;
-
 SafeNumeric<uint64_t> Thread::id_counter(1); // The first value after .increment() is 2, hence by default the main thread ID should be 1.
 
 thread_local Thread::ID Thread::caller_id = Thread::UNASSIGNED_ID;
+#endif
+
+Thread::PlatformFunctions Thread::platform_functions;
 
 void Thread::_set_platform_functions(const PlatformFunctions &p_functions) {
 	platform_functions = p_functions;
 }
 
+#ifdef THREADS_ENABLED
 void Thread::callback(ID p_caller_id, const Settings &p_settings, Callback p_callback, void *p_userdata) {
 	Thread::caller_id = p_caller_id;
 	if (platform_functions.set_priority) {
@@ -107,4 +110,6 @@ Thread::~Thread() {
 	}
 }
 
+#endif // THREADS_ENABLED
+
 #endif // PLATFORM_THREAD_OVERRIDE

+ 62 - 2
core/os/thread.h

@@ -53,6 +53,8 @@
 
 class String;
 
+#ifdef THREADS_ENABLED
+
 class Thread {
 public:
 	typedef void (*Callback)(void *p_userdata);
@@ -86,6 +88,8 @@ public:
 private:
 	friend class Main;
 
+	static PlatformFunctions platform_functions;
+
 	ID id = UNASSIGNED_ID;
 	static SafeNumeric<uint64_t> id_counter;
 	static thread_local ID caller_id;
@@ -93,8 +97,6 @@ private:
 
 	static void callback(ID p_caller_id, const Settings &p_settings, Thread::Callback p_callback, void *p_userdata);
 
-	static PlatformFunctions platform_functions;
-
 	static void make_main_thread() { caller_id = MAIN_ID; }
 	static void release_main_thread() { caller_id = UNASSIGNED_ID; }
 
@@ -125,6 +127,64 @@ public:
 	~Thread();
 };
 
+#else // No threads.
+
+class Thread {
+public:
+	typedef void (*Callback)(void *p_userdata);
+
+	typedef uint64_t ID;
+
+	enum : ID {
+		UNASSIGNED_ID = 0,
+		MAIN_ID = 1
+	};
+
+	enum Priority {
+		PRIORITY_LOW,
+		PRIORITY_NORMAL,
+		PRIORITY_HIGH
+	};
+
+	struct Settings {
+		Priority priority;
+		Settings() { priority = PRIORITY_NORMAL; }
+	};
+
+	struct PlatformFunctions {
+		Error (*set_name)(const String &) = nullptr;
+		void (*set_priority)(Thread::Priority) = nullptr;
+		void (*init)() = nullptr;
+		void (*wrapper)(Thread::Callback, void *) = nullptr;
+		void (*term)() = nullptr;
+	};
+
+private:
+	friend class Main;
+
+	static PlatformFunctions platform_functions;
+
+	static void make_main_thread() {}
+	static void release_main_thread() {}
+
+public:
+	static void _set_platform_functions(const PlatformFunctions &p_functions);
+
+	_FORCE_INLINE_ ID get_id() const { return 0; }
+	_FORCE_INLINE_ static ID get_caller_id() { return MAIN_ID; }
+	_FORCE_INLINE_ static ID get_main_id() { return MAIN_ID; }
+
+	_FORCE_INLINE_ static bool is_main_thread() { return true; }
+
+	static Error set_name(const String &p_name) { return ERR_UNAVAILABLE; }
+
+	void start(Thread::Callback p_callback, void *p_user, const Settings &p_settings = Settings()) {}
+	bool is_started() const { return false; }
+	void wait_to_finish() {}
+};
+
+#endif // THREADS_ENABLED
+
 #endif // THREAD_H
 
 #endif // PLATFORM_THREAD_OVERRIDE

+ 2 - 0
drivers/unix/os_unix.cpp

@@ -153,7 +153,9 @@ int OS_Unix::unix_initialize_audio(int p_audio_driver) {
 }
 
 void OS_Unix::initialize_core() {
+#ifdef THREADS_ENABLED
 	init_thread_posix();
+#endif
 
 	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES);
 	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_USERDATA);

+ 8 - 0
editor/editor_file_system.cpp

@@ -2354,7 +2354,11 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
 
 	reimport_files.sort();
 
+#ifdef THREADS_ENABLED
 	bool use_multiple_threads = GLOBAL_GET("editor/import/use_multiple_threads");
+#else
+	bool use_multiple_threads = false;
+#endif
 
 	int from = 0;
 	for (int i = 0; i < reimport_files.size(); i++) {
@@ -2680,6 +2684,10 @@ void EditorFileSystem::remove_import_format_support_query(Ref<EditorFileSystemIm
 }
 
 EditorFileSystem::EditorFileSystem() {
+#ifdef THREADS_ENABLED
+	use_threads = true;
+#endif
+
 	ResourceLoader::import = _resource_import;
 	reimport_on_missing_imported_files = GLOBAL_GET("editor/import/reimport_missing_imported_files");
 	singleton = this;

+ 1 - 1
editor/editor_file_system.h

@@ -165,7 +165,7 @@ class EditorFileSystem : public Node {
 		EditorFileSystemDirectory::FileInfo *new_file = nullptr;
 	};
 
-	bool use_threads = true;
+	bool use_threads = false;
 	Thread thread;
 	static void _thread_func(void *_userdata);
 

+ 12 - 6
main/main.cpp

@@ -1615,12 +1615,18 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
 	}
 
 	// Initialize WorkerThreadPool.
-	if (editor || project_manager) {
-		WorkerThreadPool::get_singleton()->init(-1, 0.75);
-	} else {
-		int worker_threads = GLOBAL_GET("threading/worker_pool/max_threads");
-		float low_priority_ratio = GLOBAL_GET("threading/worker_pool/low_priority_thread_ratio");
-		WorkerThreadPool::get_singleton()->init(worker_threads, low_priority_ratio);
+	{
+#ifdef THREADS_ENABLED
+		if (editor || project_manager) {
+			WorkerThreadPool::get_singleton()->init(-1, 0.75);
+		} else {
+			int worker_threads = GLOBAL_GET("threading/worker_pool/max_threads");
+			float low_priority_ratio = GLOBAL_GET("threading/worker_pool/low_priority_thread_ratio");
+			WorkerThreadPool::get_singleton()->init(worker_threads, low_priority_ratio);
+		}
+#else
+		WorkerThreadPool::get_singleton()->init(0, 0);
+#endif
 	}
 
 #ifdef TOOLS_ENABLED

+ 5 - 3
misc/dist/html/editor.html

@@ -23,7 +23,7 @@
 		<link id="-gd-engine-icon" rel="icon" type="image/png" href="favicon.png">
 		<link rel="apple-touch-icon" type="image/png" href="favicon.png">
 		<link rel="manifest" href="manifest.json">
-		<title>Godot Engine Web Editor (@GODOT_VERSION@)</title>
+		<title>Godot Engine Web Editor (___GODOT_VERSION___)</title>
 		<style>
 *:focus {
 	/* More visible outline for better keyboard navigation. */
@@ -294,7 +294,7 @@ a:active {
 					<br >
 					<img src="logo.svg" alt="Godot Engine logo" width="1024" height="414" style="width: auto; height: auto; max-width: min(85%, 50vh); max-height: 250px">
 					<br >
-					@GODOT_VERSION@
+					___GODOT_VERSION___
 					<br >
 					<a href="releases/">Need an old version?</a>
 					<br >
@@ -384,7 +384,9 @@ window.addEventListener('load', () => {
 		});
 	}
 
-	const missing = Engine.getMissingFeatures();
+	const missing = Engine.getMissingFeatures({
+		threads: ___GODOT_THREADS_ENABLED___,
+	});
 	if (missing.length) {
 		// Display error dialog as threading support is required for the editor.
 		document.getElementById('startButton').disabled = 'disabled';

+ 4 - 1
misc/dist/html/full-size.html

@@ -136,6 +136,7 @@ body {
 		<script src="$GODOT_URL"></script>
 		<script>
 const GODOT_CONFIG = $GODOT_CONFIG;
+const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;
 const engine = new Engine(GODOT_CONFIG);
 
 (function () {
@@ -213,7 +214,9 @@ const engine = new Engine(GODOT_CONFIG);
 		initializing = false;
 	}
 
-	const missing = Engine.getMissingFeatures();
+	const missing = Engine.getMissingFeatures({
+		threads: GODOT_THREADS_ENABLED,
+	});
 	if (missing.length !== 0) {
 		const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
 		displayFailureNotice(missingMsg + missing.join('\n'));

+ 6 - 6
misc/dist/html/service-worker.js

@@ -3,14 +3,14 @@
 // that they need an Internet connection to run the project if desired.
 // Incrementing CACHE_VERSION will kick off the install event and force
 // previously cached resources to be updated from the network.
-const CACHE_VERSION = "@GODOT_VERSION@";
-const CACHE_PREFIX = "@GODOT_NAME@-sw-cache-";
+const CACHE_VERSION = "___GODOT_VERSION___";
+const CACHE_PREFIX = "___GODOT_NAME___-sw-cache-";
 const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
-const OFFLINE_URL = "@GODOT_OFFLINE_PAGE@";
+const OFFLINE_URL = "___GODOT_OFFLINE_PAGE___";
 // Files that will be cached on load.
-const CACHED_FILES = @GODOT_CACHE@;
+const CACHED_FILES = ___GODOT_CACHE___;
 // Files that we might not want the user to preload, and will only be cached on first load.
-const CACHABLE_FILES = @GODOT_OPT_CACHE@;
+const CACHABLE_FILES = ___GODOT_OPT_CACHE___;
 const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
 
 self.addEventListener("install", (event) => {
@@ -22,7 +22,7 @@ self.addEventListener("activate", (event) => {
 		function (keys) {
 			// Remove old caches.
 			return Promise.all(keys.filter(key => key.startsWith(CACHE_PREFIX) && key != CACHE_NAME).map(key => caches.delete(key)));
-		}).then(function() {
+		}).then(function () {
 			// Enable navigation preload if available.
 			return ("navigationPreload" in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
 		})

+ 8 - 1
modules/svg/register_types.cpp

@@ -34,6 +34,12 @@
 
 #include <thorvg.h>
 
+#ifdef THREADS_ENABLED
+#define TVG_THREADS 1
+#else
+#define TVG_THREADS 0
+#endif
+
 static Ref<ImageLoaderSVG> image_loader_svg;
 
 void initialize_svg_module(ModuleInitializationLevel p_level) {
@@ -42,7 +48,8 @@ void initialize_svg_module(ModuleInitializationLevel p_level) {
 	}
 
 	tvg::CanvasEngine tvgEngine = tvg::CanvasEngine::Sw;
-	if (tvg::Initializer::init(tvgEngine, 1) != tvg::Result::Success) {
+
+	if (tvg::Initializer::init(tvgEngine, TVG_THREADS) != tvg::Result::Success) {
 		return;
 	}
 

+ 2 - 0
platform/web/.eslintrc.html.js

@@ -15,5 +15,7 @@ module.exports = {
 		"Godot": true,
 		"Engine": true,
 		"$GODOT_CONFIG": true,
+		"$GODOT_THREADS_ENABLED": true,
+		"___GODOT_THREADS_ENABLED___": true,
 	},
 };

+ 2 - 2
platform/web/SCsub

@@ -13,7 +13,7 @@ if "serve" in COMMAND_LINE_TARGETS or "run" in COMMAND_LINE_TARGETS:
     except Exception:
         print("GODOT_WEB_TEST_PORT must be a valid integer")
         sys.exit(255)
-    serve(env.Dir("#bin/.web_zip").abspath, port, "run" in COMMAND_LINE_TARGETS)
+    serve(env.Dir(env.GetTemplateZipPath()).abspath, port, "run" in COMMAND_LINE_TARGETS)
     sys.exit(0)
 
 web_files = [
@@ -95,7 +95,7 @@ engine = [
     "js/engine/engine.js",
 ]
 externs = [env.File("#platform/web/js/engine/engine.externs.js")]
-js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs)
+js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs, env["threads"])
 env.Depends(js_engine, externs)
 
 wrap_list = [

+ 52 - 0
platform/web/audio_driver_web.cpp

@@ -30,6 +30,8 @@
 
 #include "audio_driver_web.h"
 
+#include "godot_audio.h"
+
 #include "core/config/project_settings.h"
 
 #include <emscripten.h>
@@ -184,6 +186,8 @@ Error AudioDriverWeb::input_stop() {
 	return OK;
 }
 
+#ifdef THREADS_ENABLED
+
 /// AudioWorkletNode implementation (threads)
 void AudioDriverWorklet::_audio_thread_func(void *p_data) {
 	AudioDriverWorklet *driver = static_cast<AudioDriverWorklet *>(p_data);
@@ -245,3 +249,51 @@ void AudioDriverWorklet::finish_driver() {
 	quit = true; // Ask thread to quit.
 	thread.wait_to_finish();
 }
+
+#else // No threads.
+
+/// AudioWorkletNode implementation (no threads)
+AudioDriverWorklet *AudioDriverWorklet::singleton = nullptr;
+
+Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) {
+	if (!godot_audio_has_worklet()) {
+		return ERR_UNAVAILABLE;
+	}
+	return (Error)godot_audio_worklet_create(p_channels);
+}
+
+void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
+	_audio_driver_process();
+	godot_audio_worklet_start_no_threads(p_out_buf, p_out_buf_size, &_process_callback, p_in_buf, p_in_buf_size, &_capture_callback);
+}
+
+void AudioDriverWorklet::_process_callback(int p_pos, int p_samples) {
+	AudioDriverWorklet *driver = AudioDriverWorklet::get_singleton();
+	driver->_audio_driver_process(p_pos, p_samples);
+}
+
+void AudioDriverWorklet::_capture_callback(int p_pos, int p_samples) {
+	AudioDriverWorklet *driver = AudioDriverWorklet::get_singleton();
+	driver->_audio_driver_capture(p_pos, p_samples);
+}
+
+/// ScriptProcessorNode implementation
+AudioDriverScriptProcessor *AudioDriverScriptProcessor::singleton = nullptr;
+
+void AudioDriverScriptProcessor::_process_callback() {
+	AudioDriverScriptProcessor::get_singleton()->_audio_driver_capture();
+	AudioDriverScriptProcessor::get_singleton()->_audio_driver_process();
+}
+
+Error AudioDriverScriptProcessor::create(int &p_buffer_samples, int p_channels) {
+	if (!godot_audio_has_script_processor()) {
+		return ERR_UNAVAILABLE;
+	}
+	return (Error)godot_audio_script_create(&p_buffer_samples, p_channels);
+}
+
+void AudioDriverScriptProcessor::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
+	godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback);
+}
+
+#endif // THREADS_ENABLED

+ 51 - 0
platform/web/audio_driver_web.h

@@ -90,6 +90,7 @@ public:
 	AudioDriverWeb() {}
 };
 
+#ifdef THREADS_ENABLED
 class AudioDriverWorklet : public AudioDriverWeb {
 private:
 	enum {
@@ -120,4 +121,54 @@ public:
 	virtual void unlock() override;
 };
 
+#else
+
+class AudioDriverWorklet : public AudioDriverWeb {
+private:
+	static void _process_callback(int p_pos, int p_samples);
+	static void _capture_callback(int p_pos, int p_samples);
+
+	static AudioDriverWorklet *singleton;
+
+protected:
+	virtual Error create(int &p_buffer_size, int p_output_channels) override;
+	virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
+
+public:
+	virtual const char *get_name() const override {
+		return "AudioWorklet";
+	}
+
+	virtual void lock() override {}
+	virtual void unlock() override {}
+
+	static AudioDriverWorklet *get_singleton() { return singleton; }
+
+	AudioDriverWorklet() { singleton = this; }
+};
+
+class AudioDriverScriptProcessor : public AudioDriverWeb {
+private:
+	static void _process_callback();
+
+	static AudioDriverScriptProcessor *singleton;
+
+protected:
+	virtual Error create(int &p_buffer_size, int p_output_channels) override;
+	virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
+	virtual void finish_driver() override;
+
+public:
+	virtual const char *get_name() const override { return "ScriptProcessor"; }
+
+	virtual void lock() override {}
+	virtual void unlock() override {}
+
+	static AudioDriverScriptProcessor *get_singleton() { return singleton; }
+
+	AudioDriverScriptProcessor() { singleton = this; }
+};
+
+#endif // THREADS_ENABLED
+
 #endif // AUDIO_DRIVER_WEB_H

+ 16 - 8
platform/web/detect.py

@@ -8,6 +8,7 @@ from emscripten_helpers import (
     add_js_pre,
     add_js_externs,
     create_template_zip,
+    get_template_zip_path,
 )
 from methods import get_compiler_version
 from SCons.Util import WhereIs
@@ -161,6 +162,9 @@ def configure(env: "Environment"):
     # Add method that joins/compiles our Engine files.
     env.AddMethod(create_engine_file, "CreateEngineFile")
 
+    # Add method for getting the final zip path
+    env.AddMethod(get_template_zip_path, "GetTemplateZipPath")
+
     # Add method for creating the final zip file
     env.AddMethod(create_template_zip, "CreateTemplateZip")
 
@@ -209,13 +213,17 @@ def configure(env: "Environment"):
     stack_size_opt = "STACK_SIZE" if cc_semver >= (3, 1, 25) else "TOTAL_STACK"
     env.Append(LINKFLAGS=["-s", "%s=%sKB" % (stack_size_opt, env["stack_size"])])
 
-    # Thread support (via SharedArrayBuffer).
-    env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"])
-    env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"])
-    env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"])
-    env.Append(LINKFLAGS=["-s", "DEFAULT_PTHREAD_STACK_SIZE=%sKB" % env["default_pthread_stack_size"]])
-    env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"])
-    env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"])
+    if env["threads"]:
+        # Thread support (via SharedArrayBuffer).
+        env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"])
+        env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"])
+        env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"])
+        env.Append(LINKFLAGS=["-s", "DEFAULT_PTHREAD_STACK_SIZE=%sKB" % env["default_pthread_stack_size"]])
+        env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"])
+        env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"])
+    elif env["proxy_to_pthread"]:
+        print('"threads=no" support requires "proxy_to_pthread=no", disabling proxy to pthread.')
+        env["proxy_to_pthread"] = False
 
     if env["lto"] != "none":
         # Workaround https://github.com/emscripten-core/emscripten/issues/19781.
@@ -224,7 +232,7 @@ def configure(env: "Environment"):
 
     if env["dlink_enabled"]:
         if env["proxy_to_pthread"]:
-            print("GDExtension support requires proxy_to_pthread=no, disabling")
+            print("GDExtension support requires proxy_to_pthread=no, disabling proxy to pthread.")
             env["proxy_to_pthread"] = False
 
         if cc_semver < (3, 1, 14):

+ 4 - 0
platform/web/doc_classes/EditorExportPlatformWeb.xml

@@ -47,6 +47,10 @@
 		</member>
 		<member name="variant/extensions_support" type="bool" setter="" getter="">
 		</member>
+		<member name="variant/thread_support" type="bool" setter="" getter="">
+			If enabled, the exported game will support threads. It requires [url=https://web.dev/articles/coop-coep]a "cross-origin isolated" website[/url], which can be difficult to setup and brings some limitations (e.g. not being able to communicate with third-party websites).
+			If disabled, the exported game will not support threads. As a result, it is more prone to performance and audio issues, but will only require to be run on a HTTPS website.
+		</member>
 		<member name="vram_texture_compression/for_desktop" type="bool" setter="" getter="">
 		</member>
 		<member name="vram_texture_compression/for_mobile" type="bool" setter="" getter="">

+ 28 - 13
platform/web/emscripten_helpers.py

@@ -4,7 +4,12 @@ from SCons.Util import WhereIs
 
 
 def run_closure_compiler(target, source, env, for_signature):
-    closure_bin = os.path.join(os.path.dirname(WhereIs("emcc")), "node_modules", ".bin", "google-closure-compiler")
+    closure_bin = os.path.join(
+        os.path.dirname(WhereIs("emcc")),
+        "node_modules",
+        ".bin",
+        "google-closure-compiler",
+    )
     cmd = [WhereIs("node"), closure_bin]
     cmd.extend(["--compilation_level", "ADVANCED_OPTIMIZATIONS"])
     for f in env["JSEXTERNS"]:
@@ -31,27 +36,29 @@ def get_build_version():
     return v
 
 
-def create_engine_file(env, target, source, externs):
+def create_engine_file(env, target, source, externs, threads_enabled):
     if env["use_closure_compiler"]:
         return env.BuildJS(target, source, JSEXTERNS=externs)
-    return env.Textfile(target, [env.File(s) for s in source])
+    subst_dict = {"___GODOT_THREADS_ENABLED": "true" if threads_enabled else "false"}
+    return env.Substfile(target=target, source=[env.File(s) for s in source], SUBST_DICT=subst_dict)
 
 
 def create_template_zip(env, js, wasm, worker, side):
     binary_name = "godot.editor" if env.editor_build else "godot"
-    zip_dir = env.Dir("#bin/.web_zip")
+    zip_dir = env.Dir(env.GetTemplateZipPath())
     in_files = [
         js,
         wasm,
-        worker,
         "#platform/web/js/libs/audio.worklet.js",
     ]
     out_files = [
         zip_dir.File(binary_name + ".js"),
         zip_dir.File(binary_name + ".wasm"),
-        zip_dir.File(binary_name + ".worker.js"),
         zip_dir.File(binary_name + ".audio.worklet.js"),
     ]
+    if env["threads"]:
+        in_files.append(worker)
+        out_files.append(zip_dir.File(binary_name + ".worker.js"))
     # Dynamic linking (extensions) specific.
     if env["dlink_enabled"]:
         in_files.append(side)  # Side wasm (contains the actual Godot code).
@@ -65,18 +72,20 @@ def create_template_zip(env, js, wasm, worker, side):
             "godot.editor.html",
             "offline.html",
             "godot.editor.js",
-            "godot.editor.worker.js",
             "godot.editor.audio.worklet.js",
             "logo.svg",
             "favicon.png",
         ]
+        if env["threads"]:
+            cache.append("godot.editor.worker.js")
         opt_cache = ["godot.editor.wasm"]
         subst_dict = {
-            "@GODOT_VERSION@": get_build_version(),
-            "@GODOT_NAME@": "GodotEngine",
-            "@GODOT_CACHE@": json.dumps(cache),
-            "@GODOT_OPT_CACHE@": json.dumps(opt_cache),
-            "@GODOT_OFFLINE_PAGE@": "offline.html",
+            "___GODOT_VERSION___": get_build_version(),
+            "___GODOT_NAME___": "GodotEngine",
+            "___GODOT_CACHE___": json.dumps(cache),
+            "___GODOT_OPT_CACHE___": json.dumps(opt_cache),
+            "___GODOT_OFFLINE_PAGE___": "offline.html",
+            "___GODOT_THREADS_ENABLED___": "true" if env["threads"] else "false",
         }
         html = env.Substfile(target="#bin/godot${PROGSUFFIX}.html", source=html, SUBST_DICT=subst_dict)
         in_files.append(html)
@@ -88,7 +97,9 @@ def create_template_zip(env, js, wasm, worker, side):
         out_files.append(zip_dir.File("favicon.png"))
         # PWA
         service_worker = env.Substfile(
-            target="#bin/godot${PROGSUFFIX}.service.worker.js", source=service_worker, SUBST_DICT=subst_dict
+            target="#bin/godot${PROGSUFFIX}.service.worker.js",
+            source=service_worker,
+            SUBST_DICT=subst_dict,
         )
         in_files.append(service_worker)
         out_files.append(zip_dir.File("service.worker.js"))
@@ -115,6 +126,10 @@ def create_template_zip(env, js, wasm, worker, side):
     )
 
 
+def get_template_zip_path(env):
+    return "#bin/.web_zip"
+
+
 def add_js_libraries(env, libraries):
     env.Append(JS_LIBS=env.File(libraries))
 

+ 18 - 8
platform/web/export/export_plugin.cpp

@@ -169,6 +169,13 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito
 	replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name");
 	replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
 	replaces["$GODOT_CONFIG"] = str_config;
+
+	if (p_preset->get("variant/thread_support")) {
+		replaces["$GODOT_THREADS_ENABLED"] = "true";
+	} else {
+		replaces["$GODOT_THREADS_ENABLED"] = "false";
+	}
+
 	_replace_strings(replaces, p_html);
 }
 
@@ -216,9 +223,9 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
 	const String name = p_path.get_file().get_basename();
 	bool extensions = (bool)p_preset->get("variant/extensions_support");
 	HashMap<String, String> replaces;
-	replaces["@GODOT_VERSION@"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());
-	replaces["@GODOT_NAME@"] = proj_name.substr(0, 16);
-	replaces["@GODOT_OFFLINE_PAGE@"] = name + ".offline.html";
+	replaces["___GODOT_VERSION___"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());
+	replaces["___GODOT_NAME___"] = proj_name.substr(0, 16);
+	replaces["___GODOT_OFFLINE_PAGE___"] = name + ".offline.html";
 
 	// Files cached during worker install.
 	Array cache_files;
@@ -231,7 +238,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
 	}
 	cache_files.push_back(name + ".worker.js");
 	cache_files.push_back(name + ".audio.worklet.js");
-	replaces["@GODOT_CACHE@"] = Variant(cache_files).to_json_string();
+	replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string();
 
 	// Heavy files that are cached on demand.
 	Array opt_cache_files;
@@ -243,7 +250,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
 			opt_cache_files.push_back(p_shared_objects[i].path.get_file());
 		}
 	}
-	replaces["@GODOT_OPT_CACHE@"] = Variant(opt_cache_files).to_json_string();
+	replaces["___GODOT_OPT_CACHE___"] = Variant(opt_cache_files).to_json_string();
 
 	const String sw_path = dir.path_join(name + ".service.worker.js");
 	Vector<uint8_t> sw;
@@ -335,6 +342,7 @@ void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options)
 	r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
 
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/extensions_support"), false)); // Export type.
+	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/thread_support"), true)); // Thread support (i.e. run with or without COEP/COOP headers).
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC
 	r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer
 
@@ -377,10 +385,11 @@ bool EditorExportPlatformWeb::has_valid_export_configuration(const Ref<EditorExp
 	String err;
 	bool valid = false;
 	bool extensions = (bool)p_preset->get("variant/extensions_support");
+	bool thread_support = (bool)p_preset->get("variant/thread_support");
 
 	// Look for export templates (first official, and if defined custom templates).
-	bool dvalid = exists_export_template(_get_template_name(extensions, true), &err);
-	bool rvalid = exists_export_template(_get_template_name(extensions, false), &err);
+	bool dvalid = exists_export_template(_get_template_name(extensions, thread_support, true), &err);
+	bool rvalid = exists_export_template(_get_template_name(extensions, thread_support, false), &err);
 
 	if (p_preset->get("custom_template/debug") != "") {
 		dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
@@ -454,7 +463,8 @@ Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_p
 	template_path = template_path.strip_edges();
 	if (template_path.is_empty()) {
 		bool extensions = (bool)p_preset->get("variant/extensions_support");
-		template_path = find_export_template(_get_template_name(extensions, p_debug));
+		bool thread_support = (bool)p_preset->get("variant/thread_support");
+		template_path = find_export_template(_get_template_name(extensions, thread_support, p_debug));
 	}
 
 	if (!template_path.is_empty() && !FileAccess::exists(template_path)) {

+ 4 - 1
platform/web/export/export_plugin.h

@@ -56,11 +56,14 @@ class EditorExportPlatformWeb : public EditorExportPlatform {
 	Mutex server_lock;
 	Thread server_thread;
 
-	String _get_template_name(bool p_extension, bool p_debug) const {
+	String _get_template_name(bool p_extension, bool p_thread_support, bool p_debug) const {
 		String name = "web";
 		if (p_extension) {
 			name += "_dlink";
 		}
+		if (!p_thread_support) {
+			name += "_nothreads";
+		}
 		if (p_debug) {
 			name += "_debug.zip";
 		} else {

+ 16 - 6
platform/web/js/engine/features.js

@@ -72,8 +72,14 @@ const Features = { // eslint-disable-line no-unused-vars
 	 *
 	 * @returns {Array<string>} A list of human-readable missing features.
 	 * @function Engine.getMissingFeatures
+	 * @typedef {{ threads: boolean }} SupportedFeatures
+	 * @param {SupportedFeatures} supportedFeatures
 	 */
-	getMissingFeatures: function () {
+	getMissingFeatures: function (supportedFeatures = {}) {
+		const {
+			threads: supportsThreads = true,
+		} = supportedFeatures;
+
 		const missing = [];
 		if (!Features.isWebGLAvailable(2)) {
 			missing.push('WebGL2 - Check web browser configuration and hardware support');
@@ -84,12 +90,16 @@ const Features = { // eslint-disable-line no-unused-vars
 		if (!Features.isSecureContext()) {
 			missing.push('Secure Context - Check web server configuration (use HTTPS)');
 		}
-		if (!Features.isCrossOriginIsolated()) {
-			missing.push('Cross Origin Isolation - Check web server configuration (send correct headers)');
-		}
-		if (!Features.isSharedArrayBufferAvailable()) {
-			missing.push('SharedArrayBuffer - Check web server configuration (send correct headers)');
+
+		if (supportsThreads) {
+			if (!Features.isCrossOriginIsolated()) {
+				missing.push('Cross-Origin Isolation - Check that the web server configuration sends the correct headers.');
+			}
+			if (!Features.isSharedArrayBufferAvailable()) {
+				missing.push('SharedArrayBuffer - Check that the web server configuration sends the correct headers.');
+			}
 		}
+
 		// Audio is normally optional since we have a dummy fallback.
 		return missing;
 	},

+ 2 - 2
platform/web/js/libs/audio.worklet.js

@@ -167,7 +167,7 @@ class GodotProcessor extends AudioWorkletProcessor {
 				GodotProcessor.write_input(this.input_buffer, input);
 				this.input.write(this.input_buffer);
 			} else {
-				this.port.postMessage('Input buffer is full! Skipping input frame.');
+				// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
 			}
 		}
 		const process_output = GodotProcessor.array_has_data(outputs);
@@ -184,7 +184,7 @@ class GodotProcessor extends AudioWorkletProcessor {
 					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
 				}
 			} else {
-				this.port.postMessage('Output buffer has not enough frames! Skipping output frame.');
+				// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
 			}
 		}
 		this.process_notify();

+ 2 - 0
scene/main/http_request.cpp

@@ -503,7 +503,9 @@ void HTTPRequest::_notification(int p_what) {
 
 void HTTPRequest::set_use_threads(bool p_use) {
 	ERR_FAIL_COND(get_http_client_status() != HTTPClient::STATUS_DISCONNECTED);
+#ifdef THREADS_ENABLED
 	use_threads.set_to(p_use);
+#endif
 }
 
 bool HTTPRequest::is_using_threads() const {

+ 8 - 0
servers/register_server_types.cpp

@@ -88,7 +88,11 @@
 ShaderTypes *shader_types = nullptr;
 
 static PhysicsServer3D *_createGodotPhysics3DCallback() {
+#ifdef THREADS_ENABLED
 	bool using_threads = GLOBAL_GET("physics/3d/run_on_separate_thread");
+#else
+	bool using_threads = false;
+#endif
 
 	PhysicsServer3D *physics_server_3d = memnew(GodotPhysicsServer3D(using_threads));
 
@@ -96,7 +100,11 @@ static PhysicsServer3D *_createGodotPhysics3DCallback() {
 }
 
 static PhysicsServer2D *_createGodotPhysics2DCallback() {
+#ifdef THREADS_ENABLED
 	bool using_threads = GLOBAL_GET("physics/2d/run_on_separate_thread");
+#else
+	bool using_threads = false;
+#endif
 
 	PhysicsServer2D *physics_server_2d = memnew(GodotPhysicsServer2D(using_threads));
 

+ 7 - 3
servers/rendering/rendering_server_default.cpp

@@ -395,15 +395,19 @@ RenderingServerDefault::RenderingServerDefault(bool p_create_thread) :
 		command_queue(p_create_thread) {
 	RenderingServer::init();
 
+#ifdef THREADS_ENABLED
 	create_thread = p_create_thread;
-
-	if (!p_create_thread) {
+	if (!create_thread) {
 		server_thread = Thread::get_caller_id();
 	} else {
 		server_thread = 0;
 	}
+#else
+	create_thread = false;
+	server_thread = Thread::get_main_id();
+#endif
+	RSG::threaded = create_thread;
 
-	RSG::threaded = p_create_thread;
 	RSG::canvas = memnew(RendererCanvasCull);
 	RSG::viewport = memnew(RendererViewport);
 	RendererSceneCull *sr = memnew(RendererSceneCull);

+ 1 - 1
servers/rendering/rendering_server_default.h

@@ -78,7 +78,7 @@ class RenderingServerDefault : public RenderingServer {
 	static void _thread_callback(void *_instance);
 	void _thread_loop();
 
-	Thread::ID server_thread;
+	Thread::ID server_thread = 0;
 	SafeFlag exit;
 	Thread thread;
 	SafeFlag draw_thread_up;