Files
Mujoco_WASM/src/experimental/studio/emscripten.cc
T
Saran Tunyasuvunakool 7c47a89cd0 Add asset file download indicator to the loading screen.
This provides visual feedback to the user while loading large models.

PiperOrigin-RevId: 918708877
Change-Id: I77ea475cce6d9718e6cf8b33cb72b3f524a6ee63
2026-05-20 16:26:40 -07:00

315 lines
10 KiB
C++

// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Main entry point for the Filament-based MuJoCo web app.
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <mujoco/mujoco.h>
#include "experimental/platform/hal/graphics_mode.h"
#include "experimental/studio/app.h"
// Global app instance. Lifetime is controlled by Init/Deinit calls which are
// triggered by Javascript.
mujoco::studio::App* g_app = nullptr;
// Static registry of assets that are loaded in JSON before the main App is
// initialized.
class AssetRegistry {
public:
// Returns the singleton instance of the registry.
static AssetRegistry& Instance() {
static AssetRegistry instance;
return instance;
}
// Registers asset contents with the given filename.
void RegisterAsset(std::string filename, std::string contents) {
std::filesystem::path(filename).filename().string();
assets_[filename] = std::move(contents);
}
const std::string& Get(std::string_view filename) const {
filename = filename.substr(filename.find_first_of(':') + 1);
static std::string empty;
auto it = assets_.find(std::string(filename));
return it != assets_.end() ? it->second : empty;
}
private:
std::unordered_map<std::string, std::string> assets_;
};
// ---------------------------------------------------------------------------
// HTTP/HTTPS resource fetching via the JS fetch API (uses ASYNCIFY to yield).
// ---------------------------------------------------------------------------
// Fetches a URL using the JS fetch API. Returns a malloc'd buffer and its size.
// The caller is responsible for freeing the buffer. Returns 0 on failure.
EM_ASYNC_JS(int, FetchUrl,
(const char* url, char** out_data, std::int32_t* out_size), {
const urlStr = UTF8ToString(url);
const filename = urlStr.split('/').pop() || urlStr;
const dlEl = document.getElementById('loadingDownload');
const dlFileEl = document.getElementById('loadingDownloadFile');
if (dlEl) {
dlEl.style.display = 'block';
}
if (dlFileEl) {
dlFileEl.textContent = filename + '\u2026';
}
try {
const response = await fetch(urlStr);
if (!response.ok) {
console.error('Fetch failed: ' + response.status + ' ' +
urlStr);
return 0;
}
const buffer = await response.arrayBuffer();
const bytes = new Uint8Array(buffer);
const ptr = _malloc(bytes.length);
HEAPU8.set(bytes, ptr);
setValue(out_data, ptr, '*');
setValue(out_size, bytes.length, 'i32');
return 1;
} catch (e) {
console.error('Fetch error:', e);
return 0;
} finally {
if (dlEl) {
dlEl.style.display = 'none';
}
if (dlFileEl) {
dlFileEl.textContent = "";
}
}
});
// Cache for data fetched via HTTP/HTTPS. Stores the downloaded bytes keyed by
// the resource name (URL) so that read() can return a pointer to the data.
class FetchCache {
public:
static FetchCache& Instance() {
static FetchCache instance;
return instance;
}
// Fetches the URL and stores the result. Returns the size (>0) on success.
int Fetch(const char* url) {
char* data = nullptr;
std::int32_t size = 0;
if (!FetchUrl(url, &data, &size)) {
return 0;
}
entries_[url] = Entry{UniquePtrWasm<char[]>(data), size};
return size;
}
// Returns pointer and size for a previously fetched URL.
int Read(const char* url, const void** buffer) {
auto it = entries_.find(url);
if (it == entries_.end()) {
return -1;
}
*buffer = it->second.data.get();
return it->second.size;
}
// Frees the data for a URL.
void Close(const char* url) { entries_.erase(url); }
private:
struct FreeDeleter {
void operator()(void* p) const { std::free(p); }
};
template <typename T>
using UniquePtrWasm = std::unique_ptr<T, FreeDeleter>;
struct Entry {
UniquePtrWasm<char[]> data;
int size;
};
std::unordered_map<std::string, Entry> entries_;
};
// ---------------------------------------------------------------------------
// Javascript-facing function to register an asset.
void RegisterAsset(std::string filename, std::string contents) {
AssetRegistry::Instance().RegisterAsset(std::move(filename),
std::move(contents));
}
// Javascript-facing function to initialize the app.
void Init(const std::string& title, bool dark_theme) {
// Note: dimensions do not matter as window will be resized to fit canvas.
const int width = 100;
const int height = 100;
const std::string ini_path = "";
mjpResourceProvider resource_provider;
mjp_defaultResourceProvider(&resource_provider);
resource_provider.open = [](mjResource* resource) {
AssetRegistry& r = AssetRegistry::Instance();
return static_cast<int>(r.Get(resource->name).size());
};
resource_provider.read = [](mjResource* resource, const void** buffer) {
AssetRegistry& r = AssetRegistry::Instance();
const std::string& contents = r.Get(resource->name);
*buffer = contents.data();
return static_cast<int>(contents.size());
};
resource_provider.close = [](mjResource* resource) {};
resource_provider.prefix = "font";
mjp_registerResourceProvider(&resource_provider);
resource_provider.prefix = "filament";
mjp_registerResourceProvider(&resource_provider);
// Register HTTP/HTTPS resource providers so that models loaded from URLs
// can automatically fetch referenced assets (meshes, textures, etc.) over
// the network.
mjpResourceProvider http_provider;
mjp_defaultResourceProvider(&http_provider);
http_provider.open = [](mjResource* resource) {
return FetchCache::Instance().Fetch(resource->name);
};
http_provider.read = [](mjResource* resource, const void** buffer) {
return FetchCache::Instance().Read(resource->name, buffer);
};
http_provider.close = [](mjResource* resource) {
FetchCache::Instance().Close(resource->name);
};
http_provider.prefix = "http";
mjp_registerResourceProvider(&http_provider);
http_provider.prefix = "https";
mjp_registerResourceProvider(&http_provider);
// Register a "github:" resource provider that resolves
// github:org/repo/branch/path/to/file.xml to
// https://raw.githubusercontent.com/org/repo/branch/path/to/file.xml
mjpResourceProvider github_provider;
mjp_defaultResourceProvider(&github_provider);
github_provider.open = [](mjResource* resource) {
std::string name(resource->name);
// Strip the "github:" prefix and prepend the raw.githubusercontent URL.
std::string url =
"https://raw.githubusercontent.com/" + name.substr(strlen("github:"));
return FetchCache::Instance().Fetch(url.c_str());
};
github_provider.read = [](mjResource* resource, const void** buffer) {
std::string name(resource->name);
std::string url =
"https://raw.githubusercontent.com/" + name.substr(strlen("github:"));
return FetchCache::Instance().Read(url.c_str(), buffer);
};
github_provider.close = [](mjResource* resource) {
std::string name(resource->name);
std::string url =
"https://raw.githubusercontent.com/" + name.substr(strlen("github:"));
FetchCache::Instance().Close(url.c_str());
};
github_provider.prefix = "github";
mjp_registerResourceProvider(&github_provider);
mujoco::studio::App::Config config;
config.width = width;
config.height = height;
config.ini_path = ini_path;
config.gfx_mode = mujoco::platform::GraphicsMode::FilamentWebGl;
config.initial_theme = dark_theme ? mujoco::platform::GuiTheme::kDark
: mujoco::platform::GuiTheme::kLight;
if (!title.empty()) {
config.title = title;
}
g_app = new mujoco::studio::App(std::move(config));
g_app->InitEmptyModel();
}
// Javascript-facing function to load a model from an uploaded file.
void LoadFile(const std::string& filename, const std::string& data) {
if (!g_app) {
return;
}
std::string content_type;
if (filename.ends_with(".mjb")) {
content_type = "application/mjb";
} else if (filename.ends_with(".mjz")) {
content_type = "application/zip";
} else if (filename.ends_with(".zip")) {
content_type = "application/zip";
} else if (filename.ends_with(".xml")) {
content_type = "text/xml";
} else {
return;
}
const auto ptr = reinterpret_cast<const std::byte*>(data.data());
g_app->LoadModelFromBuffer({ptr, ptr + data.size()}, content_type, filename);
}
// Javascript-facing function to load a model from a URL.
// The URL is passed directly to LoadModelFromFile, which will use the
// registered HTTP/HTTPS resource providers to fetch the model and any
// referenced assets.
void LoadUrl(const std::string& url) {
if (!g_app) {
return;
}
g_app->LoadModelFromFile(url);
}
// Javascript-facing function to render a single frame.
void RenderFrame() {
if (g_app) {
if (g_app->Update()) {
g_app->BuildGui();
g_app->Render();
}
}
}
// Javascript-facing function to deinitialize the app.
void Deinit() {
delete g_app;
g_app = nullptr;
}
EMSCRIPTEN_BINDINGS(studio_bindings) {
emscripten::function("registerAsset", &RegisterAsset);
emscripten::function("init", &Init);
emscripten::function("loadFile", &LoadFile);
emscripten::function("loadUrl", &LoadUrl);
emscripten::function("renderFrame", &RenderFrame);
emscripten::function("deinit", &Deinit);
}