Back to Powder Tool V600Billion
SOURCE / PINNED RELEASE

Made of little things.

Powder Tool V600Billion

Release
142767edcab8…
Author-recorded commit
6d92971effd0…
License
LICENSE
Author’s source reference
nostr://npub1fllw8kw0thjj55wds0uugcnp5kej2nfxd36eruq39d56wwz8r44q5q78wj/wss%3A%2F%2Fgit.napplet.soy%2F/powder-toy

Archive hash verified: ed7d6a8ea7083197…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

source/patches/the-powder-toy-single-thread.patch
The Powder Toy v100.1.400 (d768aeb89acad986bd252d7e904bf44bb374545f) for the web
without threads, for the Powder Toy napplet (BIMbeamFLX/powder-toy-napplet, GPL-3.0 like upstream).

Upstream's Emscripten build uses pthreads, which need SharedArrayBuffer and so a
cross-origin isolated page; a napplet frame (sandbox="allow-scripts", srcdoc) is not.
With -Demscripten_threads=false (default true, upstream's behaviour):

- meson: no USE_PTHREADS, no threads dependency (Meson makes it -pthread), no
  pthread-only link symbol.
- Task::Start does the work at once; Poll reports it as before.
- The tasks that waited for HTTP on a worker (SaveUploadTask, and removing,
  (un)publishing and (un)favouriting selected saves) become RequestTasks: their
  requests start one after another from Start and Poll, nothing blocks. This
  form needs no thread in any build.
- The separate rendering thread stays off; gravity's FFT runs on dispatch.
- The request manager defers its wake-up with emscripten_async_call.

Applied by scripts/build-wasm.sh with git apply.

diff --git a/meson.build b/meson.build
index dad30d7..9dd0ff8 100644
--- a/meson.build
+++ b/meson.build
@@ -225,7 +225,12 @@ project_link_args = []
 project_cpp_args = []
 
 fftw_dep = dependency('fftw3f', static: is_static)
-threads_dep = dependency('threads')
+emscripten_threads = host_platform == 'emscripten' and get_option('emscripten_threads')
+if host_platform == 'emscripten' and not emscripten_threads
+	threads_dep = []
+else
+	threads_dep = dependency('threads')
+endif
 if host_platform == 'emscripten'
 	app_exe_jssafe = app_exe.underscorify()
 	png_dep = []
@@ -241,18 +246,26 @@ if host_platform == 'emscripten'
 		'-s', 'FS_DEBUG',
 		'-s', 'MODULARIZE',
 		'-s', 'EXPORT_NAME=create_' + app_exe_jssafe,
-		'-Wl,-u,_emscripten_run_callback_on_thread',
 		'-lidbfs.js',
 		'--source-map-base=./',
 	]
+	if emscripten_threads
+		project_link_args += [
+			'-Wl,-u,_emscripten_run_callback_on_thread',
+		]
+	endif
 	emcc_args = [
 		'-s', 'USE_SDL=2',
 		'-s', 'USE_BZIP2=1',
 		'-s', 'USE_LIBPNG',
-		'-s', 'USE_PTHREADS',
 		'-s', 'DISABLE_EXCEPTION_CATCHING=0',
 		'-gsource-map',
 	]
+	if emscripten_threads
+		emcc_args += [
+			'-s', 'USE_PTHREADS',
+		]
+	endif
 	project_link_args += emcc_args
 	project_cpp_args += emcc_args
 else
diff --git a/meson_options.txt b/meson_options.txt
index fb453c3..bd76359 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -300,3 +300,9 @@ option(
 	value: false,
 	description: 'Run clang-tidy to lint programming issues'
 )
