From e2180ff744126d20250afda5ace546f7a9a9ce3a Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Wed, 21 Jan 2026 02:46:40 -0800 Subject: [PATCH] First-pass of a plugin system for GUI elements. Refactor filament's debug drawing to be one such plugin. PiperOrigin-RevId: 859002002 Change-Id: Ibe9000dec19fce648eef6d57170ce435b9a8c6ff --- .../filament/filament/filament_context.cc | 6 +- .../filament/filament/filament_context.h | 4 +- .../filament/filament/imgui_editor.cc | 26 +------- .../filament/render_context_filament.cc | 6 ++ .../filament/render_context_filament.h | 2 + src/experimental/platform/CMakeLists.txt | 2 + src/experimental/platform/plugin.cc | 63 +++++++++++++++++++ src/experimental/platform/plugin.h | 58 +++++++++++++++++ src/experimental/platform/renderer.cc | 12 ++++ src/experimental/studio/app.cc | 26 ++++++++ 10 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 src/experimental/platform/plugin.cc create mode 100644 src/experimental/platform/plugin.h diff --git a/src/experimental/filament/filament/filament_context.cc b/src/experimental/filament/filament/filament_context.cc index a5d4095b..6b6c8b52 100644 --- a/src/experimental/filament/filament/filament_context.cc +++ b/src/experimental/filament/filament/filament_context.cc @@ -146,8 +146,6 @@ void FilamentContext::Render(const mjrRect& viewport, const mjvScene* scene, // Draw the GUI. We do this after processing the scene in case there are any // label elements in the scene. if (gui_view_ && !render_to_texture_) { - DrawGui(scene_view_.get()); - // Prepare the filament Renderable that contains the GUI draw commands. We // must call this function even if we do not plan on rendering the GUI to // ensure the ImGui state is updated. @@ -339,4 +337,8 @@ double FilamentContext::GetFrameRate() const { return 1.0e9 / static_cast(ns); } +void FilamentContext::UpdateGui() { + DrawGui(scene_view_.get()); +} + } // namespace mujoco diff --git a/src/experimental/filament/filament/filament_context.h b/src/experimental/filament/filament/filament_context.h index 25d6248b..1721057b 100644 --- a/src/experimental/filament/filament/filament_context.h +++ b/src/experimental/filament/filament/filament_context.h @@ -56,6 +56,8 @@ class FilamentContext { double GetFrameRate() const; + void UpdateGui(); + FilamentContext(const FilamentContext&) = delete; FilamentContext& operator=(const FilamentContext&) = delete; @@ -63,8 +65,6 @@ class FilamentContext { void PrepareRenderTargets(int width, int height); void DestroyRenderTargets(); - SceneView* GetSceneView(const mjvScene* scene); - mjrFilamentConfig config_; mjrContext* context_ = nullptr; const mjModel* model_ = nullptr; diff --git a/src/experimental/filament/filament/imgui_editor.cc b/src/experimental/filament/filament/imgui_editor.cc index 76b74265..da118829 100644 --- a/src/experimental/filament/filament/imgui_editor.cc +++ b/src/experimental/filament/filament/imgui_editor.cc @@ -581,7 +581,7 @@ void DrawLightGui(filament::LightManager& lm, } } -void DrawSceneViewGui(SceneView* scene_view) { +void DrawGui(SceneView* scene_view) { filament::View* view = scene_view->GetDefaultRenderView(); filament::Engine* engine = scene_view->GetEngine(); filament::LightManager& lm = engine->getLightManager(); @@ -669,28 +669,4 @@ void DrawSceneViewGui(SceneView* scene_view) { ImGui::TreePop(); } } - -void DrawGui(SceneView* scene_view) { - static bool display = false; - if (ImGui::BeginMainMenuBar()) { - if (ImGui::BeginMenu("View")) { - ImGui::Separator(); - if (ImGui::MenuItem("Filament", "", display)) { - display = !display; - } - ImGui::EndMenu(); - } - ImGui::EndMainMenuBar(); - } - - if (!display) { - return; - } - - ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_Appearing); - ImGui::Begin("Filament"); - DrawSceneViewGui(scene_view); - ImGui::End(); -} - } // namespace mujoco diff --git a/src/experimental/filament/render_context_filament.cc b/src/experimental/filament/render_context_filament.cc index 1913ed53..4695599f 100644 --- a/src/experimental/filament/render_context_filament.cc +++ b/src/experimental/filament/render_context_filament.cc @@ -113,4 +113,10 @@ double mjr_getFrameRate(const mjrContext* con) { return g_filament_context->GetFrameRate(); } +void mjr_updateGui(const mjrContext* con) { + if (g_filament_context != nullptr) { + g_filament_context->UpdateGui(); + } +} + } // extern "C" diff --git a/src/experimental/filament/render_context_filament.h b/src/experimental/filament/render_context_filament.h index c8642016..c0423e4a 100644 --- a/src/experimental/filament/render_context_filament.h +++ b/src/experimental/filament/render_context_filament.h @@ -71,6 +71,8 @@ uintptr_t mjr_uploadGuiImage(uintptr_t tex_id, const unsigned char* pixels, int width, int height, int bpp, const mjrContext* con); +void mjr_updateGui(const mjrContext* con); + #if defined(__cplusplus) } // extern "C" #endif diff --git a/src/experimental/platform/CMakeLists.txt b/src/experimental/platform/CMakeLists.txt index f5eb81cb..ef5ef471 100644 --- a/src/experimental/platform/CMakeLists.txt +++ b/src/experimental/platform/CMakeLists.txt @@ -47,6 +47,8 @@ target_sources(${MUJOCO_PLATFORM_TARGET_NAME} interaction.h picture_gui.h picture_gui.cc + plugin.cc + plugin.h renderer.cc renderer.h sim_history.cc diff --git a/src/experimental/platform/plugin.cc b/src/experimental/platform/plugin.cc new file mode 100644 index 00000000..f76eba91 --- /dev/null +++ b/src/experimental/platform/plugin.cc @@ -0,0 +1,63 @@ +// 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 +#include + +#include +#include "experimental/platform/plugin.h" +#include "engine/engine_global_table.h" + +namespace mujoco::platform { + +void RegisterGuiPlugin(const GuiPlugin* plugin) { + if (plugin->name == nullptr || plugin->name[0] == '\0') { + mju_error("Plugin name must not be empty or null."); + } + GlobalTable::GetSingleton().AppendIfUnique(*plugin); +} + +void ForEachGuiPlugin(const std::function& fn) { + auto& table = GlobalTable::GetSingleton(); + for (int i = 0; i < table.count(); ++i) { + const GuiPlugin* plugin = table.GetAtSlot(i); + fn(const_cast(plugin)); + } +} + +} // namespace mujoco::platform + +using mujoco::GlobalTable; +using GuiPlugin = mujoco::platform::GuiPlugin; + +template <> +const char* GlobalTable::HumanReadableTypeName() { + return "gui plugin"; +} + +template <> +std::string_view GlobalTable::ObjectKey(const GuiPlugin& plugin) { + return std::string_view(plugin.name); +} + +template <> +bool GlobalTable::ObjectEqual(const GuiPlugin& p1, const GuiPlugin& p2) { + return CaseInsensitiveEqual(p1.name, p2.name); +} + +template <> +bool GlobalTable::CopyObject(GuiPlugin& dst, const GuiPlugin& src, ErrorMessage& err) { + dst = src; + return true; +} diff --git a/src/experimental/platform/plugin.h b/src/experimental/platform/plugin.h new file mode 100644 index 00000000..7207f43a --- /dev/null +++ b/src/experimental/platform/plugin.h @@ -0,0 +1,58 @@ +// 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_PLUGIN_H_ +#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_PLUGIN_H_ + +#include + +namespace mujoco::platform { + +// Important: Do not inherit from these plugin structs. They are copied by value +// and therefore any derived classes will be sliced. We assume plugins are +// effectively globals and so any pointers will be valid for the lifetime +// of the process. + +// Plugin for processing custom UI windows. The plugin will be listed in the +// "Plugins" main menu and, when selected, an ImGui window will be opened with +// the name of the plugin as the title. The `update` function can then be used +// to process the GUI. All functions will be called by passing `this` as the +// first argument. +struct GuiPlugin final { + using UpdateFn = void (*)(GuiPlugin* self); + + // Whether or not to display the plugin window. + bool active = false; + + // The name of the plugin; must be unique. + const char* name = ""; + + // The function that will update the plugin's window. Plugin GUI updates + // happen when the window is `active` and after all other Studio GUI updates. + UpdateFn update = nullptr; + + // Optional data pointer. + void* data = nullptr; +}; + +// Registers a plugin with a global registry. The plugin must have a +// case-insensitive unique name. +void RegisterGuiPlugin(const GuiPlugin* plugin); + +// Executes the given function for each registered plugin. +void ForEachGuiPlugin(const std::function& fn); + +} // namespace mujoco::platform + +#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_PLUGIN_H_ diff --git a/src/experimental/platform/renderer.cc b/src/experimental/platform/renderer.cc index ed5a821b..c7ba2e3b 100644 --- a/src/experimental/platform/renderer.cc +++ b/src/experimental/platform/renderer.cc @@ -20,6 +20,7 @@ #if defined(USE_FILAMENT_OPENGL) || defined(USE_FILAMENT_VULKAN) #include "experimental/filament/render_context_filament.h" +#include "experimental/platform/plugin.h" #elif defined(USE_CLASSIC_OPENGL) #include #include @@ -136,3 +137,14 @@ int Renderer::UploadImage(int texture_id, const std::byte* pixels, int width, double Renderer::GetFps() { return fps_; } } // namespace mujoco::platform + +#if !defined(USE_CLASSIC_OPENGL) +mjPLUGIN_LIB_INIT { + mujoco::platform::GuiPlugin plugin; + plugin.name = "Filament"; + plugin.update = [](mujoco::platform::GuiPlugin* self) { + mjr_updateGui(nullptr); + }; + mujoco::platform::RegisterGuiPlugin(&plugin); +} +#endif diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 98e61a19..2937c78f 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -37,6 +37,7 @@ #include "experimental/platform/imgui_widgets.h" #include "experimental/platform/interaction.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" @@ -854,6 +855,26 @@ void App::BuildGui() { ImGui::End(); } + platform::ForEachGuiPlugin([](platform::GuiPlugin* plugin) { + if (!plugin->update) { + return; + } + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("Plugins")) { + if (ImGui::MenuItem(plugin->name, "", plugin->active)) { + plugin->active = !plugin->active; + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } + if (plugin->active) { + ImGui::Begin(plugin->name); + plugin->update(plugin); + ImGui::End(); + } + }); + ImGuiIO& io = ImGui::GetIO(); if (tmp_.first_frame) { LoadSettings(); @@ -1534,6 +1555,11 @@ void App::MainMenuGui() { } ImGui::EndMenu(); } + if (ImGui::BeginMenu("Plugins")) { + // Placeholder menu item that will be populated by plugins later on. We + // do this now in so that the menu is present at the right place. + ImGui::EndMenu(); + } if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("Help", "F1", tmp_.help)) { ToggleWindow(tmp_.help);