Skip to content

Commit 5a8a2a4

Browse files
committed
Add checks and guards related to memory allocation in web exports
This is mainly relevant for Safari which has strict budget for each page/worker. This is the main reason why we cannot support it. Still, this may be fixed by Apple at some point, and with these changes we will be ready. This also adds an early check for memory allocation compatibility, so we can even warn the user ahead of loading.
1 parent 18a163d commit 5a8a2a4

File tree

4 files changed

+191
-29
lines changed

4 files changed

+191
-29
lines changed

dist/web-shell.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<meta property="og:image:height" content="630">
1414

1515
$GODOT_HEAD_INCLUDE
16-
<link href="styles.css" rel="stylesheet">
16+
<link href="styles.css?2" rel="stylesheet">
1717
</head>
1818
<body>
1919
<canvas id="canvas">
@@ -41,7 +41,8 @@ <h3>✅ Your browser is compatible!</h3>
4141
</p>
4242
</div>
4343
<div id="boot-compat-failed">
44-
<h3>⛔ Your browser appears to be incompatible.</h3>
44+
<h3 id="boot-compat-failed-error">⛔ Your browser appears to be incompatible.</h3>
45+
<h3 id="boot-compat-failed-warning">⚠️ Your browser may be incompatible.</h3>
4546
<p>
4647
Sorry about that! 💙<br>
4748
You can still try to launch Bosca, but probably it will not work.<br>
@@ -74,8 +75,8 @@ <h3>⛔ Your browser appears to be incompatible.</h3>
7475
</div>
7576

7677
<script src="$GODOT_URL"></script>
77-
<script src="boscaweb.patches.js?2"></script>
78-
<script src="boscaweb.main.js?2"></script>
78+
<script src="boscaweb.patches.js?3"></script>
79+
<script src="boscaweb.main.js?3"></script>
7980
<script>
8081
const GODOT_CONFIG = $GODOT_CONFIG;
8182
const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;

dist/web_assets/boscaweb.main.js

Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,23 @@ const BOSCAWEB_STATE_PROGRESS = 1;
99
const BOSCAWEB_STATE_READY = 2;
1010
const BOSCAWEB_STATE_FAILED = 3;
1111

