diff --git a/misc/dist/html/full-size.html b/misc/dist/html/full-size.html index d7bfbcdb0a5f7..a7fce39545aee 100644 --- a/misc/dist/html/full-size.html +++ b/misc/dist/html/full-size.html @@ -66,7 +66,17 @@ image-rendering: pixelated; } -#status-progress, #status-notice { +#status-progress, #status-phase { + color: #b0b0b0; + font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif; + font-size: 0.85em; + line-height: 1.4em; + margin-top: 0.5em; + text-align: center; + display: none; +} + +#status-notice { display: none; } @@ -76,6 +86,16 @@ margin: 0 auto; } +#status-phase { + /* Positioned like the bar itself: the absolutely-positioned splash paints + above normal-flow content, so an unpositioned phase line renders + underneath it (measured: elementFromPoint at the text hit the splash). */ + position: absolute; + left: 0; + right: 0; + bottom: calc(10% - 2.2em); +} + #status-notice { background-color: #5b3943; border-radius: 0.5rem; @@ -104,6 +124,7 @@
+
@@ -218,7 +239,19 @@ statusProgress.removeAttribute('max'); } }, + // Reported after the download finishes, just before the engine's + // synchronous start-up blocks the main thread. Swap the full + // progress bar for a legible phase message so the loading screen + // does not look frozen while the engine initializes. + 'onStatusChange': function (phase) { + // A quiet line under the (full) progress bar — enough to say the + // silence is expected, without an alarming notice box. + const el = document.getElementById('status-phase'); + el.textContent = phase === 'shader-compile' ? 'Compiling shaders…' : 'Initializing engine…'; + el.style.display = 'block'; + }, }).then(() => { + document.getElementById('status-phase').style.display = 'none'; setStatusMode('hidden'); }, displayFailureNotice); } diff --git a/misc/dist/html/webgpu-full-size.html b/misc/dist/html/webgpu-full-size.html index e257d44d7c5cc..e1c6d008372af 100644 --- a/misc/dist/html/webgpu-full-size.html +++ b/misc/dist/html/webgpu-full-size.html @@ -66,7 +66,17 @@ image-rendering: pixelated; } -#status-progress, #status-notice { +#status-progress, #status-phase { + color: #b0b0b0; + font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif; + font-size: 0.85em; + line-height: 1.4em; + margin-top: 0.5em; + text-align: center; + display: none; +} + +#status-notice { display: none; } @@ -76,6 +86,16 @@ margin: 0 auto; } +#status-phase { + /* Positioned like the bar itself: the absolutely-positioned splash paints + above normal-flow content, so an unpositioned phase line renders + underneath it (measured: elementFromPoint at the text hit the splash). */ + position: absolute; + left: 0; + right: 0; + bottom: calc(10% - 2.2em); +} + #status-notice { background-color: #5b3943; border-radius: 0.5rem; @@ -104,6 +124,7 @@
+
@@ -289,7 +310,10 @@ } else { // No WebGPU device — the engine will fall back to gl_compatibility // if opengl3=yes was also compiled in. Otherwise it will fail at init. + // Correct the config so downstream consumers (boot-phase reporting, + // diagnostics) see the driver that will actually run. console.warn('WebGPU unavailable, engine will attempt fallback.'); + engineConfig['renderingDriver'] = 'opengl3'; } const engine = new Engine(engineConfig); @@ -303,6 +327,17 @@ statusProgress.removeAttribute('max'); } }, + // Reported after the download finishes, just before the engine's + // synchronous start-up blocks the main thread. Swap the full + // progress bar for a legible phase message so the loading screen + // does not look frozen while shaders/pipelines compile. + 'onStatusChange': function (phase) { + // A quiet line under the (full) progress bar — enough to say the + // silence is expected, without an alarming notice box. + const el = document.getElementById('status-phase'); + el.textContent = phase === 'shader-compile' ? 'Compiling shaders…' : 'Initializing engine…'; + el.style.display = 'block'; + }, }).then(() => { setStatusMode('hidden'); }, displayFailureNotice); diff --git a/platform/web/js/engine/config.js b/platform/web/js/engine/config.js index 85ee6fd3bc294..9fbe202c99887 100644 --- a/platform/web/js/engine/config.js +++ b/platform/web/js/engine/config.js @@ -215,6 +215,23 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- * @type {?function(number, number)} */ onProgress: null, + /** + * A callback function for reporting the engine boot phase after the + * download completes. Called with a machine-readable boot-phase token + * (``'shader-compile'`` when a pre-initialized WebGPU device will run + * shader/pipeline compilation, ``'engine-init'`` otherwise) immediately + * before the synchronous engine start-up runs, so the shell can show a + * localized message and keep the loading screen legible while the main + * thread is blocked. + * + * @callback EngineConfig.onStatusChange + * @param {string} phase The boot-phase token. + */ + /** + * @ignore + * @type {?function(string)} + */ + onStatusChange: null, /** * A callback function for handling the standard output stream. This method should usually only be used in debug pages. * @@ -278,6 +295,7 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- this.onPrintError = parse('onPrintError', this.onPrintError); this.onPrint = parse('onPrint', this.onPrint); this.onProgress = parse('onProgress', this.onProgress); + this.onStatusChange = parse('onStatusChange', this.onStatusChange); // Godot config this.canvas = parse('canvas', this.canvas); diff --git a/platform/web/js/engine/engine.js b/platform/web/js/engine/engine.js index 4a3d8b7757b87..253fb73781cbf 100644 --- a/platform/web/js/engine/engine.js +++ b/platform/web/js/engine/engine.js @@ -172,10 +172,57 @@ const Engine = (function () { me.rtenv['copyToFS'](file.path, file.buffer); } preloader.preloadedFiles.length = 0; // Clear memory - me.rtenv['callMain'](me.config.args); - initPromise = null; - me.installServiceWorker(); - resolve(); + // Once the download completes the progress bar sits at 100% + // while `callMain` runs the engine's synchronous start-up + // (module/physics init and, on WebGPU, shader/pipeline + // compilation). That work blocks the main thread with no + // progress reporting, so the page can look frozen. Surface + // a boot-phase message and force it to paint *before* the + // blocking call, so the last thing drawn is legible text + // instead of a stalled bar. + function runMain() { + me.rtenv['callMain'](me.config.args); + initPromise = null; + me.installServiceWorker(); + resolve(); + } + if (typeof me.config.onStatusChange === 'function') { + // Machine-readable phase token; the shell owns the (localized) + // display text. 'shader-compile' only when a WebGPU device was + // actually pre-initialized — with renderingDriver=webgpu but no + // device, the engine falls back to GL and 'engine-init' is the + // honest phase. + const phase = (me.config.renderingDriver === 'webgpu' && me.config.preinitializedWebGPUDevice) + ? 'shader-compile' + : 'engine-init'; + me.config.onStatusChange(phase); + // Yield to the browser so the message actually paints before + // `callMain` blocks the thread. A double rAF is NOT a + // composite guarantee: measured under post-download load + // (CDP screencast), the two rAF callbacks fired 2-8ms apart + // with no frame between them, and the message never reached + // a single compositor frame. Instead, align to a frame + // boundary with one rAF, then give the compositor real time + // to present the committed change before blocking. rAF is + // suspended entirely in background tabs, so keep an absolute + // fallback timer (run-once latch) or a backgrounded page + // would not boot until focused. + let mainStarted = false; + const runMainOnce = function () { + if (!mainStarted) { + mainStarted = true; + runMain(); + } + }; + if (typeof requestAnimationFrame === 'function') { + requestAnimationFrame(function () { + setTimeout(runMainOnce, 100); + }); + } + setTimeout(runMainOnce, 1000); + } else { + runMain(); + } }); }); },