MuJoCo Web Viewer: Implement parallel chunked model downloading and in-place model reloading.

This change introduces parallel chunked downloading for large model files (.mjb) directly into WASM linear memory, enables model loading without full page reloads and reduces memory overhead.

Key changes:
- Implement a chunked model endpoint in the Python web server to support range requests.
- Add parallel chunked fetching in the frontend with retry logic and a single-fetch fallback.
- Increase initial WASM memory to 3 GB to accommodate large models and prevent heap fragmentation.
- Support in-place model reloading in the C++ client, including texture cache invalidation when the Filament context is recreated.
- Display a model download progress bar and model parsing/loading banner to the UI.
- Fixes model drag and drop (caused by typo in sessionId, corrected to session_id).

PiperOrigin-RevId: 960249236
Change-Id: Icac89e6a4ca099882aaf9b111744c1b6ab0cc6c1
This commit is contained in:
Matija Kecman
2026-08-06 05:51:01 -07:00
committed by Copybara-Service
parent 7fd2061f5c
commit 84950fa371
11 changed files with 406 additions and 98 deletions
@@ -28,6 +28,7 @@
#include <emscripten/websocket.h>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
@@ -65,6 +66,10 @@ struct SessionView {
uint64_t sim_bytes_per_sec = 0;
bool have_remote_frame = false;
int camera_mode = 0; // [SpectatorCamMode].
bool is_downloading = true;
size_t bytes_downloaded = 0;
size_t total_bytes = 0;
int retry_count = 0;
};
// User intent reported by the role window. Session implements this; the
@@ -115,6 +120,8 @@ class Session : public SessionActions {
virtual bool ReadyForPayload() = 0;
// Applies a parsed payload to the application.
virtual void OnPayload(const StatePayloadView& view) = 0;
// Server swapped models; fetch and load the new one in-place.
virtual void OnModelChanged() = 0;
// Role transition: claim the controller slot.
virtual void ConnectRemoteUi() = 0;
// Role transition: drop the stream when spectating
@@ -139,10 +146,6 @@ class Session : public SessionActions {
// True only while the WebSocket is actually open.
bool Connected() const { return connected_; }
// True once a payload with a new model has scheduled a page reload; all
// traffic is dropped from then on.
bool ReloadPending() const { return reload_pending_; }
// The close code from the server deliberately ending this connection (codes
// 4000-4999, e.g. kWsCloseSessionFull), else 0. Such conditions are transient
// (a slot frees up, the user returns to the tab), so the page shows a notice
@@ -221,11 +224,10 @@ class Session : public SessionActions {
EMSCRIPTEN_WEBSOCKET_T socket_ = 0;
bool connected_ = false;
// CRC32 of the model this page loaded. When the payload's crc changes, the
// Python side has swapped models so reload the page; this refetches
// /model.mjb and reconnects everything.
// CRC32 of the model this page loaded. When the payload's CRC changes, the
// Python side has swapped models so we trigger an in-place reload that
// refetches /model and reinitializes the scene.
std::optional<uint32_t> model_crc32_;
bool reload_pending_ = false;
int server_close_code_ = 0;