From 1e04c568e8b74abf9667738583a1ca1ff722d514 Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Mon, 29 Jun 2026 07:07:22 -0700 Subject: [PATCH] Studio launcher PiperOrigin-RevId: 939814201 Change-Id: If3fb1a1a9f9114835452fce63f39931db7818db3 --- src/experimental/studio/CMakeLists.txt | 7 +- src/experimental/studio/app.cc | 18 ++- src/experimental/studio/app.h | 1 + src/experimental/studio/launcher.cc | 160 +++++++++++++++++++++++++ src/experimental/studio/launcher.h | 49 ++++++++ src/experimental/studio/main.cc | 144 ++-------------------- 6 files changed, 240 insertions(+), 139 deletions(-) create mode 100644 src/experimental/studio/launcher.cc create mode 100644 src/experimental/studio/launcher.h diff --git a/src/experimental/studio/CMakeLists.txt b/src/experimental/studio/CMakeLists.txt index 8128ce26..0bf76f8e 100644 --- a/src/experimental/studio/CMakeLists.txt +++ b/src/experimental/studio/CMakeLists.txt @@ -118,7 +118,12 @@ if(NOT EMSCRIPTEN) set(STUDIO_RESOURCE_FILES "") endif() - add_executable(mujoco_studio main.cc ${STUDIO_RESOURCE_FILES}) + add_executable(mujoco_studio + main.cc + launcher.cc + launcher.h + ${STUDIO_RESOURCE_FILES} + ) if(APPLE AND MUJOCO_BUILD_MACOS_FRAMEWORKS) set_target_properties(mujoco_studio PROPERTIES MACOSX_BUNDLE TRUE diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 06d470bf..d56e1767 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -943,17 +943,19 @@ void App::BuildGui() { } ImGui::End(); - if (ImGui::Begin("Editor", &tmp_.inspector_panel)) { - SpecEditorGui(); - } - ImGui::End(); - if (ImGui::Begin("Inspector", &tmp_.inspector_panel)) { DataInspectorGui(); } ImGui::End(); } + if (tmp_.editor_panel) { + if (ImGui::Begin("Editor", &tmp_.editor_panel)) { + SpecEditorGui(); + } + ImGui::End(); + } + if (tmp_.profiler) { if (ImGui::Begin("Profiler", &tmp_.profiler, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { @@ -1994,6 +1996,12 @@ void App::MainMenuGui() { "Shift+Tab")) { tmp_.inspector_panel = !tmp_.inspector_panel; } + if (ImGui::MenuItem(tmp_.editor_panel ? "Hide Editor" : "Show Editor")) { + tmp_.editor_panel = !tmp_.editor_panel; + if (tmp_.editor_panel) { + tmp_.inspector_panel = true; + } + } if (ImGui::MenuItem( tmp_.toolbar ? "Hide Toolbar" : "Show Toolbar")) { tmp_.toolbar = !tmp_.toolbar; diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index 9529679f..2fae3e8e 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -138,6 +138,7 @@ class App { bool toolbar = false; bool status_bar = false; bool inspector_panel = true; + bool editor_panel = false; bool full_screen = false; bool style_editor = false; bool imgui_demo = false; diff --git a/src/experimental/studio/launcher.cc b/src/experimental/studio/launcher.cc new file mode 100644 index 00000000..ac04c56c --- /dev/null +++ b/src/experimental/studio/launcher.cc @@ -0,0 +1,160 @@ +// 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/studio/launcher.h" + +#include +#include +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include + +#include +#include "experimental/platform/hal/graphics_mode.h" +#include "experimental/platform/sys_utils.h" +#include "experimental/studio/app.h" + +namespace mujoco::studio { +namespace { + +std::string Resolve(std::string_view path) { + std::string_view subpath = path.substr(path.find(':') + 1); + std::filesystem::path exe_dir = mujoco::platform::GetModuleDir((void*)&Resolve); + if (exe_dir.empty()) { + return std::string("assets/") + std::string(subpath); + } + std::filesystem::path resources_dir = exe_dir.parent_path() / "Resources"; + if (std::filesystem::exists(resources_dir / "assets")) { + return (resources_dir / "assets" / subpath).string(); + } + return (exe_dir / "assets" / subpath).string(); +} + +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; +}; + +} // namespace + +int LaunchStudio(int argc, char** argv, LauncherConfig config) { + const char* home = std::getenv("HOME"); + const std::string ini_path = std::string(home ? home : ".") + "/.mujoco.ini"; + + mjpResourceProvider resource_provider; + mjp_defaultResourceProvider(&resource_provider); + + resource_provider.open = [](mjResource* resource) { + const std::string resolved_path = Resolve(resource->name); + FileResource* f = new FileResource(resolved_path); + if (f->Size() == 0) { + delete f; + return 0; + } + 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->data = nullptr; + }; + + resource_provider.prefix = "font"; + mjp_registerResourceProvider(&resource_provider); + resource_provider.prefix = "filament"; + mjp_registerResourceProvider(&resource_provider); + + if (config.gfx_mode.empty()) { + const char* display = std::getenv("DISPLAY"); + if (display && strcmp(display, ":20") == 0) { + config.gfx_mode = "opengl_headless"; + } + } + + const char* session_type = std::getenv("XDG_SESSION_TYPE"); + const char* wayland_display = std::getenv("WAYLAND_DISPLAY"); + if ((session_type && std::string_view(session_type) == "wayland") || + wayland_display) { + if (config.gfx_mode.empty()) { + config.gfx_mode = "opengl_headless"; + } else if (config.gfx_mode == "classic" || config.gfx_mode == "opengl") { + mju_error( + "Wayland does not support '%s' graphics mode. " + "Restart with a different graphics mode, or login using X11.", + config.gfx_mode.c_str()); + } + } + + mujoco::platform::GraphicsMode gfx_mode = + mujoco::platform::GraphicsModeFromString( + config.gfx_mode, mujoco::platform::GraphicsMode::FilamentOpenGl); + + // Use config values if they are set (non-default), otherwise use flags. + mujoco::studio::App app({ + .width = config.window_width, + .height = config.window_height, + .ini_path = ini_path, + .gfx_mode = gfx_mode, + .title = config.title, + }); + + if (config.model_file.empty()) { + app.InitEmptyModel(); + } else { + app.LoadModelFromFile(config.model_file); + } + + while (app.Update()) { + app.BuildGui(); + app.Render(); + } + return 0; +} + +} // namespace mujoco::studio diff --git a/src/experimental/studio/launcher.h b/src/experimental/studio/launcher.h new file mode 100644 index 00000000..a92cb74d --- /dev/null +++ b/src/experimental/studio/launcher.h @@ -0,0 +1,49 @@ +// 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_STUDIO_LAUNCHER_H_ +#define MUJOCO_SRC_EXPERIMENTAL_STUDIO_LAUNCHER_H_ + +#include +#include "experimental/studio/app.h" + +namespace mujoco::studio { + +struct LauncherConfig { + // Title of the window. + std::string title = "MuJoCo Studio"; + + // Initial window width. + int window_width = 1400; + + // Initial window height. + int window_height = 720; + + // Model file to load on startup. If empty, the launcher will try to load + // from flags or command line arguments. + std::string model_file = ""; + + std::string gfx_mode = ""; +}; + +// Runs the MuJoCo Studio application. +// This function handles resource provider registration, graphics mode detection, +// app creation, and the main loop. +// +// Expects absl::ParseCommandLine to have been called before this. +int LaunchStudio(int argc, char** argv, LauncherConfig config = {}); + +} // namespace mujoco::studio + +#endif // MUJOCO_SRC_EXPERIMENTAL_STUDIO_LAUNCHER_H_ diff --git a/src/experimental/studio/main.cc b/src/experimental/studio/main.cc index 7fa7afe1..c0f6ce7b 100644 --- a/src/experimental/studio/main.cc +++ b/src/experimental/studio/main.cc @@ -1,4 +1,4 @@ -// Copyright 2025 DeepMind Technologies Limited +// 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. @@ -12,151 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Main entry point for the Filament-based MuJoCo renderer. - -#include -#include -#include // NOLINT(build/c++17) -#include -#include -#include #include -#include -#include #include #include -#include -#include "experimental/platform/hal/graphics_mode.h" -#include "experimental/platform/sys_utils.h" -#include "experimental/studio/app.h" +#include "experimental/studio/launcher.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."); ABSL_FLAG(std::string, gfx, "", "Graphics API"); -std::string Resolve(std::string_view path) { - std::string_view subpath = path.substr(path.find(':') + 1); - std::filesystem::path exe_dir = mujoco::platform::GetModuleDir((void*)&Resolve); - if (exe_dir.empty()) { - return std::string("assets/") + std::string(subpath); - } - std::filesystem::path resources_dir = exe_dir.parent_path() / "Resources"; - if (std::filesystem::exists(resources_dir / "assets")) { - return (resources_dir / "assets" / subpath).string(); - } - return (exe_dir / "assets" / subpath).string(); -} - -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) { +int main(int argc, char** argv) { absl::ParseCommandLine(argc, argv); - const char* home = std::getenv("HOME"); - const std::string ini_path = std::string(home ? home : ".") + "/.mujoco.ini"; - - mjpResourceProvider resource_provider; - mjp_defaultResourceProvider(&resource_provider); - - resource_provider.open = [](mjResource* resource) { - const std::string resolved_path = Resolve(resource->name); - FileResource* f = new FileResource(resolved_path); - if (f->Size() == 0) { - delete f; - return 0; - } - 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->data = nullptr; - }; - - resource_provider.prefix = "font"; - mjp_registerResourceProvider(&resource_provider); - resource_provider.prefix = "filament"; - mjp_registerResourceProvider(&resource_provider); - - std::string gfx = absl::GetFlag(FLAGS_gfx); - - const char* session_type = std::getenv("XDG_SESSION_TYPE"); - const char* wayland_display = std::getenv("WAYLAND_DISPLAY"); - if ((session_type && std::string_view(session_type) == "wayland") || - wayland_display) { - if (gfx.empty()) { - gfx = "opengl_headless"; - } else if (gfx == "classic" || gfx == "opengl") { - mju_error( - "Wayland does not support '%s' graphics mode. " - "Restart with a different graphics mode, or login using X11.", - gfx.c_str()); - } - } - - mujoco::platform::GraphicsMode gfx_mode = - mujoco::platform::GraphicsModeFromString( - gfx, mujoco::platform::GraphicsMode::FilamentOpenGl); - - const int width = absl::GetFlag(FLAGS_window_width); - const int height = absl::GetFlag(FLAGS_window_height); - mujoco::studio::App app({ - .width = width, - .height = height, - .ini_path = ini_path, - .gfx_mode = gfx_mode, - }); - - // If the model file is not specified, try to load it from the first argument std::string model_file = absl::GetFlag(FLAGS_model_file); - if (model_file.empty() && argc > 1 && argv[1][0] != '-') model_file = argv[1]; - - if (model_file.empty()) { - app.InitEmptyModel(); - } else { - app.LoadModelFromFile(model_file); + if (model_file.empty() && argc > 1 && argv[1][0] != '-') { + model_file = argv[1]; } - while (app.Update()) { - app.BuildGui(); - app.Render(); - } - return 0; + return mujoco::studio::LaunchStudio(argc, argv, { + .window_width = absl::GetFlag(FLAGS_window_width), + .window_height = absl::GetFlag(FLAGS_window_height), + .model_file = model_file, + .gfx_mode = absl::GetFlag(FLAGS_gfx), + }); }