Release GIL in MuJoCo Studio Python bindings.

This change adds py::gil_scoped_release to various functions in the parser, renderer, sim and ux modules of the MuJoCo Studio Python bindings. This allows other Python threads to execute while these C++ functions, which can sometimes be time-consuming, are running. The two heaviest operations Present() (rendering) and Advance() (physics) now both release the GIL in C++.

PiperOrigin-RevId: 930645361
Change-Id: Ib239fb54edc7ac0d8b1d94bec8c57a447d5f4d27
This commit is contained in:
Matija Kecman
2026-06-11 11:31:28 -07:00
committed by Copybara-Service
parent ab17ff5552
commit 37084a5c0b
4 changed files with 46 additions and 6 deletions
+6 -1
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
#include <memory>
#include <string_view> #include <string_view>
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
@@ -24,7 +25,11 @@ namespace mujoco::python {
// Loads, parses, and compiles a MuJoCo model from the given file. Returns the // Loads, parses, and compiles a MuJoCo model from the given file. Returns the
// python mjData object (which also contains the compiled mjModel). // python mjData object (which also contains the compiled mjModel).
py::object Parse(std::string_view filepath) { py::object Parse(std::string_view filepath) {
auto holder = platform::ModelHolder::FromFile(filepath); std::unique_ptr<platform::ModelHolder> holder;
{
py::gil_scoped_release no_gil;
holder = platform::ModelHolder::FromFile(filepath);
}
if (!holder->ok()) { if (!holder->ok()) {
throw py::value_error( throw py::value_error(
std::string("Failed to load model from '") + std::string("Failed to load model from '") +
+12 -5
View File
@@ -36,12 +36,16 @@ class Renderer {
using GraphicsMode = mujoco::platform::GraphicsMode; using GraphicsMode = mujoco::platform::GraphicsMode;
Renderer(const std::string& graphics_mode_str) { Renderer(const std::string& graphics_mode_str) {
py::gil_scoped_release no_gil;
const GraphicsMode mode = mujoco::platform::GraphicsModeFromString( const GraphicsMode mode = mujoco::platform::GraphicsModeFromString(
graphics_mode_str, GraphicsMode::FilamentOpenGl); graphics_mode_str, GraphicsMode::FilamentOpenGl);
impl_ = std::make_unique<RendererImpl>(nullptr, mode); impl_ = std::make_unique<RendererImpl>(nullptr, mode);
} }
void Init(const MjModelWrapper& model) { impl_->Init(model.get()); } void Init(const MjModelWrapper& model) {
py::gil_scoped_release no_gil;
impl_->Init(model.get());
}
pybind11::bytes Render(const MjModelWrapper& model, MjDataWrapper& data, pybind11::bytes Render(const MjModelWrapper& model, MjDataWrapper& data,
std::optional<MjvPerturbWrapper>& perturb, std::optional<MjvPerturbWrapper>& perturb,
@@ -49,10 +53,13 @@ class Renderer {
std::optional<MjvOptionWrapper>& vis_option, int width, std::optional<MjvOptionWrapper>& vis_option, int width,
int height) { int height) {
std::vector<std::byte> pixels(width * height * 3); std::vector<std::byte> pixels(width * height * 3);
impl_->Render( {
model.get(), data.get(), perturb ? perturb.value().get() : nullptr, py::gil_scoped_release no_gil;
camera ? camera.value().get() : nullptr, impl_->Render(
vis_option ? vis_option.value().get() : nullptr, width, height, pixels); model.get(), data.get(), perturb ? perturb.value().get() : nullptr,
camera ? camera.value().get() : nullptr,
vis_option ? vis_option.value().get() : nullptr, width, height, pixels);
}
return pybind11::bytes((const char*)pixels.data(), pixels.size()); return pybind11::bytes((const char*)pixels.data(), pixels.size());
} }
+1
View File
@@ -48,6 +48,7 @@ PYBIND11_MODULE(sim, m) {
auto& model = py::cast<mujoco::python::MjModelWrapper&>(model_obj); auto& model = py::cast<mujoco::python::MjModelWrapper&>(model_obj);
auto& data = py::cast<mujoco::python::MjDataWrapper&>(data_obj); auto& data = py::cast<mujoco::python::MjDataWrapper&>(data_obj);
if (step_fn.is_none()) { if (step_fn.is_none()) {
py::gil_scoped_release no_gil;
return self.Advance(model.get(), data.get()); return self.Advance(model.get(), data.get());
} else { } else {
return self.Advance( return self.Advance(
+27
View File
@@ -91,6 +91,7 @@ PYBIND11_MODULE(ux, m) {
m.def( m.def(
"setup_theme", "setup_theme",
[](mujoco::platform::GuiTheme theme) { [](mujoco::platform::GuiTheme theme) {
py::gil_scoped_release no_gil;
mujoco::platform::SetupTheme(theme); mujoco::platform::SetupTheme(theme);
}, },
py::arg("theme"), "Set up Dear ImGui visual theme."); py::arg("theme"), "Set up Dear ImGui visual theme.");
@@ -105,6 +106,7 @@ PYBIND11_MODULE(ux, m) {
m.def( m.def(
"configure_docking_layout", "configure_docking_layout",
[]() { []() {
py::gil_scoped_release no_gil;
ImVec4 r = mujoco::platform::ConfigureDockingLayout(); ImVec4 r = mujoco::platform::ConfigureDockingLayout();
return std::make_tuple(r.x, r.y, r.z, r.w); return std::make_tuple(r.x, r.y, r.z, r.w);
}, },
@@ -115,6 +117,7 @@ PYBIND11_MODULE(ux, m) {
"step_control_gui", "step_control_gui",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::platform::StepControl* step_control, UxState& ux_state) { mujoco::platform::StepControl* step_control, UxState& ux_state) {
py::gil_scoped_release no_gil;
mujoco::platform::StepControlGui(model.get(), step_control, mujoco::platform::StepControlGui(model.get(), step_control,
ux_state.speed_index); ux_state.speed_index);
}, },
@@ -125,6 +128,7 @@ PYBIND11_MODULE(ux, m) {
m.def( m.def(
"theme_select_gui", "theme_select_gui",
[](mujoco::platform::GuiTheme theme) { [](mujoco::platform::GuiTheme theme) {
py::gil_scoped_release no_gil;
bool changed = mujoco::platform::ThemeSelectGui(&theme); bool changed = mujoco::platform::ThemeSelectGui(&theme);
return std::make_tuple(changed, theme); return std::make_tuple(changed, theme);
}, },
@@ -134,6 +138,7 @@ PYBIND11_MODULE(ux, m) {
m.def( m.def(
"label_selection_gui", "label_selection_gui",
[](mujoco::python::MjvOptionWrapper& vis_options) { [](mujoco::python::MjvOptionWrapper& vis_options) {
py::gil_scoped_release no_gil;
return mujoco::platform::LabelSelectionGui(vis_options.get()); return mujoco::platform::LabelSelectionGui(vis_options.get());
}, },
py::arg("vis_options"), "Render the visualization label selection GUI."); py::arg("vis_options"), "Render the visualization label selection GUI.");
@@ -141,6 +146,7 @@ PYBIND11_MODULE(ux, m) {
m.def( m.def(
"frame_selection_gui", "frame_selection_gui",
[](mujoco::python::MjvOptionWrapper& vis_options) { [](mujoco::python::MjvOptionWrapper& vis_options) {
py::gil_scoped_release no_gil;
return mujoco::platform::FrameSelectionGui(vis_options.get()); return mujoco::platform::FrameSelectionGui(vis_options.get());
}, },
py::arg("vis_options"), "Render the visualization frame selection GUI."); py::arg("vis_options"), "Render the visualization frame selection GUI.");
@@ -150,6 +156,7 @@ PYBIND11_MODULE(ux, m) {
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data, mujoco::python::MjDataWrapper& data,
mujoco::python::MjvCameraWrapper& camera, UxState& ux_state) { mujoco::python::MjvCameraWrapper& camera, UxState& ux_state) {
py::gil_scoped_release no_gil;
bool changed = mujoco::platform::CameraSelectionGui( bool changed = mujoco::platform::CameraSelectionGui(
model.get(), data.get(), *camera.get(), ux_state.camera_index); model.get(), data.get(), *camera.get(), ux_state.camera_index);
return changed; return changed;
@@ -162,6 +169,7 @@ PYBIND11_MODULE(ux, m) {
"set_camera", "set_camera",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjvCameraWrapper& camera, int request_idx) { mujoco::python::MjvCameraWrapper& camera, int request_idx) {
py::gil_scoped_release no_gil;
return mujoco::platform::SetCamera(model.get(), camera.get(), request_idx); return mujoco::platform::SetCamera(model.get(), camera.get(), request_idx);
}, },
py::arg("model"), py::arg("camera"), py::arg("request_idx"), py::arg("model"), py::arg("camera"), py::arg("request_idx"),
@@ -170,6 +178,7 @@ PYBIND11_MODULE(ux, m) {
m.def( m.def(
"set_speed_index", "set_speed_index",
[](mujoco::platform::StepControl* step_control, UxState& ux_state, int idx) { [](mujoco::platform::StepControl* step_control, UxState& ux_state, int idx) {
py::gil_scoped_release no_gil;
mujoco::platform::SetSpeedIndex(step_control, ux_state.speed_index, idx); mujoco::platform::SetSpeedIndex(step_control, ux_state.speed_index, idx);
}, },
py::arg("step_control"), py::arg("ux_state"), py::arg("idx"), py::arg("step_control"), py::arg("ux_state"), py::arg("idx"),
@@ -178,6 +187,7 @@ PYBIND11_MODULE(ux, m) {
m.def( m.def(
"physics_gui", "physics_gui",
[](mujoco::python::MjModelWrapper& model, float min_width) { [](mujoco::python::MjModelWrapper& model, float min_width) {
py::gil_scoped_release no_gil;
mujoco::platform::PhysicsGui(model.get(), min_width); mujoco::platform::PhysicsGui(model.get(), min_width);
}, },
py::arg("model"), py::arg("min_width") = 150.0f, py::arg("model"), py::arg("min_width") = 150.0f,
@@ -188,6 +198,7 @@ PYBIND11_MODULE(ux, m) {
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjvOptionWrapper& vis_options, mujoco::python::MjvOptionWrapper& vis_options,
RenderFlags& render_flags) { RenderFlags& render_flags) {
py::gil_scoped_release no_gil;
mjtByte flags[mjNRNDFLAG] = {0}; mjtByte flags[mjNRNDFLAG] = {0};
for (int i = 0; i < mjNRNDFLAG; ++i) { for (int i = 0; i < mjNRNDFLAG; ++i) {
flags[i] = render_flags.flags[i]; flags[i] = render_flags.flags[i];
@@ -205,6 +216,7 @@ PYBIND11_MODULE(ux, m) {
"groups_gui", "groups_gui",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjvOptionWrapper& vis_options, float min_width) { mujoco::python::MjvOptionWrapper& vis_options, float min_width) {
py::gil_scoped_release no_gil;
mujoco::platform::GroupsGui(model.get(), vis_options.get(), min_width); mujoco::platform::GroupsGui(model.get(), vis_options.get(), min_width);
}, },
py::arg("model"), py::arg("vis_options"), py::arg("min_width") = 150.0f, py::arg("model"), py::arg("vis_options"), py::arg("min_width") = 150.0f,
@@ -215,6 +227,7 @@ PYBIND11_MODULE(ux, m) {
[](mujoco::python::MjModelWrapper& model, [](mujoco::python::MjModelWrapper& model,
mujoco::python::MjvOptionWrapper& vis_options, mujoco::python::MjvOptionWrapper& vis_options,
mujoco::python::MjvCameraWrapper& camera, float min_width) { mujoco::python::MjvCameraWrapper& camera, float min_width) {
py::gil_scoped_release no_gil;
mujoco::platform::VisualizationGui(model.get(), vis_options.get(), mujoco::platform::VisualizationGui(model.get(), vis_options.get(),
camera.get(), min_width); camera.get(), min_width);
}, },
@@ -226,6 +239,7 @@ PYBIND11_MODULE(ux, m) {
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data, mujoco::python::MjDataWrapper& data,
mujoco::python::MjvOptionWrapper& vis_options) { mujoco::python::MjvOptionWrapper& vis_options) {
py::gil_scoped_release no_gil;
mujoco::platform::ControlsGui(model.get(), data.get(), mujoco::platform::ControlsGui(model.get(), data.get(),
vis_options.get()); vis_options.get());
}, },
@@ -237,6 +251,7 @@ PYBIND11_MODULE(ux, m) {
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data, mujoco::python::MjDataWrapper& data,
mujoco::python::MjvOptionWrapper& vis_options) { mujoco::python::MjvOptionWrapper& vis_options) {
py::gil_scoped_release no_gil;
mujoco::platform::JointsGui(model.get(), data.get(), vis_options.get()); mujoco::platform::JointsGui(model.get(), data.get(), vis_options.get());
}, },
py::arg("model"), py::arg("data"), py::arg("vis_options"), py::arg("model"), py::arg("data"), py::arg("vis_options"),
@@ -246,6 +261,7 @@ PYBIND11_MODULE(ux, m) {
"sensor_gui", "sensor_gui",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data) { mujoco::python::MjDataWrapper& data) {
py::gil_scoped_release no_gil;
mujoco::platform::SensorGui(model.get(), data.get()); mujoco::platform::SensorGui(model.get(), data.get());
}, },
py::arg("model"), py::arg("data"), "Render the sensor data plot."); py::arg("model"), py::arg("data"), "Render the sensor data plot.");
@@ -255,6 +271,7 @@ PYBIND11_MODULE(ux, m) {
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data, UxState& ux_state, mujoco::python::MjDataWrapper& data, UxState& ux_state,
float min_width) { float min_width) {
py::gil_scoped_release no_gil;
mujoco::platform::StateGui(model.get(), data.get(), ux_state.state, mujoco::platform::StateGui(model.get(), data.get(), ux_state.state,
ux_state.state_sig, min_width); ux_state.state_sig, min_width);
}, },
@@ -266,6 +283,7 @@ PYBIND11_MODULE(ux, m) {
"watch_gui", "watch_gui",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data, UxState& ux_state) { mujoco::python::MjDataWrapper& data, UxState& ux_state) {
py::gil_scoped_release no_gil;
mujoco::platform::WatchGui( mujoco::platform::WatchGui(
model.get(), data.get(), ux_state.watch_field_name, model.get(), data.get(), ux_state.watch_field_name,
sizeof(ux_state.watch_field_name), ux_state.watch_field_index); sizeof(ux_state.watch_field_name), ux_state.watch_field_index);
@@ -278,6 +296,7 @@ PYBIND11_MODULE(ux, m) {
"noise_gui", "noise_gui",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data, UxState& ux_state) { mujoco::python::MjDataWrapper& data, UxState& ux_state) {
py::gil_scoped_release no_gil;
mujoco::platform::NoiseGui(model.get(), data.get(), mujoco::platform::NoiseGui(model.get(), data.get(),
ux_state.noise_scale, ux_state.noise_rate); ux_state.noise_scale, ux_state.noise_rate);
}, },
@@ -289,6 +308,7 @@ PYBIND11_MODULE(ux, m) {
"convergence_gui", "convergence_gui",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data) { mujoco::python::MjDataWrapper& data) {
py::gil_scoped_release no_gil;
mujoco::platform::ConvergenceGui(model.get(), data.get()); mujoco::platform::ConvergenceGui(model.get(), data.get());
}, },
py::arg("model"), py::arg("data"), py::arg("model"), py::arg("data"),
@@ -298,6 +318,7 @@ PYBIND11_MODULE(ux, m) {
"counts_gui", "counts_gui",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data) { mujoco::python::MjDataWrapper& data) {
py::gil_scoped_release no_gil;
mujoco::platform::CountsGui(model.get(), data.get()); mujoco::platform::CountsGui(model.get(), data.get());
}, },
py::arg("model"), py::arg("data"), "Render the solver counts chart."); py::arg("model"), py::arg("data"), "Render the solver counts chart.");
@@ -306,6 +327,7 @@ PYBIND11_MODULE(ux, m) {
"stats_gui", "stats_gui",
[](const mujoco::python::MjModelWrapper& model, [](const mujoco::python::MjModelWrapper& model,
mujoco::python::MjDataWrapper& data, bool paused, float fps) { mujoco::python::MjDataWrapper& data, bool paused, float fps) {
py::gil_scoped_release no_gil;
mujoco::platform::StatsGui(model.get(), data.get(), paused, fps); mujoco::platform::StatsGui(model.get(), data.get(), paused, fps);
}, },
py::arg("model"), py::arg("data"), py::arg("paused"), py::arg("fps"), py::arg("model"), py::arg("data"), py::arg("paused"), py::arg("fps"),
@@ -331,6 +353,7 @@ PYBIND11_MODULE(ux, m) {
const mujoco::python::MjDataWrapper& data, const mujoco::python::MjDataWrapper& data,
mujoco::python::MjvCameraWrapper& cam, mujoco::python::MjvCameraWrapper& cam,
mujoco::platform::CameraMotion motion, mjtNum dx, mjtNum dy) { mujoco::platform::CameraMotion motion, mjtNum dx, mjtNum dy) {
py::gil_scoped_release no_gil;
mujoco::platform::MoveCamera(model.get(), data.get(), cam.get(), motion, mujoco::platform::MoveCamera(model.get(), data.get(), cam.get(), motion,
dx, dy); dx, dy);
}, },
@@ -343,6 +366,7 @@ PYBIND11_MODULE(ux, m) {
const mujoco::python::MjDataWrapper& data, const mujoco::python::MjDataWrapper& data,
const mujoco::python::MjvCameraWrapper& cam, const mujoco::python::MjvCameraWrapper& cam,
mujoco::python::MjvPerturbWrapper& pert, int active) { mujoco::python::MjvPerturbWrapper& pert, int active) {
py::gil_scoped_release no_gil;
mujoco::platform::InitPerturb(model.get(), data.get(), cam.get(), mujoco::platform::InitPerturb(model.get(), data.get(), cam.get(),
pert.get(), pert.get(),
static_cast<mjtPertBit>(active)); static_cast<mjtPertBit>(active));
@@ -357,6 +381,7 @@ PYBIND11_MODULE(ux, m) {
const mujoco::python::MjvCameraWrapper& cam, const mujoco::python::MjvCameraWrapper& cam,
mujoco::python::MjvPerturbWrapper& pert, int action, mjtNum reldx, mujoco::python::MjvPerturbWrapper& pert, int action, mjtNum reldx,
mjtNum reldy) { mjtNum reldy) {
py::gil_scoped_release no_gil;
mujoco::platform::MovePerturb(model.get(), data.get(), cam.get(), mujoco::platform::MovePerturb(model.get(), data.get(), cam.get(),
pert.get(), static_cast<mjtMouse>(action), pert.get(), static_cast<mjtMouse>(action),
reldx, reldy); reldx, reldy);
@@ -388,6 +413,7 @@ PYBIND11_MODULE(ux, m) {
const mujoco::python::MjDataWrapper& data, const mujoco::python::MjDataWrapper& data,
const mujoco::python::MjvCameraWrapper& cam, float x, float y, const mujoco::python::MjvCameraWrapper& cam, float x, float y,
float aspect_ratio, const mujoco::python::MjvOptionWrapper& opt) { float aspect_ratio, const mujoco::python::MjvOptionWrapper& opt) {
py::gil_scoped_release no_gil;
return mujoco::platform::Pick(model.get(), data.get(), cam.get(), x, y, return mujoco::platform::Pick(model.get(), data.get(), cam.get(), x, y,
aspect_ratio, opt.get()); aspect_ratio, opt.get());
}, },
@@ -399,6 +425,7 @@ PYBIND11_MODULE(ux, m) {
"camera_to_string", "camera_to_string",
[](const mujoco::python::MjDataWrapper& data, [](const mujoco::python::MjDataWrapper& data,
const mujoco::python::MjvCameraWrapper& camera) { const mujoco::python::MjvCameraWrapper& camera) {
py::gil_scoped_release no_gil;
return mujoco::platform::CameraToString(data.get(), camera.get()); return mujoco::platform::CameraToString(data.get(), camera.get());
}, },
py::arg("data"), py::arg("camera"), py::arg("data"), py::arg("camera"),