Optimize and robustify asset prefetching and material loading.

- Parallelize asset prefetching (max 4 workers) with retry logic and on-screen error banner, replacing silent failures and cryptic WASM out-of-bounds crashes with explicit missing-file errors.
- Support binary Uint8Array/ArrayBuffer in C++ asset registry via typed_memory_view, preventing Embind string coercion from corrupting binary shader packages into ASCII text.
- Automatically glob .mat files in CMake instead of maintaining a manual list.
- Add missing reflection materials to prefetch arrays.
- Disable caching for index.html in web server so template/script updates apply on normal page reload without stale cache issues, the file is small so caching had a negligible upside.
- Add null resource and payload validation in ObjectManager::LoadMaterial before constructing Filament materials.

PiperOrigin-RevId: 963364509
Change-Id: I7de79df6a5cede4e54683415ee193f1c5a665286
This commit is contained in:
Matija Kecman
2026-08-12 04:38:02 -07:00
committed by Copybara-Service
parent fb03be0e4f
commit fca913b4f5
6 changed files with 95 additions and 54 deletions
@@ -214,13 +214,15 @@ limitations under the License.
}
return prefix + path;
},
onRuntimeInitialized: () => {
onRuntimeInitialized: async () => {
const assetsToPrefetch = [
"AtkinsonHyperlegibleNext[wght].ttf",
"AtkinsonHyperlegibleMono-Regular.ttf",
"fontawesome-webfont.ttf",
"ibl.ktx",
"pbr.filamat",
"pbr_reflect.filamat",
"pbr_packed_reflect.filamat",
"pbr_transparent.filamat",
"pbr_packed.filamat",
"pbr_packed_transparent.filamat",
@@ -245,28 +247,55 @@ limitations under the License.
"unlit_ui.filamat",
];
const assetPromises = assetsToPrefetch.map(async (filename) => {
for (let attempt = 0; attempt < 3; attempt++) {
try {
if (attempt > 0) {
await new Promise((r) => setTimeout(r, 500 * attempt));
const PARALLEL = 4;
const failedAssets = [];
let nextIndex = 0;
async function prefetchWorker() {
while (nextIndex < assetsToPrefetch.length) {
const filename = assetsToPrefetch[nextIndex++];
let loaded = false;
for (let attempt = 0; attempt < 3; attempt++) {
try {
if (attempt > 0) {
await new Promise((r) => setTimeout(r, 500 * attempt));
}
const response = await fetch("assets/" + filename);
if (!response.ok) continue;
const buffer = await response.arrayBuffer();
Module.registerAsset(filename, new Uint8Array(buffer));
loaded = true;
break;
} catch (error) {
console.warn(`Attempt ${attempt + 1} failed for ${filename}:`, error);
}
const response = await fetch("assets/" + filename);
if (!response.ok) continue;
const buffer = await response.arrayBuffer();
Module.registerAsset(filename, new Uint8Array(buffer));
return;
} catch (error) {
console.warn(`Attempt ${attempt + 1} failed for ${filename}:`, error);
}
if (!loaded) {
console.error(`Error prefetching asset ${filename} after 3 attempts`);
failedAssets.push(filename);
}
}
console.error(`Error prefetching asset ${filename} after 3 attempts`);
});
}
Promise.all(assetPromises).then(() => {
Module.startApp();
reloadModel();
});
const workers = [];
for (let i = 0; i < Math.min(PARALLEL, assetsToPrefetch.length); i++) {
workers.push(prefetchWorker());
}
await Promise.all(workers);
if (failedAssets.length > 0) {
const msg = `Failed to load required assets (${failedAssets.join(", ")}). Please check server runfiles or connection and reload.`;
console.error(msg);
const errDiv = document.createElement("div");
errDiv.style.cssText =
"position:absolute;top:20px;left:20px;right:20px;padding:16px;background:#7a1a1a;color:#fff;font-family:sans-serif;font-size:14px;border-radius:8px;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,0.5);";
errDiv.textContent = msg;
document.body.appendChild(errDiv);
return;
}
Module.startApp();
reloadModel();
},
};
// Set only the CSS size; SDL owns canvas.width/height. Use whole
@@ -715,9 +715,30 @@ class AssetRegistry {
};
// Exposed to JS (see EMSCRIPTEN_BINDINGS): the page calls this once per asset.
void RegisterAsset(std::string filename, std::string contents) {
AssetRegistry::Instance().RegisterAsset(std::move(filename),
std::move(contents));
void RegisterAsset(std::string filename, emscripten::val contents) {
std::string data;
if (contents.typeOf().as<std::string>() == "string") {
data = contents.as<std::string>();
} else if (contents.instanceof(emscripten::val::global("Uint8Array")) ||
contents.hasOwnProperty("length")) {
size_t len = contents["length"].as<size_t>();
data.resize(len);
if (len > 0) {
emscripten::val memory_view =
emscripten::val(emscripten::typed_memory_view(len, data.data()));
memory_view.call<void>("set", contents);
}
} else if (contents.instanceof(emscripten::val::global("ArrayBuffer"))) {
emscripten::val u8 = emscripten::val::global("Uint8Array").new_(contents);
size_t len = u8["length"].as<size_t>();
data.resize(len);
if (len > 0) {
emscripten::val memory_view =
emscripten::val(emscripten::typed_memory_view(len, data.data()));
memory_view.call<void>("set", u8);
}
}
AssetRegistry::Instance().RegisterAsset(std::move(filename), std::move(data));
}
// Registers resource providers so that "filament:" and "font:" asset requests
@@ -479,8 +479,12 @@ def _run_server(
content_type = _CONTENT_TYPES.get(
os.path.splitext(full)[1], "application/octet-stream"
)
cacheable = not rel.endswith("index.html")
return Response(
200, "OK", _http_headers(content_type, len(body), cacheable=True), body
200,
"OK",
_http_headers(content_type, len(body), cacheable=cacheable),
body,
)
async def main_loop() -> None: