Last week I ported Bosca Ceoil Blue for the web platform. For the most part everything just worked, as both Godot (the engine that I use) and GDSiON (the synth library that I use) can run natively on web via WebAssembly. However, an application running in a browser environment needs to consider a couple of things to maintain reasonable user experience and compromise on no feature. In this two-parter I will cover how I went on adapting Bosca for Web, so you can do this too for your project!
In the first part I've talked about saving and loading file on in a web browser. Today I'll be discussing how to make the first-time experience better and improve the presentation before the user even launches the app. Let's make a better landing page!
For each environment where you want to run your project in it must be wrapped in a certain way to conform the environment's rules. Windows executables have a special structure, and so do macOS app folders. On web the wrapper is a web page, an HTML file that handles loading and initialization. And unlike those other platform, the exact nature of that HTML file and scripts embedded into it are up to you to decide.
Godot comes with a ready-made template that makes your project runnable on web. Here it is! It's just a normal HTML file with a few uppercase dollar-prefixed strings dotted here and there. This is what Godot calls an HTML shell, and we're going to make our own.
So, why would you make your own? The shell that Godot provides out of the box is decently serviceable and covers a lot of basics already. In most cases, especially if we're talking about generally supported cases, you should have no problem using it with no modifications. But there are circumstances where it doesn't provide good enough feedback to user if things go wrong or when browsers exhibit edge-case behavior. It also lacks a bit of polish to my eye, and we should never skimp on a good opportunity to personalize the page to better fit our project.
Things that I want to have on the landing page are:
To that end, I don't want the page to load the project immediately. I want the user to be informed before we begin any initialization. Think about my dream landing page as a launcher for the application. And this is what it actually looks like:
Official documentation has a pretty good article about configuring yourself a custom HTML file to serve as a shell for web exports. In short, grab the default shell and put it somewhere in your project. Then go into the export settings and open your web export preset, locate Custom HTML Shell under HTML, and select your file there. This should not be confused with Custom Templates options, which refer to Godot export templates!
Now, whenever you export your project for web, this file is going to be used. Note that it includes CSS and some JavaScript code necessary to initialize the project. We'll talk later about extracting them somewhere else, but for now make sure to keep everything as is.
This is our starting point.
Don't you hate it when the loading indicator doesn't correctly represent the state of loading, hanging close to one edge for what feels like an eternity while skipping significant parts of itself in mere moments? There is some of that in the standard progress tracker that comes with Godot web exports. It gets worse if you use GDExtension, though! The process to load the main assembly is different from the one used to load dependencies, and so it is not covered by the standard mechanism.
This is what standard exports suggest for the progress bar:
engine.startGame({
'onProgress': function (current, total) {
if (current > 0 && total > 0) {
statusProgress.value = current;
statusProgress.max = total;
} else {
statusProgress.removeAttribute('value');
statusProgress.removeAttribute('max');
}
},
})
The indicator is based on the precomputed sizes for index.pck
(your project) and index.wasm
(the engine), which are compared with the size of the loaded data. In my experience, this already isn't reported correctly and the progress bar tends to skip to the end rather quickly. When you enable threading, there is also index.side.wasm
loaded for each worker thread, and then if you have extensions, they are also loaded from separate files. These last two groups are not accounted for, and don't have their sizes precomputed — yet they can be quite substantial and cause a visible delay.
There are several potential ways to resolve this, but first we will need to get the missing file sizes somehow. This is important as for a number of reason the server that you end up using may not be able to provide the size of file in one of the headers. Or it may be reported compressed, if gzip is used. This is likely why the engine does it to begin with, and so it remains the best approach for us as well.
This means we need an editor export plugin! Export plugins allow you to run code when the project is exported, and modify the exporter behavior, add more files, perform some extra tasks, etc. We will be passive observers for the most part and collect the information as the export happens, and then do everything we need at the end.
An export plugin starts with a basic definition, that names it and ensures it is triggered only for supported platforms.
@tool
extends EditorExportPlugin
func _get_name() -> String:
return "BoscaWebExportPlugin"
func _supports_platform(platform: EditorExportPlatform) -> bool:
return platform.get_os_name() == "Web"
Export plugins must be registered with generic editor plugins, so let's get this out of the way now as well.
@tool
extends EditorPlugin
const BoscaWebExportPlugin := preload("res://addons/bosca_exports/BoscaWebExportPlugin.gd")
var _export_plugin_web: EditorExportPlugin = null
func _enter_tree() -> void:
_export_plugin_web = BoscaWebExportPlugin.new()
add_export_plugin(_export_plugin_web)
func _exit_tree() -> void:
if is_instance_valid(_export_plugin_web):
remove_export_plugin(_export_plugin_web)
Note that EditorExportPlugin
is reference counted and doesn't need to be explicitly freed.
Now we need to collect some details about the export. While you may be inclined to hardcode some of the values here, it's better to ensure that we're not assuming anything and go from pure data. This means that the result will always be correct for anyone in your team, and on build server. And don't forget, that the Godot editor has a built-in web server that you can use for testing, and it also goes through the export routine.
We need two things: the base path for the generated output, and the names of all extensions. Ideally we'd want to gather exact names of extension files being copied to the output folder, but I couldn't find a way to track that. Alternatively, you could just read sizes of all .wasm
files in the export folder. But I decided to make sure I only gather what I need.
For the base path, the _export_begin()
hook is perfect, which is supplied with the target path for the .html
file. For extensions, we can hook into _export_file()
, which is called for all .gdextension
files, and we can even filter the input based on its reported type. This relies on the fact that may not be necessarily true — the assumption is that the .gdextension
file and the .wasm
file start with the same prefix. This is true in my case (libgdsion.gdextension
and libgdsion.web.template_release.wasm32.wasm
), but your mileage may vary. As mentioned, you can always just read sizes of all .wasm
files in the export folder.
var _target_path: String = ""
var _target_gdextensions: PackedStringArray = PackedStringArray()
func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void:
_target_path = path
_target_gdextensions.clear()
func _export_file(path: String, type: String, features: PackedStringArray) -> void:
if type == "GDExtension":
_target_gdextensions.push_back(path.get_file().get_basename())
Finally, we use the target path and a list of know suffixes to find all the key files, as well as a list of GDExtension libraries to find their files as well. We collect them all into a simple dictionary with file names and file sizes in bytes as keys and values respectively.
const TARGET_SUFFIXES := [ "pck", "wasm", "side.wasm" ]
func _export_end() -> void:
# Collect extra data to pass to the HTML shell.
var bosca_file_sizes := {}
var target_dir := _target_path.get_base_dir()
var target_name := _target_path.get_file().get_basename()
# Collect file sizes, including files not accounted by Godot.
for suffix in TARGET_SUFFIXES:
var file_name := "%s.%s" % [ target_name, suffix ]
var file_path := target_dir.path_join(file_name)
var file := FileAccess.open(file_path, FileAccess.READ)
bosca_file_sizes[file_name] = file.get_length()
print("BoscaWebExportPlugin: Calculated size for %s (%d)" % [ file_name, bosca_file_sizes[file_name] ])
if not _target_gdextensions.is_empty():
var fs := DirAccess.open(target_dir)
var files := fs.get_files()
for gdextension in _target_gdextensions:
for file_name in files:
if file_name.begins_with(gdextension):
var file_path := target_dir.path_join(file_name)
var file := FileAccess.open(file_path, FileAccess.READ)
bosca_file_sizes[file_name] = file.get_length()
print("BoscaWebExportPlugin: Calculated size for %s (%d)" % [ file_name, bosca_file_sizes[file_name] ])
To finish this off we need to write this information somewhere where it can be accessed by the frontend code. Godot already embeds some information into the HTML file. Those uppercase dollar-prefixed strings that I mentioned in the premise? The default export routine substitutes them with JSON strings converted from engine dictionaries. And we can do this as well. First, let's add our own line next to the two existing ones:
const GODOT_CONFIG = $GODOT_CONFIG;
const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;
const BOSCA_FILE_SIZES = $BOSCA_FILE_SIZES;
And then we must complete our _export_end()
routine by reading the contents of the HTML file and doing a regular text replacement.
# Prepare the HTML shell for editing.
var html_file := FileAccess.open(_target_path, FileAccess.READ_WRITE)
var html_text := html_file.get_as_text()
# Replace placeholders with data.
html_text = html_text.replace("$BOSCA_FILE_SIZES", JSON.stringify(bosca_file_sizes))
# Finish.
html_file.store_string(html_text)
There are two approaches to tracking progress: listening to signals/data changes, or intercepting calls. I've combed through everything that is exposed or can potentially be used or abused, and unfortunately there isn't a good way to officially declare our desire to be informed about download progress.
One potentially non-intrusive hook is Module["monitorRunDependencies"]
, which is a (hidden) configuration option that allows you to be informed when something is marked as a critical dependency, blocking the project from running. To be clear, this is not a list of modules or files. Instead it's a list of tags, like loadDylibs
, and we are only informed about their number. This can be utilized, but it is unlikely to make the progress bar better or clearer as you add and remove singular digits from a progress that is otherwise tracked in downloaded bytes, of which there are many.
At this point the only options worth exploring involve monkey-patching something. Monkey-patching is an act of wrapping method implementations and adding some extra logic on top — all while taking over the original method identifier. This is possible because JavaScript is very malleable, and even method signatures are merely a suggestion.
A good candidate for monkey-patching is WebAssembly.instantiateStreaming()
, which is exactly what is used for loading files which Godot doesn't account for. For example, here's a PR that does exactly that for the web version of ImHex (amazing tool, btw!). However, I ran into some issues caused by our intervention, and had to look for a different approach.
If we strip everything down to its basic form, then there is one common denominator. Both main files and extra files are downloaded using fetch
. And fetch
is perfect for monkey-patching. Please be careful with this, though! You should not do something as invasive if you plan to host on a platform like itch.io. I'm self-hosting Bosca and know for sure that these changes will not cause any interference. But it's not something you should take for granted when being hosted by a third-party service.
Anyway, here's how you can do it. Keep a reference to the original method and assign directly to window.fetch
. This function takes up to two arguments, and we will pass them on as is to the original implementation. If the requested file is not on the list of known files, we completely skip any custom logic and give control back to the original method. If it is a known file, we use ReadableStream
to track the download progress and report it to the external method (setLoadingProgress()
) that updates the progress bar. When the stream is exhausted, we must pass the results back to the original caller and present them as if they were not meddled with.
(function(window){
const _orig_fetch = window.fetch;
window.fetch = async function(resource, options) {
if (!(resource in BOSCA_FILE_SIZES)) {
return await _orig_fetch(resource, options);
}
const response = await _orig_fetch(resource, options);
const innerStream = new ReadableStream(
{
async start(controller) {
const totalBytes = BOSCA_FILE_SIZES[resource];
let loadedBytes = 0;
const reader = response.body.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
setLoadingProgress(resource, totalBytes, totalBytes);
break;
}
loadedBytes += value.byteLength
setLoadingProgress(resource, loadedBytes, totalBytes);
controller.enqueue(value);
}
reader.releaseLock();
controller.close();
}
},
{
status: response.status,
statusText: response.statusText
}
)
const forwardedResponse = new Response(innerStream);
for (const pair of response.headers.entries()) {
forwardedResponse.headers.set(pair[0], pair[1]);
}
return forwardedResponse;
}
})(window);
And that's it! You will now receive progress information with precision, and you also have reliable details about file sizes throughout. This allows us to create a smooth progress bar that naturally grows as more data is downloaded, which was one of the original goals.
For extra flavor we can also enhance the progress bar with a status message, and switch from "Loading" to "Launching" when we reach 100%. This is actually pretty useful to do because the initialization step can still take a bit of time as Godot is warming up, and there isn't really a way to track progress for that stage.
So far we've had to cram all this code and all our style adjustments into the HTML file, but that's not very practical. Extracting CSS and JavaScript into separate files itself is as straightforward as it can be.
style.css
, put the contents of the <style>
tag inside of it, replace the <style>
tag and its contents with <link href="styles.css" rel="stylesheet">
.script.js
, put all method declarations from the <script>
tag into the file, but keep any line that has a placeholder string (e.g. $GODOT_CONFIG
) and anything that gets immediately executed in place; add a new tag to load the file, <script src="script.js"></script>
.Ideally, the only thing that has to remain in the HTML files is the final initialization call. Here's what I ended up with in Bosca:
<script src="$GODOT_URL"></script>
<script src="boscaweb.patches.js"></script>
<script src="boscaweb.main.js"></script>
<script>
const GODOT_CONFIG = $GODOT_CONFIG;
const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;
const BOSCA_FILE_SIZES = $BOSCA_FILE_SIZES;
window.bosca = new BoscaWeb();
bosca.checkCompatibility();
</script>
If you do that, you'll notice immediately that extra files do not get copied over to the export folder, and everything is, thus, broken. Unfortunately, Godot doesn't provide a setting to copy certain files alongside the HTML shell. Unlike other platforms, which are neatly bundled into a single file, on web you probably want to have a bunch of extra assets, like stylesheets, scripts, images. These are not engine-recognized resources and they should not be packed with the project. We only want to copy them, and so we must do it manually.
Good thing is, we already have an export plugin from the previous step. All we need to do is add one more step to the _export_end()
hook. Personally, I put all my extra files into res://dist/web_assets
, so I can just copy everything from that folder into the target directory. Since this folder can contain images, I also added .gdignore
to it, to prevent files from importing and generating .import
files. So the .gdignore
file has to be ignored by our script, but that's the only caveat.
const WEB_ASSETS_PATH := "res://dist/web_assets"
func _export_end() -> void:
_copy_assets()
_update_html_shell()
func _copy_assets() -> void:
var target_dir := _target_path.get_base_dir()
var fs := DirAccess.open(WEB_ASSETS_PATH)
var asset_names := fs.get_files()
for file_name in asset_names:
if file_name == ".gdignore":
continue
var source_path := WEB_ASSETS_PATH.path_join(file_name)
var target_path := target_dir.path_join(file_name)
fs.copy(source_path, target_path)
print("BoscaWebExportPlugin: Copying asset %s to %s" % [ file_name, target_path ])
func _update_html_shell() -> void:
# Same as before ...
I'll admin, I went a bit lazy here and hardcoded the source path. But you can do this a bit better! One cool thing which you can do with export plugins is define your own export configuration options. This way you will be able to set up the folder from the editor GUI in your export configuration. Check out the official documentation for more details on that.
It still feels a bit like magic that you can take a C++ application and just run it on web natively. That, of course, implies several requirements for the browser. At the time of writing, web exports require support for fetch()
and WebGL2, and your website must establish a secure context. On top of that, when multithreading is enabled you must also configure your web server for cross-origin isolation so that SharedArrayBuffer is available for the engine to use.
These are the things which the engine already checks for, and it even exposes API should you want to do those checks yourself. It's a method that returns a list of strings which are all formatted as <Feature Name> - <Extra information for the developer>
. You can't find the method in the documentation, but it is exactly what the default shell uses:
const missing = Engine.getMissingFeatures({
threads: GODOT_THREADS_ENABLED,
});
My goal for the landing page was to perform those checks before showing any loading and give the user a little explanation about the requirements and supported platforms. I wanted it to be clear when things are expected to work or not expected to work, and give the user a bit of guidance if possible. And, ultimately, I designed this page with the consideration that I am going to self-host it, so it's going to be the entry point for the app. It's not going to be embedded on itch.io where I can put extra instructions or warnings.
The code is not really important here, because most of it is specific to the layout that I've designed. The only critical part is the engine method mentioned above. So, for an inspiration, here are some of the iterations for the design:
Most of the required features will be available in any given browser at this point, but threading is a bit more particular. You see, Godot uses a multithreaded model on web because the audio server needs to run in a separate thread, otherwise the sound gets corrupted. And since it has to be threaded, the engine fully embraces it. So that part is not actually optional, and we need both a secure context and cross-origin isolation. You shouldn't have issues establishing a secure context these days, but cross-origin isolation requires a particular server configuration to work.
Your server must send Cross-Origin-Opener-Policy: same-origin
and Cross-Origin-Embedder-Policy
with either require-corp
or credentialless
. require-corp
is a more strict option which means any third-party resource your page wants to access must be clearly labeled as such. Unfortunately, not all services would set up their headers correctly to support this behavior. In which case credentialless
can be used, which allows third-party resources to be accessed, but send no information about the user alongside web requests. And to be clear, by third-party resources I mean anything requested from a different domain, like an image or, perhaps, an ad banner.
Apple's favorite child, Safari, throws a wrench into our plans here, as it doesn't consider the credentialless
mode as providing cross-origin isolation. So if this is what you use, and you open your web export in Safari, it will simply fail to launch claiming that not all features are available. This is the case with itch.io, and likely many other hosting platforms. At this point I should mention that Adam Scott created a set of workarounds for Godot 4.3, making it possible to run in a single-threaded mode, removing requirements for shared buffers and cross-origin isolation. This may work for you, but unfortunately this is something I cannot use with Bosca Ceoil Blue specifically.
The new workarounds rely on a different approach to audio handling which involves samples. And as a tool that synthesizes audio on the fly, Bosca cannot benefit from ahead-of-time sampling. It needs to be able to stream the data, and so it needs the regular audio server, and so it needs threading on web. Luckily, I self-host Bosca and can configure the server to satisfy Apple's demands.
Unluckily, this is not the only limitation that Apple enforces which nobody else does.
Modern browsers are notorious for being memory hungry, and web workers that run our WebAssembly code are not different. Godot uses Emscripten to generate WebAssembly-compatible code, and it's Emscripten who handles memory management on the browser side. The way it works, our JavaScript runtime asks the browser for a chunk of memory, giving it the initial size and the maximum size that it plans to use. In theory, the maximum size is only a suggestion, and you can grow the memory beyond it if you need and if the browser allows it. But in the multithreaded context the memory must be declared as shared and in that case the maximum size must always be specified. It remains a suggestion, but you have to suggest it explicitly 🫠
While it's configurable during the compilation, the default maximum value is approximately equal to 2 GB. Godot doesn't change that value. This might seem excessive if you aren't trying to run some high-fidelity 3D game in your browser, but from my testing even something like Bosca Ceoil Blue running in Chrome reports memory usage in 800-900 MB while doing practically nothing.
All the while on desktop it doesn't reach 100 MB. So yeah, web applications are not memory efficient at all. In theory, this can be helped by building a custom export template for Godot, stripping it of the stuff that you don't use. In my experience, building with Emscripten is kind of painful and its hard to match Godot's official builds, but it's ultimately doable. No idea how much we could shave this way, though.
So for now, we need to deal with the reality of it. And in that reality Safari offers yet another punch to the gut. Whether you're using macOS, iOS, or iPadOS, all versions of Safari act the same and do not allow us to allocate 2 GB of total memory. Most of the time it allows around 1 GB, but I managed to convince it to fork over 1.5 GB. If you request too much, it throws an exception, and that exception is not caught by either Emscripten or Godot's browser runtime. At this point Safari users opening our landing page on a correctly configured server will see a green checkmark and assume everything is okay, only to face a permanent hang during the loading process.
We need to deal with this, somehow. Monkey-patching could've been the answer here once again (and in part we will return to it in a bit), but in a brilliant turn of events Emscripten actually allows you to pre-allocate the memory in whichever way you see fit and pass the buffer to it as a configuration option. To allocate memory for a WebAssembly worker we must call WebAssembly.Memory()
with a configuration object. Note that memory is requested in the number of pages, not the number of bytes. From my understanding, you don't want pages of any size other than 64 KB.
const BOSCAWEB_INITIAL_MEMORY = 33554432; // 32 MB
const BOSCAWEB_MAXIMUM_MEMORY = 2147483648; // 2 GB
const BOSCAWEB_MEMORY_PAGE_SIZE = 65536;
let wasmMemory = null;
try {
wasmMemory = new WebAssembly.Memory({
initial: BOSCAWEB_INITIAL_MEMORY / BOSCAWEB_MEMORY_PAGE_SIZE,
maximum: BOSCAWEB_MAXIMUM_MEMORY / BOSCAWEB_MEMORY_PAGE_SIZE,
shared: true
});
} catch (err) {
displayFailureNotice(err);
wasmMemory = null;
}
There is some work to be done here still, but the more pressing issue is sharing this now-allocated memory with the engine runtime. While Emscripten makes it configurable, Godot doesn't expect us to want to configure it. So there is no exposed configuration option that we could pass to, say, engine.startGame()
. Here's where monkey-patching will save us again. Behind the scenes, the engine creates a global initialization method called, well, just Godot()
. It's a method that gets supplied with configuration options for Emscripten, and the Engine
type is responsible for generating these options.
We will patch Godot()
, and achieve two things. First, we will supply it with our memory buffer. Second, the runtime can still throw exceptions which are not properly caught. We should hopefully take care of the memory-related one, but you never know what else can break. So to make sure the user doesn't just look at the stuck progress bar in perpetuity, we'll take care of exceptions as well. Note that it's not enough to try-catch the call to engine.startGame()
itself, as the nested mess of async calls and promises seems to break the bubbling of exceptions at some point and they are left uncaught. We'll catch them, though.
(function(window){
const _orig_Godot = window.Godot;
window.Godot = function(Module) {
// Use a pre-allocated buffer that uses a safer amount of maximum memory, which
// avoids instant crashes in Safari. Although, there can still be memory issues
// in Safari (both macOS and iOS/iPadOS), with some indication of improvements
// starting with Safari 18.
if (wasmMemory != null) {
Module["wasmMemory"] = wasmMemory;
}
// The initializer can still throw exceptions, including an out of memory exception.
// Due to nested levels of async and promise handling, this is not captured by
// try-catching Engine.startGame(). But it can be captured here.
try {
return _orig_Godot(Module);
} catch (err) {
displayFailureNotice(err);
}
}
})(window);
You need to make sure wasmMemory
is accessible here. In my case, everything is encapsulated by a class instance and I can access this buffer with window.bosca.memory
, but this will of course depend on your setup. The displayFailureNotice
method is the one that displays a fatal error message in the default HTML shell, you may want to handle the error in a different way.
Now Emscripten will use our allocated buffer, and hopefully will never crash from an exception again. Since our code still tries to allocate 2 GB of total memory, Safari will continue to experience critical failure. A simple fix would be to set this limit to a way lower value. 1 GB should work every time, but you can go lower (just not lower than the initial size!). Compatible browsers will be able to grow the buffer to the required size regardless of the limit you put.
Safari, however, will fail at some point down the line anyway. At least, for now. Even if we don't ask for 2 GB or even 1 GB straight away, at some point we'll need more memory, and then it's back to the original issue that there is a hard limit in the Apple's browser, and it's not going anywhere. When that happens, the tab is going to crash and reload. In my tests, this already happens on load most of the time, although sometimes you will be able to get to the app's UI. The only feasible way to address this right now is to reduce the size of web exports, if that can even help.
To be honest, using Safari is not the only situation where memory allocation may fail. User's device may simply not have enough memory, or free memory at least. And, in theory, more memory may become available while the app is in use. Heck, Safari may lift its restriction one day! So it'd be nice if we had some redundancy system in place to probe for the memory limit and try our best to run, with a decent warning of course. A warning like this:
You cannot just ask the browser for the maximum allowed amount of memory available, alas. What I noticed in my experiments is that when you request too much memory, you can still try to request less. So the solution I've come up with involves gradually requesting less and less memory, until we succeed or run out of options. This can be done with a simple loop and a predefined list of reduction steps.
We will try 100%, 75%, 50%, and, lastly, 25% of the budget that we actually want. The rest should be pretty self-explanatory, and extra error checks should make things a bit nicer and safer.
let allocatedMemory = 0;
function _allocateWasmMemory() {
const reductionSteps = [ 1, 0.75, 0.5, 0.25 ];
let reductionIndex = 0;
let wasmMemory = null;
let sizeMessage = '';
while (wasmMemory == null && reductionIndex < reductionSteps.length) {
const reduction = reductionSteps[reductionIndex];
allocatedMemory = BOSCAWEB_MAXIMUM_MEMORY * reduction;
sizeMessage = `${_humanizeSize(BOSCAWEB_INITIAL_MEMORY)} out of ${_humanizeSize(allocatedMemory)}`;
try {
wasmMemory = new WebAssembly.Memory({
initial: BOSCAWEB_INITIAL_MEMORY / BOSCAWEB_MEMORY_PAGE_SIZE,
maximum: reduction * BOSCAWEB_MAXIMUM_MEMORY / BOSCAWEB_MEMORY_PAGE_SIZE,
shared: true
});
} catch (err) {
console.error(err);
wasmMemory = null;
}
reductionIndex += 1;
}
if (wasmMemory == null) {
console.error(`Failed to allocate WebAssembly memory (${sizeMessage}); check the limits.`);
return null;
}
if (!(wasmMemory.buffer instanceof SharedArrayBuffer)) {
console.error(`Trying to allocate WebAssembly memory (${sizeMessage}), but returned buffer is not SharedArrayBuffer; this indicates that threading is probably not supported.`);
return null;
}
console.log(`Successfully allocated WebAssembly memory (${sizeMessage}).`);
return wasmMemory;
}
The _humanizeSize()
method is just this:
function _humanizeSize(size) {
const labels = [ 'B', 'KB', 'MB', 'GB', 'TB', ];
let label = labels[0];
let value = size;
let index = 0;
while (value >= 1024 && index < labels.length) {
index += 1;
value = value / 1024;
label = labels[index];
}
return `${value.toFixed(2)} ${label}`;
}
Now the _allocateWasmMemory()
method can be used to try and allocate as much memory as we can, safely handling all exceptions. When it's done, we can check the result to see if it was successful and check the allocatedMemory
variable for the actual size of the allocated memory limit. Based on this an appropriate warning can be displayed, while the app is still allowed to be launched with hopes that it might still work. If Safari lifts its restrictions, the app will just work. If the system is able to gradually allocate more memory, the app will just work. If nothing works, at least there is a clear indication for the reason now.
I hope this inspires you to make a better landing page for your game or app made with Godot! You can see the one I made for Bosca Ceoil Blue here:
And all the code is, naturally, open and freely available on GitHub.
If you liked the article, or enjoy the fact that Bosca will soon be available on Web once again, please consider supporting my work via itch.io or Patreon. Making tools for and sharing knowledge with other developers is my full-time occupation, and I'd love to continue doing this for the foreseeable future!
Cheers and happy holidays <3
Loading comments and feedback...