Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion misc/dist/html/full-size.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down Expand Up @@ -104,6 +124,7 @@
<div id="status">
<img id="status-splash" class="$GODOT_SPLASH_CLASSES" src="$GODOT_SPLASH" alt="">
<progress id="status-progress"></progress>
<div id="status-phase"></div>
<div id="status-notice"></div>
</div>

Expand Down Expand Up @@ -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);
}
Expand Down
37 changes: 36 additions & 1 deletion misc/dist/html/webgpu-full-size.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down Expand Up @@ -104,6 +124,7 @@
<div id="status">
<img id="status-splash" class="$GODOT_SPLASH_CLASSES" src="$GODOT_SPLASH" alt="">
<progress id="status-progress"></progress>
<div id="status-phase"></div>
<div id="status-notice"></div>
</div>

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions platform/web/js/engine/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
Expand Down
55 changes: 51 additions & 4 deletions platform/web/js/engine/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
});
},
Expand Down