From 3e8a81383aa5e50cb9b576d3fea710da3d52b232 Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Fri, 6 Feb 2026 00:50:18 -0800 Subject: [PATCH] Move mjSpec/mjModel/mjData management into a separate class. PiperOrigin-RevId: 866327584 Change-Id: I72f6ec656e193f67dee22214fe01c517432f87e5 --- src/experimental/platform/CMakeLists.txt | 2 + src/experimental/platform/model_holder.cc | 140 ++++++++++++++++++++++ src/experimental/platform/model_holder.h | 77 ++++++++++++ src/experimental/studio/app.cc | 133 ++++---------------- src/experimental/studio/app.h | 23 ++-- 5 files changed, 251 insertions(+), 124 deletions(-) create mode 100644 src/experimental/platform/model_holder.cc create mode 100644 src/experimental/platform/model_holder.h diff --git a/src/experimental/platform/CMakeLists.txt b/src/experimental/platform/CMakeLists.txt index 197a8b5e..6429db47 100644 --- a/src/experimental/platform/CMakeLists.txt +++ b/src/experimental/platform/CMakeLists.txt @@ -48,6 +48,8 @@ target_sources(${MUJOCO_PLATFORM_TARGET_NAME} imgui_widgets.h interaction.cc interaction.h + model_holder.cc + model_holder.h picture_gui.h picture_gui.cc plugin.cc diff --git a/src/experimental/platform/model_holder.cc b/src/experimental/platform/model_holder.cc new file mode 100644 index 00000000..4a53e450 --- /dev/null +++ b/src/experimental/platform/model_holder.cc @@ -0,0 +1,140 @@ +// Copyright 2026 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. + +#include "experimental/platform/model_holder.h" + +#include +#include +#include +#include +#include + +#include +#include "user/user_resource.h" + +namespace mujoco::platform { + +struct BufferProvider : public mjpResourceProvider { + BufferProvider(std::span buffer) : buffer(buffer) { + mjp_defaultResourceProvider(this); + open = [](mjResource* resource) { + return 1; + }; + read = [](mjResource* resource, const void** buffer) { + BufferProvider* self = (BufferProvider*)resource->provider; + *buffer = self->buffer.data(); + return static_cast(self->buffer.size()); + }; + close = [](mjResource* resource) {}; + } + std::span buffer; +}; + +std::unique_ptr ModelHolder::FromSpec(mjSpec* spec) { + auto mh = std::unique_ptr(new ModelHolder()); + mh->InitFromSpec(spec); + return mh; +} + +std::unique_ptr ModelHolder::FromFile(std::string_view filepath) { + auto mh = std::unique_ptr(new ModelHolder()); + mh->InitFromFile(filepath); + return mh; +} + +std::unique_ptr ModelHolder::FromBuffer( + std::span buffer, std::string_view content_type, + std::string_view filename) { + auto mh = std::unique_ptr(new ModelHolder()); + mh->InitFromBuffer(buffer, content_type, filename); + return mh; +} + +ModelHolder::~ModelHolder() { + if (data_) { + mj_deleteData(data_); + } + if (model_) { + mj_deleteModel(model_); + } + if (spec_) { + mj_deleteSpec(spec_); + } + mj_deleteVFS(&vfs_); +} + +void ModelHolder::PostInit() { + if (spec_ && !model_) { + model_ = mj_compile(spec_, &vfs_); + if (!model_) { + SetLoadError("Error compiling model from spec."); + return; + } + } + data_ = mj_makeData(model_); + if (!data_) { + SetLoadError("Error making data for model."); + } +} + +void ModelHolder::InitFromSpec(mjSpec* spec) { + spec_ = spec; + PostInit(); +} + +void ModelHolder::InitFromFile(std::string_view filepath) { + mj_defaultVFS(&vfs_); + if (filepath.ends_with(".mjb")) { + model_ = mj_loadModel(filepath.data(), &vfs_); + } else { + spec_ = mj_parse(filepath.data(), nullptr, &vfs_, error_, sizeof(error_)); + } + if (error_[0] == 0) { + PostInit(); + } +} + +void ModelHolder::InitFromBuffer(std::span buffer, + std::string_view content_type, + std::string_view filename) { + mj_defaultVFS(&vfs_); + + if (content_type == "text/xml") { + const char* ptr = reinterpret_cast(buffer.data()); + spec_ = mj_parseXMLString(ptr, nullptr, error_, sizeof(error_)); + } else if (content_type == "application/mjb") { + model_ = mj_loadModelBuffer(buffer.data(), buffer.size()); + } else if (content_type == "application/zip") { + BufferProvider provider(buffer); + mjResource resource; + std::memset(&resource, 0, sizeof(mjResource)); + resource.vfs = &vfs_; + resource.provider = &provider; + resource.name = const_cast(filename.data()); + spec_ = mju_decodeResource(&resource, content_type.data(), &vfs_); + } else { + SetLoadError( + "Unknown content type; expected text/xml or application/mjb"); + } + if (error_[0] == 0) { + PostInit(); + } +} + +void ModelHolder::SetLoadError(std::string_view error) { + strncpy(error_, error.data(), sizeof(error_) - 1); + error_[sizeof(error_) - 1] = 0; +} + +} // namespace mujoco::platform diff --git a/src/experimental/platform/model_holder.h b/src/experimental/platform/model_holder.h new file mode 100644 index 00000000..88b6d166 --- /dev/null +++ b/src/experimental/platform/model_holder.h @@ -0,0 +1,77 @@ +// Copyright 2026 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. + +#ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_MODEL_HOLDER_H_ +#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_MODEL_HOLDER_H_ + +#include +#include +#include +#include + +#include + +namespace mujoco::platform { + +// Container for storing an mjModel as well as its associated mjData. Also, if +// applicable, stores the mjSpec and mjVFS from which the model was constructed. +class ModelHolder { + public: + ModelHolder(const ModelHolder&) = delete; + ModelHolder& operator=(const ModelHolder&) = delete; + ~ModelHolder(); + + // Creates an mjModel from an existing mjSpec. This class takes ownership of + // the spec and will `mj_deleteSpec` it on destruction. + static std::unique_ptr FromSpec(mjSpec* spec); + + // Creates an mjModel by attempting to open and parse the give file. + static std::unique_ptr FromFile(std::string_view filepath); + + // Creates an mjModel by attempting to decode the given buffer. + static std::unique_ptr FromBuffer( + std::span buffer, std::string_view content_type, + std::string_view filename); + + // Accessors to the MuJoCo structures managed by this object. + mjVFS* vfs() { return &vfs_; } + mjSpec* spec() { return spec_; } + mjData* data() { return data_; } + mjModel* model() { return model_; } + + // Returns true if the holder holds a valid model (and data). + bool ok() const { return error_[0] == 0 && model_ && data_; } + + // Returns the error message if the model failed to load. + std::string_view error() const { return error_; } + + private: + ModelHolder() = default; + void InitFromSpec(mjSpec* spec); + void InitFromFile(std::string_view filepath); + void InitFromBuffer(std::span buffer, + std::string_view content_type, + std::string_view filename); + void PostInit(); + void SetLoadError(std::string_view error); + + mjVFS vfs_; + mjSpec* spec_ = nullptr; + mjModel* model_ = nullptr; + mjData* data_ = nullptr; + char error_[1000] = ""; +}; +} // namespace mujoco::platform + +#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_MODEL_HOLDER_H_ diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 3c2c9505..52d4b8f9 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -37,12 +37,12 @@ #include "experimental/platform/helpers.h" #include "experimental/platform/imgui_widgets.h" #include "experimental/platform/interaction.h" +#include "experimental/platform/model_holder.h" #include "experimental/platform/picture_gui.h" #include "experimental/platform/plugin.h" #include "experimental/platform/renderer.h" #include "experimental/platform/step_control.h" #include "experimental/platform/window.h" -#include "user/user_resource.h" namespace mujoco::studio { @@ -120,20 +120,8 @@ App::App(Config config) } void App::ClearModel() { - if (model_) { - mj_deleteData(data_); - data_ = nullptr; - mj_deleteModel(model_); - model_ = nullptr; - - if (spec_) { - mj_deleteSpec(spec_); - spec_ = nullptr; - } - } - + model_holder_.reset(); window_->SetTitle("MuJoCo Studio"); - step_control_.SetSpeed(100.f); profiler_.Clear(); tmp_ = UiTempState(); @@ -152,138 +140,61 @@ void App::RequestModelReload() { } void App::InitEmptyModel() { - mjSpec* spec = mj_makeSpec(); - mjModel* model = mj_compile(spec, nullptr); - InitModel(model, spec, nullptr, "", kEmptyModel); + model_holder_ = platform::ModelHolder::FromSpec(mj_makeSpec()); + OnModelLoaded("", kEmptyModel); } void App::LoadModelFromFile(const std::string& filepath) { - mjModel* model = nullptr; - mjSpec* spec = nullptr; - mjVFS vfs; - mj_defaultVFS(&vfs); const std::string resolved_file = platform::ResolveFile(filepath, search_paths_); - if (resolved_file.empty()) { SetLoadError("File not found: " + filepath); return; } - char err[1000] = ""; - if (resolved_file.ends_with(".mjb")) { - model = mj_loadModel(resolved_file.c_str(), &vfs); + model_holder_ = platform::ModelHolder::FromFile(resolved_file); + if (model_holder_->ok()) { + OnModelLoaded(filepath, kModelFromFile); + UpdateFilePaths(resolved_file); + window_->SetTitle("MuJoCo Studio : " + filepath); } else { - spec = mj_parse(resolved_file.c_str(), nullptr, &vfs, err, sizeof(err)); + SetLoadError(std::string(model_holder_->error())); } - if (err[0]) { - SetLoadError(err); - return; - } - - InitModel(model, spec, &vfs, filepath, kModelFromFile); - UpdateFilePaths(resolved_file); - window_->SetTitle("MuJoCo Studio : " + filepath); - - mj_deleteVFS(&vfs); } -struct BufferProvider : public mjpResourceProvider { - BufferProvider(std::span buffer) : buffer(buffer) { - mjp_defaultResourceProvider(this); - open = [](mjResource* resource) { - return 1; - }; - read = [](mjResource* resource, const void** buffer) { - BufferProvider* self = (BufferProvider*)resource->provider; - *buffer = self->buffer.data(); - return static_cast(self->buffer.size()); - }; - close = [](mjResource* resource) {}; - } - std::span buffer; -}; - void App::LoadModelFromBuffer(std::span buffer, std::string_view content_type, std::string_view filename) { - mjModel* model = nullptr; - mjSpec* spec = nullptr; - - mjVFS vfs; - mj_defaultVFS(&vfs); - - char err[1000] = ""; - if (content_type == "text/xml") { - const char* ptr = reinterpret_cast(buffer.data()); - spec = mj_parseXMLString(ptr, nullptr, err, sizeof(err)); - } else if (content_type == "application/mjb") { - model = mj_loadModelBuffer(buffer.data(), buffer.size()); - } else if (content_type == "application/zip") { - BufferProvider provider(buffer); - mjResource resource; - memset(&resource, 0, sizeof(mjResource)); - resource.vfs = &vfs; - resource.provider = &provider; - resource.name = (char*)filename.data(); - spec = mju_decodeResource(&resource, content_type.data(), &vfs); + model_holder_ = + platform::ModelHolder::FromBuffer(buffer, content_type, filename); + if (model_holder_->ok()) { + OnModelLoaded(std::string(filename), kModelFromFile); } else { - SetLoadError("Unknown content type; expected text/xml or application/mjb"); - return; + SetLoadError(std::string(model_holder_->error())); } - if (err[0]) { - SetLoadError(err); - return; - } - - InitModel(model, spec, &vfs, std::string(filename), kModelFromBuffer); - - mj_deleteVFS(&vfs); } -void App::InitModel(mjModel* model, mjSpec* spec, mjVFS* vfs, - std::string filename, ModelKind model_kind) { +void App::OnModelLoaded(std::string filename, ModelKind model_kind) { + model_path_ = std::move(filename); + if (model_kind_ == kEmptyModel) { step_control_.Unpause(); } - ClearModel(); - - model_path_ = std::move(filename); model_kind_ = model_kind; if (model_kind_ == kEmptyModel) { step_control_.Pause(); } - spec_ = spec; - model_ = model; - - // If we have a spec but not a model, we need to compile the model from the spec. - if (spec_ && !model_) { - model_ = mj_compile(spec_, vfs); - if (!model_) { - SetLoadError("Error compiling model from spec."); - return; - } - } - - if (!model_) { - mju_error("Error making data for model: %s", model_path_.c_str()); - } - - data_ = mj_makeData(model_); - if (!data_) { - mju_error("Error making data for model: %s", model_path_.c_str()); - } - // Reset/reinitialize everything that depends on the new mjModel. - renderer_->Init(model_); - const int state_size = mj_stateSize(model_, mjSTATE_INTEGRATION); + mjModel* model = model_holder_->model(); + renderer_->Init(model); + const int state_size = mj_stateSize(model, mjSTATE_INTEGRATION); history_.Init(state_size); // Initialize the speed based on the model's default real-time setting. float min_error = FLT_MAX; - const float desired = mju_log(100 * model_->vis.global.realtime); + const float desired = mju_log(100 * model->vis.global.realtime); for (int i = 0; i < kPercentRealTime.size(); ++i) { const float speed = std::stof(kPercentRealTime[i]); const float error = mju_abs(mju_log(speed) - desired); diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index a46dd03d..bbab8bac 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -27,6 +27,7 @@ #include #include "experimental/platform/gui.h" #include "experimental/platform/interaction.h" +#include "experimental/platform/model_holder.h" #include "experimental/platform/picture_gui.h" #include "experimental/platform/renderer.h" #include "experimental/platform/sim_history.h" @@ -168,8 +169,7 @@ class App { // Updates the currently loaded model to the given model. If model is null, // then compile the spec to a model. - void InitModel(mjModel* model, mjSpec* spec, mjVFS* vfs, std::string filename, - ModelKind model_kind); + void OnModelLoaded(std::string filename, ModelKind model_kind); void SetLoadError(std::string error); void UpdateFilePaths(const std::string& resolved_path); @@ -207,12 +207,13 @@ class App { float GetExpectedLabelWidth(); std::vector GetCameraNames(); - mjSpec* spec() { return spec_; } - mjModel* model() { return model_; } - mjData* data() { return data_; } - bool has_spec() const { return spec_ != nullptr; } - bool has_model() const { return model_ != nullptr; } - bool has_data() const { return data_ != nullptr; } + mjSpec* spec() { return model_holder_->spec(); } + mjModel* model() { return model_holder_->model(); } + mjData* data() { return model_holder_->data(); } + bool has_spec() const { return model_holder_ && model_holder_->spec(); } + bool has_model() const { return model_holder_ && model_holder_->model(); } + bool has_data() const { return model_holder_ && model_holder_->data(); } + std::string ini_path_; std::string model_name_; // Used if model_kind_ is kModelFromBuffer. @@ -224,15 +225,11 @@ class App { std::unique_ptr window_; std::unique_ptr renderer_; + std::unique_ptr model_holder_; platform::StepControl step_control_; platform::SimProfiler profiler_; platform::SimHistory history_; - std::vector search_paths_; - - mjSpec* spec_ = nullptr; - mjModel* model_ = nullptr; - mjData* data_ = nullptr; std::vector pixels_; mjvCamera camera_;