+option(
+	'emscripten_threads',
+	type: 'boolean',
+	value: true,
+	description: 'Build the Emscripten target with pthreads; without them it runs in pages that are not cross-origin isolated'
+)
diff --git a/src/client/http/requestmanager/Emscripten.cpp b/src/client/http/requestmanager/Emscripten.cpp
index ce06564..7093276 100644
--- a/src/client/http/requestmanager/Emscripten.cpp
+++ b/src/client/http/requestmanager/Emscripten.cpp
@@ -130,7 +130,12 @@ namespace http
 
 	void RequestManagerImpl::Wake()
 	{
+#ifdef __EMSCRIPTEN_PTHREADS__
 		emscripten_async_run_in_main_runtime_thread(EM_FUNC_SIG_V, &HandleWakeThunk);
+#else
+		// already on the main thread; still leave the caller's stack first, like the threaded version
+		emscripten_async_call([](void *) { HandleWakeThunk(); }, nullptr, 0);
+#endif
 	}
 
 	void RequestManagerImpl::RegisterRequestHandle(std::shared_ptr<RequestHandle> requestHandle)
diff --git a/src/gui/game/GameModel.cpp b/src/gui/game/GameModel.cpp
index 90fa9c2..7a958da 100644
--- a/src/gui/game/GameModel.cpp
+++ b/src/gui/game/GameModel.cpp
@@ -112,7 +112,7 @@ GameModel::GameModel(GameView *newView):
 	rendererSettings.gravityFieldEnabled = prefs.Get("Renderer.GravityField", false);
 	rendererSettings.decorationLevel = prefs.Get("Renderer.Decorations", true) ? RendererSettings::decorationEnabled : RendererSettings::decorationDisabled;
 	rendererSettings.gridCheckerboard = prefs.Get("Renderer.GridCheckerboard", false);
-	threadedRendering = prefs.Get("Renderer.SeparateThread", true);
+	SetThreadedRendering(prefs.Get("Renderer.SeparateThread", true));
 
 	//Load config into simulation
 	edgeMode = prefs.Get("Simulation.EdgeMode", NUM_EDGEMODES, EDGE_VOID);
@@ -338,6 +338,10 @@ void GameModel::SetTemperatureScale(TempScale temperatureScale)
 
 void GameModel::SetThreadedRendering(bool newThreadedRendering)
 {
+#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
+	// there is no thread to render on
+	newThreadedRendering = false;
+#endif
 	threadedRendering = newThreadedRendering;
 }
 
diff --git a/src/gui/options/OptionsView.cpp b/src/gui/options/OptionsView.cpp
index 380337a..dd0fb9b 100644
--- a/src/gui/options/OptionsView.cpp
+++ b/src/gui/options/OptionsView.cpp
@@ -423,6 +423,9 @@ OptionsView::OptionsView() : ui::Window(ui::Point(-1, -1), ui::Point(320, 340))
 	threadedRendering = addCheckbox(0, "Separate rendering thread", "May increase framerate when fancy effects are in use", [this] {
 		c->SetThreadedRendering(threadedRendering->GetChecked());
 	});
