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
This commit is contained in:
Haroon Qureshi
2026-01-21 02:46:40 -08:00
committed by Copybara-Service
parent bf05bb1885
commit e2180ff744
10 changed files with 176 additions and 29 deletions
@@ -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<double>(ns);
}
void FilamentContext::UpdateGui() {
DrawGui(scene_view_.get());
}
} // namespace mujoco
@@ -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;
@@ -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
@@ -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"
@@ -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
+2
View File
@@ -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
+63
View File
@@ -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 <functional>
#include <string_view>
#include <mujoco/mujoco.h>
#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<GuiPlugin>::GetSingleton().AppendIfUnique(*plugin);
}
void ForEachGuiPlugin(const std::function<void(GuiPlugin*)>& fn) {
auto& table = GlobalTable<GuiPlugin>::GetSingleton();
for (int i = 0; i < table.count(); ++i) {
const GuiPlugin* plugin = table.GetAtSlot(i);
fn(const_cast<GuiPlugin*>(plugin));
}
}
} // namespace mujoco::platform
using mujoco::GlobalTable;
using GuiPlugin = mujoco::platform::GuiPlugin;
template <>
const char* GlobalTable<GuiPlugin>::HumanReadableTypeName() {
return "gui plugin";
}
template <>
std::string_view GlobalTable<GuiPlugin>::ObjectKey(const GuiPlugin& plugin) {
return std::string_view(plugin.name);
}
template <>
bool GlobalTable<GuiPlugin>::ObjectEqual(const GuiPlugin& p1, const GuiPlugin& p2) {
return CaseInsensitiveEqual(p1.name, p2.name);
}
template <>
bool GlobalTable<GuiPlugin>::CopyObject(GuiPlugin& dst, const GuiPlugin& src, ErrorMessage& err) {
dst = src;
return true;
}
+58
View File
@@ -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 <functional>
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<void(GuiPlugin*)>& fn);
} // namespace mujoco::platform
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_PLUGIN_H_
+12
View File
@@ -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 <imgui.h>
#include <backends/imgui_impl_opengl3.h>
@@ -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
+26
View File
@@ -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);