diff --git a/misc/dist/html/editor.html b/misc/dist/html/editor.html
index 7c44dd45a19..ac00270d50d 100644
--- a/misc/dist/html/editor.html
+++ b/misc/dist/html/editor.html
@@ -259,31 +259,20 @@
>Web editor documentation for usage instructions and limitations.
-
+
- The web server does not support cross-origin isolation,
- which is required for the Godot Web Editor to function.
-
-
- Reasons for cross-origin isolation being disabled:
-
- -
- This page is not served from a secure context (HTTPS or localhost).
-
- -
- This page may not be served with cross-origin isolation headers
- (check with the developer tools' Network tab).
-
+ The following features required by the Godot Web Editor are missing:
+
If you are self-hosting the web editor,
refer to
Exporting for the Web - Threads for more information.
+ >Exporting for the Web for more information.
@@ -394,16 +383,22 @@
});
}
- if (!crossOriginIsolated) {
+ const missing = Engine.getMissingFeatures();
+ if (missing.length) {
// Display error dialog as threading support is required for the editor.
setButtonEnabled('startButton', false);
document.getElementById("welcome-modal-description").style.display = "none";
- document.getElementById("welcome-modal-description-no-cross-origin-isolation").style.display = "block";
+ document.getElementById("welcome-modal-missing-description").style.display = "block";
document.getElementById("welcome-modal-dismiss").style.display = "none";
- document.getElementById("welcome-modal-reason-not-secure").style.display = window.isSecureContext ? "none" : "list-item";
+ const list = document.getElementById("welcome-modal-missing-list");
+ for (let i = 0; i < missing.length; i++) {
+ const node = document.createElement("li");
+ node.innerText = missing[i];
+ list.appendChild(node);
+ }
}
- if (!crossOriginIsolated || localStorage.getItem("welcomeModalDismissed") !== 'true') {
+ if (missing.length || localStorage.getItem("welcomeModalDismissed") !== 'true') {
document.getElementById("welcome-modal").style.display = "block";
document.getElementById("welcome-modal-dismiss").focus();
}
diff --git a/misc/dist/html/full-size.html b/misc/dist/html/full-size.html
index f385b82f1d5..d5b0050cfd8 100644
--- a/misc/dist/html/full-size.html
+++ b/misc/dist/html/full-size.html
@@ -215,8 +215,10 @@ $GODOT_HEAD_INCLUDE
initializing = false;
};
- if (!Engine.isWebGLAvailable()) {
- displayFailureNotice('WebGL not available');
+ const missing = Engine.getMissingFeatures();
+ if (missing.length !== 0) {
+ const missingMsg = 'Warning!\nThe following features required to run Godot projects on the Web are missing:\n';
+ displayFailureNotice(missingMsg + missing.join("\n"));
} else {
setStatusMode('indeterminate');
engine.startGame({
diff --git a/platform/web/.eslintrc.engine.js b/platform/web/.eslintrc.engine.js
index 78df6d41d98..a76bd46b9ef 100644
--- a/platform/web/.eslintrc.engine.js
+++ b/platform/web/.eslintrc.engine.js
@@ -5,6 +5,7 @@ module.exports = {
"globals": {
"InternalConfig": true,
"Godot": true,
+ "Features": true,
"Preloader": true,
},
};
diff --git a/platform/web/SCsub b/platform/web/SCsub
index e8d0181ede0..013b734be25 100644
--- a/platform/web/SCsub
+++ b/platform/web/SCsub
@@ -66,6 +66,7 @@ sys_env.Depends(build[0], sys_env["JS_PRE"])
sys_env.Depends(build[0], sys_env["JS_EXTERNS"])
engine = [
+ "js/engine/features.js",
"js/engine/preloader.js",
"js/engine/config.js",
"js/engine/engine.js",
diff --git a/platform/web/js/engine/engine.js b/platform/web/js/engine/engine.js
index 6f0d51b2be7..9227aa1f052 100644
--- a/platform/web/js/engine/engine.js
+++ b/platform/web/js/engine/engine.js
@@ -60,20 +60,6 @@ const Engine = (function () {
loadPromise = null;
};
- /**
- * Check whether WebGL is available. Optionally, specify a particular version of WebGL to check for.
- *
- * @param {number=} [majorVersion=1] The major WebGL version to check for.
- * @returns {boolean} If the given major version of WebGL is available.
- * @function Engine.isWebGLAvailable
- */
- Engine.isWebGLAvailable = function (majorVersion = 1) {
- try {
- return !!document.createElement('canvas').getContext(['webgl', 'webgl2'][majorVersion - 1]);
- } catch (e) { /* Not available */ }
- return false;
- };
-
/**
* Safe Engine constructor, creates a new prototype for every new instance to avoid prototype pollution.
* @ignore
@@ -265,14 +251,21 @@ const Engine = (function () {
// Also expose static methods as instance methods
Engine.prototype['load'] = Engine.load;
Engine.prototype['unload'] = Engine.unload;
- Engine.prototype['isWebGLAvailable'] = Engine.isWebGLAvailable;
return new Engine(initConfig);
}
// Closure compiler exported static methods.
SafeEngine['load'] = Engine.load;
SafeEngine['unload'] = Engine.unload;
- SafeEngine['isWebGLAvailable'] = Engine.isWebGLAvailable;
+
+ // Feature-detection utilities.
+ SafeEngine['isWebGLAvailable'] = Features.isWebGLAvailable;
+ SafeEngine['isFetchAvailable'] = Features.isFetchAvailable;
+ SafeEngine['isSecureContext'] = Features.isSecureContext;
+ SafeEngine['isCrossOriginIsolated'] = Features.isCrossOriginIsolated;
+ SafeEngine['isSharedArrayBufferAvailable'] = Features.isSharedArrayBufferAvailable;
+ SafeEngine['isAudioWorkletAvailable'] = Features.isAudioWorkletAvailable;
+ SafeEngine['getMissingFeatures'] = Features.getMissingFeatures;
return SafeEngine;
}());
diff --git a/platform/web/js/engine/features.js b/platform/web/js/engine/features.js
new file mode 100644
index 00000000000..f91a4eff81a
--- /dev/null
+++ b/platform/web/js/engine/features.js
@@ -0,0 +1,96 @@
+const Features = { // eslint-disable-line no-unused-vars
+ /**
+ * Check whether WebGL is available. Optionally, specify a particular version of WebGL to check for.
+ *
+ * @param {number=} [majorVersion=1] The major WebGL version to check for.
+ * @returns {boolean} If the given major version of WebGL is available.
+ * @function Engine.isWebGLAvailable
+ */
+ isWebGLAvailable: function (majorVersion = 1) {
+ try {
+ return !!document.createElement('canvas').getContext(['webgl', 'webgl2'][majorVersion - 1]);
+ } catch (e) { /* Not available */ }
+ return false;
+ },
+
+ /**
+ * Check whether the Fetch API available and supports streaming responses.
+ *
+ * @returns {boolean} If the Fetch API is available and supports streaming responses.
+ * @function Engine.isFetchAvailable
+ */
+ isFetchAvailable: function () {
+ return 'fetch' in window && 'Response' in window && 'body' in window.Response.prototype;
+ },
+
+ /**
+ * Check whether the engine is running in a Secure Context.
+ *
+ * @returns {boolean} If the engine is running in a Secure Context.
+ * @function Engine.isSecureContext
+ */
+ isSecureContext: function () {
+ return window['isSecureContext'] === true;
+ },
+
+ /**
+ * Check whether the engine is cross origin isolated.
+ * This value is dependent on Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers sent by the server.
+ *
+ * @returns {boolean} If the engine is running in a Secure Context.
+ * @function Engine.isSecureContext
+ */
+ isCrossOriginIsolated: function () {
+ return window['crossOriginIsolated'] === true;
+ },
+
+ /**
+ * Check whether SharedBufferArray is available.
+ *
+ * Most browsers require the page to be running in a secure context, and the
+ * the server to provide specific CORS headers for SharedArrayBuffer to be available.
+ *
+ * @returns {boolean} If SharedArrayBuffer is available.
+ * @function Engine.isSharedArrayBufferAvailable
+ */
+ isSharedArrayBufferAvailable: function () {
+ return 'SharedArrayBuffer' in window;
+ },
+
+ /**
+ * Check whether the AudioContext supports AudioWorkletNodes.
+ *
+ * @returns {boolean} If AudioWorkletNode is available.
+ * @function Engine.isAudioWorkletAvailable
+ */
+ isAudioWorkletAvailable: function () {
+ return 'AudioContext' in window && 'audioWorklet' in AudioContext.prototype;
+ },
+
+ /**
+ * Return an array of missing required features (as string).
+ *
+ * @returns {Array} A list of human-readable missing features.
+ * @function Engine.getMissingFeatures
+ */
+ getMissingFeatures: function () {
+ const missing = [];
+ if (!Features.isWebGLAvailable(2)) {
+ missing.push('WebGL2');
+ }
+ if (!Features.isFetchAvailable()) {
+ missing.push('Fetch');
+ }
+ if (!Features.isSecureContext()) {
+ missing.push('Secure Context');
+ }
+ if (!Features.isCrossOriginIsolated()) {
+ missing.push('Cross Origin Isolation');
+ }
+ if (!Features.isSharedArrayBufferAvailable()) {
+ missing.push('SharedArrayBuffer');
+ }
+ // Audio is normally optional since we have a dummy fallback.
+ return missing;
+ },
+};
diff --git a/platform/web/package.json b/platform/web/package.json
index a57205415af..0a8d9e43343 100644
--- a/platform/web/package.json
+++ b/platform/web/package.json
@@ -4,7 +4,7 @@
"version": "1.0.0",
"description": "Development and linting setup for Godot's Web platform code",
"scripts": {
- "docs": "jsdoc --template js/jsdoc2rst/ js/engine/engine.js js/engine/config.js --destination ''",
+ "docs": "jsdoc --template js/jsdoc2rst/ js/engine/engine.js js/engine/config.js js/engine/features.js --destination ''",
"lint": "npm run lint:engine && npm run lint:libs && npm run lint:modules && npm run lint:tools",
"lint:engine": "eslint \"js/engine/*.js\" --no-eslintrc -c .eslintrc.engine.js",
"lint:libs": "eslint \"js/libs/*.js\" --no-eslintrc -c .eslintrc.libs.js",