From 9743c2d38595616f356b5374851e0e807f9e4e6d Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Wed, 7 Jan 2026 03:02:06 -0800 Subject: [PATCH] Use mjResourceProvider instead of custom LoadAsset callbacks. PiperOrigin-RevId: 853172824 Change-Id: I60ef1207611c4da119cc3f0a7819088cffca6e4f --- .../filament/filament/object_manager.cc | 34 ++++----- .../filament/render_context_filament.cc | 38 +--------- .../filament/render_context_filament.h | 10 --- src/experimental/platform/helpers.h | 3 - src/experimental/platform/renderer.cc | 32 +------- src/experimental/platform/renderer.h | 8 +- src/experimental/platform/window.cc | 36 +++++---- src/experimental/platform/window.h | 3 +- src/experimental/studio/app.cc | 11 ++- src/experimental/studio/app.h | 4 +- src/experimental/studio/main.cc | 75 +++++++++++++++---- src/experimental/studio/wasm.cc | 55 ++++++++------ 12 files changed, 142 insertions(+), 167 deletions(-) diff --git a/src/experimental/filament/filament/object_manager.cc b/src/experimental/filament/filament/object_manager.cc index b74bfa7e..20443c25 100644 --- a/src/experimental/filament/filament/object_manager.cc +++ b/src/experimental/filament/filament/object_manager.cc @@ -16,7 +16,6 @@ #include #include -#include #include #include #include @@ -35,32 +34,32 @@ #include "experimental/filament/filament/model_util.h" #include "experimental/filament/filament/texture_util.h" #include "experimental/filament/render_context_filament.h" +#include "user/user_resource.h" namespace mujoco { namespace { // Loads binary data from a file using mjrFilamentConfig callbacks. struct Asset { - Asset(const char* filename, const mjrFilamentConfig* config) { - const int error = config->load_asset(filename, config->load_asset_user_data, - &payload, &size); - if (error) { - mju_error("Failed to load file: %s (error: %d)", filename, error); - } + explicit Asset(std::string_view filename) { + std::string path = "filament:" + std::string(filename); + + resource = mju_openResource("", path.c_str(), nullptr, nullptr, 0); + size = mju_readResource(resource, const_cast(&payload)); } ~Asset() { - if (payload) { - free(payload); - payload = nullptr; + if (resource) { + mju_closeResource(resource); } } - uint64_t size = 0; - unsigned char* payload = nullptr; - Asset(const Asset&) = delete; Asset& operator=(const Asset&) = delete; + + int size = 0; + void* payload = nullptr; + mjResource* resource = nullptr; }; } // namespace @@ -79,8 +78,8 @@ ObjectManager::ObjectManager(const mjModel* model, filament::Engine* engine, shapes_[kSphere] = CreateSphere(engine_, model_); shapes_[kTriangle] = CreateTriangle(engine_, model_); - auto LoadMaterial = [this](const char* filename) { - Asset asset(filename, config_); + auto LoadMaterial = [this](std::string_view filename) { + Asset asset(filename); filament::Material::Builder material_builder; material_builder.package(asset.payload, asset.size); return material_builder.build(*this->engine_); @@ -331,14 +330,15 @@ filament::IndirectLight* ObjectManager::CreateIndirectLight(int tex_id, filament::IndirectLight* ObjectManager::LoadFallbackIndirectLight( std::string_view filename, float intensity) { - Asset asset(std::string(filename).c_str(), config_); + Asset asset(filename); if (asset.size == 0) { return nullptr; } filament::math::float3 spherical_harmonics[9]; filament::Texture* tex = - CreateKtxTexture(engine_, asset.payload, asset.size, spherical_harmonics); + CreateKtxTexture(engine_, reinterpret_cast(asset.payload), + asset.size, spherical_harmonics); return CreateIndirectLight(tex, &spherical_harmonics, intensity); } diff --git a/src/experimental/filament/render_context_filament.cc b/src/experimental/filament/render_context_filament.cc index a74536aa..1913ed53 100644 --- a/src/experimental/filament/render_context_filament.cc +++ b/src/experimental/filament/render_context_filament.cc @@ -15,11 +15,7 @@ #include "experimental/filament/render_context_filament.h" #include -#include #include -#include -#include -#include #include #include @@ -27,50 +23,19 @@ #include #include "experimental/filament/filament/filament_context.h" + #if defined(TLS_FILAMENT_CONTEXT) static thread_local mujoco::FilamentContext* g_filament_context = nullptr; #else static mujoco::FilamentContext* g_filament_context = nullptr; #endif -// Default asset loader to use when calling mjr_makeContext. This is only -// intended for basic backwards compatibility with the existing mjr_makeContext -// API. If this doesn't work as expected, you should be calling -// mjr_makeFilamentContext instead and provide your own asset loading callbacks. -// Returns 0 on success and non-zero to indicate an error. -static int DefaultLoadAsset(const char* asset_filename, void* user_data, - unsigned char** contents, uint64_t* out_size) { - std::filesystem::path full_path; - full_path.append("filament/assets/data"); - full_path.append(asset_filename); - - std::ifstream file(full_path, std::ios::binary); - if (!file) { - mju_error("File does not exist: %s", full_path.c_str()); - return 1; - } - - file.seekg(0, std::ios::end); - *out_size = static_cast(file.tellg()); - if (*out_size == 0) { - mju_error("File is empty: %s", full_path.c_str()); - return 1; - } - file.seekg(0, std::ios::beg); - - *contents = (unsigned char*)malloc(*out_size); - file.read((char*)*contents, *out_size); - file.close(); - return 0; -} - static void CheckFilamentContext() { if (g_filament_context == nullptr) { mju_error("Missing context; did you call mjr_makeFilamentContext?"); } } - extern "C" { void mjr_defaultFilamentConfig(mjrFilamentConfig* config) { @@ -93,7 +58,6 @@ void mjr_makeContext(const mjModel* m, mjrContext* con, int fontscale) { mjr_freeContext(con); mjrFilamentConfig cfg; mjr_defaultFilamentConfig(&cfg); - cfg.load_asset = DefaultLoadAsset; mjr_makeFilamentContext(m, con, &cfg); } diff --git a/src/experimental/filament/render_context_filament.h b/src/experimental/filament/render_context_filament.h index 65c4e66c..c8642016 100644 --- a/src/experimental/filament/render_context_filament.h +++ b/src/experimental/filament/render_context_filament.h @@ -33,16 +33,6 @@ typedef enum mjtGraphicsApi_ { // backend graphics API to use } mjtGraphicsApi; struct mjrFilamentConfig { - // "Loads" an asset, returning its contents in `out` and size in `out_size`. - // Returns 0 on success and non-zero to indicate an error. The caller must - // free `out`. - typedef int (*load_asset_fn)(const char* path, void* user_data, - unsigned char** out, uint64_t* out_size); - - // Used to load filament assets (e.g. materials, image-based lights, etc.). - load_asset_fn load_asset; - void* load_asset_user_data; - // The native window handle into which we can render directly. void* native_window; diff --git a/src/experimental/platform/helpers.h b/src/experimental/platform/helpers.h index e880fb64..3ad988d9 100644 --- a/src/experimental/platform/helpers.h +++ b/src/experimental/platform/helpers.h @@ -26,9 +26,6 @@ namespace mujoco::platform { -// Function signature for loading assets from a given path. -using LoadAssetFn = std::function(std::string_view)>; - // Save/load for simple ascii files. void SaveText(const std::string& contents, const std::string& filename); std::string LoadText(const std::string& filename); diff --git a/src/experimental/platform/renderer.cc b/src/experimental/platform/renderer.cc index 391ab421..11baa421 100644 --- a/src/experimental/platform/renderer.cc +++ b/src/experimental/platform/renderer.cc @@ -14,17 +14,9 @@ #include "experimental/platform/renderer.h" -#include -#include -#include -#include -#include -#include - #include #include -#include "experimental/platform/helpers.h" #if defined(USE_FILAMENT_OPENGL) || defined(USE_FILAMENT_VULKAN) #include "experimental/filament/render_context_filament.h" @@ -37,8 +29,7 @@ namespace mujoco::platform { -Renderer::Renderer(void* native_window, const LoadAssetFn& load_asset_fn) - : load_asset_fn_(load_asset_fn), native_window_(native_window) { +Renderer::Renderer(void* native_window) : native_window_(native_window) { #ifdef USE_CLASSIC_OPENGL ImGui_ImplOpenGL3_Init(); #endif @@ -57,8 +48,6 @@ void Renderer::Init(const mjModel* model) { mjrFilamentConfig render_config; mjr_defaultFilamentConfig(&render_config); render_config.native_window = native_window_; - render_config.load_asset = &Renderer::LoadAssetCallback; - render_config.load_asset_user_data = this; render_config.enable_gui = true; #if defined(USE_FILAMENT_OPENGL) render_config.graphics_api = mjGFX_OPENGL; @@ -140,23 +129,4 @@ void Renderer::RenderToTexture(const mjModel* model, mjData* data, double Renderer::GetFps() { return fps_; } -int Renderer::LoadAssetCallback(const char* path, void* user_data, - unsigned char** out, std::uint64_t* out_size) { - Renderer* renderer = static_cast(user_data); - std::vector bytes = (renderer->load_asset_fn_)(path); - if (bytes.empty()) { - *out_size = 0; - return 0; // Empty file - } - - *out_size = bytes.size(); - *out = reinterpret_cast(malloc(*out_size)); - if (*out == nullptr) { - mju_error("Failed to allocate memory for file %s", path); - return -1; - } - - std::memcpy(*out, bytes.data(), *out_size); - return 0; -} } // namespace mujoco::platform diff --git a/src/experimental/platform/renderer.h b/src/experimental/platform/renderer.h index 881d33ab..928cabe7 100644 --- a/src/experimental/platform/renderer.h +++ b/src/experimental/platform/renderer.h @@ -17,11 +17,9 @@ #include #include -#include #include #include -#include "experimental/platform/helpers.h" namespace mujoco::platform { @@ -34,7 +32,7 @@ class Renderer { using Seconds = std::chrono::duration; using Milliseconds = std::chrono::duration; - Renderer(void* native_window, const LoadAssetFn& load_asset_fn); + explicit Renderer(void* native_window); ~Renderer(); Renderer(const Renderer&) = delete; @@ -64,10 +62,6 @@ class Renderer { // Resets the renderer; no rendering will occur until Init() is called again. void Deinit(); - static int LoadAssetCallback(const char* path, void* user_data, - unsigned char** out, std::uint64_t* out_size); - - LoadAssetFn load_asset_fn_; void* native_window_ = nullptr; mjrContext render_context_; mjvScene scene_; diff --git a/src/experimental/platform/window.cc b/src/experimental/platform/window.cc index 1e9e77f3..3664036c 100644 --- a/src/experimental/platform/window.cc +++ b/src/experimental/platform/window.cc @@ -26,8 +26,8 @@ #include #include #include -#include "experimental/platform/helpers.h" #include +#include "user/user_resource.h" // Because X11/Xlib.h defines Status. #ifdef Status @@ -40,8 +40,7 @@ extern void* GetNativeWindowOsx(void* window); namespace mujoco::platform { -static void InitImGui(SDL_Window* window, const LoadAssetFn& load_asset_fn, - bool load_fonts, bool build_fonts) { +static void InitImGui(SDL_Window* window, bool load_fonts, bool build_fonts) { ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); @@ -52,28 +51,36 @@ static void InitImGui(SDL_Window* window, const LoadAssetFn& load_asset_fn, ImGui_ImplSDL2_InitForOther(window); if (load_fonts) { - // Note: fonts are stored statically because they aren't actually loaded - // until the fonts are built. + mjResource* font = nullptr; + int size = 0; + void* data = nullptr; + ImFontConfig main_cfg; - static auto main_font = load_asset_fn("OpenSans-Regular.ttf"); - io.Fonts->AddFontFromMemoryTTF(main_font.data(), main_font.size(), 20.f, - &main_cfg); + main_cfg.FontDataOwnedByAtlas = false; + font = + mju_openResource("", "font:OpenSans-Regular.ttf", nullptr, nullptr, 0); + size = mju_readResource(font, const_cast(&data)); + io.Fonts->AddFontFromMemoryTTF(data, size, 20.f, &main_cfg); ImFontConfig icon_cfg; + icon_cfg.FontDataOwnedByAtlas = false; icon_cfg.MergeMode = true; - static auto icon_font = load_asset_fn("fontawesome-webfont.ttf"); + font = mju_openResource("", "font:fontawesome-webfont.ttf", nullptr, + nullptr, 0); + size = mju_readResource(font, const_cast(&data)); constexpr ImWchar icon_ranges[] = {0xf000, 0xf3ff, 0x000}; - io.Fonts->AddFontFromMemoryTTF(icon_font.data(), icon_font.size(), 14.f, - &icon_cfg, icon_ranges); + io.Fonts->AddFontFromMemoryTTF(data, size, 14.f, &icon_cfg, icon_ranges); if (build_fonts) { io.Fonts->Build(); } + + // Note: we purposefully do not "close" the font resources as ImGui may + // need them again to resize fonts. } } -Window::Window(std::string_view title, int width, int height, Config config, - const LoadAssetFn& load_asset_fn) +Window::Window(std::string_view title, int width, int height, Config config) : width_(width), height_(height), config_(config) { SDL_SetHint(SDL_HINT_FRAMEBUFFER_ACCELERATION, "1"); SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); @@ -113,8 +120,7 @@ Window::Window(std::string_view title, int width, int height, Config config, mju_error("Error creating window: %s", SDL_GetError()); } - InitImGui(sdl_window_, load_asset_fn, config.load_fonts, - (render_config != kClassicOpenGL)); + InitImGui(sdl_window_, config.load_fonts, (render_config != kClassicOpenGL)); if (render_config == kFilamentWebGL || render_config == kClassicOpenGL) { SDL_GLContext gl_context = SDL_GL_CreateContext(sdl_window_); diff --git a/src/experimental/platform/window.h b/src/experimental/platform/window.h index 28617e90..a4124328 100644 --- a/src/experimental/platform/window.h +++ b/src/experimental/platform/window.h @@ -43,8 +43,7 @@ class Window { bool load_fonts = true; }; - Window(std::string_view title, int width, int height, Config config, - const LoadAssetFn& load_asset_fn); + Window(std::string_view title, int width, int height, Config config); ~Window(); Window(const Window&) = delete; diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 30cd48c1..311920f4 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -111,13 +111,12 @@ static constexpr std::array kPercentRealTime = { }; // clang-format on -App::App(int width, int height, std::string ini_path, - const platform::LoadAssetFn& load_asset_fn) - : ini_path_(std::move(ini_path)), load_asset_fn_(load_asset_fn) { +App::App(int width, int height, std::string ini_path) + : ini_path_(std::move(ini_path)) { window_ = std::make_unique("MuJoCo Studio", width, height, - kWindowConfig, load_asset_fn); - renderer_ = std::make_unique( - window_->GetNativeWindowHandle(), load_asset_fn); + kWindowConfig); + renderer_ = + std::make_unique(window_->GetNativeWindowHandle()); ImPlot::CreateContext(); mjv_defaultPerturb(&perturb_); diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index 728d428c..42a7ce0b 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -36,8 +36,7 @@ namespace mujoco::studio { // Owns, updates, and renders a MuJoCo simulation. class App { public: - App(int width, int height, std::string ini_path, - const platform::LoadAssetFn& load_asset_fn); + App(int width, int height, std::string ini_path); enum ContentType { kFilepath, // Path to a model file. @@ -168,7 +167,6 @@ class App { std::unique_ptr window_; std::unique_ptr renderer_; - platform::LoadAssetFn load_asset_fn_; platform::StepControl step_control_; platform::SimProfiler profiler_; platform::SimHistory history_; diff --git a/src/experimental/studio/main.cc b/src/experimental/studio/main.cc index 5059b768..ee4fae59 100644 --- a/src/experimental/studio/main.cc +++ b/src/experimental/studio/main.cc @@ -15,43 +15,88 @@ // Main entry point for the Filament-based MuJoCo renderer. #include -#include #include #include +#include +#include +#include #include #include #include #include +#include #include "experimental/studio/app.h" ABSL_FLAG(int, window_width, 1400, "Window width"); ABSL_FLAG(int, window_height, 720, "Window height"); ABSL_FLAG(std::string, model_file, "", "MuJoCo model file."); -static std::vector LoadAsset(std::string_view path) { - std::string fullpath = std::string("assets/") + std::string(path); - std::ifstream file(fullpath, std::ios::binary | std::ios::ate); - if (!file.is_open()) { - return {}; - } - std::streampos file_size = file.tellg(); - file.seekg(0, std::ios::beg); - std::vector buffer(file_size); - if (!file.read(reinterpret_cast(buffer.data()), file_size)) { - return {}; - } - return buffer; +std::string Resolve(std::string_view path) { + return std::string("assets/") + std::string(path); } +class FileResource { + public: + explicit FileResource(const std::string& path) + : file_(path, std::ios::binary | std::ios::ate) { + if (!file_.is_open()) { + mju_warning("Cannot open file %s", path.c_str()); + return; + } + + size_ = file_.tellg(); + file_.seekg(0, std::ios::beg); + } + + int Read(const void** buffer) { + buffer_.resize(size_); + if (!file_.read(reinterpret_cast(buffer_.data()), size_)) { + return 0; + } + *buffer = buffer_.data(); + return size_; + } + + int Size() const { return size_; } + + FileResource(const FileResource&) = delete; + FileResource& operator=(const FileResource&) = delete; + + private: + std::ifstream file_; + std::vector buffer_; + int size_ = 0; +}; + int main(int argc, char** argv, char** envp) { const char* home = getenv("HOME"); const std::string ini_path = std::string(home ? home : ".") + "/.mujoco.ini"; + mjpResourceProvider resource_provider; + resource_provider.open = [](mjResource* resource) { + const std::string resolved_path = Resolve(resource->name); + FileResource* f = new FileResource(resolved_path); + resource->data = f; + return f->Size(); + }; + resource_provider.read = [](mjResource* resource, const void** buffer) { + FileResource* f = static_cast(resource->data); + return f->Read(buffer); + }; + resource_provider.close = [](mjResource* resource) { + delete static_cast(resource->data); + }; + + resource_provider.prefix = "font"; + mjp_registerResourceProvider(&resource_provider); + resource_provider.prefix = "filament"; + mjp_registerResourceProvider(&resource_provider); + const int width = absl::GetFlag(FLAGS_window_width); const int height = absl::GetFlag(FLAGS_window_height); - mujoco::studio::App app(width, height, ini_path, LoadAsset); + mujoco::studio::App app(width, height, ini_path); // If the model file is not specified, try to load it from the first argument std::string model_file = absl::GetFlag(FLAGS_model_file); diff --git a/src/experimental/studio/wasm.cc b/src/experimental/studio/wasm.cc index 0253ca2f..0429e3b2 100644 --- a/src/experimental/studio/wasm.cc +++ b/src/experimental/studio/wasm.cc @@ -14,17 +14,17 @@ // Main entry point for the Filament-based MuJoCo web app. -#include -#include -#include -#include -#include -#include - #include #include #include +#include +#include +#include +#include +#include + +#include #include "experimental/studio/app.h" // Global app instance. Lifetime is controlled by Init/Deinit calls which are @@ -43,31 +43,25 @@ class AssetRegistry { // 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); } - // Returns the contents of the given asset by name. - std::vector LoadAsset(std::string_view filename) { - if (auto it = assets_.find(std::string(filename)); it != assets_.end()) { - const std::byte* begin = reinterpret_cast(it->second.data()); - const std::byte* end = begin + it->second.size(); - return std::vector(begin, end); - } - return {}; + 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 assets_; }; -static std::vector LoadAsset(std::string_view filename) { - return AssetRegistry::Instance().LoadAsset(filename); -} - // Javascript-facing function to register an asset. void RegisterAsset(std::string filename, std::string contents) { AssetRegistry::Instance().RegisterAsset(std::move(filename), - std::move(contents)); + std::move(contents)); } // Javascript-facing function to initialize the app. @@ -76,7 +70,26 @@ void Init() { const int width = 100; const int height = 100; const std::string ini_path = ""; - g_app = new mujoco::studio::App(width, height, ini_path, LoadAsset); + + mjpResourceProvider resource_provider; + resource_provider.open = [](mjResource* resource) { + AssetRegistry& r = AssetRegistry::Instance(); + return static_cast(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(contents.size()); + }; + resource_provider.close = [](mjResource* resource) {}; + + resource_provider.prefix = "font"; + mjp_registerResourceProvider(&resource_provider); + resource_provider.prefix = "filament"; + mjp_registerResourceProvider(&resource_provider); + + g_app = new mujoco::studio::App(width, height, ini_path); g_app->LoadModel("", mujoco::studio::App::ContentType::kModelXml); }