diff --git a/platform/web/SCsub b/platform/web/SCsub index bd001fd6dc49..e5b67b9ab103 100644 --- a/platform/web/SCsub +++ b/platform/web/SCsub @@ -82,13 +82,44 @@ if len(env["EXPORTED_FUNCTIONS"]): if len(env["EXPORTED_RUNTIME_METHODS"]): sys_env.Append(LINKFLAGS=["-sEXPORTED_RUNTIME_METHODS=" + repr(sorted(list(set(env["EXPORTED_RUNTIME_METHODS"]))))]) +# We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly. +sys_env.Append(LIBS=["idbfs.js"]) + +# Actually build the program. +main_js_file = sys_env.File("#bin/godot${PROGSUFFIX}.js") +main_wasm_file = sys_env.File("#bin/godot${PROGSUFFIX}.wasm") +side_wasm_file = None +main_wasm_dwarf_file = None +main_wasm_dwarf_package_file = None +side_wasm_dwarf_file = None +side_wasm_dwarf_package_file = None + build = [] -build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"] +build_targets = [main_js_file, main_wasm_file] + + +def separate_debug_symbols(target_env, wasm_file): + wasm_dwarf_file = target_env.File(f"{str(wasm_file).removesuffix('.wasm')}.dwarf.wasm") + target_env.Append(LINKFLAGS=[f"-gseparate-dwarf={wasm_dwarf_file}"]) + target_env.Append(CCFLAGS=["-gsplit-dwarf"]) + target_env.Append(LINKFLAGS=["-gsplit-dwarf"]) + target_env.Append(LINKFLAGS=[f"-sSEPARATE_DWARF_URL=./{wasm_dwarf_file.name}"]) + wasm_dwarf_package_file = target_env.File(f"{wasm_dwarf_file}.dwp") + target_env.Command( + action=f"{env.Detect('emdwp')} -e {wasm_dwarf_file} -o {wasm_dwarf_package_file}", + target=wasm_dwarf_package_file, + source=wasm_dwarf_file, + ) + return wasm_dwarf_file, wasm_dwarf_package_file + + +if env["debug_symbols"] and env["separate_debug_symbols"]: + main_wasm_dwarf_file, main_wasm_dwarf_package_file = separate_debug_symbols(sys_env, main_wasm_file) + build_targets.append(main_wasm_dwarf_file) + if env["dlink_enabled"]: # Reset libraries. The main runtime will only link emscripten libraries, not godot ones. sys_env["LIBS"] = [] - # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly. - sys_env.Append(LIBS=["idbfs.js"]) # Configure it as a main module (dynamic linking support). sys_env["CCFLAGS"].remove("-sSIDE_MODULE=2") sys_env["LINKFLAGS"].remove("-sSIDE_MODULE=2") @@ -103,11 +134,14 @@ if env["dlink_enabled"]: sys = sys_env.add_program(build_targets, ["web_runtime.cpp"]) # The side library, containing all Godot code. - wasm = env.add_program("#bin/godot.side${PROGSUFFIX}.wasm", web_files) + side_wasm_file = env.File("#bin/godot.side${PROGSUFFIX}.wasm") + side_wasm_build = [side_wasm_file] + if env["debug_symbols"] and env["separate_debug_symbols"]: + side_wasm_dwarf_file, side_wasm_dwarf_package_file = separate_debug_symbols(env, side_wasm_file) + side_wasm_build.append(side_wasm_dwarf_file) + wasm = env.add_program(side_wasm_build, web_files) build = sys + [wasm[0]] else: - # We use IDBFS. Since Emscripten 1.39.1 it needs to be linked explicitly. - sys_env.Append(LIBS=["idbfs.js"]) build = sys_env.add_program(build_targets, web_files + ["web_runtime.cpp"]) sys_env.Depends(build[0], sys_env["JS_LIBS"]) @@ -133,7 +167,12 @@ js_wrapped = env.NoCache( env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js") ) -# 0 - unwrapped js file (use wrapped one instead) -# 1 - wasm file -# 2 - wasm side (when dlink is enabled). -env.CreateTemplateZip(js_wrapped, build[1], build[2] if len(build) > 2 else None) +env.CreateTemplateZip( + js=js_wrapped, + main_wasm=main_wasm_file, + side_wasm=side_wasm_file, + main_wasm_dwarf=main_wasm_dwarf_file, + main_wasm_dwarf_package=main_wasm_dwarf_package_file, + side_wasm_dwarf=side_wasm_dwarf_file, + side_wasm_dwarf_package=side_wasm_dwarf_package_file, +) diff --git a/platform/web/emscripten_helpers.py b/platform/web/emscripten_helpers.py index 6f3c0517079e..12fbb37459e0 100644 --- a/platform/web/emscripten_helpers.py +++ b/platform/web/emscripten_helpers.py @@ -30,25 +30,46 @@ def create_engine_file(env, target, source, externs, threads_enabled): return env.Substfile(target=target, source=[env.File(s) for s in source], SUBST_DICT=subst_dict) -def create_template_zip(env, js, wasm, side): +def create_template_zip( + env, + js, + main_wasm, + side_wasm=None, + main_wasm_dwarf=None, + main_wasm_dwarf_package=None, + side_wasm_dwarf=None, + side_wasm_dwarf_package=None, +): binary_name = "godot.editor" if env.editor_build else "godot" zip_dir = env.Dir(env.GetTemplateZipPath()) - in_files = [ - js, - wasm, - "#platform/web/js/libs/audio.worklet.js", - "#platform/web/js/libs/audio.position.worklet.js", - ] - out_files = [ - zip_dir.File(binary_name + ".js"), - zip_dir.File(binary_name + ".wasm"), - zip_dir.File(binary_name + ".audio.worklet.js"), - zip_dir.File(binary_name + ".audio.position.worklet.js"), - ] + + in_files = [] + out_files = [] + + def add_to_template(in_file, zip_file): + out_file = zip_dir.File(zip_file) + in_files.append(in_file) + out_files.append(out_file) + + add_to_template(js, binary_name + ".js") + add_to_template(main_wasm, binary_name + ".wasm") + add_to_template("#platform/web/js/libs/audio.worklet.js", binary_name + ".audio.worklet.js") + add_to_template("#platform/web/js/libs/audio.position.worklet.js", binary_name + ".audio.position.worklet.js") + # Dynamic linking (extensions) specific. - if env["dlink_enabled"]: - in_files.append(side) # Side wasm (contains the actual Godot code). - out_files.append(zip_dir.File(binary_name + ".side.wasm")) + if side_wasm is not None: + add_to_template(side_wasm, binary_name + ".side.wasm") + + # Those files cannot be renamed, as their relative .wasm file has their name baked in the binary. + # They must also reside besides their original .wasm files. + if main_wasm_dwarf is not None: + add_to_template(main_wasm_dwarf, main_wasm_dwarf.name) + if main_wasm_dwarf_package is not None: + add_to_template(main_wasm_dwarf_package, main_wasm_dwarf_package.name) + if side_wasm_dwarf is not None: + add_to_template(side_wasm_dwarf, side_wasm_dwarf.name) + if side_wasm_dwarf_package is not None: + add_to_template(side_wasm_dwarf_package, side_wasm_dwarf_package.name) service_worker = "#misc/dist/html/service-worker.js" if env.editor_build: @@ -74,33 +95,24 @@ def create_template_zip(env, js, wasm, side): "___GODOT_ENSURE_CROSSORIGIN_ISOLATION_HEADERS___": "true", } html = env.Substfile(target="#bin/godot${PROGSUFFIX}.html", source=html, SUBST_DICT=subst_dict) - in_files.append(html) - out_files.append(zip_dir.File(binary_name + ".html")) + add_to_template(html, binary_name + ".html") # And logo/favicon - in_files.append("#misc/dist/html/logo.svg") - out_files.append(zip_dir.File("logo.svg")) - in_files.append("#icon.png") - out_files.append(zip_dir.File("favicon.png")) + add_to_template("#misc/dist/html/logo.svg", "logo.svg") + add_to_template("#icon.png", "favicon.svg") # PWA service_worker = env.Substfile( target="#bin/godot${PROGSUFFIX}.service.worker.js", source=service_worker, SUBST_DICT=subst_dict, ) - in_files.append(service_worker) - out_files.append(zip_dir.File("service.worker.js")) - in_files.append("#misc/dist/html/manifest.json") - out_files.append(zip_dir.File("manifest.json")) - in_files.append("#misc/dist/html/offline.html") - out_files.append(zip_dir.File("offline.html")) + add_to_template(service_worker, "service.worker.js") + add_to_template("#misc/dist/html/manifest.json", "manifest.json") + add_to_template("#misc/dist/html/offline.html", "offline.html") else: # HTML - in_files.append("#misc/dist/html/full-size.html") - out_files.append(zip_dir.File(binary_name + ".html")) - in_files.append(service_worker) - out_files.append(zip_dir.File(binary_name + ".service.worker.js")) - in_files.append("#misc/dist/html/offline-export.html") - out_files.append(zip_dir.File("godot.offline.html")) + add_to_template("#misc/dist/html/full-size.html", binary_name + ".html") + add_to_template(service_worker, binary_name + ".service.worker.js") + add_to_template("#misc/dist/html/offline-export.html", binary_name + ".offline.html") zip_files = env.NoCache(env.InstallAs(out_files, in_files)) env.NoCache( diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index 102d536f4e12..97bbc7ceaba1 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -86,7 +86,15 @@ Error EditorExportPlatformWeb::_extract_template(const String &p_template, const unzCloseCurrentFile(pkg); //write - String dst = p_dir.path_join(file.replace("godot", p_name)); + String dst; + // We cannot rename some files. + if (likely(!file.begins_with("godot") || (!file.ends_with(".dwarf.wasm") && !file.ends_with(".dwarf.wasm.dwp")))) { + String new_file_name = file.replace("godot", p_name); + dst = p_dir.path_join(file.replace("godot", p_name)); + } else { + dst = p_dir.path_join(file); + } + Ref f = FileAccess::open(dst, FileAccess::WRITE); if (f.is_null()) { add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Could not write file: \"%s\"."), dst)); diff --git a/platform/web/js/engine/engine.js b/platform/web/js/engine/engine.js index 1aeeb62f18de..ee6a8d734111 100644 --- a/platform/web/js/engine/engine.js +++ b/platform/web/js/engine/engine.js @@ -74,7 +74,7 @@ const Engine = (function () { * @param {string=} basePath Base path of the engine to load. * @return {Promise} A ``Promise`` that resolves once the engine is loaded and initialized. */ - init: function (basePath) { + init: async function (basePath) { if (initPromise) { return initPromise; } @@ -86,28 +86,18 @@ const Engine = (function () { Engine.load(basePath, this.config.fileSizes[`${basePath}.wasm`]); } const me = this; - function doInit(promise) { - // Care! Promise chaining is bogus with old emscripten versions. - // This caused a regression with the Mono build (which uses an older emscripten version). - // Make sure to test that when refactoring. - return new Promise(function (resolve, reject) { - promise.then(function (response) { - const cloned = new Response(response.clone().body, { 'headers': [['content-type', 'application/wasm']] }); - Godot(me.config.getModuleConfig(loadPath, cloned)).then(function (module) { - const paths = me.config.persistentPaths; - module['initFS'](paths).then(function (err) { - me.rtenv = module; - if (me.config.unloadAfterInit) { - Engine.unload(); - } - resolve(); - }); - }); - }); - }); + async function doInit(response) { + const module = await Godot(me.config.getModuleConfig(loadPath, response)); + const paths = me.config.persistentPaths; + // TODO: Handle `err` + const err = await module['initFS'](paths); // eslint-disable-line no-unused-vars + me.rtenv = module; // eslint-disable-line require-atomic-updates + if (me.config.unloadAfterInit) { + Engine.unload(); + } } preloader.setProgressFunc(this.config.onProgress); - initPromise = doInit(loadPromise); + initPromise = doInit(await loadPromise); // eslint-disable-line require-atomic-updates return initPromise; }, diff --git a/platform/web/js/engine/preloader.js b/platform/web/js/engine/preloader.js index 564c68d26449..5a9794b970d7 100644 --- a/platform/web/js/engine/preloader.js +++ b/platform/web/js/engine/preloader.js @@ -1,29 +1,28 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars - function getTrackedResponse(response, load_status) { - function onloadprogress(reader, controller) { - return reader.read().then(function (result) { - if (load_status.done) { - return Promise.resolve(); - } - if (result.value) { - controller.enqueue(result.value); - load_status.loaded += result.value.length; - } - if (!result.done) { - return onloadprogress(reader, controller); - } - load_status.done = true; + function trackResponse(response, load_status) { + async function trackReader(reader) { + const result = await reader.read(); + if (load_status.done) { return Promise.resolve(); - }); + } + if (result.value) { + load_status.loaded += result.value.length; + } + if (!result.done) { + return trackReader(reader); + } + load_status.done = true; + return Promise.resolve(); } - const reader = response.body.getReader(); - return new Response(new ReadableStream({ - start: function (controller) { - onloadprogress(reader, controller).then(function () { - controller.close(); - }); - }, - }), { headers: response.headers }); + + // Start the reader in parallel of the main response. + const reader = response.clone().body.getReader(); + trackReader(reader).catch((_err) => { + // TODO: Handle `_err` + }); + + // We need to keep the original response intact as much as possible. + return response; } function loadFetch(file, tracker, fileSize, raw) { @@ -36,11 +35,11 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un if (!response.ok) { return Promise.reject(new Error(`Failed loading file '${file}'`)); } - const tr = getTrackedResponse(response, tracker[file]); + trackResponse(response, tracker[file]); if (raw) { - return Promise.resolve(tr); + return response; } - return tr.arrayBuffer(); + return response.arrayBuffer(); }); }