12+
// Can technically be configured with Module["INITIAL_MEMORY"], but we don't have
13+
// access to that as early as we need it. It seems to be unset though, so using
14+
// default should be safe.
15+
const BOSCAWEB_INITIAL_MEMORY = 33554432;
16+
const BOSCAWEB_MAXIMUM_MEMORY = 2147483648; // 2 GB
17+
const BOSCAWEB_MEMORY_PAGE_SIZE = 65536;
18+
19+
const BOSCAWEB_COMPATIBILITY_OK = 0;
20+
const BOSCAWEB_COMPATIBILITY_WARNING = 1;
21+
const BOSCAWEB_COMPATIBILITY_FAILURE = 2;
22+
1223
class BoscaWeb {
1324
constructor() {
1425
this.initializing = true;
1526
this.engine = new Engine(GODOT_CONFIG);
27+
this._allocatedMemory = 0;
28+
this.memory = this._allocateWasmMemory();
1629

1730
this._bootOverlay = document.getElementById('boot-overlay');
1831
this._boot_initialState = document.getElementById('boot-menu');
@@ -34,61 +47,165 @@ class BoscaWeb {
3447

3548
this._compat_passedState = document.getElementById('boot-compat-passed');
3649
this._compat_failedState = document.getElementById('boot-compat-failed');
50+
this._compat_failedHeaderError = document.getElementById('boot-compat-failed-error');
51+
this._compat_failedHeaderWarning = document.getElementById('boot-compat-failed-warning');
3752
this._compat_failedList = document.getElementById('boot-compat-list');
3853
this._compat_tryfixButton = document.getElementById('boot-compat-tryfix');
3954
this._compat_tryfixButton.addEventListener('click', () => {
4055
this.tryFixCompatibility();
4156
});
4257

43-
this._compatible = false;
44-
this._compatFixable = (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator);
58+
this._compatLevel = BOSCAWEB_COMPATIBILITY_OK;
59+
this._compatFixable = (this.memory && GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator);
4560

4661
// Hidden by default to show native error messages, e.g. if JavaScript
4762
// is disabled in the browser.
4863
this._bootOverlay.style.visibility = 'visible';
4964
this.setState(BOSCAWEB_STATE_INITIAL);
5065
}
5166

67+
_allocateWasmMemory() {
68+
// We will try to allocate as much as possible, starting with the limit that we actually require.
69+
// In Safari this is likely to fail, so we try less and less. This is not guaranteed to work, but
70+
// at least it gives user a chance.
71+
const reductionSteps = [ 1, 0.75, 0.5, 0.25 ];
72+
let reductionIndex = 0;
73+
74+
let wasmMemory = null;
75+
let sizeMessage = '';
76+
while (wasmMemory == null && reductionIndex < reductionSteps.length) {
77+
const reduction = reductionSteps[reductionIndex];
78+
this._allocatedMemory = BOSCAWEB_MAXIMUM_MEMORY * reduction;
79+
sizeMessage = `${this._humanizeSize(BOSCAWEB_INITIAL_MEMORY)} out of ${this._humanizeSize(this._allocatedMemory)}`;
80+
81+
// This can fail if we hit the browser's limit.
82+
try {
83+
wasmMemory = new WebAssembly.Memory({
84+
initial: BOSCAWEB_INITIAL_MEMORY / BOSCAWEB_MEMORY_PAGE_SIZE,
85+
maximum: reduction * BOSCAWEB_MAXIMUM_MEMORY / BOSCAWEB_MEMORY_PAGE_SIZE,
86+
shared: true
87+
});
88+
} catch (err) {
89+
console.error(err);
90+
wasmMemory = null;
91+
}
92+
93+
reductionIndex += 1;
94+
}
95+
96+
if (wasmMemory == null) {
97+
console.error(`Failed to allocate WebAssembly memory (${sizeMessage}); check the limits.`);
98+
return null;
99+
}
100+
if (!(wasmMemory.buffer instanceof SharedArrayBuffer)) {
101+
console.error(`Trying to allocate WebAssembly memory (${sizeMessage}), but returned buffer is not SharedArrayBuffer; this indicates that threading is probably not supported.`);
102+
return null;
103+
}
104+
105+
console.log(`Successfully allocated WebAssembly memory (${sizeMessage}).`);
106+
return wasmMemory;
107+
}
108+
109+
_checkMissingFeatures() {
110+
const missingFeatures = Engine.getMissingFeatures({
111+
threads: GODOT_THREADS_ENABLED,
112+
});
113+
114+
return missingFeatures.map((item) => {
115+
const itemParts = item.split(' - ');
116+
return {
117+
'name': itemParts[0],
118+
'description': itemParts[1] || '',
119+
}
120+
});
121+
}
122+
52123
checkCompatibility() {
53124
this._bootButton.classList.remove('boot-init-suppressed');
54125
this._compat_passedState.style.display = 'none';
55126
this._compat_failedState.style.display = 'none';
127+
this._compat_failedHeaderError.style.display = 'none';
128+
this._compat_failedHeaderWarning.style.display = 'none';
56129
this._compat_tryfixButton.style.display = 'none';
57130
this._compat_failedList.style.display = 'none';
58131
this._setErrorText(this._compat_failedList, '');
59132

60-
const missingFeatures = Engine.getMissingFeatures({
61-
threads: GODOT_THREADS_ENABLED,
62-
});
133+
this._compatLevel = BOSCAWEB_COMPATIBILITY_OK;
63134

64-
if (missingFeatures.length > 0) {
65-
this._compatible = false;
66-
this._bootButton.classList.add('boot-init-suppressed');
67-
this._compat_failedState.style.display = 'flex';
68-
this._compat_failedList.style.display = 'block';
69-
this._compat_tryfixButton.style.display = (this._compatFixable ? 'inline-block' : 'none');
135+
// Check memory allocation.
136+
if (this.memory == null) {
137+
this._lowerCompatibilityLevel(BOSCAWEB_COMPATIBILITY_FAILURE);
138+
this._addCompatibilityLevelReason('Your browser does not allow enough memory');
70139

71-
const sectionHeader = document.createElement('strong');
72-
sectionHeader.textContent = 'Your browser is missing following features: ';
73-
this._compat_failedList.appendChild(sectionHeader);
140+
const reasonDescription = document.createElement('span');
141+
reasonDescription.textContent = `Bosca requested maximum limit of ${this._humanizeSize(BOSCAWEB_MAXIMUM_MEMORY)}, but was refused.`;
142+
this._compat_failedList.appendChild(reasonDescription);
143+
}
144+
else if (this._allocatedMemory < BOSCAWEB_MAXIMUM_MEMORY) {
145+
this._lowerCompatibilityLevel(BOSCAWEB_COMPATIBILITY_WARNING);
146+
this._addCompatibilityLevelReason('Your browser does not allow enough memory');
147+
148+
const reasonDescription = document.createElement('span');
149+
reasonDescription.textContent = `Bosca requested maximum limit of ${this._humanizeSize(BOSCAWEB_MAXIMUM_MEMORY)}, but was only allowed ${this._humanizeSize(this._allocatedMemory)}.`;
150+
this._compat_failedList.appendChild(reasonDescription);
151+
}
152+
153+
// Check for missing browser feature.
154+
const missingFeatures = this._checkMissingFeatures();
155+
if (missingFeatures.length > 0) {
156+
this._lowerCompatibilityLevel(BOSCAWEB_COMPATIBILITY_FAILURE);
157+
this._addCompatibilityLevelReason('Your browser is missing following features');
74158

75-
const sectionList = document.createElement('span');
76-
this._compat_failedList.appendChild(sectionList);
159+
const reasonDescription = document.createElement('span');
160+
this._compat_failedList.appendChild(reasonDescription);
77161
missingFeatures.forEach((item, index) => {
78-
const itemParts = item.split(' - ');
79-
80162
const annotatedElement = document.createElement('abbr');
81-
annotatedElement.textContent = itemParts[0];
82-
annotatedElement.title = itemParts[1];
83-
sectionList.appendChild(annotatedElement);
163+
annotatedElement.textContent = item.name;
164+
annotatedElement.title = item.description;
165+
reasonDescription.appendChild(annotatedElement);
84166

85167
if (index < missingFeatures.length - 1) {
86-
sectionList.appendChild(document.createTextNode(", "));
168+
reasonDescription.appendChild(document.createTextNode(", "));
87169
}
88170
});
89-
} else {
90-
this._compatible = true;
91-
this._compat_passedState.style.display = 'flex';
171+
}
172+
173+
switch (this._compatLevel) {
174+
case BOSCAWEB_COMPATIBILITY_OK:
175+
this._compat_passedState.style.display = 'flex';
176+
break;
177+
178+
case BOSCAWEB_COMPATIBILITY_WARNING:
179+
this._bootButton.classList.add('boot-init-suppressed');
180+
this._compat_failedState.style.display = 'flex';
181+
this._compat_failedHeaderWarning.style.display = 'inline-block';
182+
this._compat_failedList.style.display = 'block';
183+
this._compat_tryfixButton.style.display = (this._compatFixable ? 'inline-block' : 'none');
184+
break;
185+
186+
case BOSCAWEB_COMPATIBILITY_FAILURE:
187+
this._bootButton.classList.add('boot-init-suppressed');
188+
this._compat_failedState.style.display = 'flex';
189+
this._compat_failedHeaderError.style.display = 'inline-block';
190+
this._compat_failedList.style.display = 'block';
191+
this._compat_tryfixButton.style.display = (this._compatFixable ? 'inline-block' : 'none');
192+
break;
193+
}
194+
}
195+
196+
_addCompatibilityLevelReason(message) {
197+
if (this._compat_failedList.hasChildNodes()) {
198+
this._compat_failedList.appendChild(document.createElement('br'));
199+
}
200+
201+
const reasonHeader = document.createElement('strong');
202+
reasonHeader.textContent = `${message}: `;
203+
this._compat_failedList.appendChild(reasonHeader);
204+
}
205+
206+
_lowerCompatibilityLevel(level) {
207+
if (this._compatLevel < level) {
208+
this._compatLevel = level;
92209
}
93210
}
94211

@@ -205,4 +322,20 @@ class BoscaWeb {
205322
this.setState(BOSCAWEB_STATE_FAILED);
206323
this.initializing = false;
207324
}
325+
326+
_humanizeSize(size) {
327+
const labels = [ 'B', 'KB', 'MB', 'GB', 'TB', ];
328+
329+
let label = labels[0];
330+
let value = size;
331+
332+
let index = 0;
333+
while (value >= 1024 && index < labels.length) {
334+
index += 1;
335+
value = value / 1024;
336+
label = labels[index];
337+
}
338+
339+
return `${value.toFixed(2)} ${label}`;
340+
}
208341
}

dist/web_assets/boscaweb.patches.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,27 @@
5151
return forwardedResponse;
5252
}
5353
})(window);
54+
55+
// Monkey-patch the Godot initializer to influence initialization where it cannot be configured.
56+
(function(window){
57+
const _orig_Godot = window.Godot;
58+
59+
window.Godot = function(Module) {
60+
// Use a pre-allocated buffer that uses a safer amount of maximum memory, which
61+
// avoids instant crashes in Safari. Although, there can still be memory issues
62+
// in Safari (both macOS and iOS/iPadOS), with some indication of improvements
63+
// starting with Safari 18.
64+
if (window.bosca.memory != null) {
65+
Module["wasmMemory"] = window.bosca.memory;
66+
}
67+
68+
// The initializer can still throw exceptions, including an out of memory exception.
69+
// Due to nested levels of async and promise handling, this is not captured by
70+
// try-catching Engine.startGame(). But it can be captured here.
71+
try {
72+
return _orig_Godot(Module);
73+
} catch (err) {
74+
window.bosca._fatalError(err);
75+
}
76+
}
77+
})(window);

dist/web_assets/styles.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ ul {
183183
width: 100%;
184184
}
185185

186+
#boot-compat-failed-error, #boot-compat-failed-warning {
187+
display: none;
188+
}
189+
186190
#boot-compat-tryfix {
187191
font-size: 15px;
188192
padding: 6px 18px;

0 commit comments

Comments
 (0)