From 3a5a48b9f9450683f6574abe49e3f63c7f8fe61f Mon Sep 17 00:00:00 2001 From: Matija Kecman Date: Mon, 27 Apr 2026 01:58:43 -0700 Subject: [PATCH 01/13] Fix texture ID collisions in ImguiBridge Texture IDs were assigned using textures_.size() + 1, which is not monotonic: after erasures the map shrinks and previously used IDs can be reassigned to new textures. This causes multiple ImGui images to reference the same underlying Filament texture, rendering them all with the same pixel data. This fix replaces the size-based scheme with a monotonically increasing counter (next_tex_id_) that never reuses IDs. PiperOrigin-RevId: 906214496 Change-Id: I5a388861ad9565c8e00e27b39cc9c76c9750ca1e --- src/experimental/filament/compat/imgui_bridge.cc | 4 ++-- src/experimental/filament/compat/imgui_bridge.h | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/experimental/filament/compat/imgui_bridge.cc b/src/experimental/filament/compat/imgui_bridge.cc index 71c9da01..c7b83514 100644 --- a/src/experimental/filament/compat/imgui_bridge.cc +++ b/src/experimental/filament/compat/imgui_bridge.cc @@ -72,7 +72,7 @@ uintptr_t ImguiBridge::UploadImage(uintptr_t tex_id, const uint8_t* pixels, // Assign a new texture ID. if (tex_id == 0) { - tex_id = textures_.size() + 1; + tex_id = next_tex_id_++; } std::unique_ptr& texture = textures_[tex_id]; @@ -123,7 +123,7 @@ void ImguiBridge::CreateTexture(ImTextureData* data) { config.format = mjPIXEL_FORMAT_RGBA8; config.color_space = mjCOLORSPACE_LINEAR; - const uintptr_t tex_id = textures_.size() + 1; + const uintptr_t tex_id = next_tex_id_++; textures_[tex_id] = std::make_unique(scene_view_->GetEngine(), config); data->SetTexID((ImTextureID)tex_id); diff --git a/src/experimental/filament/compat/imgui_bridge.h b/src/experimental/filament/compat/imgui_bridge.h index 06807f68..953cbd1e 100644 --- a/src/experimental/filament/compat/imgui_bridge.h +++ b/src/experimental/filament/compat/imgui_bridge.h @@ -64,6 +64,7 @@ class ImguiBridge { std::vector> renderables_; std::vector> meshes_; std::unordered_map> textures_; + uintptr_t next_tex_id_ = 1; }; // Draws text at the given screen coordinates in clip space (i.e. [-1,-1,-1] to From 2e3205229c6339f3fbdb031e0b038e9bd7685426 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 27 Apr 2026 03:11:07 -0700 Subject: [PATCH 02/13] Studio: various panel usability improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reorder Physics section, move Actuator Groups to end. - Rename "Physics Settings" → "Physics", "Rendering Settings" → "Rendering". - Reduce IndentSpacing from 20 to 6. - Reduce FramePadding Y from 6 to 2 for narrower buttons and popups. - Remove explicit Unindent/Indent overrides in Rendering and Visibility Groups sections so buttons are naturally indented under their tree nodes. - Dynamically adjust convergence and counts plot X-axis limits based on actual solver iterations instead of hardcoded 20. PiperOrigin-RevId: 906244477 Change-Id: Ic3e6e6e793395158c8cb6692af885884ca3f7f50 --- src/experimental/platform/ux/gui.cc | 66 +++++++++++++++-------------- src/experimental/studio/app.cc | 4 +- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index b20993ee..e70bf13c 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -192,7 +192,7 @@ void SetupTheme(GuiTheme theme) { float rounding = 4.0f; s.DisplaySafeAreaPadding = ImVec2(0, 0); s.WindowPadding = ImVec2(hspacing, vspacing); - s.FramePadding = ImVec2(hspacing, vspacing); + s.FramePadding = ImVec2(hspacing, 2); s.ItemSpacing = ImVec2(hspacing, vspacing); s.ItemInnerSpacing = ImVec2(hspacing, vspacing); s.WindowRounding = rounding; @@ -205,7 +205,7 @@ void SetupTheme(GuiTheme theme) { s.WindowBorderSize = 0.0f; s.FrameBorderSize = 1.0f; s.PopupBorderSize = 1.0f; - s.IndentSpacing = 20.0f; + s.IndentSpacing = 6.0f; s.ScrollbarSize = 12.0f; s.GrabMinSize = 5.0f; s.WindowMenuButtonPosition = ImGuiDir_None; @@ -817,22 +817,6 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Actuator Groups")) { - if (ImGui::BeginTable("##ActuatorGroupsTable", num_cols)) { - const ImVec2 size = GetFlexElementSize(num_cols); - for (int i = 0; i < 6; ++i) { - char label[64]; - std::snprintf(label, sizeof(label), "Act Group %d", i); - ImGui::TableNextColumn(); - int flipped = ~opt.disableactuator; - ImGui_BitToggle(label, &flipped, 1 << i, size); - opt.disableactuator = ~flipped; - } - ImGui::EndTable(); - } - ImGui::TreePop(); - }; - if (ImGui::TreeNodeEx("Algorithmic Parameters")) { ImGui_Input("Timestep", &opt.timestep, {0, 1, 0.01, 0.1}); ImGui_Input("Iterations", &opt.iterations, {0, 1000, 1, 10}); @@ -867,6 +851,22 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::TreePop(); } + if (ImGui::TreeNodeEx("Actuator Groups")) { + if (ImGui::BeginTable("##ActuatorGroupsTable", num_cols)) { + const ImVec2 size = GetFlexElementSize(num_cols); + for (int i = 0; i < 6; ++i) { + char label[64]; + std::snprintf(label, sizeof(label), "Act Group %d", i); + ImGui::TableNextColumn(); + int flipped = ~opt.disableactuator; + ImGui_BitToggle(label, &flipped, 1 << i, size); + opt.disableactuator = ~flipped; + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + ImGui::PopItemWidth(); } @@ -986,8 +986,6 @@ void RenderingGui(const mjModel* model, mjvOption* vis_options, static_cast(std::floor(available_width / min_width)), 1, 6); if (ImGui::TreeNodeEx("Model Elements", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing() / 2); - if (ImGui::BeginTable("##ModelElementsTable", num_cols)) { const ImVec2 size = GetFlexElementSize(num_cols); for (int i = 0; i < mjNVISFLAG; ++i) { @@ -996,14 +994,10 @@ void RenderingGui(const mjModel* model, mjvOption* vis_options, } ImGui::EndTable(); } - - ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing() / 2); ImGui::TreePop(); } if (ImGui::TreeNodeEx("Render Flags", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing() / 2); - if (ImGui::BeginTable("##RenderFlagsTable", num_cols)) { const ImVec2 size = GetFlexElementSize(num_cols); for (int i = 0; i < mjNRNDFLAG; ++i) { @@ -1012,8 +1006,6 @@ void RenderingGui(const mjModel* model, mjvOption* vis_options, } ImGui::EndTable(); } - - ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing() / 2); ImGui::TreePop(); } } @@ -1030,8 +1022,6 @@ void GroupsGui(const mjModel* model, mjvOption* vis_options, float min_width) { auto GroupGui = [&](const char* name, mjtByte* group) { if (ImGui::TreeNodeEx(name, ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing() / 2); - char label[64]; std::snprintf(label, sizeof(label), "##%s", name); if (ImGui::BeginTable(label, num_cols)) { @@ -1044,8 +1034,6 @@ void GroupsGui(const mjModel* model, mjvOption* vis_options, float min_width) { ImGui::EndTable(); } - - ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing() / 2); ImGui::TreePop(); } }; @@ -1150,12 +1138,24 @@ void ControlsGui(const mjModel* model, const mjData* data, ImGui::PopItemWidth(); } +static int GetPlotXLimit(const mjData* data) { + int max_niter = 0; + const int nisland0 = + data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0; + for (int k = 0; k < nisland0; k++) { + max_niter = mjMAX(max_niter, data->solver_niter[k]); + } + return mjMAX(10, ((max_niter + 9) / 10) * 10); +} + void ConvergenceGui(const mjModel* model, mjData* data) { + int xlim = GetPlotXLimit(data); + if (ImPlot::BeginPlot("Convergence (log 10)", ImVec2(-1, 0), ImPlotFlags_NoMouseText)) { ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f); ImPlot::SetupAxis(ImAxis_X1, "iteration", ImPlotAxisFlags_AutoFit); - ImPlot::SetupAxisLimits(ImAxis_X1, 0, 20, ImPlotCond_Always); + ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always); ImPlot::SetupAxisFormat(ImAxis_Y1, "%.1f"); ImPlot::SetupAxisLimits(ImAxis_Y1, -20, 5, ImPlotCond_Always); ImPlot::SetupLegend(ImPlotLocation_NorthEast); @@ -1212,10 +1212,12 @@ void ConvergenceGui(const mjModel* model, mjData* data) { } void CountsGui(const mjModel* model, mjData* data) { + int xlim = GetPlotXLimit(data); + if (ImPlot::BeginPlot("Counts", ImVec2(-1, 0), ImPlotFlags_NoMouseText)) { ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f); ImPlot::SetupAxis(ImAxis_X1, "iteration", ImPlotAxisFlags_AutoFit); - ImPlot::SetupAxisLimits(ImAxis_X1, 0, 20, ImPlotCond_Always); + ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always); ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0f"); ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 80, ImPlotCond_Always); ImPlot::SetupLegend(ImPlotLocation_NorthEast); diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index e7ad6c89..24a43649 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -1011,14 +1011,14 @@ void App::ModelOptionsGui() { ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_Framed; ImGui::BeginChild("PhysicsGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Physics Settings", node_flags)) { + if (ImGui::TreeNodeEx("Physics", node_flags)) { platform::PhysicsGui(model(), min_width); ImGui::TreePop(); } ImGui::EndChild(); ImGui::BeginChild("RenderingGui", {0, 0}, child_flags); - if (ImGui::TreeNodeEx("Rendering Settings", node_flags)) { + if (ImGui::TreeNodeEx("Rendering", node_flags)) { platform::RenderingGui(model(), &vis_options_, renderer_->GetRenderFlags(), min_width); ImGui::TreePop(); From 9cd89eec2fd2ffce76206945736457deaa180fea Mon Sep 17 00:00:00 2001 From: Matija Kecman Date: Mon, 27 Apr 2026 07:42:16 -0700 Subject: [PATCH 03/13] Make Studio's monitoring charts responsive and full-window * **Dynamic Full-Window Scaling**: Charts in the Performance and Solver panels now expand to fill the entire pane, automatically arranging side-by-side or stacked based on window orientation to eliminate scrollbars. * **Responsive Decorators**: Added reusable C++ ImPlot helper functions that auto-hide titles, legends, and tick labels when the plot surface is small. * **Cleaner Visualization**: Refined chart titles (e.g., "CPU msec vs frame", "Convergence vs iter") and removed redundant axis labels for improved scannability. These changes were based on the plot behavior of //third_party/mujoco/src/experimental/py/sample/implot.py This is an incremental improvement, we can make the plots even nicer in future. PiperOrigin-RevId: 906351914 Change-Id: I8cb19c19bf807f0107c717066250a71bcf7e325f --- src/experimental/platform/sim/sim_profiler.cc | 41 +++++------ src/experimental/platform/sim/sim_profiler.h | 5 +- src/experimental/platform/ux/gui.cc | 21 +++--- src/experimental/platform/ux/gui.h | 6 +- src/experimental/platform/ux/imgui_widgets.cc | 70 +++++++++++++++++++ src/experimental/platform/ux/imgui_widgets.h | 41 +++++++++++ src/experimental/studio/app.cc | 17 +++-- 7 files changed, 162 insertions(+), 39 deletions(-) diff --git a/src/experimental/platform/sim/sim_profiler.cc b/src/experimental/platform/sim/sim_profiler.cc index 7aca0c4c..e3b794b8 100644 --- a/src/experimental/platform/sim/sim_profiler.cc +++ b/src/experimental/platform/sim/sim_profiler.cc @@ -17,12 +17,11 @@ #include #include #include +#include "experimental/platform/ux/imgui_widgets.h" namespace mujoco::platform { -SimProfiler::SimProfiler() { - Clear(); -} +SimProfiler::SimProfiler() { Clear(); } void SimProfiler::Clear() { constexpr int kProfilerMaxFrames = 200; @@ -88,21 +87,21 @@ void SimProfiler::Update(const mjModel* model, const mjData* data) { // Solver diagnostics. mjtNum sqrt_nnz = 0; int solver_niter = 0; - const int nisland = data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0; - for (int island=0; island < nisland; island++) { + const int nisland = + data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0; + for (int island = 0; island < nisland; island++) { sqrt_nnz += data->solver_nnz[island]; solver_niter += data->solver_niter[island]; } sqrt_nnz = mju_sqrt(sqrt_nnz); dim_dof_.erase(dim_dof_.begin()); - int nv = (model->opt.enableflags & mjENBL_SLEEP) ? data->nv_awake - : model->nv; + int nv = (model->opt.enableflags & mjENBL_SLEEP) ? data->nv_awake : model->nv; dim_dof_.push_back(nv); dim_body_.erase(dim_body_.begin()); int nbody = (model->opt.enableflags & mjENBL_SLEEP) ? data->nbody_awake - : model->nbody; + : model->nbody; dim_body_.push_back(nbody); dim_constraint_.erase(dim_constraint_.begin()); @@ -115,16 +114,17 @@ void SimProfiler::Update(const mjModel* model, const mjData* data) { dim_contact_.push_back(data->ncon); dim_iteration_.erase(dim_iteration_.begin()); - dim_iteration_.push_back(static_cast(solver_niter) / nisland); + dim_iteration_.push_back(static_cast(solver_niter) / + mjMAX(1, nisland)); } - -void SimProfiler::CpuTimeGraph() { - if (ImPlot::BeginPlot("CPU Time", ImVec2(-1, 0), ImPlotFlags_NoMouseText)) { +void SimProfiler::CpuTimeGraph(ImVec2 plot_size) { + ImPlotFlags flags = + ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText; + if (ImPlot::BeginPlot("CPU msec vs frame", plot_size, flags)) { ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f); - ImPlot::SetupAxis(ImAxis_X1, "frame", ImPlotAxisFlags_AutoFit); - ImPlot::SetupAxis(ImAxis_Y1, "msec", ImPlotAxisFlags_AutoFit); - ImPlot::SetupAxisFormat(ImAxis_Y1, "%.2f"); + ImPlot_SetupTimeAxis(plot_size, ""); + ImPlot_SetupValueAxis(plot_size, "", "%.2f"); ImPlot::SetupLegend(ImPlotLocation_NorthEast); ImPlot::SetupFinish(); @@ -143,12 +143,13 @@ void SimProfiler::CpuTimeGraph() { } } -void SimProfiler::DimensionsGraph() { - if (ImPlot::BeginPlot("Dimensions", ImVec2(-1, 0), ImPlotFlags_NoMouseText)) { +void SimProfiler::DimensionsGraph(ImVec2 plot_size) { + ImPlotFlags flags = + ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText; + if (ImPlot::BeginPlot("Dimensions vs frame", plot_size, flags)) { ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f); - ImPlot::SetupAxis(ImAxis_X1, "frame", ImPlotAxisFlags_AutoFit); - ImPlot::SetupAxis(ImAxis_Y1, "count", ImPlotAxisFlags_AutoFit); - ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0f"); + ImPlot_SetupTimeAxis(plot_size, ""); + ImPlot_SetupValueAxis(plot_size, "", "%.0f"); ImPlot::SetupLegend(ImPlotLocation_NorthEast); ImPlot::SetupFinish(); diff --git a/src/experimental/platform/sim/sim_profiler.h b/src/experimental/platform/sim/sim_profiler.h index 4e9cce46..c1f1ef8e 100644 --- a/src/experimental/platform/sim/sim_profiler.h +++ b/src/experimental/platform/sim/sim_profiler.h @@ -17,6 +17,7 @@ #include +#include #include namespace mujoco::platform { @@ -33,8 +34,8 @@ class SimProfiler { void Update(const mjModel* model, const mjData* data); // Displays the profiling data using ImPlot. - void CpuTimeGraph(); - void DimensionsGraph(); + void CpuTimeGraph(ImVec2 plot_size = ImVec2(-1, 0)); + void DimensionsGraph(ImVec2 plot_size = ImVec2(-1, 0)); private: std::vector cpu_total_; diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index e70bf13c..9098092c 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -506,7 +506,7 @@ bool CameraSelectionGui(const mjModel* model, mjData* data, mjvCamera& camera, auto select = [&](int type, int idx) { if (ImGui::Selectable(GetCameraName(model, camera, type).c_str(), - (type == idx))) { + (type == idx))) { return true; } return false; @@ -1148,13 +1148,13 @@ static int GetPlotXLimit(const mjData* data) { return mjMAX(10, ((max_niter + 9) / 10) * 10); } -void ConvergenceGui(const mjModel* model, mjData* data) { +void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) { int xlim = GetPlotXLimit(data); - - if (ImPlot::BeginPlot("Convergence (log 10)", ImVec2(-1, 0), - ImPlotFlags_NoMouseText)) { + ImPlotFlags flags = + ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText; + if (ImPlot::BeginPlot("Convergence (log 10) vs iter", plot_size, flags)) { ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f); - ImPlot::SetupAxis(ImAxis_X1, "iteration", ImPlotAxisFlags_AutoFit); + ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_AutoFit); ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always); ImPlot::SetupAxisFormat(ImAxis_Y1, "%.1f"); ImPlot::SetupAxisLimits(ImAxis_Y1, -20, 5, ImPlotCond_Always); @@ -1211,12 +1211,13 @@ void ConvergenceGui(const mjModel* model, mjData* data) { } } -void CountsGui(const mjModel* model, mjData* data) { +void CountsGui(const mjModel* model, mjData* data, ImVec2 plot_size) { int xlim = GetPlotXLimit(data); - - if (ImPlot::BeginPlot("Counts", ImVec2(-1, 0), ImPlotFlags_NoMouseText)) { + ImPlotFlags flags = + ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText; + if (ImPlot::BeginPlot("Counts vs iter", plot_size, flags)) { ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f); - ImPlot::SetupAxis(ImAxis_X1, "iteration", ImPlotAxisFlags_AutoFit); + ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_AutoFit); ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always); ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0f"); ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 80, ImPlotCond_Always); diff --git a/src/experimental/platform/ux/gui.h b/src/experimental/platform/ux/gui.h index 938466bb..4dc8b69c 100644 --- a/src/experimental/platform/ux/gui.h +++ b/src/experimental/platform/ux/gui.h @@ -138,10 +138,12 @@ void NoiseGui(const mjModel* model, const mjData* data, float& noise_scale, float& noise_rate); // UX for the solver convergence chart. -void ConvergenceGui(const mjModel* model, mjData* data); +void ConvergenceGui(const mjModel* model, mjData* data, + ImVec2 plot_size = ImVec2(-1, 0)); // UX for the solver counts chart. -void CountsGui(const mjModel* model, mjData* data); +void CountsGui(const mjModel* model, mjData* data, + ImVec2 plot_size = ImVec2(-1, 0)); // UX for displaying basic simulation information. Note that the pause state and // FPS needs to be tracked by the caller and passed here to be displayed. diff --git a/src/experimental/platform/ux/imgui_widgets.cc b/src/experimental/platform/ux/imgui_widgets.cc index 61d2989a..7d0f1cba 100644 --- a/src/experimental/platform/ux/imgui_widgets.cc +++ b/src/experimental/platform/ux/imgui_widgets.cc @@ -14,6 +14,7 @@ #include "experimental/platform/ux/imgui_widgets.h" +#include #include #include #include @@ -23,6 +24,7 @@ #include #include +#include #include namespace mujoco::platform { @@ -378,4 +380,72 @@ void MaybeSaveToClipboard(const std::string& contents) { } } +ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size) { + ImPlotFlags flags = ImPlotFlags_None; + if (plot_size.x > 0 && plot_size.y > 0) { + const float min_dim = std::min(plot_size.x, plot_size.y); + if (min_dim < 300) { + flags |= ImPlotFlags_NoTitle; + } + if (min_dim < 200) { + flags |= ImPlotFlags_NoLegend; + } + } + return flags; +} + +void ImPlot_SetupTimeAxis(ImVec2 plot_size, const char* label, + ImPlotAxisFlags extra_flags) { + ImPlotAxisFlags flags = extra_flags; + if (plot_size.x > 0 && plot_size.x < 300) { + flags |= ImPlotAxisFlags_NoTickLabels; + } + ImPlot::SetupAxis(ImAxis_X1, label, flags); +} + +void ImPlot_SetupValueAxis(ImVec2 plot_size, const char* label, + const char* format, ImPlotAxisFlags extra_flags) { + ImPlotAxisFlags flags = extra_flags; + if (plot_size.y > 0 && plot_size.y < 150) { + flags |= ImPlotAxisFlags_NoTickLabels; + } + ImPlot::SetupAxis(ImAxis_Y1, label, flags); + if (format) { + ImPlot::SetupAxisFormat(ImAxis_Y1, format); + } +} + +void ImPlot_SetupFixedAxis(ImVec2 plot_size, double y_min, double y_max, + const char* label, const char* format, + const double* tick_values, + const char* const* tick_labels, int n_ticks) { + ImPlotAxisFlags flags = ImPlotAxisFlags_None; + if (plot_size.y > 0 && plot_size.y < 150) { + flags |= ImPlotAxisFlags_NoTickLabels; + } + ImPlot::SetupAxis(ImAxis_Y1, label, flags); + ImPlot::SetupAxisLimits(ImAxis_Y1, y_min, y_max, ImPlotCond_Always); + if (format) { + ImPlot::SetupAxisFormat(ImAxis_Y1, format); + } + if (tick_values && n_ticks > 0) { + ImPlot::SetupAxisTicks(ImAxis_Y1, tick_values, n_ticks, tick_labels); + } +} + +ImPlotPairLayout ImPlot_ComputePairLayout() { + ImVec2 avail = ImGui::GetContentRegionAvail(); + bool is_wide = avail.x > avail.y; + + const float item_spacing = ImGui::GetStyle().ItemSpacing.y; + ImVec2 plot_size(is_wide ? (avail.x - item_spacing) * 0.5f : avail.x, + is_wide ? avail.y : (avail.y - item_spacing) * 0.5f); + + return { + plot_size, + is_wide ? ImPlotLayoutDirection::kHorizontal + : ImPlotLayoutDirection::kVertical, + }; +} + } // namespace mujoco::platform diff --git a/src/experimental/platform/ux/imgui_widgets.h b/src/experimental/platform/ux/imgui_widgets.h index 619e1055..187dabba 100644 --- a/src/experimental/platform/ux/imgui_widgets.h +++ b/src/experimental/platform/ux/imgui_widgets.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "experimental/platform/ux/enum_utils.h" @@ -566,6 +567,46 @@ inline void EndBoxSection() { ImGui::EndTable(); } // Saves the given contents to the clipboard if the clipboard is available. void MaybeSaveToClipboard(const std::string& contents); +// Returns plot flags with title/legend conditionally hidden when the plot +// area is too small. `plot_size` is the final rendered size of the plot. +ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size); + +// Sets up the X axis as a "time/frame" axis. +// Hides tick labels when the plot is narrow. +// Uses `label` as the axis label (empty string to hide) and auto-fit limits. +void ImPlot_SetupTimeAxis( + ImVec2 plot_size, const char* label = "", + ImPlotAxisFlags extra_flags = ImPlotAxisFlags_AutoFit); + +// Sets up a Y axis with auto-fit limits. +// Hides tick labels when the plot is short. +void ImPlot_SetupValueAxis( + ImVec2 plot_size, const char* label = "", const char* format = nullptr, + ImPlotAxisFlags extra_flags = ImPlotAxisFlags_AutoFit); + +// Sets up a Y axis with fixed limits and optional explicit ticks. +// Hides tick labels when the plot is short. +void ImPlot_SetupFixedAxis(ImVec2 plot_size, double y_min, double y_max, + const char* label = "", const char* format = nullptr, + const double* tick_values = nullptr, + const char* const* tick_labels = nullptr, + int n_ticks = 0); + +enum class ImPlotLayoutDirection { + kHorizontal, + kVertical, +}; + +struct ImPlotPairLayout { + ImVec2 plot_size; // Size for each individual plot. + ImPlotLayoutDirection direction; +}; + +// Computes a responsive layout for two plots that share the available +// content region. When the region is wider than tall, the plots are placed +// side-by-side; otherwise they are stacked vertically. +ImPlotPairLayout ImPlot_ComputePairLayout(); + } // namespace mujoco::platform #endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_UX_IMGUI_WIDGETS_H_ diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 24a43649..dec270ae 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -884,8 +884,12 @@ void App::BuildGui() { ImGui::SetNextWindowPos(chart_pos, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(chart_size, ImGuiCond_FirstUseEver); if (ImGui::Begin("Performance", &tmp_.chart_performance)) { - profiler_.CpuTimeGraph(); - profiler_.DimensionsGraph(); + auto layout = platform::ImPlot_ComputePairLayout(); + profiler_.CpuTimeGraph(layout.plot_size); + if (layout.direction == platform::ImPlotLayoutDirection::kHorizontal) { + ImGui::SameLine(); + } + profiler_.DimensionsGraph(layout.plot_size); } ImGui::End(); } @@ -894,8 +898,12 @@ void App::BuildGui() { ImGui::SetNextWindowPos(chart_pos, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(chart_size, ImGuiCond_FirstUseEver); if (ImGui::Begin("Solver", &tmp_.chart_solver)) { - platform::CountsGui(model(), data()); - platform::ConvergenceGui(model(), data()); + auto layout = platform::ImPlot_ComputePairLayout(); + platform::CountsGui(model(), data(), layout.plot_size); + if (layout.direction == platform::ImPlotLayoutDirection::kHorizontal) { + ImGui::SameLine(); + } + platform::ConvergenceGui(model(), data(), layout.plot_size); } ImGui::End(); } @@ -1452,7 +1460,6 @@ void App::ToolBarGui() { } ImGui::SetItemTooltip("%s", "Reset"); - // Combined (Normal Pause, Viscous Pause, Play) widget and Speed selection. ImGui::SameLine(0, separator_width); platform::StepControlGui(model(), &step_control_, tmp_.speed_index); From e6354b43687cd2b72827dd731d9a9d11078255bf Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 27 Apr 2026 13:32:20 -0700 Subject: [PATCH 04/13] Add flex_interp to the MJX Model. PiperOrigin-RevId: 906518134 Change-Id: If716e7e67827a6659fb72a6064809910fe99944f --- mjx/mujoco/mjx/_src/types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index faf47012..e29d4aca 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -775,6 +775,7 @@ class Model(PyTreeNode): mesh_texcoord: np.ndarray flex_vertadr: np.ndarray flex_vertnum: np.ndarray + flex_interp: np.ndarray flex_vert0: np.ndarray flex_nodeadr: np.ndarray flex_nodenum: np.ndarray From 25a9114705bf93013c83398b26391a7ef612d607 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 28 Apr 2026 02:10:44 -0700 Subject: [PATCH 05/13] No-op refactor of PGS and NoSlip solvers in preparation for island support. PiperOrigin-RevId: 906826882 Change-Id: I2003097e1bb81ebabda3a7075f1f8200d4d5ff95 --- src/engine/engine_solver.c | 281 +++++++++++++++++++++---------------- 1 file changed, 157 insertions(+), 124 deletions(-) diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index 1644f841..5700a3e5 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -78,9 +78,11 @@ static void dualFinish(const mjModel* m, mjData* d) { // compute 1/diag(AR) +// res[c] = 1 / AR[efclist[c], efclist[c]] for c = 0..nefc-1 +// efclist is NULL for monolithic (sequential) iteration // TODO: b/295296178 - add island support to Dual solvers -static void ARdiaginv(const mjModel* m, const mjData* d, mjtNum* res, int flg_subR) { - int nefc = d->nefc; +static void ARdiaginv(const mjModel* m, const mjData* d, mjtNum* res, + int nefc, const int* efclist, int flg_subR) { const mjtNum *AR = d->efc_AR; const mjtNum *R = d->efc_R; @@ -90,12 +92,13 @@ static void ARdiaginv(const mjModel* m, const mjData* d, mjtNum* res, int flg_su const int *rownnz = d->efc_AR_rownnz; const int *colind = d->efc_AR_colind; - for (int i=0; i < nefc; i++) { + for (int c=0; c < nefc; c++) { + int i = efclist ? efclist[c] : c; int nnz = rownnz[i]; for (int j=0; j < nnz; j++) { int adr = rowadr[i] + j; if (i == colind[adr]) { - res[i] = 1 / (flg_subR ? mju_max(mjMINVAL, AR[adr] - R[i]) : AR[adr]); + res[c] = 1 / (flg_subR ? mju_max(mjMINVAL, AR[adr] - R[i]) : AR[adr]); break; } } @@ -104,9 +107,11 @@ static void ARdiaginv(const mjModel* m, const mjData* d, mjtNum* res, int flg_su // dense else { - for (int i=0; i < nefc; i++) { - int adr = i * (nefc + 1); - res[i] = 1 / (flg_subR ? mju_max(mjMINVAL, AR[adr] - R[i]) : AR[adr]); + int d_nefc = d->nefc; // global nefc + for (int c=0; c < nefc; c++) { + int i = efclist ? efclist[c] : c; + int adr = i * (d_nefc + 1); + res[c] = 1 / (flg_subR ? mju_max(mjMINVAL, AR[adr] - R[i]) : AR[adr]); } } } @@ -229,9 +234,10 @@ static mjtNum costChange(const mjtNum* A, mjtNum* force, const mjtNum* oldforce, // set efc_state to dual constraint state; return nactive +// iterates over efclist (or sequentially if NULL), classifies by ne/nf ranges // TODO: b/295296178 - add island support to Dual solvers -static int dualState(const mjModel* m, const mjData* d, int* state) { - int ne = d->ne, nf = d->nf, nefc = d->nefc; +static int dualState(const mjData* d, int* state, + int ne, int nf, int nefc, const int* efclist) { const mjtNum* force = d->efc_force; const mjtNum* floss = d->efc_frictionloss; @@ -239,10 +245,14 @@ static int dualState(const mjModel* m, const mjData* d, int* state) { int nactive = ne + nf; // equality - mju_fillInt(state, mjCNSTRSTATE_QUADRATIC, ne); + for (int c=0; c < ne; c++) { + int i = efclist ? efclist[c] : c; + state[i] = mjCNSTRSTATE_QUADRATIC; + } // friction - for (int i=ne; i < ne+nf; i++) { + for (int c=ne; c < ne+nf; c++) { + int i = efclist ? efclist[c] : c; if (force[i] <= -floss[i]) { state[i] = mjCNSTRSTATE_LINEARPOS; // opposite of primal } else if (force[i] >= floss[i]) { @@ -253,7 +263,9 @@ static int dualState(const mjModel* m, const mjData* d, int* state) { } // limit and contact - for (int i=ne+nf; i < nefc; i++) { + for (int c=ne+nf; c < nefc; c++) { + int i = efclist ? efclist[c] : c; + // non-negative if (d->efc_type[i] != mjCNSTR_CONTACT_ELLIPTIC) { if (force[i] <= 0) { @@ -302,7 +314,7 @@ static int dualState(const mjModel* m, const mjData* d, int* state) { mju_fillInt(state+i, result, dim); // advance - i += (dim-1); + c += (dim-1); } } @@ -310,26 +322,86 @@ static int dualState(const mjModel* m, const mjData* d, int* state) { } +// update constraint state, return nactive and nchange +static int dualStateChange(const mjData* d, int* state, int* oldstate, + int ne, int nf, int nefc, + const int* efclist, int* nchange) { + // save old state + for (int c=0; c < nefc; c++) { + int i = efclist ? efclist[c] : c; + oldstate[c] = state[i]; + } + + // update state + int nactive = dualState(d, state, ne, nf, nefc, efclist); + + // count state changes + *nchange = 0; + for (int c=0; c < nefc; c++) { + int i = efclist ? efclist[c] : c; + *nchange += (oldstate[c] != state[i]); + } + + return nactive; +} + + +// solve QCQP and project onto friction ellipsoid, write to force[i+1..i+dim-1] +static void solveQCQP(mjtNum* force, int i, int dim, + mjtNum* Ac, mjtNum* bc, const mjtNum* mu) { + int flg_active; + mjtNum v[6]; + + // solve + if (dim == 3) { + flg_active = mju_QCQP2(v, Ac, bc, mu, force[i]); + } else if (dim == 4) { + flg_active = mju_QCQP3(v, Ac, bc, mu, force[i]); + } else { // dim == 5 + flg_active = mju_QCQP(v, Ac, bc, mu, force[i], dim-1); + } + + // on constraint: put v on ellipsoid, in case QCQP is approximate + if (flg_active) { + mjtNum s = 0; + for (int j=0; j < dim-1; j++) { + s += v[j]*v[j] / (mu[j]*mu[j]); + } + s = mju_sqrt(force[i]*force[i] / mju_max(mjMINVAL, s)); + for (int j=0; j < dim-1; j++) { + v[j] *= s; + } + } + + // assign + mju_copy(force+i+1, v, dim-1); +} + + //---------------------------- PGS solver ---------------------------------------------------------- +// core PGS solver: iterates over constraints specified by efclist +// island: island index for stats (use -1 for monolithic, mapped to 0) +// ne, nf, nefc: constraint type counts +// efclist: maps list position c to monolithic efc index (NULL for sequential) // TODO: b/295296178 - add island support to Dual solvers -void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { - int ne = d->ne, nf = d->nf, nefc = d->nefc; +static void solPGS(const mjModel* m, mjData* d, int island, + int ne, int nf, int nefc, + const int* efclist, int maxiter) { const mjtNum *floss = d->efc_frictionloss; mjtNum *force = d->efc_force; mj_markStack(d); mjtNum* ARinv = mjSTACKALLOC(d, nefc, mjtNum); int* oldstate = mjSTACKALLOC(d, nefc, int); - // TODO: b/295296178 - Use island index (currently hardcoded to 0) - int island = 0; + int island_stat = mjMAX(0, island); // island index for diagnostic stats mjtNum scale = 1 / (m->stat.meaninertia * mjMAX(1, m->nv)); // precompute inverse diagonal of AR - ARdiaginv(m, d, ARinv, 0); + ARdiaginv(m, d, ARinv, nefc, efclist, 0); // initial constraint state - dualState(m, d, d->efc_state); + dualState(d, d->efc_state, ne, nf, nefc, efclist); // main iteration int iter = 0; @@ -338,7 +410,9 @@ void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { mjtNum improvement = 0; // perform one sweep - for (int i=0; i < nefc; i++) { + for (int c=0; c < nefc; c++) { + int i = efclist ? efclist[c] : c; + // get constraint dimensionality int dim; if (d->efc_type[i] == mjCNSTR_CONTACT_ELLIPTIC) { @@ -361,16 +435,16 @@ void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { // simple constraint if (d->efc_type[i] != mjCNSTR_CONTACT_ELLIPTIC) { // unconstrained minimum - force[i] -= res[0]*ARinv[i]; + force[i] -= res[0]*ARinv[c]; // impose interval and inequality constraints - if (i >= ne && i < ne+nf) { + if (c >= ne && c < ne+nf) { if (force[i] < -floss[i]) { force[i] = -floss[i]; } else if (force[i] > floss[i]) { force[i] = floss[i]; } - } else if (i >= ne+nf) { + } else if (c >= ne+nf) { if (force[i] < 0) { force[i] = 0; } @@ -380,7 +454,7 @@ void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { // elliptic cone constraint else { // get friction - mjtNum *mu = d->contact[d->efc_id[i]].friction; + mjtNum *mu = d->contact[d->efc_id[i]].friction; //-------------------- perform normal or ray update @@ -390,7 +464,7 @@ void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { // normal force too small: normal update if (force[i] < mjMINVAL) { // unconstrained minimum - force[i] -= res[0]*ARinv[i]; + force[i] -= res[0]*ARinv[c]; // clamp if (force[i] < 0) { @@ -447,61 +521,31 @@ void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { // QCQP else { - int flg_active; - mjtNum v[6]; - - // solve - if (dim == 3) { - flg_active = mju_QCQP2(v, Ac, bc, mu, force[i]); - } else if (dim == 4) { - flg_active = mju_QCQP3(v, Ac, bc, mu, force[i]); - } else { - flg_active = mju_QCQP(v, Ac, bc, mu, force[i], dim-1); - } - - // on constraint: put v on ellipsoid, in case QCQP is approximate - if (flg_active) { - mjtNum s = 0; - for (int j=0; j < dim-1; j++) { - s += v[j]*v[j] / (mu[j]*mu[j]); - } - s = mju_sqrt(force[i]*force[i] / mju_max(mjMINVAL, s)); - for (int j=0; j < dim-1; j++) { - v[j] *= s; - } - } - - // assign - mju_copy(force+i+1, v, dim-1); + solveQCQP(force, i, dim, Ac, bc, mu); } } // accumulate improvement if (dim == 1) { - Athis[0] = 1/ARinv[i]; + Athis[0] = 1/ARinv[c]; } improvement -= costChange(Athis, force+i, oldforce, res, dim); // skip the rest of this constraint - i += (dim-1); + c += (dim-1); } - // process state - mju_copyInt(oldstate, d->efc_state, nefc); - int nactive = dualState(m, d, d->efc_state); - int nchange = 0; - for (int i=0; i < nefc; i++) { - nchange += (oldstate[i] != d->efc_state[i]); - } + // update constraint state + int nchange; + int nactive = dualStateChange(d, d->efc_state, oldstate, ne, nf, nefc, efclist, &nchange); // scale improvement, save stats improvement *= scale; - saveStats(m, d, island, iter, improvement, 0, 0, nactive, nchange, 0, 0); + saveStats(m, d, island_stat, iter, improvement, 0, 0, nactive, nchange, 0, 0); // increment iteration count iter++; - // terminate if (improvement < m->opt.tolerance) { break; @@ -509,51 +553,59 @@ void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { } // finalize statistics - if (island < mjNISLAND) { + if (island_stat < mjNISLAND) { // update solver iterations - d->solver_niter[island] += iter; + d->solver_niter[island_stat] += iter; // set nnz if (mj_isSparse(m)) { - d->solver_nnz[island] = 0; - for (int i=0; i < nefc; i++) { - d->solver_nnz[island] += d->efc_AR_rownnz[i]; + d->solver_nnz[island_stat] = 0; + for (int c=0; c < nefc; c++) { + d->solver_nnz[island_stat] += d->efc_AR_rownnz[efclist ? efclist[c] : c]; } } else { - d->solver_nnz[island] = nefc*nefc; + d->solver_nnz[island_stat] = nefc*nefc; } } - // map to joint space - dualFinish(m, d); - mj_freeStack(d); } +// PGS entry point (monolithic) +void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { + solPGS(m, d, /*island=*/-1, d->ne, d->nf, d->nefc, /*efclist=*/NULL, maxiter); + dualFinish(m, d); +} + + //---------------------------- NoSlip solver ------------------------------------------------------- -// TODO: b/295296178 - add island support to Dual solvers -void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { - int dim, iter = 0, ne = d->ne, nf = d->nf, nefc = d->nefc; +// core NoSlip solver: iterates over constraints specified by efclist +// island: island index for stats (use -1 for monolithic, mapped to 0) +// ne, nf, nefc: constraint type counts +// efclist: maps list position c to monolithic efc index (NULL for sequential) +static void solNoSlip(const mjModel* m, mjData* d, int island, + int ne, int nf, int nefc, + const int* efclist, int maxiter) { + int dim, iter = 0; const mjtNum *floss = d->efc_frictionloss; mjtNum *force = d->efc_force; mjtNum *mu, improvement; - mjtNum v[5], Ac[25], bc[5], res[5], oldforce[5], delta[5], mid, y, K0, K1; + mjtNum Ac[25], bc[5], res[5], oldforce[5], delta[5], mid, y, K0, K1; mjContact* con; mj_markStack(d); mjtNum* ARinv = mjSTACKALLOC(d, nefc, mjtNum); int* oldstate = mjSTACKALLOC(d, nefc, int); - // TODO: b/295296178 - Use island index (currently hardcoded to 0) - int island = 0; + int island_stat = mjMAX(0, island); mjtNum scale = 1 / (m->stat.meaninertia * mjMAX(1, m->nv)); // precompute inverse diagonal of A - ARdiaginv(m, d, ARinv, 1); + ARdiaginv(m, d, ARinv, nefc, efclist, 1); // initial constraint state - dualState(m, d, d->efc_state); + dualState(d, d->efc_state, ne, nf, nefc, efclist); // main iteration while (iter < maxiter) { @@ -562,19 +614,22 @@ void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { // correct for cost change at iter 0 if (iter == 0) { - for (int i=0; i < nefc; i++) { + for (int c=0; c < nefc; c++) { + int i = efclist ? efclist[c] : c; improvement += 0.5*force[i]*force[i]*d->efc_R[i]; } } // perform one sweep: dry friction - for (int i=ne; i < ne+nf; i++) { + for (int c=ne; c < ne+nf; c++) { + int i = efclist ? efclist[c] : c; + // compute residual, save old residual(m, d, res, i, 1, 1); oldforce[0] = force[i]; // unconstrained minimum - force[i] -= res[0]*ARinv[i]; + force[i] -= res[0]*ARinv[c]; // impose interval constraints if (force[i] < -floss[i]) { @@ -585,11 +640,13 @@ void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { // add to improvement delta[0] = force[i] - oldforce[0]; - improvement -= 0.5*delta[0]*delta[0]/ARinv[i] + delta[0]*res[0]; + improvement -= 0.5*delta[0]*delta[0]/ARinv[c] + delta[0]*res[0]; } // perform one sweep: contact friction - for (int i=ne+nf; i < nefc; i++) { + for (int c=ne+nf; c < nefc; c++) { + int i = efclist ? efclist[c] : c; + // pyramidal contact if (d->efc_type[i] == mjCNSTR_CONTACT_PYRAMIDAL) { // get contact info @@ -648,7 +705,7 @@ void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { } // skip the rest of this contact - i += 2*(dim-1)-1; + c += 2*(dim-1)-1; } // elliptic contact @@ -678,55 +735,27 @@ void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { // QCQP else { - int flg_active = 0; - - // solve - if (dim == 3) { - flg_active = mju_QCQP2(v, Ac, bc, mu, force[i]); - } else if (dim == 4) { - flg_active = mju_QCQP3(v, Ac, bc, mu, force[i]); - } else { - flg_active = mju_QCQP(v, Ac, bc, mu, force[i], dim-1); - } - - // on constraint: put v on ellipsoid, in case QCQP is approximate - if (flg_active) { - mjtNum s = 0; - for (int j=0; j < dim-1; j++) { - s += v[j]*v[j]/(mu[j]*mu[j]); - } - s = mju_sqrt(force[i]*force[i] / mju_max(mjMINVAL, s)); - for (int j=0; j < dim-1; j++) { - v[j] *= s; - } - } - - // assign - mju_copy(force+i+1, v, dim-1); + solveQCQP(force, i, dim, Ac, bc, mu); } // accumulate improvement improvement -= costChange(Ac, force+i+1, oldforce, res, dim-1); // skip the rest of this contact - i += (dim-1); + c += (dim-1); } } - // process state - mju_copyInt(oldstate, d->efc_state, nefc); - int nactive = dualState(m, d, d->efc_state); - int nchange = 0; - for (int i=0; i < nefc; i++) { - nchange += (oldstate[i] != d->efc_state[i]); - } + // update constraint state + int nchange; + int nactive = dualStateChange(d, d->efc_state, oldstate, ne, nf, nefc, efclist, &nchange); // scale improvement, save stats improvement *= scale; // save noslip stats after all the entries from regular solver - int stats_iter = iter + d->solver_niter[island]; - saveStats(m, d, island, stats_iter, improvement, 0, 0, nactive, nchange, 0, 0); + int stats_iter = iter + d->solver_niter[island_stat]; + saveStats(m, d, island_stat, stats_iter, improvement, 0, 0, nactive, nchange, 0, 0); // increment iteration count iter++; @@ -738,15 +767,19 @@ void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { } // update solver iterations - d->solver_niter[island] += iter; - - // map to joint space - dualFinish(m, d); + d->solver_niter[island_stat] += iter; mj_freeStack(d); } +// NoSlip entry point (monolithic) +void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { + solNoSlip(m, d, /*island=*/-1, d->ne, d->nf, d->nefc, /*efclist=*/NULL, maxiter); + dualFinish(m, d); +} + + //------------------------- Primal solvers --------------------------------------------------------- // Primal context From 6f275b79c6d8e3f0c09118f57167890d6d75e6df Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 28 Apr 2026 03:12:12 -0700 Subject: [PATCH 06/13] Studio: toolbar cosmetic improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove hard-coded button sizes (48×32) from toolbar icons; buttons now use default ImGui sizing, matching the existing copy-camera button. - Remove the Unload button (available via menu). - Add ImGui_ColorButtonEx widget with per-corner rounding via ImDrawFlags, enabling segmented button-group visuals. - Use segmented rounding for the pause/viscous/play triplet: rounded-left, square, rounded-right. - Make pause and play buttons 40% wider than the viscous button for emphasis. - Replace the theme combo dropdown with a cycling button. - Reduce toolbar height from 48px to 34px and vertically center content via WindowPadding. - Zero out table CellPadding in the toolbar for precise vertical centering. - Remove the speed combo's inflated vertical padding. PiperOrigin-RevId: 906853958 Change-Id: I6e4ec7243adb9e0f1f06f8779fa896a6865c46c7 --- src/experimental/platform/ux/gui.cc | 79 +++++++++---------- src/experimental/platform/ux/gui.h | 2 +- src/experimental/platform/ux/imgui_widgets.cc | 4 +- src/experimental/platform/ux/imgui_widgets.h | 49 ++++++++++++ src/experimental/studio/app.cc | 49 ++++-------- 5 files changed, 103 insertions(+), 80 deletions(-) diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index 9098092c..ccf1ed91 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -97,15 +97,15 @@ void SetupTheme(GuiTheme theme) { c[ImGuiCol_TextSelectedBg] = ImVec4(0.73, 0.73, 0.73, 0.35); c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80, 0.80, 0.80, 0.35); c[ImGuiCol_DragDropTarget] = ImVec4(1.00, 1.00, 0.00, 0.90); - c[ImGuiCol_NavHighlight] = ImVec4(0.26, 0.59, 0.98, 1.00); + c[ImGuiCol_NavCursor] = ImVec4(0.26, 0.59, 0.98, 1.00); c[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00, 1.00, 1.00, 0.70); c[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80, 0.80, 0.80, 0.20); c[ImGuiCol_DockingEmptyBg] = ImVec4(0.38, 0.38, 0.38, 1.00); c[ImGuiCol_Tab] = ImVec4(0.25, 0.25, 0.25, 1.00); c[ImGuiCol_TabHovered] = ImVec4(0.40, 0.40, 0.40, 1.00); - c[ImGuiCol_TabActive] = ImVec4(0.33, 0.33, 0.33, 1.00); - c[ImGuiCol_TabUnfocused] = ImVec4(0.25, 0.25, 0.25, 1.00); - c[ImGuiCol_TabUnfocusedActive] = ImVec4(0.33, 0.33, 0.33, 1.00); + c[ImGuiCol_TabSelected] = ImVec4(0.33, 0.33, 0.33, 1.00); + c[ImGuiCol_TabDimmed] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_TabDimmedSelected] = ImVec4(0.33, 0.33, 0.33, 1.00); c[ImGuiCol_DockingPreview] = ImVec4(0.85, 0.85, 0.85, 0.28); c[ImGuiCol_WindowBg].w = 1.0f; } else if (theme == GuiTheme::kLight) { @@ -220,7 +220,7 @@ ImVec4 ConfigureDockingLayout() { const float kOptionsRelWidth = 0.22f; const float kInspectorRelWidth = 0.22f; const float kStatsRelHeight = 0.3f; - const float kToolsBarHeight = 48.f * scale; + const float kToolsBarHeight = 36.f * scale; const float kStatusBarHeight = 32.f * scale; const ImVec2 dockspace_pos{viewport->WorkPos.x, @@ -303,6 +303,9 @@ ImVec4 ConfigureDockingLayout() { platform::ScopedStyle style; style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); style.Var(ImGuiStyleVar_WindowRounding, 0.0f); + const float toolbar_vpad = + std::max(0.f, (kToolsBarHeight - ImGui::GetFrameHeight()) * 0.5f); + style.Var(ImGuiStyleVar_WindowPadding, ImVec2(4, toolbar_vpad)); ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kToolsBarHeight), ImGuiCond_Always); @@ -335,18 +338,23 @@ ImVec4 ConfigureDockingLayout() { void StepControlGui(const mjModel* model, StepControl* step_control, int& speed_index) { platform::ScopedStyle style; - style.Var(ImGuiStyleVar_FrameRounding, 2.f); + style.Var(ImGuiStyleVar_FrameRounding, 8.f); const ImColor yellow(255, 215, 0, 255); const ImColor green(40, 180, 40, 255); - const float scale = ImGui::GetWindowDpiScale(); - ImVec2 button_size(48.f * scale, 32.f * scale); auto make_button = [&](const char* icon, StepControl::PauseState target_state, - ImColor color, const char* tooltip = "", - float hover_alpha = 1.f) { + ImColor color, ImDrawFlags corners, + const char* tooltip = "", + float hover_alpha = 1.f, float width_scale = 1.f) { + ImVec2 size(0, 0); + if (width_scale != 1.f) { + const ImGuiStyle& s = ImGui::GetStyle(); + const float w = ImGui::CalcTextSize(icon).x + s.FramePadding.x * 2; + size.x = w * width_scale; + } bool active = step_control->GetPauseState() == target_state; - if (ImGui_ColorButton(icon, active, color, button_size, hover_alpha)) { + if (ImGui_ColorButtonEx(icon, active, color, corners, size, hover_alpha)) { step_control->SetPauseState(target_state); } if (!std::string_view(tooltip).empty()) { @@ -355,26 +363,28 @@ void StepControlGui(const mjModel* model, StepControl* step_control, }; make_button(ICON_FA_PAUSE, StepControl::PauseState::kNormalPaused, yellow, - "Pause"); + ImDrawFlags_RoundCornersLeft, "Pause", .3f, 1.6f); ImGui::SameLine(0.f, 0.f); make_button(ICON_FA_MAGIC, StepControl::PauseState::kViscousPaused, yellow, - "Viscous Pause"); + ImDrawFlags_RoundCornersNone, "Viscous Pause", .3f, 1.3f); ImGui::SameLine(0.f, 0.f); - make_button(ICON_FA_PLAY, StepControl::PauseState::kUnpaused, green, "", .6f); + make_button(ICON_FA_PLAY, StepControl::PauseState::kUnpaused, green, + ImDrawFlags_RoundCornersRight, "", .3f, 1.6f); // Speed selection. - ImGui::SameLine(); - const float pad_y = (button_size.y - ImGui::GetFontSize()) * .5f; + style.Reset(); + ImGui::SameLine(0, ImGui::GetFrameHeight() * .6f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, - ImVec2(ImGui::GetStyle().FramePadding.x + 5.f, pad_y)); + ImVec2(ImGui::GetStyle().FramePadding.x + 5.f, + ImGui::GetStyle().FramePadding.y)); const auto [misaligned, measured] = IsSpeedMisaligned(*step_control); char speed_preview[64]; if (misaligned) { - snprintf(speed_preview, sizeof(speed_preview), "%s%s (%-4.1f%%)", + snprintf(speed_preview, sizeof(speed_preview), "%s %s (%-4.1f%%)", ICON_FA_TACHOMETER, kPercentRealTime[speed_index], measured); } else { - snprintf(speed_preview, sizeof(speed_preview), "%s%s", ICON_FA_TACHOMETER, + snprintf(speed_preview, sizeof(speed_preview), "%s %s", ICON_FA_TACHOMETER, kPercentRealTime[speed_index]); } @@ -400,39 +410,22 @@ void StepControlGui(const mjModel* model, StepControl* step_control, } } -bool ThemeSelectGui(GuiTheme* theme) { +bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size) { static constexpr const char* ICON_DARKMODE = ICON_FA_CIRCLE; static constexpr const char* ICON_LIGHTMODE = ICON_FA_CIRCLE_O; static constexpr const char* ICON_CLASSICMODE = ICON_FA_ADJUST; const char* theme_icons[] = {ICON_LIGHTMODE, ICON_DARKMODE, ICON_CLASSICMODE}; const char* theme_tooltips[] = {"Light Mode", "Dark Mode", "Classic Mode"}; - const GuiTheme theme_values[] = { - GuiTheme::kLight, - GuiTheme::kDark, - GuiTheme::kClassic, - }; - - bool changed = false; int theme_idx = static_cast(*theme); - ImGui::SetNextItemWidth(ImGui::CalcTextSize(theme_icons[0]).x + - ImGui::GetStyle().FramePadding.x * 2); - if (ImGui::BeginCombo("##Theme", theme_icons[theme_idx], - ImGuiComboFlags_NoArrowButton)) { - for (int n = 0; n < IM_ARRAYSIZE(theme_icons); n++) { - if (ImGui::Selectable(theme_icons[n], (theme_idx == n))) { - *theme = theme_values[n]; - changed = true; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", theme_tooltips[n]); - } - } - ImGui::EndCombo(); + if (ImGui::Button(theme_icons[theme_idx], size)) { + theme_idx = (theme_idx + 1) % IM_ARRAYSIZE(theme_icons); + *theme = static_cast(theme_idx); + return true; } - ImGui::SetItemTooltip("%s", "Theme"); + ImGui::SetItemTooltip("%s", theme_tooltips[theme_idx]); - return changed; + return false; } bool LabelSelectionGui(mjvOption* opts) { diff --git a/src/experimental/platform/ux/gui.h b/src/experimental/platform/ux/gui.h index 4dc8b69c..e9b23dfc 100644 --- a/src/experimental/platform/ux/gui.h +++ b/src/experimental/platform/ux/gui.h @@ -81,7 +81,7 @@ void StepControlGui(const mjModel* model, StepControl* step_control, int& speed_index); // UX for selecting the GUI theme. -bool ThemeSelectGui(GuiTheme* theme); +bool ThemeSelectGui(GuiTheme* theme, const ImVec2& size = ImVec2(0, 0)); // UX for selecting the visualization label option. bool LabelSelectionGui(mjvOption* opts); diff --git a/src/experimental/platform/ux/imgui_widgets.cc b/src/experimental/platform/ux/imgui_widgets.cc index 7d0f1cba..305f292c 100644 --- a/src/experimental/platform/ux/imgui_widgets.cc +++ b/src/experimental/platform/ux/imgui_widgets.cc @@ -375,9 +375,7 @@ void ImGui_EndHSplit(bool open) { } void MaybeSaveToClipboard(const std::string& contents) { - if (ImGui::GetIO().SetClipboardTextFn) { - ImGui::GetIO().SetClipboardTextFn(nullptr, contents.c_str()); - } + ImGui::SetClipboardText(contents.c_str()); } ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size) { diff --git a/src/experimental/platform/ux/imgui_widgets.h b/src/experimental/platform/ux/imgui_widgets.h index 187dabba..3ee85efa 100644 --- a/src/experimental/platform/ux/imgui_widgets.h +++ b/src/experimental/platform/ux/imgui_widgets.h @@ -551,6 +551,55 @@ inline bool ImGui_ColorButton(const char* label, bool active, ImColor color, return ImGui::Button(label, size); } +// Like ImGui_ColorButton, but with per-corner rounding control via ImDrawFlags. +// Use ImDrawFlags_RoundCornersLeft, ImDrawFlags_RoundCornersRight, +// ImDrawFlags_RoundCornersNone, ImDrawFlags_RoundCornersAll, etc. +inline bool ImGui_ColorButtonEx(const char* label, bool active, ImColor color, + ImDrawFlags corners, + const ImVec2& size = ImVec2(0, 0), + float hover_alpha = 0.5f) { + const ImGuiStyle& s = ImGui::GetStyle(); + const ImVec2 label_size = ImGui::CalcTextSize(label, nullptr, true); + const ImVec2 btn_size( + size.x > 0 ? size.x : label_size.x + s.FramePadding.x * 2, + size.y > 0 ? size.y : label_size.y + s.FramePadding.y * 2); + + const ImVec2 pos = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton(label, btn_size); + const bool clicked = ImGui::IsItemClicked(); + const bool hovered = ImGui::IsItemHovered(); + + // Determine background color. + const ImColor hover_color(color.Value.x, color.Value.y, color.Value.z, + color.Value.w * hover_alpha); + ImColor bg; + if (active) { + bg = color; + } else if (hovered) { + bg = hover_color; + } else { + bg = ImGui::GetColorU32(ImGuiCol_Button); + } + + // Draw background with per-corner rounding. + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImVec2 max(pos.x + btn_size.x, pos.y + btn_size.y); + dl->AddRectFilled(pos, max, bg, s.FrameRounding, corners); + + // Draw border. + if (s.FrameBorderSize > 0) { + dl->AddRect(pos, max, ImGui::GetColorU32(ImGuiCol_Border), + s.FrameRounding, corners, s.FrameBorderSize); + } + + // Draw label centered. + const ImVec2 text_pos(pos.x + (btn_size.x - label_size.x) * 0.5f, + pos.y + (btn_size.y - label_size.y) * 0.5f); + dl->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text), label); + + return clicked; +} + // Begin a boxed section with outer borders - use EndBoxSection to close. inline bool BeginBoxSection(const char* id, ImGuiTableFlags extra_flags = 0) { ImGuiTableFlags flags = ImGuiTableFlags_BordersOuter | extra_flags; diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index dec270ae..25dd1676 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -72,13 +72,11 @@ static void SelectParentPerturb(const mjModel* model, mjvPerturb& perturb) { } static constexpr const char* ICON_COPY_CAMERA = platform::ICON_FA_COPY; -static constexpr const char* ICON_UNLOAD_MODEL = platform::ICON_FA_EJECT; static constexpr const char* ICON_RELOAD_MODEL = platform::ICON_FA_REFRESH; static constexpr const char* ICON_RESET_MODEL = platform::ICON_FA_UNDO; static constexpr const char* ICON_PREV_FRAME = platform::ICON_FA_CARET_LEFT; static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT; static constexpr const char* ICON_CURR_FRAME = platform::ICON_FA_FAST_FORWARD; -static constexpr const char* ICON_RELOAD_SPEC = platform::ICON_FA_REFRESH; static constexpr const char* ICON_UNDO_SPEC = platform::ICON_FA_UNDO; static constexpr const char* ICON_REDO_SPEC = platform::ICON_FA_REPEAT; @@ -1402,13 +1400,12 @@ void App::HelpGui() { } void App::ToolBarGui() { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0, 0)); if (ImGui::BeginTable("##ToolBarTable", 2)) { platform::ScopedStyle style; - const ImColor red(220, 40, 40, 255); - - const float scale = ImGui::GetWindowDpiScale(); - const ImVec2 button_size(48.f * scale, 32.f * scale); - const ImVec2 play_button_size(80.f * scale, 32.f * scale); + style.Var(ImGuiStyleVar_ItemSpacing, + ImVec2(ImGui::GetStyle().ItemSpacing.x * 2.0f, + ImGui::GetStyle().ItemSpacing.y)); const float label_width = GetExpectedLabelWidth(); const float copy_btn_width = ImGui::CalcTextSize(ICON_COPY_CAMERA).x + @@ -1420,42 +1417,29 @@ void App::ToolBarGui() { const float right_width = label_width + sp + label_width + sp + label_width + sp + copy_btn_width + sp + theme_width; - const float separator_width = .2f * button_size.x; + const float separator_width = ImGui::GetFrameHeight() * .6f; ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, right_width); ImGui::TableNextColumn(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetStyle().WindowPadding.x); - // Combined (Unload, Reload) widget + const float btn_size = ImGui::GetFrameHeight(); + const ImVec2 square_size(btn_size, btn_size); + + // Reload button. { style.Var(ImGuiStyleVar_FrameRounding, 2.0f); - - // Unload button. - { - const ImColor a = red; - const ImColor h(a.Value.x, a.Value.y, a.Value.z, a.Value.w * 0.6f); - style.Color(ImGuiCol_ButtonHovered, h); - style.Color(ImGuiCol_ButtonActive, a); - - if (ImGui::Button(ICON_UNLOAD_MODEL, button_size)) { - InitEmptyModel(); - } - ImGui::SetItemTooltip("%s", "Unload"); - style.Reset(); - } - - // Reload button. - ImGui::SameLine(0, 0); - if (ImGui::Button(ICON_RELOAD_MODEL, button_size)) { + if (ImGui::Button(ICON_RELOAD_MODEL, square_size)) { RequestModelReload(); } ImGui::SetItemTooltip("%s", "Reload"); } // Reset button. - ImGui::SameLine(0, separator_width); - if (ImGui::Button(ICON_RESET_MODEL, button_size)) { + ImGui::SameLine(0, 0.5 * separator_width); + if (ImGui::Button(ICON_RESET_MODEL, square_size)) { ResetPhysics(); } ImGui::SetItemTooltip("%s", "Reset"); @@ -1465,10 +1449,8 @@ void App::ToolBarGui() { platform::StepControlGui(model(), &step_control_, tmp_.speed_index); ImGui::TableNextColumn(); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + - (button_size.y - ImGui::GetFrameHeight()) * 0.5f); - if (ImGui::Button(ICON_COPY_CAMERA)) { + if (ImGui::Button(ICON_COPY_CAMERA, square_size)) { std::string camera_string = platform::CameraToString(data(), &camera_); platform::MaybeSaveToClipboard(camera_string); } @@ -1488,13 +1470,14 @@ void App::ToolBarGui() { ImGui::SameLine(); ImGui::SetNextItemWidth(GetExpectedLabelWidth()); - if (platform::ThemeSelectGui(&ui_.theme)) { + if (platform::ThemeSelectGui(&ui_.theme, square_size)) { platform::SetupTheme(ui_.theme); ImGui::GetIO().WantSaveIniSettings = true; } ImGui::EndTable(); } + ImGui::PopStyleVar(); } void App::StatusBarGui() { From 32aeb377b5defe490b24c1d88c954835f6e670c4 Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Tue, 28 Apr 2026 03:55:28 -0700 Subject: [PATCH 07/13] Move filament::Skybox object management into SceneView. ModelObjects no longer directly manages any filament objects. PiperOrigin-RevId: 906873925 Change-Id: Iaa317cbeb9c5236745a5f0d04a740985bec54a5c --- .../filament/compat/model_objects.cc | 55 ++----------------- .../filament/compat/model_objects.h | 15 +---- .../filament/compat/scene_bridge.cc | 5 +- .../filament/filament/scene_view.cc | 22 +++++--- .../filament/filament/scene_view.h | 4 +- 5 files changed, 23 insertions(+), 78 deletions(-) diff --git a/src/experimental/filament/compat/model_objects.cc b/src/experimental/filament/compat/model_objects.cc index 1a52b6f1..67e07a08 100644 --- a/src/experimental/filament/compat/model_objects.cc +++ b/src/experimental/filament/compat/model_objects.cc @@ -26,12 +26,7 @@ #include #include -#include -#include -#include #include -#include -#include #include #include #include @@ -47,7 +42,6 @@ namespace mujoco { using filament::math::float2; using filament::math::float3; using filament::math::float4; -using filament::math::mat3f; enum class MeshType { kNormal, @@ -534,12 +528,6 @@ ModelObjects::ModelObjects(const mjModel* model, filament::Engine* engine) } ModelObjects::~ModelObjects() { - for (auto& iter : skyboxes_) { - engine_->destroy(iter); - } - for (auto& iter : indirect_lights_) { - engine_->destroy(iter); - } meshes_.clear(); textures_.clear(); } @@ -681,48 +669,13 @@ const Texture* ModelObjects::GetTexture(int mat_id, int role) const { return GetTexture(tex_id); } -filament::IndirectLight* ModelObjects::CreateIndirectLight(int tex_id, - float intensity) { - filament::Texture* texture = nullptr; - const Texture::SphericalHarmonics* spherical_harmonics = nullptr; - auto texture_iter = textures_.find(tex_id); - if (texture_iter != textures_.end()) { - texture = texture_iter->second->GetFilamentTexture(); - spherical_harmonics = texture_iter->second->GetSphericalHarmonics(); - } - - filament::IndirectLight::Builder builder; - builder.reflections(texture); - if (spherical_harmonics != nullptr) { - builder.irradiance(3, *spherical_harmonics); - } - builder.intensity(intensity); - // Rotate the light to match mujoco's Z-up convention. - builder.rotation(mat3f::rotation(filament::math::f::PI / 2, float3{1, 0, 0})); - filament::IndirectLight* indirect_light = builder.build(*engine_); - indirect_lights_.push_back(indirect_light); - return indirect_light; -} - -filament::Skybox* ModelObjects::CreateSkybox() { - filament::Texture* skybox_texture = nullptr; +const Texture* ModelObjects::GetSkyboxTexture() const { for (auto& iter : textures_) { - const int texture_type = model_->tex_type[iter.first]; - if (texture_type == mjTEXTURE_SKYBOX) { - skybox_texture = iter.second->GetFilamentTexture(); - break; + if (model_->tex_type[iter.first] == mjTEXTURE_SKYBOX) { + return iter.second.get(); } } - - if (skybox_texture == nullptr) { - return nullptr; - } - - filament::Skybox::Builder builder; - builder.environment(skybox_texture); - filament::Skybox* skybox = builder.build(*engine_); - skyboxes_.push_back(skybox); - return skybox; + return nullptr; } } // namespace mujoco diff --git a/src/experimental/filament/compat/model_objects.h b/src/experimental/filament/compat/model_objects.h index db528d36..4b7b0afd 100644 --- a/src/experimental/filament/compat/model_objects.h +++ b/src/experimental/filament/compat/model_objects.h @@ -18,11 +18,8 @@ #include #include #include -#include #include -#include -#include #include #include #include "experimental/filament/filament/mesh.h" @@ -30,7 +27,7 @@ namespace mujoco { -// Creates and owns various filament objects based on the data in a mjrContext. +// Creates and owns various filament objects based on the mjModel. class ModelObjects { public: ModelObjects(const mjModel* model, filament::Engine* engine); @@ -58,10 +55,6 @@ class ModelObjects { void CreateSkinFlexMesh(const mjvScene* scene, const mjvGeom& geom); - // Returns the filament engine used by the ModelObjects to create filament - // objects. - filament::Engine* GetEngine() const { return engine_; } - // Returns the cached instance of a filament object created from the mjModel. const Mesh* GetShapeBuffer(ShapeType shape) const; const Mesh* GetMeshBuffer(int data_id) const; @@ -69,9 +62,7 @@ class ModelObjects { const Mesh* GetFlexSkinGeomMesh(int geom_id) const; const Texture* GetTexture(int tex_id) const; const Texture* GetTexture(int mat_id, int role) const; - - filament::Skybox* CreateSkybox(); - filament::IndirectLight* CreateIndirectLight(int tex_id, float intensity); + const Texture* GetSkyboxTexture() const; float GetSpecularMultiplier() const { return specular_multiplier_; } float GetShininessMultiplier() const { return shininess_multiplier_; } @@ -85,8 +76,6 @@ class ModelObjects { private: const mjModel* model_ = nullptr; filament::Engine* engine_ = nullptr; - std::vector skyboxes_; - std::vector indirect_lights_; std::array, kNumShapes> shapes_; std::unordered_map> meshes_; std::unordered_map> convex_hulls_; diff --git a/src/experimental/filament/compat/scene_bridge.cc b/src/experimental/filament/compat/scene_bridge.cc index 6ab29a36..aebb85ef 100644 --- a/src/experimental/filament/compat/scene_bridge.cc +++ b/src/experimental/filament/compat/scene_bridge.cc @@ -322,10 +322,7 @@ void SceneBridge::PrepareLights() { } } - filament::Skybox* skybox = model_objects_->CreateSkybox(); - if (skybox) { - scene_view_->AddToScene(skybox); - } + scene_view_->SetSkybox(model_objects_->GetSkyboxTexture()); } filament::math::mat4 CalculateClipFromWorld(const mjrRect& viewport, diff --git a/src/experimental/filament/filament/scene_view.cc b/src/experimental/filament/filament/scene_view.cc index 997b048f..d11edc13 100644 --- a/src/experimental/filament/filament/scene_view.cc +++ b/src/experimental/filament/filament/scene_view.cc @@ -158,6 +158,10 @@ SceneView::SceneView(filament::Engine* engine) : engine_(engine) { } SceneView::~SceneView() { + if (skybox_) { + scene_->setSkybox(nullptr); + engine_->destroy(skybox_); + } for (auto& light : lights_) { light->RemoveFromScene(scene_); } @@ -210,15 +214,17 @@ void SceneView::RemoveFromScene(Renderable* renderable) { } } -void SceneView::AddToScene(filament::Skybox* skybox) { - skybox_ = skybox; - scene_->setSkybox(skybox); -} - -void SceneView::RemoveFromScene(filament::Skybox* skybox) { - if (skybox_ == skybox) { - skybox_ = nullptr; +void SceneView::SetSkybox(const Texture* skybox_texture) { + if (skybox_) { scene_->setSkybox(nullptr); + engine_->destroy(skybox_); + skybox_ = nullptr; + } + if (skybox_texture) { + filament::Skybox::Builder builder; + builder.environment(skybox_texture->GetFilamentTexture()); + skybox_ = builder.build(*engine_); + scene_->setSkybox(skybox_); } } diff --git a/src/experimental/filament/filament/scene_view.h b/src/experimental/filament/filament/scene_view.h index b6d5dfa6..59a3e9b3 100644 --- a/src/experimental/filament/filament/scene_view.h +++ b/src/experimental/filament/filament/scene_view.h @@ -31,6 +31,7 @@ #include "experimental/filament/filament/light.h" #include "experimental/filament/filament/renderable.h" #include "experimental/filament/filament/render_target.h" +#include "experimental/filament/filament/texture.h" namespace mujoco { @@ -49,8 +50,7 @@ class SceneView { void RemoveFromScene(Light* light); void AddToScene(Renderable* renderable); void RemoveFromScene(Renderable* renderable); - void AddToScene(filament::Skybox* skybox); - void RemoveFromScene(filament::Skybox* skybox); + void SetSkybox(const Texture* skybox_texture); // Parameters for rendering the scene. struct RenderRequest { From 00fff8c780ede515c375599f4bff81f835cb12cb Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Tue, 28 Apr 2026 04:40:05 -0700 Subject: [PATCH 08/13] Update APIs to use opaque handle types instead of concrete types. PiperOrigin-RevId: 906895053 Change-Id: Id61d437ea814e038f25d92375eb747a6c6775c32 --- .../filament/filament/filament_context.cc | 14 +++++++++----- .../filament/filament/filament_context.h | 19 +++++++++++++------ src/experimental/filament/filament/light.cc | 5 +++-- src/experimental/filament/filament/light.h | 12 ++++++++++-- src/experimental/filament/filament/mesh.h | 14 +++++++++++--- .../filament/filament/render_target.h | 10 +++++++++- .../filament/filament/renderable.h | 10 +++++++++- .../filament/filament/scene_view.h | 14 +++++++++++--- src/experimental/filament/filament/texture.h | 16 +++++++++++----- .../filament/render_context_filament.h | 11 +++++++++++ 10 files changed, 97 insertions(+), 28 deletions(-) diff --git a/src/experimental/filament/filament/filament_context.cc b/src/experimental/filament/filament/filament_context.cc index e367b44f..906c32e3 100644 --- a/src/experimental/filament/filament/filament_context.cc +++ b/src/experimental/filament/filament/filament_context.cc @@ -87,7 +87,7 @@ FilamentContext::FrameHandle FilamentContext::Render( } bool render_began = false; - RenderTarget* current_target = nullptr; + mjrRenderTarget* current_target = nullptr; for (const RenderRequest& request : requests) { if (request.target != current_target && render_began) { renderer_->endFrame(); @@ -126,7 +126,8 @@ FilamentContext::FrameHandle FilamentContext::Render( scene_view_request.draw_mode = request.draw_mode; scene_view_request.viewport = {0, 0, request.width, request.height}; scene_view_request.camera = request.camera; - request.scene->Render(renderer_, scene_view_request); + SceneView* scene_view = SceneView::downcast(request.scene); + scene_view->Render(renderer_, scene_view_request); } } else { if (read_requests.empty()) { @@ -146,13 +147,16 @@ FilamentContext::FrameHandle FilamentContext::Render( break; } if (render_began) { + RenderTarget* render_target = RenderTarget::downcast(request.target); + SceneView::RenderRequest scene_view_request; scene_view_request.draw_mode = request.draw_mode; scene_view_request.viewport = {0, 0, request.width, request.height}; scene_view_request.camera = request.camera; - scene_view_request.target = request.target; - request.scene->Render(renderer_, scene_view_request); - request.target->ReadColorPixels(renderer_, read_request.output, + scene_view_request.target = render_target; + SceneView* scene_view = SceneView::downcast(request.scene); + scene_view->Render(renderer_, scene_view_request); + render_target->ReadColorPixels(renderer_, read_request.output, read_request.num_bytes); } } diff --git a/src/experimental/filament/filament/filament_context.h b/src/experimental/filament/filament/filament_context.h index 391e9818..2e98acaf 100644 --- a/src/experimental/filament/filament/filament_context.h +++ b/src/experimental/filament/filament/filament_context.h @@ -35,15 +35,18 @@ namespace mujoco { // Manages the filament renderer and provides APIs for rendering scenes. -class FilamentContext { +class FilamentContext : public mjrfContext { public: explicit FilamentContext(const mjrFilamentConfig* config); ~FilamentContext(); + FilamentContext(const FilamentContext&) = delete; + FilamentContext& operator=(const FilamentContext&) = delete; + // Information needed to render a single image of a scene. struct RenderRequest { // The scene to render. - SceneView* scene = nullptr; + mjrScene* scene = nullptr; // The method (e.g. Color, Depth, Segmentation, etc.) to use for rendering. DrawMode draw_mode = DrawMode::Color; @@ -58,12 +61,12 @@ class FilamentContext { // The render target into which to render the image. If nullptr, the image // will be rendered to the window (as previously configured in // mjrFilamentConfig::native_window). - RenderTarget* target = nullptr; + mjrRenderTarget* target = nullptr; }; // Information needed to read pixels from a render target. struct ReadPixelsRequest { - RenderTarget* target = nullptr; + mjrRenderTarget* target = nullptr; // The buffer into which the read pixels will be written. uint8_t* output = nullptr; @@ -107,8 +110,12 @@ class FilamentContext { ObjectManager* GetObjectManager() const { return object_manager_.get(); } - FilamentContext(const FilamentContext&) = delete; - FilamentContext& operator=(const FilamentContext&) = delete; + static FilamentContext* downcast(mjrfContext* context) { + return static_cast(context); + } + static const FilamentContext* downcast(const mjrfContext* context) { + return static_cast(context); + } private: mjrFilamentConfig config_; diff --git a/src/experimental/filament/filament/light.cc b/src/experimental/filament/filament/light.cc index bdc1f2cd..d79a1551 100644 --- a/src/experimental/filament/filament/light.cc +++ b/src/experimental/filament/filament/light.cc @@ -56,9 +56,10 @@ Light::Light(filament::Engine* engine, const mjrLightParams& params) filament::IndirectLight::Builder builder; if (params.texture) { // Allow null textures for fallback lights. - builder.reflections(params.texture->GetFilamentTexture()); + const Texture* texture = Texture::downcast(params.texture); + builder.reflections(texture->GetFilamentTexture()); const Texture::SphericalHarmonics* spherical_harmonics = - params.texture->GetSphericalHarmonics(); + texture->GetSphericalHarmonics(); if (spherical_harmonics != nullptr) { builder.irradiance(3, *spherical_harmonics); } diff --git a/src/experimental/filament/filament/light.h b/src/experimental/filament/filament/light.h index 93ad0bb8..d58dbdd0 100644 --- a/src/experimental/filament/filament/light.h +++ b/src/experimental/filament/filament/light.h @@ -21,6 +21,7 @@ #include #include #include "experimental/filament/filament/texture.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { @@ -31,7 +32,7 @@ struct mjrLightParams { // The type of light (e.g. spot, point, directional, etc.) mjrLightType type; // The texture to use for image lights. - const Texture* texture; + const mjrTexture* texture; // The color of the light. float color[3]; // The intensity of the light, in candela. @@ -53,7 +54,7 @@ struct mjrLightParams { void mjr_defaultLightParams(mjrLightParams* params); // Manages the filament Entities for a single mjvLight. -class Light { +class Light : public mjrLight { public: Light(filament::Engine* engine, const mjrLightParams& params); ~Light() noexcept; @@ -84,6 +85,13 @@ class Light { void Enable(); void Disable(); + static Light* downcast(mjrLight* light) { + return static_cast(light); + } + static const Light* downcast(const mjrLight* light) { + return static_cast(light); + } + private: filament::Engine* engine_ = nullptr; filament::IndirectLight* ibl_ = nullptr; diff --git a/src/experimental/filament/filament/mesh.h b/src/experimental/filament/filament/mesh.h index 3b7fc5ab..9746e4ab 100644 --- a/src/experimental/filament/filament/mesh.h +++ b/src/experimental/filament/filament/mesh.h @@ -31,6 +31,7 @@ #include #include #include +#include "experimental/filament/render_context_filament.h" // Functions for creating filament vertex and index buffers. namespace mujoco { @@ -138,13 +139,16 @@ struct mjrMeshData { void mjr_defaultMeshData(mjrMeshData* data); // Owns a Vertex and Index buffer representing a geometry mesh. -class Mesh { +class Mesh : public mjrMesh { public: // Creates a Mesh from the given MeshData. Mesh(filament::Engine* engine, const mjrMeshData& data); ~Mesh(); + Mesh(const Mesh&) = delete; + Mesh& operator=(const Mesh&) = delete; + // Returns the filament IndexBuffer for the mesh. filament::IndexBuffer* GetFilamentIndexBuffer() const; @@ -163,8 +167,12 @@ class Mesh { // Returns the bounds of the mesh. filament::Box GetBounds() const; - Mesh(const Mesh&) = delete; - Mesh& operator=(const Mesh&) = delete; + static Mesh* downcast(mjrMesh* mesh) { + return static_cast(mesh); + } + static const Mesh* downcast(const mjrMesh* mesh) { + return static_cast(mesh); + } private: void BuildVertexBuffer(const mjrMeshData& data); diff --git a/src/experimental/filament/filament/render_target.h b/src/experimental/filament/filament/render_target.h index b731a567..9e5aed68 100644 --- a/src/experimental/filament/filament/render_target.h +++ b/src/experimental/filament/filament/render_target.h @@ -22,6 +22,7 @@ #include #include #include "experimental/filament/filament/texture.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { @@ -35,7 +36,7 @@ struct mjrRenderTargetConfig { void mjr_defaultRenderTargetConfig(mjrRenderTargetConfig* config); // Manages a filament RenderTarget and the textures which are bound to it. -class RenderTarget { +class RenderTarget : public mjrRenderTarget { public: // Defines the types of textures to create for the color and depth // attachments. @@ -62,6 +63,13 @@ class RenderTarget { // Returns the underlying filament render target. filament::RenderTarget* GetFilamentRenderTarget() const; + static RenderTarget* downcast(mjrRenderTarget* render_target) { + return static_cast(render_target); + } + static const RenderTarget* downcast(const mjrRenderTarget* render_target) { + return static_cast(render_target); + } + private: void Destroy(); diff --git a/src/experimental/filament/filament/renderable.h b/src/experimental/filament/filament/renderable.h index a0da8166..35f9028a 100644 --- a/src/experimental/filament/filament/renderable.h +++ b/src/experimental/filament/filament/renderable.h @@ -29,6 +29,7 @@ #include "experimental/filament/filament/math_util.h" #include "experimental/filament/filament/mesh.h" #include "experimental/filament/filament/object_manager.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { @@ -61,7 +62,7 @@ void mjr_defaultRenderableParams(mjrRenderableParams* params); // the user specifies the MaterialParams and MaterialTextures to use with the // ShadingModel. Its these properties that ultimately define the actual material // of the Renderable. -class Renderable { +class Renderable : public mjrRenderable { public: // Default filament values for priority and layer mask. static constexpr std::uint8_t kDefaultPriority = 4; @@ -139,6 +140,13 @@ class Renderable { // Returns the filament Engine managing the renderables. filament::Engine* GetEngine(); + static Renderable* downcast(mjrRenderable* renderable) { + return static_cast(renderable); + } + static const Renderable* downcast(const mjrRenderable* renderable) { + return static_cast(renderable); + } + private: struct Part { utils::Entity entity; diff --git a/src/experimental/filament/filament/scene_view.h b/src/experimental/filament/filament/scene_view.h index 59a3e9b3..f35110f8 100644 --- a/src/experimental/filament/filament/scene_view.h +++ b/src/experimental/filament/filament/scene_view.h @@ -32,6 +32,7 @@ #include "experimental/filament/filament/renderable.h" #include "experimental/filament/filament/render_target.h" #include "experimental/filament/filament/texture.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { @@ -40,11 +41,14 @@ namespace mujoco { // The filament Scene is populated with the objects (e.g. lights, renderables, // skybox, etc.). It manages multiple views to support a variety of draw modes // (e.g. normal, depth, segmentation, etc.) as well as reflective surfaces. -class SceneView { +class SceneView : public mjrScene { public: SceneView(filament::Engine* engine); ~SceneView(); + SceneView(const SceneView&) = delete; + SceneView& operator=(const SceneView&) = delete; + // Adds/removes entities from the scene. void AddToScene(Light* light); void RemoveFromScene(Light* light); @@ -90,8 +94,12 @@ class SceneView { ColorGradingOptions GetColorGradingOptions() const; void SetColorGradingOptions(const ColorGradingOptions& opts); - SceneView(const SceneView&) = delete; - SceneView& operator=(const SceneView&) = delete; + static SceneView* downcast(mjrScene* scene) { + return static_cast(scene); + } + static const SceneView* downcast(const mjrScene* scene) { + return static_cast(scene); + } private: // Marks a renderable as reflective. Reflective renderables have to be diff --git a/src/experimental/filament/filament/texture.h b/src/experimental/filament/filament/texture.h index e51bbfff..764ae911 100644 --- a/src/experimental/filament/filament/texture.h +++ b/src/experimental/filament/filament/texture.h @@ -15,13 +15,12 @@ #ifndef MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_TEXTURE_H_ #define MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_TEXTURE_H_ -#include - #include #include #include #include #include +#include "experimental/filament/render_context_filament.h" // Functions for creating filament textures. namespace mujoco { @@ -84,7 +83,7 @@ struct mjrTextureConfig { void mjr_defaultTextureConfig(mjrTextureConfig* config); // Wrapper around a filament::Texture. -class Texture { +class Texture : public mjrTexture { public: // Flags for internal use. struct InternalFlags { @@ -99,6 +98,9 @@ class Texture { ~Texture(); + Texture(const Texture&) = delete; + Texture& operator=(const Texture&) = delete; + // Uploads the given data to the texture. void Upload(const mjrTextureData& data); @@ -117,8 +119,12 @@ class Texture { return has_spherical_harmonics_ ? &spherical_harmonics_ : nullptr; } - Texture(const Texture&) = delete; - Texture& operator=(const Texture&) = delete; + static Texture* downcast(mjrTexture* texture) { + return static_cast(texture); + } + static const Texture* downcast(const mjrTexture* texture) { + return static_cast(texture); + } private: void ReleaseData(); diff --git a/src/experimental/filament/render_context_filament.h b/src/experimental/filament/render_context_filament.h index 28183fec..ef6f88ea 100644 --- a/src/experimental/filament/render_context_filament.h +++ b/src/experimental/filament/render_context_filament.h @@ -29,6 +29,17 @@ extern "C" { // IMPORTANT: This API should still be considered experimental and is likely // change frequently. +// Opaque types. +struct mjrTexture {}; +struct mjrMesh {}; +struct mjrScene {}; +struct mjrLight {}; +struct mjrRenderable {}; +struct mjrRenderTarget {}; + +// Opaque type for the filament rendering context. +struct mjrfContext {}; + typedef enum mjrGraphicsApi_ { // backend graphics API to use mjGRAPHICS_API_DEFAULT = 0, // default based on platform mjGRAPHICS_API_OPENGL, // OpenGL (desktop) / WebGL From 647af382c11224d8a64ba124fc4bfa949c268d97 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 28 Apr 2026 05:13:22 -0700 Subject: [PATCH 09/13] Add per-island PGS solver dispatch. Total testspeed runtime for `2humanoids100.xml` reduced by 27.6% (63.4 -> 49.5s) due to early termination on small islands PiperOrigin-RevId: 906910915 Change-Id: If55ad468c3680ef44eda7000455a77f8003b3122 --- doc/changelog.rst | 5 ++ doc/computation/index.rst | 6 -- simulate/simulate.cc | 2 +- src/engine/engine_forward.c | 93 +++++++++++++++++++------------ src/engine/engine_solver.c | 51 ++++++++++++----- src/engine/engine_solver.h | 9 +++ test/engine/engine_island_test.cc | 37 ++++++++++++ 7 files changed, 146 insertions(+), 57 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 79c01e44..4a8f22d4 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +Upcoming version (not yet released) +----------------------------------- + +- Added island support for the :ref:`PGS solver`. + Version 3.8.0 (April 24, 2026) ------------------------------ diff --git a/doc/computation/index.rst b/doc/computation/index.rst index 07f9adb5..000abcc4 100644 --- a/doc/computation/index.rst +++ b/doc/computation/index.rst @@ -1424,12 +1424,6 @@ While islanding is not free (see implementation in `engine_island.c - Unconstrained DOFs are completely untouched by the solver, which otherwise needs to discover that they are unaffected. - Solving separate islands can be multi-threaded. -.. admonition:: Known issues - :class: note - - Islanding is not yet supported by the PGS solver. - - .. _soParameters: Parameters diff --git a/simulate/simulate.cc b/simulate/simulate.cc index b4e29e35..76e2b788 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -349,7 +349,7 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) { sim->figcost.linepnt[start + 2] = 0; } - for (int i=0; ifigcost.linepnt[0]; i++) { + for (int i=0; ifigcost.linedata[start + 0][2*i] = i; sim->figcost.linedata[start + 1][2*i] = i; diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index dbd3a74e..0df4c0f1 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -953,7 +953,7 @@ static void solve_threaded(const mjModel* m, mjData* d, int flg_Newton) { // compute efc_b, efc_force, qfrc_constraint; update qacc void mj_fwdConstraint(const mjModel* m, mjData* d) { TM_START; - int nv = m->nv, nefc = d->nefc, nisland = d->nisland; + int nv = m->nv, nefc = d->nefc, nisland = d->nisland, nidof; // always clear qfrc_constraint mju_zero(d->qfrc_constraint, nv); @@ -970,50 +970,69 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { mj_mulJacVec(m, d, d->efc_b, d->qacc_smooth); mju_subFrom(d->efc_b, d->efc_aref, nefc); + // check for invalid solver type + if (m->opt.solver != mjSOL_PGS && m->opt.solver != mjSOL_CG && m->opt.solver != mjSOL_NEWTON) { + mjERROR("unknown solver type %d", m->opt.solver); + } + // warmstart solver warmstart(m, d); mju_zeroInt(d->solver_niter, mjNISLAND); // check if islands are supported - int islands_supported = !mjDISABLED(mjDSBL_ISLAND) && - nisland > 0 && - m->opt.noslip_iterations == 0 && - (m->opt.solver == mjSOL_CG || m->opt.solver == mjSOL_NEWTON); + int islands_supported = !mjDISABLED(mjDSBL_ISLAND) && nisland > 0; // run solver over constraint islands if (islands_supported) { - int nidof = d->nidof; - - // copy inputs to islands (vel+acc deps, pos-dependent already copied in mj_island) - mju_gather(d->ifrc_smooth, d->qfrc_smooth, d->map_idof2dof, nidof); - mju_gather(d->ifrc_constraint, d->qfrc_constraint, d->map_idof2dof, nidof); - mju_gather(d->iacc_smooth, d->qacc_smooth, d->map_idof2dof, nidof); - mju_gather(d->iacc, d->qacc, d->map_idof2dof, nidof); - mju_gather(d->iefc_force, d->efc_force, d->map_iefc2efc, nefc); - mju_gather(d->iefc_aref, d->efc_aref, d->map_iefc2efc, nefc); - - // solve per island, with or without threads - if (!d->threadpool) { - // no threadpool, loop over islands + switch ((mjtSolver) m->opt.solver) { + case mjSOL_PGS: for (int island=0; island < nisland; island++) { - if (m->opt.solver == mjSOL_NEWTON) { - mj_solNewton_island(m, d, island, m->opt.iterations); - } else { - mj_solCG_island(m, d, island, m->opt.iterations); - } + mj_solPGS_island(m, d, island, m->opt.iterations); } - } else { - // have threadpool, solve using threads - solve_threaded(m, d, m->opt.solver == mjSOL_NEWTON); + break; + + case mjSOL_CG: + case mjSOL_NEWTON: + // copy inputs to islands (vel+acc deps, pos-dependent already copied in mj_island) + nidof = d->nidof; + mju_gather(d->ifrc_smooth, d->qfrc_smooth, d->map_idof2dof, nidof); + mju_gather(d->ifrc_constraint, d->qfrc_constraint, d->map_idof2dof, nidof); + mju_gather(d->iacc_smooth, d->qacc_smooth, d->map_idof2dof, nidof); + mju_gather(d->iacc, d->qacc, d->map_idof2dof, nidof); + mju_gather(d->iefc_force, d->efc_force, d->map_iefc2efc, nefc); + mju_gather(d->iefc_aref, d->efc_aref, d->map_iefc2efc, nefc); + + // solve per island, with or without threads + if (!d->threadpool) { + // no threadpool, loop over islands + for (int island=0; island < nisland; island++) { + if (m->opt.solver == mjSOL_NEWTON) { + mj_solNewton_island(m, d, island, m->opt.iterations); + } else { + mj_solCG_island(m, d, island, m->opt.iterations); + } + } + } else { + // have threadpool, solve using threads + solve_threaded(m, d, m->opt.solver == mjSOL_NEWTON); + } + + // copy back solver outputs (scatter dofs since ni <= nv) + mju_scatter(d->qacc, d->iacc, d->map_idof2dof, nidof); + mju_scatter(d->qfrc_constraint, d->ifrc_constraint, d->map_idof2dof, nidof); + mju_gather(d->efc_force, d->iefc_force, d->map_efc2iefc, nefc); + break; } - // copy back solver outputs (scatter dofs since ni <= nv) - mju_scatter(d->qacc, d->iacc, d->map_idof2dof, nidof); - mju_scatter(d->qfrc_constraint, d->ifrc_constraint, d->map_idof2dof, nidof); - mju_gather(d->efc_force, d->iefc_force, d->map_efc2iefc, nefc); + // run noslip solver per island if enabled + if (m->opt.noslip_iterations > 0) { + for (int island=0; island < nisland; island++) { + mj_solNoSlip_island(m, d, island, m->opt.noslip_iterations); + } + } } - // run solver over all constraints + // run solver over all constraints (monolithic) else { switch ((mjtSolver) m->opt.solver) { case mjSOL_PGS: // PGS @@ -1027,15 +1046,17 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { case mjSOL_NEWTON: // Newton mj_solNewton(m, d, m->opt.iterations); break; + } - default: - mjERROR("unknown solver type %d", m->opt.solver); + // run noslip solver if enabled + if (m->opt.noslip_iterations > 0) { + mj_solNoSlip(m, d, m->opt.noslip_iterations); } } - // run noslip solver if enabled - if (m->opt.noslip_iterations > 0) { - mj_solNoSlip(m, d, m->opt.noslip_iterations); + // dual solvers: map efc_force to joint space (always monolithic) + if (m->opt.solver == mjSOL_PGS || m->opt.noslip_iterations > 0) { + mj_dualFinish(m, d); } TM_END(mjTIMER_CONSTRAINT); diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index 5700a3e5..8a7b1d00 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -66,7 +66,6 @@ static void saveStats(const mjModel* m, mjData* d, int island, int iter, // finalize dual solver: map to joint space -// TODO: b/295296178 - add island support to Dual solvers static void dualFinish(const mjModel* m, mjData* d) { // map constraint force to joint space mj_mulJacTVec(m, d, d->qfrc_constraint, d->efc_force); @@ -77,10 +76,15 @@ static void dualFinish(const mjModel* m, mjData* d) { } +// PGS: map efc_force to joint space +void mj_dualFinish(const mjModel* m, mjData* d) { + dualFinish(m, d); +} + + // compute 1/diag(AR) // res[c] = 1 / AR[efclist[c], efclist[c]] for c = 0..nefc-1 // efclist is NULL for monolithic (sequential) iteration -// TODO: b/295296178 - add island support to Dual solvers static void ARdiaginv(const mjModel* m, const mjData* d, mjtNum* res, int nefc, const int* efclist, int flg_subR) { const mjtNum *AR = d->efc_AR; @@ -118,7 +122,6 @@ static void ARdiaginv(const mjModel* m, const mjData* d, mjtNum* res, // extract diagonal block from AR, clamp diag to 1e-10 if flg_subR -// TODO: b/295296178 - add island support to Dual solvers static void extractBlock(const mjModel* m, const mjData* d, mjtNum* Ac, int start, int n, int flg_subR) { int nefc = d->nefc; @@ -178,7 +181,6 @@ static void extractBlock(const mjModel* m, const mjData* d, mjtNum* Ac, // compute residual for one block -// TODO: b/295296178 - add island support to Dual solvers static void residual(const mjModel* m, const mjData* d, mjtNum* res, int i, int dim, int flg_subR) { int nefc = d->nefc; @@ -208,7 +210,6 @@ static void residual(const mjModel* m, const mjData* d, mjtNum* res, int i, int // compute cost change -// TODO: b/295296178 - add island support to Dual solvers static mjtNum costChange(const mjtNum* A, mjtNum* force, const mjtNum* oldforce, const mjtNum* res, int dim) { mjtNum change; @@ -235,7 +236,6 @@ static mjtNum costChange(const mjtNum* A, mjtNum* force, const mjtNum* oldforce, // set efc_state to dual constraint state; return nactive // iterates over efclist (or sequentially if NULL), classifies by ne/nf ranges -// TODO: b/295296178 - add island support to Dual solvers static int dualState(const mjData* d, int* state, int ne, int nf, int nefc, const int* efclist) { const mjtNum* force = d->efc_force; @@ -384,7 +384,6 @@ static void solveQCQP(mjtNum* force, int i, int dim, // island: island index for stats (use -1 for monolithic, mapped to 0) // ne, nf, nefc: constraint type counts // efclist: maps list position c to monolithic efc index (NULL for sequential) -// TODO: b/295296178 - add island support to Dual solvers static void solPGS(const mjModel* m, mjData* d, int island, int ne, int nf, int nefc, const int* efclist, int maxiter) { @@ -572,10 +571,20 @@ static void solPGS(const mjModel* m, mjData* d, int island, } -// PGS entry point (monolithic) +// PGS entry point (monolithic, no dualFinish — caller handles it) void mj_solPGS(const mjModel* m, mjData* d, int maxiter) { solPGS(m, d, /*island=*/-1, d->ne, d->nf, d->nefc, /*efclist=*/NULL, maxiter); - dualFinish(m, d); +} + + +// PGS entry point (one island) +void mj_solPGS_island(const mjModel* m, mjData* d, int island, int maxiter) { + int ne = d->island_ne[island]; + int nf = d->island_nf[island]; + int nefc = d->island_nefc[island]; + int iefcadr = d->island_iefcadr[island]; + + solPGS(m, d, island, ne, nf, nefc, d->map_iefc2efc + iefcadr, maxiter); } @@ -754,8 +763,10 @@ static void solNoSlip(const mjModel* m, mjData* d, int island, improvement *= scale; // save noslip stats after all the entries from regular solver - int stats_iter = iter + d->solver_niter[island_stat]; - saveStats(m, d, island_stat, stats_iter, improvement, 0, 0, nactive, nchange, 0, 0); + if (island_stat < mjNISLAND) { + int stats_iter = iter + d->solver_niter[island_stat]; + saveStats(m, d, island_stat, stats_iter, improvement, 0, 0, nactive, nchange, 0, 0); + } // increment iteration count iter++; @@ -767,16 +778,28 @@ static void solNoSlip(const mjModel* m, mjData* d, int island, } // update solver iterations - d->solver_niter[island_stat] += iter; + if (island_stat < mjNISLAND) { + d->solver_niter[island_stat] += iter; + } mj_freeStack(d); } -// NoSlip entry point (monolithic) +// NoSlip entry point (monolithic, no dualFinish — caller handles it) void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { solNoSlip(m, d, /*island=*/-1, d->ne, d->nf, d->nefc, /*efclist=*/NULL, maxiter); - dualFinish(m, d); +} + + +// NoSlip entry point (one island) +void mj_solNoSlip_island(const mjModel* m, mjData* d, int island, int maxiter) { + int ne = d->island_ne[island]; + int nf = d->island_nf[island]; + int nefc = d->island_nefc[island]; + int iefcadr = d->island_iefcadr[island]; + + solNoSlip(m, d, island, ne, nf, nefc, d->map_iefc2efc + iefcadr, maxiter); } diff --git a/src/engine/engine_solver.h b/src/engine/engine_solver.h index 7ee007de..46657619 100644 --- a/src/engine/engine_solver.h +++ b/src/engine/engine_solver.h @@ -35,10 +35,19 @@ void mj_solNewton(const mjModel* m, mjData* d, int maxiter); //------------------------------ per-island solvers ------------------------------------------------ +// PGS solver (one island, no dualFinish — caller handles it) +void mj_solPGS_island(const mjModel* m, mjData* d, int island, int maxiter); + +// NoSlip solver (one island, no dualFinish — caller handles it) +void mj_solNoSlip_island(const mjModel* m, mjData* d, int island, int maxiter); + // CG solver void mj_solCG_island(const mjModel* m, mjData* d, int island, int maxiter); // Newton entry point void mj_solNewton_island(const mjModel* m, mjData* d, int island, int maxiter); +// map efc_force to joint space (used after dual island dispatch) +void mj_dualFinish(const mjModel* m, mjData* d); + #endif // MUJOCO_SRC_ENGINE_ENGINE_SOLVER_H_ diff --git a/test/engine/engine_island_test.cc b/test/engine/engine_island_test.cc index d934f852..c8055293 100644 --- a/test/engine/engine_island_test.cc +++ b/test/engine/engine_island_test.cc @@ -15,6 +15,7 @@ // Tests for engine/engine_island.c. #include +#include #include #include @@ -602,5 +603,41 @@ TEST_F(IslandTest, EqualityConstraintOfTendons) { mj_deleteModel(model); } +TEST_F(IslandTest, PGSIslandExact) { + const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); + char error[1024]; + mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error)); + ASSERT_THAT(m, NotNull()) << error; + mjData* d = mj_makeData(m); + + // simulate to get a non-trivial state + while (d->time < 0.5) { + mj_step(m, d); + } + + // switch to PGS, disable early termination + m->opt.solver = mjSOL_PGS; + m->opt.tolerance = 0; + + // solve with islands + m->opt.disableflags &= ~mjDSBL_ISLAND; + mj_forward(m, d); + ASSERT_GT(d->nisland, 1); + std::vector qfrc_island(d->qfrc_constraint, + d->qfrc_constraint + m->nv); + + // solve without islands + m->opt.disableflags |= mjDSBL_ISLAND; + mj_forward(m, d); + std::vector qfrc_mono(d->qfrc_constraint, + d->qfrc_constraint + m->nv); + + // expect exact match + EXPECT_EQ(qfrc_island, qfrc_mono); + + mj_deleteData(d); + mj_deleteModel(m); +} + } // namespace } // namespace mujoco From 5751b09ff63f8e026eb5fb36c0a1c753349d19d4 Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Tue, 28 Apr 2026 05:59:15 -0700 Subject: [PATCH 10/13] Initial pass at exposing the new mjr APIs. Moves all internal types and structs to the "public" render_context_filament.h header file. PiperOrigin-RevId: 906929736 Change-Id: I7e7c315fb99a3601b09841073eaeec795718b999 --- .../filament/compat/mjr_filament_renderer.cc | 28 +- .../filament/compat/mjr_filament_renderer.h | 2 +- .../filament/compat/scene_geom_util.cc | 4 +- .../filament/filament/draw_mode.h | 36 -- .../filament/filament/filament_context.cc | 14 +- .../filament/filament/filament_context.h | 57 +--- src/experimental/filament/filament/light.cc | 16 +- src/experimental/filament/filament/light.h | 29 -- .../filament/filament/material.cc | 44 +-- src/experimental/filament/filament/material.h | 37 +- src/experimental/filament/filament/mesh.cc | 6 +- src/experimental/filament/filament/mesh.h | 104 ------ .../filament/filament/render_target.cc | 5 - .../filament/filament/render_target.h | 9 - .../filament/filament/renderable.cc | 28 +- .../filament/filament/renderable.h | 24 +- .../filament/filament/scene_view.cc | 28 +- .../filament/filament/scene_view.h | 5 +- src/experimental/filament/filament/texture.cc | 8 - src/experimental/filament/filament/texture.h | 57 ---- .../filament/render_context_filament.cc | 80 +++++ .../filament/render_context_filament.h | 322 ++++++++++++++++++ 22 files changed, 468 insertions(+), 475 deletions(-) delete mode 100644 src/experimental/filament/filament/draw_mode.h diff --git a/src/experimental/filament/compat/mjr_filament_renderer.cc b/src/experimental/filament/compat/mjr_filament_renderer.cc index 48242258..8953e2a2 100644 --- a/src/experimental/filament/compat/mjr_filament_renderer.cc +++ b/src/experimental/filament/compat/mjr_filament_renderer.cc @@ -25,7 +25,6 @@ #include "experimental/filament/compat/imgui_bridge.h" #include "experimental/filament/compat/imgui_editor.h" #include "experimental/filament/compat/scene_bridge.h" -#include "experimental/filament/filament/draw_mode.h" #include "experimental/filament/filament/filament_context.h" #include "experimental/filament/filament/model_util.h" #include "experimental/filament/filament/render_target.h" @@ -42,11 +41,14 @@ void MjrFilamentRenderer::Init(const mjModel* model) { scene_bridge_ = std::make_unique(GetObjectManager(), model); imgui_bridge_ = std::make_unique(GetObjectManager()); + mjr_defaultRenderRequest(&render_requests_[0]); + mjr_defaultRenderRequest(&render_requests_[1]); + render_requests_[0].scene = scene_bridge_->GetSceneView(); - render_requests_[0].draw_mode = DrawMode::Color; + render_requests_[0].draw_mode = mjDRAW_MODE_COLOR; render_requests_[1].scene = imgui_bridge_->GetSceneView(); - render_requests_[1].draw_mode = DrawMode::Color; + render_requests_[1].draw_mode = mjDRAW_MODE_COLOR; // The UX camera is a fixed orthographic camera. We only need to change the // width/height based on the viewport per frame. @@ -78,11 +80,11 @@ void MjrFilamentRenderer::Render(const mjrRect& viewport, const mjvScene* scene) } if (scene->flags[mjRND_SEGMENT]) { - render_requests_[0].draw_mode = DrawMode::Segmentation; + render_requests_[0].draw_mode = mjDRAW_MODE_SEGMENTATION; } else if (scene->flags[mjRND_DEPTH]) { - render_requests_[0].draw_mode = DrawMode::Depth; + render_requests_[0].draw_mode = mjDRAW_MODE_DEPTH; } else { - render_requests_[0].draw_mode = DrawMode::Color; + render_requests_[0].draw_mode = mjDRAW_MODE_COLOR; } render_requests_[0].width = viewport.width; @@ -142,10 +144,11 @@ void MjrFilamentRenderer::ReadPixels(mjrRect viewport, unsigned char* rgb, const size_t num_requests = (mode_ == FrameBufferMode::OffScreenWithGui) ? 2 : 1; - ReadPixelsRequest read_request; + mjrReadPixelsRequest read_request; + mjr_defaultReadPixelsRequest(&read_request); read_request.output = rgb; read_request.num_bytes = viewport.width * viewport.height * 3; - const FrameHandle frame = FilamentContext::Render( + const mjrFrameHandle frame = FilamentContext::Render( {&render_requests_[0], num_requests}, {&read_request, 1}); FilamentContext::WaitForFrame(frame); @@ -163,13 +166,14 @@ void MjrFilamentRenderer::ReadPixels(mjrRect viewport, unsigned char* rgb, render_requests_[0].target = target.get(); render_requests_[1].target = target.get(); - DrawMode last_draw_mode = render_requests_[0].draw_mode; - render_requests_[0].draw_mode = DrawMode::Depth; + mjrDrawMode last_draw_mode = render_requests_[0].draw_mode; + render_requests_[0].draw_mode = mjDRAW_MODE_DEPTH; - ReadPixelsRequest read_request; + mjrReadPixelsRequest read_request; + mjr_defaultReadPixelsRequest(&read_request); read_request.output = reinterpret_cast(depth); read_request.num_bytes = viewport.width * viewport.height * sizeof(float); - const FrameHandle frame = + const mjrFrameHandle frame = FilamentContext::Render({&render_requests_[0], 1}, {&read_request, 1}); FilamentContext::WaitForFrame(frame); diff --git a/src/experimental/filament/compat/mjr_filament_renderer.h b/src/experimental/filament/compat/mjr_filament_renderer.h index a1c97edc..3d87e624 100644 --- a/src/experimental/filament/compat/mjr_filament_renderer.h +++ b/src/experimental/filament/compat/mjr_filament_renderer.h @@ -76,7 +76,7 @@ class MjrFilamentRenderer : public FilamentContext { }; FrameBufferMode mode_ = FrameBufferMode::Window; - RenderRequest render_requests_[2]; + mjrRenderRequest render_requests_[2]; std::unique_ptr scene_bridge_; std::unique_ptr imgui_bridge_; }; diff --git a/src/experimental/filament/compat/scene_geom_util.cc b/src/experimental/filament/compat/scene_geom_util.cc index 527fe56f..ddc85bee 100644 --- a/src/experimental/filament/compat/scene_geom_util.cc +++ b/src/experimental/filament/compat/scene_geom_util.cc @@ -30,12 +30,12 @@ #include #include #include "experimental/filament/compat/model_objects.h" -#include "experimental/filament/filament/material.h" #include "experimental/filament/filament/math_util.h" #include "experimental/filament/filament/mesh.h" #include "experimental/filament/filament/object_manager.h" #include "experimental/filament/filament/renderable.h" #include "experimental/filament/filament/texture.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { @@ -424,7 +424,7 @@ static void UpdateGeomMaterial(Renderable& renderable, const mjvGeom& geom, // the programmatic UVs. if (textures.color) { - if (textures.color->GetFilamentTexture()->getTarget() == + if (Texture::downcast(textures.color)->GetFilamentTexture()->getTarget() == filament::Texture::Sampler::SAMPLER_2D) { // For 2D textures, `tex_repeat` specifies how many times the texture // image is repeated. The `tex_uniform` flag determines if the repetition diff --git a/src/experimental/filament/filament/draw_mode.h b/src/experimental/filament/filament/draw_mode.h deleted file mode 100644 index 15bb2bca..00000000 --- a/src/experimental/filament/filament/draw_mode.h +++ /dev/null @@ -1,36 +0,0 @@ -// 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_FILAMENT_FILAMENT_DRAW_MODE_H_ -#define MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_DRAW_MODE_H_ - -namespace mujoco { - -// The different modes that can be used to render the scene. -enum class DrawMode { - // Render the scene with "normal" colors and lighting. - Color, - // Render the scene as a grayscale depth map. - Depth, - // Render each object with a unique, uniform (flat) color regardless of - // lighting and texture. - Segmentation, -}; - -static constexpr int kNumDrawModes = 3; - -} // namespace mujoco - - -#endif // MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_DRAW_MODE_H_ diff --git a/src/experimental/filament/filament/filament_context.cc b/src/experimental/filament/filament/filament_context.cc index 906c32e3..4149559c 100644 --- a/src/experimental/filament/filament/filament_context.cc +++ b/src/experimental/filament/filament/filament_context.cc @@ -79,16 +79,16 @@ FilamentContext::~FilamentContext() { filament::Engine::destroy(engine_); } -FilamentContext::FrameHandle FilamentContext::Render( - std::span requests, - std::span read_requests) { +mjrFrameHandle FilamentContext::Render( + std::span requests, + std::span read_requests) { if (read_requests.size() > 1) { mju_error("Only one read request is supported for now."); } bool render_began = false; mjrRenderTarget* current_target = nullptr; - for (const RenderRequest& request : requests) { + for (const mjrRenderRequest& request : requests) { if (request.target != current_target && render_began) { renderer_->endFrame(); render_began = false; @@ -135,7 +135,7 @@ FilamentContext::FrameHandle FilamentContext::Render( "Rendering to a render target without a read request is pointless."); } - const ReadPixelsRequest& read_request = read_requests[0]; + const mjrReadPixelsRequest& read_request = read_requests[0]; if (read_request.num_bytes == 0) { mju_error("Output buffer size is zero."); } @@ -156,7 +156,7 @@ FilamentContext::FrameHandle FilamentContext::Render( scene_view_request.target = render_target; SceneView* scene_view = SceneView::downcast(request.scene); scene_view->Render(renderer_, scene_view_request); - render_target->ReadColorPixels(renderer_, read_request.output, + render_target->ReadColorPixels(renderer_, (uint8_t*)read_request.output, read_request.num_bytes); } } @@ -179,7 +179,7 @@ FilamentContext::FrameHandle FilamentContext::Render( return ++frame_counter_; } -void FilamentContext::WaitForFrame(FrameHandle frame_handle) { +void FilamentContext::WaitForFrame(mjrFrameHandle frame_handle) { if (frame_counter_ < frame_handle) { engine_->flushAndWait(); } diff --git a/src/experimental/filament/filament/filament_context.h b/src/experimental/filament/filament/filament_context.h index 2e98acaf..732229e0 100644 --- a/src/experimental/filament/filament/filament_context.h +++ b/src/experimental/filament/filament/filament_context.h @@ -15,7 +15,6 @@ #ifndef MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_FILAMENT_CONTEXT_H_ #define MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_FILAMENT_CONTEXT_H_ -#include #include #include #include @@ -25,11 +24,7 @@ #include #include #include -#include -#include "experimental/filament/filament/draw_mode.h" -#include "experimental/filament/filament/scene_view.h" #include "experimental/filament/filament/object_manager.h" -#include "experimental/filament/filament/render_target.h" #include "experimental/filament/render_context_filament.h" namespace mujoco { @@ -43,62 +38,16 @@ class FilamentContext : public mjrfContext { FilamentContext(const FilamentContext&) = delete; FilamentContext& operator=(const FilamentContext&) = delete; - // Information needed to render a single image of a scene. - struct RenderRequest { - // The scene to render. - mjrScene* scene = nullptr; - - // The method (e.g. Color, Depth, Segmentation, etc.) to use for rendering. - DrawMode draw_mode = DrawMode::Color; - - // The camera from which to render the scene. - mjvGLCamera camera; - - // The dimensions of the output image. - int width = 0; - int height = 0; - - // The render target into which to render the image. If nullptr, the image - // will be rendered to the window (as previously configured in - // mjrFilamentConfig::native_window). - mjrRenderTarget* target = nullptr; - }; - - // Information needed to read pixels from a render target. - struct ReadPixelsRequest { - mjrRenderTarget* target = nullptr; - - // The buffer into which the read pixels will be written. - uint8_t* output = nullptr; - - // The number of bytes in the output buffer. This should match the size of - // the render target texture. - std::size_t num_bytes = 0; - - // Callback when the read pixels operation is complete. This will be called - // during WaitForFrame() or in a subsequent call to Render(). This function - // can optionally be used to free the output buffer if needed. - void (*read_completed_callback)(void* user_data) = nullptr; - - // User data to pass to the completion callback. - void* user_data = nullptr; - }; - - // Rendering is asynchronous by nature. Each render request is assigned a - // unique Handle which can be used to query the status of the request. The - // Handle can also be used to block until the request is completed. - using FrameHandle = std::uint64_t; - // Queues the given render requests for rendering. This function copies the // necessary data from the requests into the renderer thread and returns // immediately afterwards. The renderer thread will then perform the actual // rendering on the GPU. Callers can use WaitForFrame to block until the // rendering is complete. - FrameHandle Render(std::span render_requests, - std::span read_requests = {}); + mjrFrameHandle Render(std::span render_requests, + std::span read_requests = {}); // Blocks until the given frame has completed rendering. - void WaitForFrame(FrameHandle frame_handle); + void WaitForFrame(mjrFrameHandle frame_handle); // Sets the clear color for the renderer. void SetClearColor(const filament::math::float4& color); diff --git a/src/experimental/filament/filament/light.cc b/src/experimental/filament/filament/light.cc index d79a1551..2918f0a0 100644 --- a/src/experimental/filament/filament/light.cc +++ b/src/experimental/filament/filament/light.cc @@ -27,27 +27,13 @@ #include #include "experimental/filament/filament/math_util.h" #include "experimental/filament/filament/texture.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { using filament::math::float3; using filament::math::mat3f; -void mjr_defaultLightParams(mjrLightParams* params) { - params->type = mjLIGHT_POINT; - params->texture = nullptr; - params->color[0] = 0; - params->color[1] = 0; - params->color[2] = 0; - params->intensity = 0.0f; - params->cast_shadows = true; - params->range = 10.0f; - params->spot_cone_angle = 180.f; - params->bulb_radius = 0.0f; - params->shadow_map_size = 2048; - params->vsm_blur_width = 0.0f; -} - Light::Light(filament::Engine* engine, const mjrLightParams& params) : engine_(engine), params_(params) { // Filament treats image-based lights (IBLs) as separate objects (i.e. diff --git a/src/experimental/filament/filament/light.h b/src/experimental/filament/filament/light.h index d58dbdd0..d0b6a884 100644 --- a/src/experimental/filament/filament/light.h +++ b/src/experimental/filament/filament/light.h @@ -20,39 +20,10 @@ #include #include #include -#include "experimental/filament/filament/texture.h" #include "experimental/filament/render_context_filament.h" namespace mujoco { -typedef mjtLightType mjrLightType; - -// Configuration parameters for a light. -struct mjrLightParams { - // The type of light (e.g. spot, point, directional, etc.) - mjrLightType type; - // The texture to use for image lights. - const mjrTexture* texture; - // The color of the light. - float color[3]; - // The intensity of the light, in candela. - float intensity; - // Whether or not the light casts shadows. - mjtByte cast_shadows; - // The range/distance in which the light is effective, in meters. - float range; - // The angle of the spot light cone, in degrees. - float spot_cone_angle; - // The radius of the bulb used for soft shadows. - float bulb_radius; - // The size of the shadow map. - int shadow_map_size; - // Blur width for EL VSM. - float vsm_blur_width; -}; - -void mjr_defaultLightParams(mjrLightParams* params); - // Manages the filament Entities for a single mjvLight. class Light : public mjrLight { public: diff --git a/src/experimental/filament/filament/material.cc b/src/experimental/filament/filament/material.cc index fcdc54fa..925e88fb 100644 --- a/src/experimental/filament/filament/material.cc +++ b/src/experimental/filament/filament/material.cc @@ -14,8 +14,6 @@ #include "experimental/filament/filament/material.h" -#include - #include #include #include @@ -25,45 +23,10 @@ #include "experimental/filament/filament/math_util.h" #include "experimental/filament/filament/texture.h" #include "experimental/filament/filament/object_manager.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { -template -static void setf(float (&arr)[N], const std::array& values) { - for (int i = 0; i < N; ++i) { - arr[i] = values[i]; - } -} - -void mjr_defaultMaterialTextures(mjrMaterialTextures* textures) { - textures->color = nullptr; - textures->normal = nullptr; - textures->metallic = nullptr; - textures->roughness = nullptr; - textures->occlusion = nullptr; - textures->orm = nullptr; - textures->emissive = nullptr; - textures->reflection = nullptr; -} - -void mjr_defaultMaterialParams(mjrMaterialParams* params) { - setf(params->color, {1.f, 1.f, 1.f, 1.f}); - setf(params->segmentation_color, {1, 1, 1, 1}); - setf(params->uv_scale, {1, 1, 1}); - setf(params->uv_offset, {0, 0, 0}); - setf(params->scissor, {0, 0, 0, 0}); - - params->emissive = -1.0f; - params->specular = -1.0f; - params->glossiness = -1.0f; - params->metallic = -1.0f; - params->roughness = -1.0f; - params->reflectance = 0.0f; - params->tex_uniform = false; - params->reflective = false; -} - - void UpdateMaterialInstance(filament::MaterialInstance* instance, const mjrMaterialParams& params, const mjrMaterialTextures& textures, @@ -118,11 +81,12 @@ void UpdateMaterialInstance(filament::MaterialInstance* instance, sampler.setMinFilter( filament::TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR); - auto TrySetTexture = [&](const char* name, const Texture* texture, + auto TrySetTexture = [&](const char* name, const mjrTexture* texture, mjtTextureRole role) { if (material->hasParameter(name)) { if (texture != nullptr) { - instance->setParameter(name, texture->GetFilamentTexture(), sampler); + instance->setParameter( + name, Texture::downcast(texture)->GetFilamentTexture(), sampler); } else { instance->setParameter(name, object_mgr->GetFallbackTexture(role), sampler); diff --git a/src/experimental/filament/filament/material.h b/src/experimental/filament/filament/material.h index 630f4844..abc0f9a1 100644 --- a/src/experimental/filament/filament/material.h +++ b/src/experimental/filament/filament/material.h @@ -17,46 +17,11 @@ #include #include -#include -#include "experimental/filament/filament/texture.h" #include "experimental/filament/filament/object_manager.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { -// The textures that can be assigned to the drawable's material. -struct mjrMaterialTextures { - const Texture* color; - const Texture* normal; - const Texture* metallic; - const Texture* roughness; - const Texture* occlusion; - const Texture* orm; - const Texture* emissive; - const Texture* reflection; -}; - -void mjr_defaultMaterialTextures(mjrMaterialTextures* textures); - -// The parameters that can be applied to the drawable's material. -struct mjrMaterialParams { - float color[4]; - float segmentation_color[4]; - float tex_repeat[2]; - float uv_scale[3]; - float uv_offset[3]; - float scissor[4]; - float specular; - float glossiness; - float metallic; - float roughness; - float emissive; - float reflectance; - mjtByte tex_uniform; - mjtByte reflective; -}; - -void mjr_defaultMaterialParams(mjrMaterialParams* params); - // Updates the material instances based on the currently set parameters and // textures. void UpdateMaterialInstance(filament::MaterialInstance* instance, diff --git a/src/experimental/filament/filament/mesh.cc b/src/experimental/filament/filament/mesh.cc index 2d7a74b7..9c06cfc7 100644 --- a/src/experimental/filament/filament/mesh.cc +++ b/src/experimental/filament/filament/mesh.cc @@ -33,6 +33,7 @@ #include #include #include "experimental/filament/filament/math_util.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { @@ -101,11 +102,6 @@ int FillSequence(std::byte* buffer, std::size_t num_bytes) { return num; } -// Initializes the mjrMeshData to default values. -void mjr_defaultMeshData(mjrMeshData* data) { - std::memset(data, 0, sizeof(mjrMeshData)); -} - Mesh::Mesh(filament::Engine* engine, const mjrMeshData& data) : engine_(engine), shared_state_(std::make_shared()) { type_ = data.primitive_type == mjMESH_PRIMITIVE_TYPE_TRIANGLES diff --git a/src/experimental/filament/filament/mesh.h b/src/experimental/filament/filament/mesh.h index 9746e4ab..206aa27d 100644 --- a/src/experimental/filament/filament/mesh.h +++ b/src/experimental/filament/filament/mesh.h @@ -16,7 +16,6 @@ #define MUJOCO_SRC_EXPERIMENTAL_FILAMENT_FILAMENT_MESH_H_ #include -#include #include #include #include @@ -30,114 +29,11 @@ #include #include #include -#include #include "experimental/filament/render_context_filament.h" // Functions for creating filament vertex and index buffers. namespace mujoco { -// The type of data stored in an index buffer. -typedef enum mjrIndexType_ { - mjINDEX_TYPE_U16 = 0, - mjINDEX_TYPE_U32 = 1, -} mjrIndexType; - -// The type of primitive to be drawn by vertex data. -typedef enum mjrMeshPrimitiveType_ { - mjMESH_PRIMITIVE_TYPE_TRIANGLES = 0, - mjMESH_PRIMITIVE_TYPE_LINES = 1, -} mjrMeshPrimitiveType; - -// The usage/purpose of an attribute of a vertex. -typedef enum mjrVertexAttributeUsage_ { - mjVERTEX_ATTRIBUTE_USAGE_POSITION = 0, - mjVERTEX_ATTRIBUTE_USAGE_NORMAL = 1, - mjVERTEX_ATTRIBUTE_USAGE_TANGENTS = 2, - mjVERTEX_ATTRIBUTE_USAGE_UV = 3, - mjVERTEX_ATTRIBUTE_USAGE_COLOR = 4, -} mjrVertexAttributeUsage; - -// The data format of an attribute of a vertex. -typedef enum mjrVertexAttributeType_ { - mjVERTEX_ATTRIBUTE_TYPE_FLOAT2 = 0, - mjVERTEX_ATTRIBUTE_TYPE_FLOAT3 = 1, - mjVERTEX_ATTRIBUTE_TYPE_FLOAT4 = 2, - mjVERTEX_ATTRIBUTE_TYPE_UBYTE4 = 3, -} mjrVertexAttributeType; - -// Maximum number of vertex attributes that can be used by a mesh. -enum { mjMAX_VERTEX_ATTRIBUTES = 16 }; - -// Information about a single attribute of a vertex. -struct mjrVertexAttribute { - // The data for the attribute. - const void* bytes; - - // The usage/purpose of the attribute. - mjrVertexAttributeUsage usage; - - // The data format of the attribute. - mjrVertexAttributeType type; -}; - -// The binary contents of a mesh. -struct mjrMeshData { - // The number of vertices in the mesh. Each of the vertex arrays below is - // assumed to have this number of elements. - mjtSize nvertices; - - // The number of attributes for each vertex in the mesh. - int nattributes; - - // Information about each attribute of a vertex in the mesh. See `interleaved` - // for more details. - mjrVertexAttribute attributes[mjMAX_VERTEX_ATTRIBUTES]; - - // Whether the vertex attributes are interleaved or not. - // - // If true, assumes that the attributes are packed in the order specified in - // the attributes array, with no padding in-between. Additionally, the - // `data` pointer for each attribute is assumed to point to the first element - // of that type. - // - // If false, assume each attribute is stored in a separate array as defined - // by the `data` field of the attribute. - mjtByte interleaved; - - // The number of indices in the mesh. The indices array is assumed to have - // this number of elements. - mjtSize nindices; - - // The indices of the mesh, stored as either ushort or uint depending on the - // index type. - const void* indices; - - // The type of data stored in the indices array. - mjrIndexType index_type; - - // The type of primitive to be drawn by vertex data. - mjrMeshPrimitiveType primitive_type; - - // Whether to compute the bounds of the mesh using the vertex positions. - mjtByte compute_bounds; - - // The bounds of the mesh. If bounds_min == bounds_max, then we assume that - // that the bounds are not set (i.e. the bounds is empty). - float bounds_min[3]; - float bounds_max[3]; - - // Because rendering may be multithreaded, we cannot make assumptions about - // when the mesh data will finish uploading to the GPU. As such, we will use - // this callback to notify callers when it is safe to free the mesh data. - void (*release_callback)(void* user_data); - - // User data to pass to the release callback. - void* user_data; -}; - -// Initializes the MeshData to default values. -void mjr_defaultMeshData(mjrMeshData* data); - // Owns a Vertex and Index buffer representing a geometry mesh. class Mesh : public mjrMesh { public: diff --git a/src/experimental/filament/filament/render_target.cc b/src/experimental/filament/filament/render_target.cc index 3e53ac51..4c083136 100644 --- a/src/experimental/filament/filament/render_target.cc +++ b/src/experimental/filament/filament/render_target.cc @@ -30,11 +30,6 @@ namespace mujoco { -void mjr_defaultRenderTargetConfig(mjrRenderTargetConfig* config) { - config->color_format = mjPIXEL_FORMAT_RGBA8; - config->depth_format = mjPIXEL_FORMAT_DEPTH32F; -} - RenderTarget::RenderTarget(filament::Engine* engine, const mjrRenderTargetConfig& config) : engine_(engine), config_(config) {} diff --git a/src/experimental/filament/filament/render_target.h b/src/experimental/filament/filament/render_target.h index 9e5aed68..92e221b3 100644 --- a/src/experimental/filament/filament/render_target.h +++ b/src/experimental/filament/filament/render_target.h @@ -26,15 +26,6 @@ namespace mujoco { -// Defines the basic properties of a render target. -struct mjrRenderTargetConfig { - mjrPixelFormat color_format; - mjrPixelFormat depth_format; -}; - -// Initializes the RenderTargetConfig to default values. -void mjr_defaultRenderTargetConfig(mjrRenderTargetConfig* config); - // Manages a filament RenderTarget and the textures which are bound to it. class RenderTarget : public mjrRenderTarget { public: diff --git a/src/experimental/filament/filament/renderable.cc b/src/experimental/filament/filament/renderable.cc index 4381cbd4..47c48849 100644 --- a/src/experimental/filament/filament/renderable.cc +++ b/src/experimental/filament/filament/renderable.cc @@ -26,20 +26,17 @@ #include #include #include -#include "experimental/filament/filament/draw_mode.h" #include "experimental/filament/filament/material.h" #include "experimental/filament/filament/math_util.h" #include "experimental/filament/filament/mesh.h" #include "experimental/filament/filament/object_manager.h" +#include "experimental/filament/filament/texture.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { using filament::math::mat4f; -void mjr_defaultRenderableParams(mjrRenderableParams* params) { - params->shading_model = mjSHADING_MODEL_SCENE_OBJECT; -} - Renderable::Renderable(ObjectManager* object_mgr, const mjrRenderableParams& params) : object_mgr_(object_mgr), params_(params) { mjr_defaultMaterialParams(&material_params_); @@ -57,7 +54,7 @@ Renderable::~Renderable() noexcept { engine->destroy(part.entity); em.destroy(part.entity); } - for (int i = 0; i < kNumDrawModes; ++i) { + for (int i = 0; i < mjNUM_DRAW_MODES; ++i) { if (instances_[i] != nullptr) { engine->destroy(instances_[i]); instances_[i] = nullptr; @@ -206,13 +203,13 @@ void Renderable::UpdateMaterial(const mjrMaterialParams& params, material_params_ = params; material_textures_ = textures; - AssignMaterial(DrawMode::Color, GetColorMaterialType()); + AssignMaterial(mjDRAW_MODE_COLOR, GetColorMaterialType()); if (params_.shading_model == mjSHADING_MODEL_SCENE_OBJECT) { - AssignMaterial(DrawMode::Depth, ObjectManager::kUnlitDepth); - AssignMaterial(DrawMode::Segmentation, ObjectManager::kUnlitSegmentation); + AssignMaterial(mjDRAW_MODE_DEPTH, ObjectManager::kUnlitDepth); + AssignMaterial(mjDRAW_MODE_SEGMENTATION, ObjectManager::kUnlitSegmentation); } - for (int i = 0; i < kNumDrawModes; ++i) { + for (int i = 0; i < mjNUM_DRAW_MODES; ++i) { if (instances_[i]) { UpdateMaterialInstance(instances_[i], material_params_, material_textures_, object_mgr_); @@ -221,7 +218,7 @@ void Renderable::UpdateMaterial(const mjrMaterialParams& params, SetDrawMode(draw_mode_); } -void Renderable::AssignMaterial(DrawMode mode, +void Renderable::AssignMaterial(mjrDrawMode mode, ObjectManager::MaterialType material_type) { const int index = static_cast(mode); @@ -248,10 +245,10 @@ const mjrMaterialTextures& Renderable::GetMaterialTextures() const { return material_textures_; } -void Renderable::SetDrawMode(DrawMode mode) { +void Renderable::SetDrawMode(mjrDrawMode mode) { // Only SceneObjects support non-color draw modes. if (params_.shading_model != mjSHADING_MODEL_SCENE_OBJECT) { - mode = DrawMode::Color; + mode = mjDRAW_MODE_COLOR; } filament::MaterialInstance* instance = instances_[static_cast(mode)]; @@ -369,6 +366,7 @@ ObjectManager::MaterialType Renderable::GetColorMaterialType() const { // geometry) and `mesh_texcoordadr` stores the address of the mesh uvs if // it has them. bool has_texcoords = false; + const Texture* color_texture = Texture::downcast(material_textures_.color); if (!parts_.empty()) { const auto attribs = parts_[0].mesh->GetVertexAttributes(); auto it = std::find(attribs.begin(), attribs.end(), @@ -376,7 +374,7 @@ ObjectManager::MaterialType Renderable::GetColorMaterialType() const { has_texcoords = (it != attribs.end()); } - if (material_textures_.color == nullptr) { + if (color_texture == nullptr) { if (material_params_.color[3] < 1.0f) { return ObjectManager::kPhongColorFade; } else if (material_params_.reflective) { @@ -384,7 +382,7 @@ ObjectManager::MaterialType Renderable::GetColorMaterialType() const { } else { return ObjectManager::kPhongColor; } - } else if (material_textures_.color->GetFilamentTexture()->getTarget() == + } else if (color_texture->GetFilamentTexture()->getTarget() == filament::Texture::Sampler::SAMPLER_CUBEMAP) { if (material_params_.color[3] < 1.0f) { return ObjectManager::kPhongCubeFade; diff --git a/src/experimental/filament/filament/renderable.h b/src/experimental/filament/filament/renderable.h index 35f9028a..9a740a19 100644 --- a/src/experimental/filament/filament/renderable.h +++ b/src/experimental/filament/filament/renderable.h @@ -24,7 +24,6 @@ #include #include #include -#include "experimental/filament/filament/draw_mode.h" #include "experimental/filament/filament/material.h" #include "experimental/filament/filament/math_util.h" #include "experimental/filament/filament/mesh.h" @@ -33,21 +32,6 @@ namespace mujoco { -// The shading model (material) for a Renderable. -typedef enum mjrShadingModel_ { - mjSHADING_MODEL_SCENE_OBJECT, - mjSHADING_MODEL_DECOR, - mjSHADING_MODEL_DECOR_LINES, - mjSHADING_MODEL_UX, -} mjrShadingModel; - -// Configuration parameters for a Renderable. -struct mjrRenderableParams { - mjrShadingModel shading_model; -}; - -void mjr_defaultRenderableParams(mjrRenderableParams* params); - // A Renderable is effectively two things: a mesh and a material. // // The mesh describes the surface geometry of the object and the material @@ -125,7 +109,7 @@ class Renderable : public mjrRenderable { // Further defines the material of the renderable. Only applies to renderables // with a SceneObject shading model. - void SetDrawMode(DrawMode mode); + void SetDrawMode(mjrDrawMode mode); // Updates the parameters for the material. void UpdateMaterial(const mjrMaterialParams& params, @@ -157,16 +141,16 @@ class Renderable : public mjrRenderable { void InitPartEntity(Part& part); - void AssignMaterial(DrawMode mode, ObjectManager::MaterialType material_type); + void AssignMaterial(mjrDrawMode mode, ObjectManager::MaterialType material_type); ObjectManager::MaterialType GetColorMaterialType() const; ObjectManager* object_mgr_; mjrRenderableParams params_; - filament::MaterialInstance* instances_[kNumDrawModes] = {nullptr}; + filament::MaterialInstance* instances_[mjNUM_DRAW_MODES] = {nullptr}; mjrMaterialParams material_params_; mjrMaterialTextures material_textures_; - DrawMode draw_mode_ = DrawMode::Color; + mjrDrawMode draw_mode_ = mjDRAW_MODE_COLOR; filament::Scene* assigned_scene_ = nullptr; std::vector parts_; filament::math::mat4f transform_; diff --git a/src/experimental/filament/filament/scene_view.cc b/src/experimental/filament/filament/scene_view.cc index d11edc13..d361fc53 100644 --- a/src/experimental/filament/filament/scene_view.cc +++ b/src/experimental/filament/filament/scene_view.cc @@ -40,13 +40,12 @@ #include #include #include "experimental/filament/filament/color_grading_options.h" -#include "experimental/filament/filament/draw_mode.h" #include "experimental/filament/filament/light.h" -#include "experimental/filament/filament/material.h" #include "experimental/filament/filament/math_util.h" #include "experimental/filament/filament/render_target.h" #include "experimental/filament/filament/renderable.h" #include "experimental/filament/filament/texture.h" +#include "experimental/filament/render_context_filament.h" namespace mujoco { @@ -54,10 +53,6 @@ using filament::math::float3; using filament::math::float4; using filament::math::mat4; -static constexpr int kNormalIndex = static_cast(DrawMode::Color); -static constexpr int kDepthIndex = static_cast(DrawMode::Depth); -static constexpr int kSegmentIndex = static_cast(DrawMode::Segmentation); - static filament::ColorGrading::Builder ToBuilder( const ColorGradingOptions& opts) { return filament::ColorGrading::Builder() @@ -146,11 +141,11 @@ SceneView::SceneView(filament::Engine* engine) : engine_(engine) { // Disable post processing for the depth and segmentation views to preserve // the values. - views_[kDepthIndex]->setPostProcessingEnabled(false); - views_[kSegmentIndex]->setPostProcessingEnabled(false); + views_[mjDRAW_MODE_DEPTH]->setPostProcessingEnabled(false); + views_[mjDRAW_MODE_SEGMENTATION]->setPostProcessingEnabled(false); // Rotate the fog to align with mujoco's +Z up space. - auto fog = views_[kNormalIndex]->getFogEntity(); + auto fog = views_[mjDRAW_MODE_COLOR]->getFogEntity(); auto& tm = engine->getTransformManager(); tm.create(fog); tm.setTransform(tm.getInstance(fog), @@ -255,7 +250,7 @@ void SceneView::Render(filament::Renderer* renderer, } // Render reflection passes. - if (request.draw_mode == DrawMode::Color && reflections_enabled_) { + if (request.draw_mode == mjDRAW_MODE_COLOR && reflections_enabled_) { for (size_t i = 0; i < reflectives_.size(); ++i) { Renderable* renderable = reflectives_[i]; @@ -317,7 +312,7 @@ void SceneView::SetColorGradingOptions(const ColorGradingOptions& opts) { auto color_grading = ToBuilder(color_grading_options_) .toneMapper(tone_mapper.get()) .build(*engine_); - views_[kNormalIndex]->setColorGrading(color_grading); + views_[mjDRAW_MODE_COLOR]->setColorGrading(color_grading); if (color_grading_) { engine_->destroy(color_grading_); } @@ -326,11 +321,11 @@ void SceneView::SetColorGradingOptions(const ColorGradingOptions& opts) { } void SceneView::EnableShadows() { - views_[kNormalIndex]->setShadowingEnabled(true); + views_[mjDRAW_MODE_COLOR]->setShadowingEnabled(true); } void SceneView::DisableShadows() { - views_[kNormalIndex]->setShadowingEnabled(false); + views_[mjDRAW_MODE_COLOR]->setShadowingEnabled(false); } void SceneView::EnableReflections() { @@ -351,19 +346,18 @@ void SceneView::DisableReflections() { textures.reflection = nullptr; renderable->UpdateMaterial(renderable->GetMaterialParams(), textures); } - } void SceneView::EnablePostProcessing() { - views_[kNormalIndex]->setPostProcessingEnabled(true); + views_[mjDRAW_MODE_COLOR]->setPostProcessingEnabled(true); } void SceneView::DisablePostProcessing() { - views_[kNormalIndex]->setPostProcessingEnabled(false); + views_[mjDRAW_MODE_COLOR]->setPostProcessingEnabled(false); } filament::View* SceneView::GetDefaultRenderView() { - return views_[kNormalIndex]; + return views_[mjDRAW_MODE_COLOR]; } ColorGradingOptions SceneView::GetColorGradingOptions() const { diff --git a/src/experimental/filament/filament/scene_view.h b/src/experimental/filament/filament/scene_view.h index f35110f8..d23a6ab2 100644 --- a/src/experimental/filament/filament/scene_view.h +++ b/src/experimental/filament/filament/scene_view.h @@ -27,7 +27,6 @@ #include #include #include "experimental/filament/filament/color_grading_options.h" -#include "experimental/filament/filament/draw_mode.h" #include "experimental/filament/filament/light.h" #include "experimental/filament/filament/renderable.h" #include "experimental/filament/filament/render_target.h" @@ -59,7 +58,7 @@ class SceneView : public mjrScene { // Parameters for rendering the scene. struct RenderRequest { // The draw mode (e.g. normal, depth, segmentation) to render. - DrawMode draw_mode = DrawMode::Color; + mjrDrawMode draw_mode = mjDRAW_MODE_COLOR; // The target viewport for the rendered image. mjrRect viewport; // The camera from which to render the scene. @@ -111,7 +110,7 @@ class SceneView : public mjrScene { filament::Camera* camera_ = nullptr; filament::ColorGrading* color_grading_ = nullptr; ColorGradingOptions color_grading_options_; - std::array views_; + std::array views_; // Scene objects. std::unordered_set lights_; diff --git a/src/experimental/filament/filament/texture.cc b/src/experimental/filament/filament/texture.cc index 81b37448..f27b89a6 100644 --- a/src/experimental/filament/filament/texture.cc +++ b/src/experimental/filament/filament/texture.cc @@ -110,14 +110,6 @@ static filament::Texture::InternalFormat GetTextureInternalFormat( } } -void mjr_defaultTextureData(mjrTextureData* data) { - std::memset(data, 0, sizeof(mjrTextureData)); -} - -void mjr_defaultTextureConfig(mjrTextureConfig* config) { - std::memset(config, 0, sizeof(mjrTextureConfig)); -} - Texture::Texture(filament::Engine* engine, const mjrTextureConfig& config, InternalFlags flags) : engine_(engine), config_(config) { diff --git a/src/experimental/filament/filament/texture.h b/src/experimental/filament/filament/texture.h index 764ae911..e5b7f305 100644 --- a/src/experimental/filament/filament/texture.h +++ b/src/experimental/filament/filament/texture.h @@ -25,63 +25,6 @@ // Functions for creating filament textures. namespace mujoco { -// Pixel formats for textures. -typedef enum mjrPixelFormat_ { - mjPIXEL_FORMAT_UNKNOWN = 0, - mjPIXEL_FORMAT_R8, - mjPIXEL_FORMAT_RGB8, - mjPIXEL_FORMAT_RGBA8, - mjPIXEL_FORMAT_R32F, - mjPIXEL_FORMAT_DEPTH32F, - mjPIXEL_FORMAT_KTX, -} mjrPixelFormat; - -typedef mjtTexture mjrTextureTarget; -typedef mjtColorSpace mjrColorSpace; - -// The binary contents of a texture. -struct mjrTextureData { - // Pointer to the image data. If null, an empty texture will be created. - const void* bytes; - - // The number of bytes in the image data. - mjtSize nbytes; - - // Because rendering may be multithreaded, we cannot make assumptions about - // when the image data will finish uploading to the GPU. As such, we will use - // this callback to notify callers when it is safe to free the image data. - void (*release_callback)(void* user_data); - - // User data to pass to the release callback. - void* user_data; -}; - -// Initializes the TextureData to default values. -void mjr_defaultTextureData(mjrTextureData* data); - -// Defines the basic properties of a texture. -struct mjrTextureConfig { - // The width of the texture. For compressed textures (e.g. KTX), this is the - // number of bytes in the compressed data. - int width; - - // The height of the texture. For compressed textures (e.g. KTX), this should - // be 0. - int height; - - // The target of the texture (e.g. 2D, cube, etc.) - mjrTextureTarget target; - - // The format of the pixels in the texture (e.g. RGB8, RGBA8, KTX, etc.) - mjrPixelFormat format; - - // The color space of the texture (e.g. LINEAR, sRGB, etc.) - mjrColorSpace color_space; -}; - -// Initializes the TextureConfig to default values. -void mjr_defaultTextureConfig(mjrTextureConfig* config); - // Wrapper around a filament::Texture. class Texture : public mjrTexture { public: diff --git a/src/experimental/filament/render_context_filament.cc b/src/experimental/filament/render_context_filament.cc index 65b4086a..30891f5a 100644 --- a/src/experimental/filament/render_context_filament.cc +++ b/src/experimental/filament/render_context_filament.cc @@ -14,6 +14,7 @@ #include "experimental/filament/render_context_filament.h" +#include #include #include @@ -36,12 +37,91 @@ static void CheckFilamentContext() { } } +template +static void setf(float (&arr)[N], const std::array& values) { + for (int i = 0; i < N; ++i) { + arr[i] = values[i]; + } +} + + extern "C" { void mjrf_defaultFilamentConfig(mjrFilamentConfig* config) { memset(config, 0, sizeof(mjrFilamentConfig)); } +void mjr_defaultTextureData(mjrTextureData* data) { + memset(data, 0, sizeof(mjrTextureData)); +} + +void mjr_defaultTextureConfig(mjrTextureConfig* config) { + memset(config, 0, sizeof(mjrTextureConfig)); +} + +void mjr_defaultMeshData(mjrMeshData* data) { + std::memset(data, 0, sizeof(mjrMeshData)); +} + +void mjr_defaultLightParams(mjrLightParams* params) { + params->type = mjLIGHT_POINT; + params->texture = nullptr; + params->color[0] = 0; + params->color[1] = 0; + params->color[2] = 0; + params->intensity = 0.0f; + params->cast_shadows = true; + params->range = 10.0f; + params->spot_cone_angle = 180.f; + params->bulb_radius = 0.0f; + params->shadow_map_size = 2048; + params->vsm_blur_width = 0.0f; +} + +void mjr_defaultMaterialTextures(mjrMaterialTextures* textures) { + textures->color = nullptr; + textures->normal = nullptr; + textures->metallic = nullptr; + textures->roughness = nullptr; + textures->occlusion = nullptr; + textures->orm = nullptr; + textures->emissive = nullptr; + textures->reflection = nullptr; +} + +void mjr_defaultMaterialParams(mjrMaterialParams* params) { + setf(params->color, {1.f, 1.f, 1.f, 1.f}); + setf(params->segmentation_color, {1, 1, 1, 1}); + setf(params->uv_scale, {1, 1, 1}); + setf(params->uv_offset, {0, 0, 0}); + setf(params->scissor, {0, 0, 0, 0}); + params->emissive = -1.0f; + params->specular = -1.0f; + params->glossiness = -1.0f; + params->metallic = -1.0f; + params->roughness = -1.0f; + params->reflectance = 0.0f; + params->tex_uniform = false; + params->reflective = false; +} + +void mjr_defaultRenderableParams(mjrRenderableParams* params) { + params->shading_model = mjSHADING_MODEL_SCENE_OBJECT; +} + +void mjr_defaultRenderTargetConfig(mjrRenderTargetConfig* config) { + config->color_format = mjPIXEL_FORMAT_RGBA8; + config->depth_format = mjPIXEL_FORMAT_DEPTH32F; +} + +void mjr_defaultRenderRequest(mjrRenderRequest* request) { + memset(request, 0, sizeof(mjrRenderRequest)); +} + +void mjr_defaultReadPixelsRequest(mjrReadPixelsRequest* request) { + memset(request, 0, sizeof(mjrReadPixelsRequest)); +} + void mjrf_makeFilamentContext(const mjModel* m, mjrContext* con, const mjrFilamentConfig* config) { // TODO: Support multiple contexts and multiple threads. For now, we'll just diff --git a/src/experimental/filament/render_context_filament.h b/src/experimental/filament/render_context_filament.h index ef6f88ea..e338a55c 100644 --- a/src/experimental/filament/render_context_filament.h +++ b/src/experimental/filament/render_context_filament.h @@ -40,12 +40,334 @@ struct mjrRenderTarget {}; // Opaque type for the filament rendering context. struct mjrfContext {}; +// The different modes that can be used to render a scene. +typedef enum mjrDrawMode_ { + // Render the scene with "normal" colors and lighting. + mjDRAW_MODE_COLOR, + // Render the scene as a grayscale depth map. + mjDRAW_MODE_DEPTH, + // Render each object with a unique, uniform (flat) color regardless of + // lighting and texture. + mjDRAW_MODE_SEGMENTATION, +} mjrDrawMode; + +enum { mjNUM_DRAW_MODES = 3 }; + +// The shading model (material) for a Renderable. +typedef enum mjrShadingModel_ { + // For renderables in the main 3D scene. + mjSHADING_MODEL_SCENE_OBJECT = 0, + // For UX renderables. + mjSHADING_MODEL_UX, + // For decorative elements in a Scene (e.g. contact points, force vectors, + // etc.). These objects will not be affected by lighting. + mjSHADING_MODEL_DECOR, + // As above, but uses a line primitives for drawing. + mjSHADING_MODEL_DECOR_LINES, +} mjrShadingModel; + +// The type of data stored in an index buffer. +typedef enum mjrIndexType_ { + mjINDEX_TYPE_U16 = 0, + mjINDEX_TYPE_U32, +} mjrIndexType; + +// The type of primitive to be drawn by vertex data. +typedef enum mjrMeshPrimitiveType_ { + mjMESH_PRIMITIVE_TYPE_TRIANGLES = 0, + mjMESH_PRIMITIVE_TYPE_LINES, +} mjrMeshPrimitiveType; + +// The usage/purpose of an attribute of a vertex. +typedef enum mjrVertexAttributeUsage_ { + mjVERTEX_ATTRIBUTE_USAGE_POSITION = 0, + mjVERTEX_ATTRIBUTE_USAGE_NORMAL, + mjVERTEX_ATTRIBUTE_USAGE_TANGENTS, + mjVERTEX_ATTRIBUTE_USAGE_UV, + mjVERTEX_ATTRIBUTE_USAGE_COLOR, +} mjrVertexAttributeUsage; + +// The data format of an attribute of a vertex. +typedef enum mjrVertexAttributeType_ { + mjVERTEX_ATTRIBUTE_TYPE_FLOAT2 = 0, + mjVERTEX_ATTRIBUTE_TYPE_FLOAT3, + mjVERTEX_ATTRIBUTE_TYPE_FLOAT4, + mjVERTEX_ATTRIBUTE_TYPE_UBYTE4, +} mjrVertexAttributeType; + +// Pixel formats for textures. +typedef enum mjrPixelFormat_ { + mjPIXEL_FORMAT_UNKNOWN = 0, + mjPIXEL_FORMAT_R8, + mjPIXEL_FORMAT_RGB8, + mjPIXEL_FORMAT_RGBA8, + mjPIXEL_FORMAT_R32F, + mjPIXEL_FORMAT_DEPTH32F, + mjPIXEL_FORMAT_KTX, +} mjrPixelFormat; + typedef enum mjrGraphicsApi_ { // backend graphics API to use mjGRAPHICS_API_DEFAULT = 0, // default based on platform mjGRAPHICS_API_OPENGL, // OpenGL (desktop) / WebGL mjGRAPHICS_API_VULKAN // Vulkan } mjrGraphicsApi; + +// Rendering is asynchronous by nature. Each render request is assigned a +// unique Handle which can be used to query the status of the request. The +// Handle can also be used to block until the request is completed. +typedef std::uint64_t mjrFrameHandle; + +// Bring some legacy mjt types into the mjr namespace. +typedef mjtTexture mjrTextureTarget; +typedef mjtColorSpace mjrColorSpace; +typedef mjtLightType mjrLightType; + +// The textures that can be assigned to the drawable's material. +struct mjrMaterialTextures { + const mjrTexture* color; + const mjrTexture* normal; + const mjrTexture* metallic; + const mjrTexture* roughness; + const mjrTexture* occlusion; + const mjrTexture* orm; + const mjrTexture* emissive; + const mjrTexture* reflection; +}; + +// Initializes the mjrMaterialTextures to default values. +void mjr_defaultMaterialTextures(mjrMaterialTextures* textures); + +// The parameters that can be applied to the drawable's material. +struct mjrMaterialParams { + float color[4]; + float segmentation_color[4]; + float tex_repeat[2]; + float uv_scale[3]; + float uv_offset[3]; + float scissor[4]; + float specular; + float glossiness; + float metallic; + float roughness; + float emissive; + float reflectance; + mjtByte tex_uniform; + mjtByte reflective; +}; + +// Initializes the mjrMaterialParams to default values. +void mjr_defaultMaterialParams(mjrMaterialParams* params); + +// The binary contents of a texture. +struct mjrTextureData { + // Pointer to the image data. If null, an empty texture will be created. + const void* bytes; + + // The number of bytes in the image data. + mjtSize nbytes; + + // Because rendering may be multithreaded, we cannot make assumptions about + // when the image data will finish uploading to the GPU. As such, we will use + // this callback to notify callers when it is safe to free the image data. + void (*release_callback)(void* user_data); + + // User data to pass to the release callback. + void* user_data; +}; + +// Initializes the mjrTextureData to default values. +void mjr_defaultTextureData(mjrTextureData* data); + +// Defines the basic properties of a texture. +struct mjrTextureConfig { + // The width of the texture. For compressed textures (e.g. KTX), this is the + // number of bytes in the compressed data. + int width; + + // The height of the texture. For compressed textures (e.g. KTX), this should + // be 0. + int height; + + // The target of the texture (e.g. 2D, cube, etc.) + mjrTextureTarget target; + + // The format of the pixels in the texture (e.g. RGB8, RGBA8, KTX, etc.) + mjrPixelFormat format; + + // The color space of the texture (e.g. LINEAR, sRGB, etc.) + mjrColorSpace color_space; +}; + +// Initializes the mjrTextureConfig to default values. +void mjr_defaultTextureConfig(mjrTextureConfig* config); + +// Configuration parameters for a Renderable. +struct mjrRenderableParams { + // The shading model to use for the Renderable. + mjrShadingModel shading_model; +}; + +// Initializes the mjrRenderableParams to default values. +void mjr_defaultRenderableParams(mjrRenderableParams* params); + +// Information about a single attribute of a vertex. +struct mjrVertexAttribute { + // The data for the attribute. + const void* bytes; + + // The usage/purpose of the attribute. + mjrVertexAttributeUsage usage; + + // The data format of the attribute. + mjrVertexAttributeType type; +}; + +// Maximum number of vertex attributes in a mesh. +enum { mjMAX_VERTEX_ATTRIBUTES = 16 }; + +// The binary contents of a mesh. +struct mjrMeshData { + // The number of vertices in the mesh. Each of the vertex arrays below is + // assumed to have this number of elements. + mjtSize nvertices; + + // The number of attributes for each vertex in the mesh. + int nattributes; + + // Information about each attribute of a vertex in the mesh. See `interleaved` + // for more details. + mjrVertexAttribute attributes[mjMAX_VERTEX_ATTRIBUTES]; + + // Whether the vertex attributes are interleaved or not. + // + // If true, assumes that the attributes are packed in the order specified in + // the attributes array, with no padding in-between. Additionally, the + // `data` pointer for each attribute is assumed to point to the first element + // of that type. + // + // If false, assume each attribute is stored in a separate array as defined + // by the `data` field of the attribute. + mjtByte interleaved; + + // The number of indices in the mesh. The indices array is assumed to have + // this number of elements. + mjtSize nindices; + + // The indices of the mesh, stored as either ushort or uint depending on the + // index type. + const void* indices; + + // The type of data stored in the indices array. + mjrIndexType index_type; + + // The type of primitive to be drawn by vertex data. + mjrMeshPrimitiveType primitive_type; + + // Whether to compute the bounds of the mesh using the vertex positions. + mjtByte compute_bounds; + + // The bounds of the mesh. If bounds_min == bounds_max, then we assume that + // that the bounds are not set (i.e. the bounds is empty). + float bounds_min[3]; + float bounds_max[3]; + + // Because rendering may be multithreaded, we cannot make assumptions about + // when the mesh data will finish uploading to the GPU. As such, we will use + // this callback to notify callers when it is safe to free the mesh data. + void (*release_callback)(void* user_data); + + // User data to pass to the release callback. + void* user_data; +}; + +// Initializes the mjrMeshData to default values. +void mjr_defaultMeshData(mjrMeshData* data); + +// Configuration parameters for a light. +struct mjrLightParams { + // The type of light (e.g. spot, point, directional, etc.) + mjrLightType type; + // The texture to use for image lights. + const mjrTexture* texture; + // The color of the light. + float color[3]; + // The intensity of the light, in candela. + float intensity; + // Whether or not the light casts shadows. + mjtByte cast_shadows; + // The range/distance in which the light is effective, in meters. + float range; + // The angle of the spot light cone, in degrees. + float spot_cone_angle; + // The radius of the bulb used for soft shadows. + float bulb_radius; + // The size of the shadow map. + int shadow_map_size; + // Blur width for EL VSM. + float vsm_blur_width; +}; + +// Initializes the mjrLightParams to default values. +void mjr_defaultLightParams(mjrLightParams* params); + +// Defines the basic properties of a render target. +struct mjrRenderTargetConfig { + mjrPixelFormat color_format; + mjrPixelFormat depth_format; +}; + +// Initializes the RenderTargetConfig to default values. +void mjr_defaultRenderTargetConfig(mjrRenderTargetConfig* config); + +// Information needed to render a single image of a scene. +struct mjrRenderRequest { + // The scene to render. + mjrScene* scene; + + // The method (e.g. Color, Depth, Segmentation, etc.) to use for rendering. + mjrDrawMode draw_mode; + + // The camera from which to render the scene. + mjvGLCamera camera; + + // The dimensions of the output image. + int width; + int height; + + // The render target into which to render the image. If nullptr, the image + // will be rendered to the window (as previously configured in + // mjrFilamentConfig::native_window). + mjrRenderTarget* target; +}; + +// Initializes the mjrRenderRequest to default values. +void mjr_defaultRenderRequest(mjrRenderRequest* request); + +// Information needed to read pixels from a render target. +struct mjrReadPixelsRequest { + mjrRenderTarget* target; + + // The buffer into which the read pixels will be written. + void* output; + + // The number of bytes in the output buffer. This should match the size of + // the render target texture. + mjtSize num_bytes; + + // Callback when the read pixels operation is complete. This will be called + // during WaitForFrame() or in a subsequent call to Render(). This function + // can optionally be used to free the output buffer if needed. + void (*read_completed_callback)(void* user_data); + + // User data to pass to the completion callback. + void* user_data; +}; + +// Initializes the mjrReadPixelsRequest to default values. +void mjr_defaultReadPixelsRequest(mjrReadPixelsRequest* request); + +// Configuration parameters for the filament rendering context. struct mjrFilamentConfig { // The native window handle into which we can render directly. void* native_window; From ace90c00b90f45b48546f368ca73a109aceaacd1 Mon Sep 17 00:00:00 2001 From: Haroon Qureshi Date: Tue, 28 Apr 2026 06:34:01 -0700 Subject: [PATCH 11/13] Remove deleted file. PiperOrigin-RevId: 906944517 Change-Id: I8471d1014888cc03632df631b47080618694f4a4 --- src/experimental/filament/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/experimental/filament/CMakeLists.txt b/src/experimental/filament/CMakeLists.txt index 9e69f162..8b655e9a 100644 --- a/src/experimental/filament/CMakeLists.txt +++ b/src/experimental/filament/CMakeLists.txt @@ -27,7 +27,6 @@ target_sources(${MUJOCO_FILAMENT_TARGET_NAME} filament/builtins.h filament/color_grading_options.cc filament/color_grading_options.h - filament/draw_mode.h filament/filament_context.cc filament/filament_context.h filament/filament_platform_factory.cc From d92fe0810c3283e0250345c9bae7838668c0730e Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Tue, 28 Apr 2026 14:22:11 -0700 Subject: [PATCH 12/13] Add MjSpec.encode method. PiperOrigin-RevId: 907177387 Change-Id: I65800298798037c4a0c0cd79ec86706be67db51e --- doc/changelog.rst | 5 ++++ doc/python.rst | 6 +++++ python/mujoco/specs.cc | 45 +++++++++++++++++++++++++++++++++ python/mujoco/specs_test.py | 50 +++++++++++++++++++++++++++++++++++++ src/user/user_api.cc | 13 ++++++++++ 5 files changed, 119 insertions(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index 4a8f22d4..bb1c7a20 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -7,6 +7,11 @@ Upcoming version (not yet released) - Added island support for the :ref:`PGS solver`. +Python +^^^^^^ + +- Added ``MjSpec.encode`` method, wrapping :ref:`mj_encode`. + Version 3.8.0 (April 24, 2026) ------------------------------ diff --git a/doc/python.rst b/doc/python.rst index 61761b0b..95beb3ff 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -547,6 +547,12 @@ Compiled ``MjSpec`` objects can be saved to XML string with the ``to_xml()`` met +Alternatively, the spec can be saved directly to a file using ``encode()``: + +.. code-block:: python + + spec.encode('model.xml', model) + Attachment ---------- diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index e58d66cc..2ba20431 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -431,6 +431,51 @@ PYBIND11_MODULE(_specs, m) { throw FatalError(std::string(err.data())); } }); + mjSpec.def( + "encode", + [](MjSpec& self, std::string filename, + std::optional model, + std::optional content_type) -> int { + raw::MjModel* m = nullptr; + if (model.has_value() && !model->is_none()) { + auto& wrapper = + py::cast<_impl::MjModelWrapper&>(*model); + m = wrapper.get(); + } + + mjVFS vfs; + mjVFS* vfs_ptr = nullptr; + if (!self.assets.empty()) { + mj_defaultVFS(&vfs); + vfs_ptr = &vfs; + for (const auto& asset : self.assets) { + std::string buffer_name = + py::cast(asset.first); + std::string buffer = + py::cast(asset.second); + mj_addBufferVFS(vfs_ptr, buffer_name.c_str(), + buffer.c_str(), buffer.size()); + } + } + + std::array err; + err[0] = '\0'; + const char* ct = + content_type.has_value() ? content_type->c_str() : nullptr; + int nbytes = mj_encode(self.ptr, m, filename.c_str(), ct, + vfs_ptr, err.data(), err.size()); + + if (vfs_ptr) { + mj_deleteVFS(vfs_ptr); + } + + if (nbytes < 0) { + throw FatalError(std::string(err.data())); + } + return nbytes; + }, + py::arg("filename"), py::arg("model") = py::none(), + py::arg("content_type") = py::none()); mjSpec.def( "add_default", [](MjSpec* spec, std::string& classname, diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 8bb9360e..fe3f45b1 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -22,6 +22,7 @@ import textwrap import typing import zipfile # pylint: disable=unused-import +from absl import flags from absl.testing import absltest from etils import epath import mujoco @@ -34,6 +35,11 @@ def get_linenumber(): class SpecsTest(absltest.TestCase): + def setUp(self): + super().setUp() + # Mark flags as parsed to avoid pytest errors about unparsed flags. + # This is needed for `create_tempdir()` calls below. + flags.FLAGS.mark_as_parsed() def test_typing(self): spec = mujoco.MjSpec() @@ -1964,5 +1970,49 @@ class SpecsTest(absltest.TestCase): self.assertGreater(cam_sd[0], cam_sd[1]) # dist > depth self.assertAlmostEqual(cam_sd[1], 2.0, places=6) # depth is still 2.0 + def test_encode_xml(self): + # Create a simple spec and compile. + spec = mujoco.MjSpec() + body = spec.worldbody.add_body() + geom = body.add_geom() + geom.size[0] = 1 + model = spec.compile() + + # Encode to XML. + filename = os.path.join(self.create_tempdir().full_path, 'output.xml') + nbytes = spec.encode(filename, model) + self.assertGreater(nbytes, 0) + + # Verify the output is valid XML that can be loaded. + reloaded = mujoco.MjSpec.from_file(filename) + reloaded_model = reloaded.compile() + self.assertEqual(reloaded_model.ngeom, model.ngeom) + + def test_encode_xml_without_model(self): + # Create a simple spec and compile so XML can be written. + spec = mujoco.MjSpec() + body = spec.worldbody.add_body() + geom = body.add_geom() + geom.size[0] = 1 + spec.compile() + + # Encode to XML without passing a model explicitly. + filename = os.path.join(self.create_tempdir().full_path, 'output.xml') + nbytes = spec.encode(filename) + self.assertGreater(nbytes, 0) + + def test_encode_no_encoder_raises(self): + # Create a simple spec and compile. + spec = mujoco.MjSpec() + body = spec.worldbody.add_body() + geom = body.add_geom() + geom.size[0] = 1 + model = spec.compile() + + # Encode with an unknown extension should fail. + filename = os.path.join(self.create_tempdir().full_path, 'output.unknown') + with self.assertRaises(mujoco.FatalError): + spec.encode(filename, model) + if __name__ == '__main__': absltest.main() diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 4b5effaf..f03b5e85 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -143,6 +144,18 @@ mjSpec* mj_parse(const char* filename, const char* content_type, int mj_encode(const mjSpec* s, const mjModel* m, const char* filename, const char* content_type, const mjVFS* vfs, char* error, int error_sz) { + // TODO(shaves) Move MJCF and URDF to encoders/decoders. + auto filepath = mujoco::user::FilePath(filename); + if (filepath.Ext() == ".xml" || + (content_type && std::strcmp(content_type, "text/xml") == 0)) { + int result = mj_saveXML(s, filename, error, error_sz); + if (result < 0) { + return -1; + } + + return std::filesystem::file_size(filename); + } + const mjpEncoder* encoder = mjp_findEncoder(filename, content_type); if (!encoder) { if (error) { From 9796345888533747d755a39b0def8face2a0b958 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 29 Apr 2026 03:41:21 -0700 Subject: [PATCH 13/13] Add font scaling to studio, bound to CTRL+MINUS/PLUS PiperOrigin-RevId: 907491215 Change-Id: I144467c3ff0e3d11f4a5bfb6ed2b1e4e88b8693d --- src/experimental/platform/ux/gui.cc | 23 +++++++++++++++++++++-- src/experimental/platform/ux/gui.h | 3 +++ src/experimental/studio/app.cc | 12 ++++++++++++ src/experimental/studio/app.h | 1 + 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index ccf1ed91..d7a3946c 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -213,15 +213,32 @@ void SetupTheme(GuiTheme theme) { s.DockingNodeHasCloseButton = false; } +void RescaleDock(float ratio) { + if (ratio == 1) return; + ImGuiID root = ImGui::GetID("Root"); + ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(root); + if (root_node) { + struct ScaleNodes { + static void Apply(ImGuiDockNode* node, float r) { + node->SizeRef.x *= r; + if (node->ChildNodes[0]) Apply(node->ChildNodes[0], r); + if (node->ChildNodes[1]) Apply(node->ChildNodes[1], r); + } + }; + ScaleNodes::Apply(root_node, ratio); + } +} + ImVec4 ConfigureDockingLayout() { ImGuiViewport* viewport = ImGui::GetMainViewport(); const float scale = ImGui::GetWindowDpiScale(); + const float font_scale = ImGui::GetIO().FontGlobalScale; const float kOptionsRelWidth = 0.22f; const float kInspectorRelWidth = 0.22f; const float kStatsRelHeight = 0.3f; - const float kToolsBarHeight = 36.f * scale; - const float kStatusBarHeight = 32.f * scale; + const float kToolsBarHeight = 36.f * scale * font_scale; + const float kStatusBarHeight = 32.f * scale * font_scale; const ImVec2 dockspace_pos{viewport->WorkPos.x, viewport->WorkPos.y + kToolsBarHeight}; @@ -303,6 +320,7 @@ ImVec4 ConfigureDockingLayout() { platform::ScopedStyle style; style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); style.Var(ImGuiStyleVar_WindowRounding, 0.0f); + style.Var(ImGuiStyleVar_WindowMinSize, ImVec2(1, 1)); const float toolbar_vpad = std::max(0.f, (kToolsBarHeight - ImGui::GetFrameHeight()) * 0.5f); style.Var(ImGuiStyleVar_WindowPadding, ImVec2(4, toolbar_vpad)); @@ -318,6 +336,7 @@ ImVec4 ConfigureDockingLayout() { platform::ScopedStyle style; style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); style.Var(ImGuiStyleVar_WindowRounding, 0.0f); + style.Var(ImGuiStyleVar_WindowMinSize, ImVec2(1, 1)); ImGui::SetNextWindowPos(ImVec2(0, viewport->Size.y - kStatusBarHeight), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kStatusBarHeight), diff --git a/src/experimental/platform/ux/gui.h b/src/experimental/platform/ux/gui.h index e9b23dfc..cc2ae08f 100644 --- a/src/experimental/platform/ux/gui.h +++ b/src/experimental/platform/ux/gui.h @@ -42,6 +42,9 @@ enum class GuiTheme { // Updates the ImGui internal style state to match the requested theme. void SetupTheme(GuiTheme theme); +// Rescales all dock node widths by the given ratio. +void RescaleDock(float ratio); + // Configures the ImGui docking module to the standard layout used by Studio. // This includes the following named sections: // "ToolBar": fixed size bar spanning the top of the window; for placing diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 25dd1676..576462a5 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -578,6 +578,14 @@ void App::HandleKeyboardEvents() { tmp_.inspector_panel = !tmp_.inspector_panel; } else if (ImGui_IsChordJustPressed(ImGuiKey_Tab)) { tmp_.options_panel = !tmp_.options_panel; + } else if (ImGui_IsChordJustPressed(ImGuiKey_Minus | ImGuiMod_Ctrl)) { + float old_scale = ui_.font_scale; + ui_.font_scale = std::clamp(ui_.font_scale - 0.1f, 0.5f, 3.0f); + platform::RescaleDock(ui_.font_scale / old_scale); + } else if (ImGui_IsChordJustPressed(ImGuiKey_Equal | ImGuiMod_Ctrl)) { + float old_scale = ui_.font_scale; + ui_.font_scale = std::clamp(ui_.font_scale + 0.1f, 0.5f, 3.0f); + platform::RescaleDock(ui_.font_scale / old_scale); } else if (ImGui_IsChordJustPressed(ImGuiKey_Minus)) { SetSpeedIndex(tmp_.speed_index + 1); } else if (ImGui_IsChordJustPressed(ImGuiKey_Equal)) { @@ -823,6 +831,8 @@ void App::BuildGui() { platform::SetupTheme(ui_.theme); } + ImGui::GetIO().FontGlobalScale = ui_.font_scale; + const ImVec4 workspace_rect = platform::ConfigureDockingLayout(); // Place charts in bottom right corner of the workspace. @@ -1841,11 +1851,13 @@ float App::GetExpectedLabelWidth() { App::UiState::Dict App::UiState::ToDict() const { return { {"theme", std::to_string(static_cast(theme))}, + {"font_scale", std::to_string(font_scale)}, }; } void App::UiState::FromDict(const Dict& dict) { *this = UiState(); theme = ReadIniValue(dict, "theme", theme); + font_scale = platform::ReadIniValue(dict, "font_scale", font_scale); } } // namespace mujoco::studio diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index 2a1c7326..bef5f6ed 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -105,6 +105,7 @@ class App { int camera_idx = platform::kTumbleCameraIdx; int key_idx = 0; platform::GuiTheme theme = platform::GuiTheme::kLight; + float font_scale = 1.0f; using Dict = std::unordered_map; Dict ToDict() const;