+#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
+	threadedRendering->Enabled = false;
+#endif
 	decoSpace = addDropDown("Colour space used by decoration tools", {
 		{ "sRGB", DECOSPACE_SRGB },
 		{ "Linear", DECOSPACE_LINEAR },
diff --git a/src/gui/save/ServerSaveActivity.cpp b/src/gui/save/ServerSaveActivity.cpp
index 1b2e2c0..fe882ee 100644
--- a/src/gui/save/ServerSaveActivity.cpp
+++ b/src/gui/save/ServerSaveActivity.cpp
@@ -13,32 +13,28 @@
 #include "client/ThumbnailRendererTask.h"
 #include "client/GameSave.h"
 #include "client/http/UploadSaveRequest.h"
-#include "tasks/Task.h"
+#include "tasks/RequestTask.h"
 #include "gui/Style.h"
 
-class SaveUploadTask: public Task
+class SaveUploadTask: public RequestTask
 {
 	SaveInfo &save;
 
-	void before() override
+	std::unique_ptr<http::Request> Next(int index) override
 	{
-
-	}
-
-	void after() override
-	{
-
+		if (index)
+		{
+			return nullptr;
+		}
+		notifyProgress(-1);
+		return std::make_unique<http::UploadSaveRequest>(save);
 	}
 
-	bool doWork() override
+	bool Done(int index, http::Request &request) override
 	{
-		notifyProgress(-1);
-		auto uploadSaveRequest = std::make_unique<http::UploadSaveRequest>(save);
-		uploadSaveRequest->Start();
-		uploadSaveRequest->Wait();
 		try
 		{
-			save.SetID(uploadSaveRequest->Finish());
+			save.SetID(static_cast<http::UploadSaveRequest &>(request).Finish());
 		}
 		catch (const http::RequestError &ex)
 		{
diff --git a/src/gui/search/SearchController.cpp b/src/gui/search/SearchController.cpp
index e70a916..b5f751f 100644
--- a/src/gui/search/SearchController.cpp
+++ b/src/gui/search/SearchController.cpp
@@ -16,7 +16,7 @@
 #include "common/platform/Platform.h"
 #include "graphics/Graphics.h"
 #include "graphics/VideoBuffer.h"
-#include "tasks/Task.h"
+#include "tasks/RequestTask.h"
 #include "tasks/TaskWindow.h"
 
 #include "gui/dialogues/ConfirmPrompt.h"
@@ -266,35 +266,43 @@ void SearchController::RemoveSelected()
 
 void SearchController::removeSelectedC()
 {
-	class RemoveSavesTask : public Task
+	class RemoveSavesTask : public RequestTask
 	{
 		SearchController *c;
 		std::vector<int> saves;
-	public:
-		RemoveSavesTask(std::vector<int> saves_, SearchController *c_) { saves = saves_; c = c_; }
-		bool doWork() override
+
+		std::unique_ptr<http::Request> Next(int i) override
 		{
-			for (size_t i = 0; i < saves.size(); i++)
+			if (i == int(saves.size()))
 			{
-				notifyStatus(String::Build("Deleting save [", saves[i], "] ..."));
-				auto deleteSaveRequest = std::make_unique<http::DeleteSaveRequest>(saves[i]);
-				deleteSaveRequest->Start();
-				deleteSaveRequest->Wait();
-				try
-				{
-					deleteSaveRequest->Finish();
-				}
-				catch (const http::RequestError &ex)
-				{
-					notifyError(String::Build("Failed to delete [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
-					c->Refresh();
-					return false;
-				}
-				notifyProgress((i + 1) * 100 / saves.size());
+				return nullptr;
 			}
-			c->Refresh();
+			notifyStatus(String::Build("Deleting save [", saves[i], "] ..."));
+			return std::make_unique<http::DeleteSaveRequest>(saves[i]);
+		}
+
+		bool Done(int i, http::Request &request) override
+		{
+			try
+			{
+				static_cast<http::DeleteSaveRequest &>(request).Finish();
+			}
+			catch (const http::RequestError &ex)
+			{
+				notifyError(String::Build("Failed to delete [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
+				return false;
+			}
+			notifyProgress((i + 1) * 100 / saves.size());
 			return true;
 		}
+
+		void Stopped(bool success) override
+		{
+			c->Refresh();
+		}
+
+	public:
+		RemoveSavesTask(std::vector<int> saves_, SearchController *c_) { saves = saves_; c = c_; }
 	};
 
 	std::vector<int> selected = searchModel->GetSelected();
@@ -317,65 +325,63 @@ void SearchController::UnpublishSelected(bool publish)
 
 void SearchController::unpublishSelectedC(bool publish)
 {
-	class UnpublishSavesTask : public Task
+	class UnpublishSavesTask : public RequestTask
 	{
 		std::vector<int> saves;
 		SearchController *c;
 		bool publish;
-	public:
-		UnpublishSavesTask(std::vector<int> saves_, SearchController *c_, bool publish_) { saves = saves_; c = c_; publish = publish_; }
-
-		void PublishSave(int saveID)
-		{
-			notifyStatus(String::Build("Publishing save [", saveID, "]"));
-			auto publishSaveRequest = std::make_unique<http::PublishSaveRequest>(saveID);
-			publishSaveRequest->Start();
-			publishSaveRequest->Wait();
-			publishSaveRequest->Finish();
-		}
 
-		void UnpublishSave(int saveID)
+		std::unique_ptr<http::Request> Next(int i) override
 		{
-			notifyStatus(String::Build("Unpublishing save [", saveID, "]"));
-			auto unpublishSaveRequest = std::make_unique<http::UnpublishSaveRequest>(saveID);
-			unpublishSaveRequest->Start();
-			unpublishSaveRequest->Wait();
-			unpublishSaveRequest->Finish();
+			if (i == int(saves.size()))
+			{
+				return nullptr;
+			}
+			if (publish)
+			{
+				notifyStatus(String::Build("Publishing save [", saves[i], "]"));
+				return std::make_unique<http::PublishSaveRequest>(saves[i]);
+			}
+			notifyStatus(String::Build("Unpublishing save [", saves[i], "]"));
+			return std::make_unique<http::UnpublishSaveRequest>(saves[i]);
 		}
 
-		bool doWork() override
+		bool Done(int i, http::Request &request) override
 		{
-			for (size_t i = 0; i < saves.size(); i++)
+			try
 			{
-				try
+				if (publish)
 				{
-					if (publish)
-					{
-						PublishSave(saves[i]);
-					}
-					else
-					{
-						UnpublishSave(saves[i]);
-					}
+					static_cast<http::PublishSaveRequest &>(request).Finish();
 				}
-				catch (const http::RequestError &ex)
+				else
 				{
-					if (publish) // uses html page so error message will be spam
-					{
-						notifyError(String::Build("Failed to publish [", saves[i], "], is this save yours?"));
-					}
-					else
-					{
-						notifyError(String::Build("Failed to unpublish [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
-					}
-					c->Refresh();
-					return false;
+					static_cast<http::UnpublishSaveRequest &>(request).Finish();
 				}
-				notifyProgress((i + 1) * 100 / saves.size());
 			}
-			c->Refresh();
+			catch (const http::RequestError &ex)
+			{
+				if (publish) // uses html page so error message will be spam
+				{
+					notifyError(String::Build("Failed to publish [", saves[i], "], is this save yours?"));
+				}
+				else
+				{
+					notifyError(String::Build("Failed to unpublish [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
+				}
+				return false;
+			}
+			notifyProgress((i + 1) * 100 / saves.size());
 			return true;
 		}
+
+		void Stopped(bool success) override
+		{
+			c->Refresh();
+		}
+
+	public:
+		UnpublishSavesTask(std::vector<int> saves_, SearchController *c_, bool publish_) { saves = saves_; c = c_; publish = publish_; }
 	};
 
 	std::vector<int> selected = searchModel->GetSelected();
@@ -384,66 +390,82 @@ void SearchController::unpublishSelectedC(bool publish)
 
 void SearchController::FavouriteSelected()
 {
-	class FavouriteSavesTask : public Task
+	class FavouriteSavesTask : public RequestTask
 	{
 		std::vector<int> saves;
 		SearchController *c;
-	public:
-		FavouriteSavesTask(std::vector<int> saves_, SearchController *c_) { saves = saves_; c = c_; }
-		bool doWork() override
+
+		std::unique_ptr<http::Request> Next(int i) override
 		{
-			for (size_t i = 0; i < saves.size(); i++)
+			if (i == int(saves.size()))
 			{
-				notifyStatus(String::Build("Favouring save [", saves[i], "]"));
-				auto favouriteSaveRequest = std::make_unique<http::FavouriteSaveRequest>(saves[i], true);
-				favouriteSaveRequest->Start();
-				favouriteSaveRequest->Wait();
-				try
-				{
-					favouriteSaveRequest->Finish();
-				}
-				catch (const http::RequestError &ex)
-				{
-					notifyError(String::Build("Failed to favourite [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
-					c->Refresh();
-					return false;
-				}
-				notifyProgress((i + 1) * 100 / saves.size());
+				return nullptr;
 			}
-			c->Refresh();
+			notifyStatus(String::Build("Favouring save [", saves[i], "]"));
+			return std::make_unique<http::FavouriteSaveRequest>(saves[i], true);
+		}
+
+		bool Done(int i, http::Request &request) override
+		{
+			try
+			{
+				static_cast<http::FavouriteSaveRequest &>(request).Finish();
+			}
+			catch (const http::RequestError &ex)
+			{
+				notifyError(String::Build("Failed to favourite [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
+				return false;
+			}
+			notifyProgress((i + 1) * 100 / saves.size());
 			return true;
 		}
+
+		void Stopped(bool success) override
+		{
+			c->Refresh();
+		}
+
+	public:
+		FavouriteSavesTask(std::vector<int> saves_, SearchController *c_) { saves = saves_; c = c_; }
 	};
 
-	class UnfavouriteSavesTask : public Task
+	class UnfavouriteSavesTask : public RequestTask
 	{
 		std::vector<int> saves;
 		SearchController *c;
-	public:
-		UnfavouriteSavesTask(std::vector<int> saves_, SearchController *c_) { saves = saves_; c = c_; }
-		bool doWork() override
+
+		std::unique_ptr<http::Request> Next(int i) override
 		{
-			for (size_t i = 0; i < saves.size(); i++)
+			if (i == int(saves.size()))
 			{
-				notifyStatus(String::Build("Unfavouring save [", saves[i], "]"));
-				auto unfavouriteSaveRequest = std::make_unique<http::FavouriteSaveRequest>(saves[i], false);
-				unfavouriteSaveRequest->Start();
-				unfavouriteSaveRequest->Wait();
-				try
-				{
-					unfavouriteSaveRequest->Finish();
-				}
-				catch (const http::RequestError &ex)
-				{
-					notifyError(String::Build("Failed to unfavourite [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
-					c->Refresh();
-					return false;
-				}
-				notifyProgress((i + 1) * 100 / saves.size());
+				return nullptr;
 			}
-			c->Refresh();
+			notifyStatus(String::Build("Unfavouring save [", saves[i], "]"));
+			return std::make_unique<http::FavouriteSaveRequest>(saves[i], false);
+		}
+
+		bool Done(int i, http::Request &request) override
+		{
+			try
+			{
+				static_cast<http::FavouriteSaveRequest &>(request).Finish();
+			}
+			catch (const http::RequestError &ex)
+			{
+				notifyError(String::Build("Failed to unfavourite [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
+				return false;
+			}
+			notifyProgress((i + 1) * 100 / saves.size());
 			return true;
 		}
+
+		void Stopped(bool success) override
+		{
+			c->Refresh();
+		}
+
+	public:
+		UnfavouriteSavesTask(std::vector<int> saves_, SearchController *c_) { saves = saves_; c = c_; }
 	};
 
 	std::vector<int> selected = searchModel->GetSelected();
diff --git a/src/simulation/gravity/Fft.cpp b/src/simulation/gravity/Fft.cpp
index 027c335..4c3b31d 100644
--- a/src/simulation/gravity/Fft.cpp
+++ b/src/simulation/gravity/Fft.cpp
@@ -74,11 +74,16 @@ GravityImpl::~GravityImpl()
 
 void GravityImpl::Dispatch()
 {
+#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
+	// no worker thread, so compute now; Exchange hands the result over next time as usual
+	Work();
+#else
 	{
 		std::unique_lock lk(stateMx);
 		working = true;
 	}
 	stateCv.notify_one();
+#endif
 }
 
 void GravityImpl::Stop()
@@ -88,7 +93,10 @@ void GravityImpl::Stop()
 		shouldStop = true;
 	}
 	stateCv.notify_one();
-	thr.join();
+	if (thr.joinable())
+	{
+		thr.join();
+	}
 }
 
 void GravityImpl::Wait()
@@ -183,6 +191,9 @@ void GravityImpl::Init()
 	//clear padded gravmap
 	std::fill(massBig.get(), massBig.get() + blocks.X * blocks.Y, 0.f);
 
+#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
+	return;
+#endif
 	thr = std::thread([this]() {
 		while (true)
 		{
diff --git a/src/tasks/RequestTask.cpp b/src/tasks/RequestTask.cpp
new file mode 100644
index 0000000..99cb34d
--- /dev/null
+++ b/src/tasks/RequestTask.cpp
@@ -0,0 +1,58 @@
+#include "RequestTask.h"
+
+void RequestTask::Start()
+{
+	before();
+	running = true;
+	Step();
+}
+
+void RequestTask::Poll()
+{
+	if (running)
+	{
+		Step();
+	}
+	Task::Poll();
+}
+
+void RequestTask::Step()
+{
+	while (running)
+	{
+		if (!request)
+		{
+			request = Next(index);
+			if (!request)
+			{
+				End(true);
+				return;
+			}
+			request->Start();
+		}
+		if (!request->CheckDone())
+		{
+			return;
+		}
+		auto ok = Done(index, *request);
+		request.reset();
+		index += 1;
+		if (!ok)
+		{
+			End(false);
+		}
+	}
+}
+
+void RequestTask::End(bool newSuccess)
+{
+	running = false;
+	Stopped(newSuccess);
+	std::lock_guard<std::mutex> g(taskMutex);
+	thSuccess = newSuccess;
+	thDone = true;
+}
+
+void RequestTask::Stopped(bool success)
+{
+}
diff --git a/src/tasks/RequestTask.h b/src/tasks/RequestTask.h
new file mode 100644
index 0000000..54e477e
--- /dev/null
+++ b/src/tasks/RequestTask.h
@@ -0,0 +1,28 @@
+#pragma once
+#include "Task.h"
+#include "client/http/Request.h"
+#include <memory>
+
+// A task made of HTTP requests, started one after another from Start and Poll.
+// Nothing blocks on the network, so no worker thread is needed.
+class RequestTask : public Task
+{
+	std::unique_ptr<http::Request> request;
+	int index = 0;
+	bool running = false;
+
+	void Step();
+	void End(bool newSuccess);
+
+protected:
+	// The request with this index, or nullptr once there are no more.
+	virtual std::unique_ptr<http::Request> Next(int index) = 0;
+	// The request with this index is done; return false to stop the task unsuccessfully.
+	virtual bool Done(int index, http::Request &request) = 0;
+	// Called once when the task stops.
+	virtual void Stopped(bool success);
+
+public:
+	void Start() override;
+	void Poll() override;
+};
diff --git a/src/tasks/Task.cpp b/src/tasks/Task.cpp
index 50d5300..2530a54 100644
--- a/src/tasks/Task.cpp
+++ b/src/tasks/Task.cpp
@@ -12,7 +12,12 @@ void Task::AddTaskListener(TaskListener * listener)
 void Task::Start()
 {
 	before();
+#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
+	// no thread to hand the work to, so do it now; Poll reports it as usual
+	doWork_wrapper();
+#else
 	std::thread([this]() { doWork_wrapper(); }).detach();
+#endif
 }
 
 int Task::GetProgress()
diff --git a/src/tasks/meson.build b/src/tasks/meson.build
index 99ba36f..7e9af66 100644
--- a/src/tasks/meson.build
+++ b/src/tasks/meson.build
@@ -1,5 +1,6 @@
 powder_files += files(
 	'AbandonableTask.cpp',
+	'RequestTask.cpp',
 	'Task.cpp',
 	'TaskWindow.cpp',
 )