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:
committed by
Copybara-Service
parent
fb03be0e4f
commit
fca913b4f5
@@ -214,13 +214,15 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
return prefix + path;
|
return prefix + path;
|
||||||
},
|
},
|
||||||
onRuntimeInitialized: () => {
|
onRuntimeInitialized: async () => {
|
||||||
const assetsToPrefetch = [
|
const assetsToPrefetch = [
|
||||||
"AtkinsonHyperlegibleNext[wght].ttf",
|
"AtkinsonHyperlegibleNext[wght].ttf",
|
||||||
"AtkinsonHyperlegibleMono-Regular.ttf",
|
"AtkinsonHyperlegibleMono-Regular.ttf",
|
||||||
"fontawesome-webfont.ttf",
|
"fontawesome-webfont.ttf",
|
||||||
"ibl.ktx",
|
"ibl.ktx",
|
||||||
"pbr.filamat",
|
"pbr.filamat",
|
||||||
|
"pbr_reflect.filamat",
|
||||||
|
"pbr_packed_reflect.filamat",
|
||||||
"pbr_transparent.filamat",
|
"pbr_transparent.filamat",
|
||||||
"pbr_packed.filamat",
|
"pbr_packed.filamat",
|
||||||
"pbr_packed_transparent.filamat",
|
"pbr_packed_transparent.filamat",
|
||||||
@@ -245,28 +247,55 @@ limitations under the License.
|
|||||||
"unlit_ui.filamat",
|
"unlit_ui.filamat",
|
||||||
];
|
];
|
||||||
|
|
||||||
const assetPromises = assetsToPrefetch.map(async (filename) => {
|
const PARALLEL = 4;
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
const failedAssets = [];
|
||||||
try {
|
let nextIndex = 0;
|
||||||
if (attempt > 0) {
|
|
||||||
await new Promise((r) => setTimeout(r, 500 * attempt));
|
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;
|
if (!loaded) {
|
||||||
const buffer = await response.arrayBuffer();
|
console.error(`Error prefetching asset ${filename} after 3 attempts`);
|
||||||
Module.registerAsset(filename, new Uint8Array(buffer));
|
failedAssets.push(filename);
|
||||||
return;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Attempt ${attempt + 1} failed for ${filename}:`, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.error(`Error prefetching asset ${filename} after 3 attempts`);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all(assetPromises).then(() => {
|
const workers = [];
|
||||||
Module.startApp();
|
for (let i = 0; i < Math.min(PARALLEL, assetsToPrefetch.length); i++) {
|
||||||
reloadModel();
|
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
|
// 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.
|
// Exposed to JS (see EMSCRIPTEN_BINDINGS): the page calls this once per asset.
|
||||||
void RegisterAsset(std::string filename, std::string contents) {
|
void RegisterAsset(std::string filename, emscripten::val contents) {
|
||||||
AssetRegistry::Instance().RegisterAsset(std::move(filename),
|
std::string data;
|
||||||
std::move(contents));
|
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
|
// Registers resource providers so that "filament:" and "font:" asset requests
|
||||||
|
|||||||
@@ -479,8 +479,12 @@ def _run_server(
|
|||||||
content_type = _CONTENT_TYPES.get(
|
content_type = _CONTENT_TYPES.get(
|
||||||
os.path.splitext(full)[1], "application/octet-stream"
|
os.path.splitext(full)[1], "application/octet-stream"
|
||||||
)
|
)
|
||||||
|
cacheable = not rel.endswith("index.html")
|
||||||
return Response(
|
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:
|
async def main_loop() -> None:
|
||||||
|
|||||||
@@ -138,13 +138,14 @@ var Module = {
|
|||||||
totalDependencies: 0,
|
totalDependencies: 0,
|
||||||
monitorRunDependencies(left) { },
|
monitorRunDependencies(left) { },
|
||||||
onRuntimeInitialized: () => {
|
onRuntimeInitialized: () => {
|
||||||
// Define assets to prefetch, relative to the WASM directory.
|
|
||||||
const assetsToPrefetch = [
|
const assetsToPrefetch = [
|
||||||
"assets/fontawesome-webfont.ttf",
|
"assets/fontawesome-webfont.ttf",
|
||||||
"assets/ibl.ktx",
|
"assets/ibl.ktx",
|
||||||
"assets/AtkinsonHyperlegibleNext[wght].ttf",
|
"assets/AtkinsonHyperlegibleNext[wght].ttf",
|
||||||
"assets/AtkinsonHyperlegibleMono-Regular.ttf",
|
"assets/AtkinsonHyperlegibleMono-Regular.ttf",
|
||||||
"assets/pbr.filamat",
|
"assets/pbr.filamat",
|
||||||
|
"assets/pbr_reflect.filamat",
|
||||||
|
"assets/pbr_packed_reflect.filamat",
|
||||||
"assets/pbr_transparent.filamat",
|
"assets/pbr_transparent.filamat",
|
||||||
"assets/pbr_packed.filamat",
|
"assets/pbr_packed.filamat",
|
||||||
"assets/pbr_packed_transparent.filamat",
|
"assets/pbr_packed_transparent.filamat",
|
||||||
|
|||||||
@@ -92,43 +92,18 @@ add_library(mujoco::filament ALIAS ${MUJOCO_FILAMENT_TARGET_NAME})
|
|||||||
set(ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
|
set(ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
|
||||||
set(OUTPUT_ASSETS_DIR "${CMAKE_CURRENT_BINARY_DIR}/assets")
|
set(OUTPUT_ASSETS_DIR "${CMAKE_CURRENT_BINARY_DIR}/assets")
|
||||||
file(MAKE_DIRECTORY ${OUTPUT_ASSETS_DIR})
|
file(MAKE_DIRECTORY ${OUTPUT_ASSETS_DIR})
|
||||||
|
|
||||||
file(COPY
|
file(COPY
|
||||||
"${ASSETS_DIR}/ibl.ktx"
|
"${ASSETS_DIR}/ibl.ktx"
|
||||||
DESTINATION ${OUTPUT_ASSETS_DIR}
|
DESTINATION ${OUTPUT_ASSETS_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
set(MATC_EXECUTABLE matc)
|
set(MATC_EXECUTABLE matc)
|
||||||
set(MATERIAL_FILES
|
file(GLOB MATERIAL_FILES CONFIGURE_DEPENDS "${ASSETS_DIR}/*.mat")
|
||||||
pbr.mat
|
|
||||||
pbr_transparent.mat
|
|
||||||
pbr_packed.mat
|
|
||||||
pbr_packed_transparent.mat
|
|
||||||
pbr_reflect.mat
|
|
||||||
pbr_packed_reflect.mat
|
|
||||||
phong_2d_fade.mat
|
|
||||||
phong_2d.mat
|
|
||||||
phong_2d_reflect.mat
|
|
||||||
phong_2d_uv_fade.mat
|
|
||||||
phong_2d_uv.mat
|
|
||||||
phong_2d_uv_reflect.mat
|
|
||||||
phong_color_fade.mat
|
|
||||||
phong_color.mat
|
|
||||||
phong_color_reflect.mat
|
|
||||||
phong_cube_fade.mat
|
|
||||||
phong_cube.mat
|
|
||||||
phong_cube_reflect.mat
|
|
||||||
outline_composite.mat
|
|
||||||
outline_flatten.mat
|
|
||||||
outline_jumpflood.mat
|
|
||||||
decor.mat
|
|
||||||
unlit_depth.mat
|
|
||||||
unlit_segmentation.mat
|
|
||||||
unlit_ui.mat
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach(MATERIAL_FILE ${MATERIAL_FILES})
|
foreach(INPUT_FILE ${MATERIAL_FILES})
|
||||||
get_filename_component(MATERIAL_NAME ${MATERIAL_FILE} NAME_WE)
|
get_filename_component(MATERIAL_NAME ${INPUT_FILE} NAME_WE)
|
||||||
set(INPUT_FILE "${ASSETS_DIR}/${MATERIAL_FILE}")
|
get_filename_component(MATERIAL_FILE ${INPUT_FILE} NAME)
|
||||||
set(OUTPUT_FILE "${OUTPUT_ASSETS_DIR}/${MATERIAL_NAME}.filamat")
|
set(OUTPUT_FILE "${OUTPUT_ASSETS_DIR}/${MATERIAL_NAME}.filamat")
|
||||||
|
|
||||||
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
|
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
|
||||||
|
|||||||
@@ -42,8 +42,19 @@ static filament::Material* LoadMaterial(filament::Engine* engine,
|
|||||||
const std::string path = ResolveFilamentAssetPath(std::string(filename));
|
const std::string path = ResolveFilamentAssetPath(std::string(filename));
|
||||||
mjResource* resource =
|
mjResource* resource =
|
||||||
mju_openResource("", path.c_str(), nullptr, nullptr, 0);
|
mju_openResource("", path.c_str(), nullptr, nullptr, 0);
|
||||||
|
if (!resource) {
|
||||||
|
mju_error("Failed to open filament asset '%.*s' at '%s'",
|
||||||
|
static_cast<int>(filename.size()), filename.data(), path.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
void* payload = nullptr;
|
void* payload = nullptr;
|
||||||
int size = mju_readResource(resource, const_cast<const void**>(&payload));
|
int size = mju_readResource(resource, const_cast<const void**>(&payload));
|
||||||
|
if (size <= 0 || !payload) {
|
||||||
|
mju_error("Failed to read filament asset '%.*s' (size=%d)",
|
||||||
|
static_cast<int>(filename.size()), filename.data(), size);
|
||||||
|
mju_closeResource(resource);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
filament::Material::Builder material_builder;
|
filament::Material::Builder material_builder;
|
||||||
material_builder.package(payload, size);
|
material_builder.package(payload, size);
|
||||||
filament::Material* material = material_builder.build(*engine);
|
filament::Material* material = material_builder.build(*engine);
|
||||||
|
|||||||
Reference in New Issue
Block a user