Fix GLFW crash when Python exits while the passive viewer is running.

Fixes #790

PiperOrigin-RevId: 549440435
Change-Id: I808f6aaab6662225e5fe1131e0da72eca6011ba9
This commit is contained in:
Saran Tunyasuvunakool
2023-07-19 15:19:44 -07:00
committed by Copybara-Service
parent 8386b28877
commit f7847ba73e
7 changed files with 192 additions and 116 deletions
+2
View File
@@ -30,6 +30,8 @@ Python bindings
``update_texture`` methods to allow users to update renderable assets. ``update_texture`` methods to allow users to update renderable assets.
(`#812 <https://github.com/deepmind/mujoco/issues/812>`_, `#958 <https://github.com/deepmind/mujoco/issues/958>`_, (`#812 <https://github.com/deepmind/mujoco/issues/812>`_, `#958 <https://github.com/deepmind/mujoco/issues/958>`_,
`#965 <https://github.com/deepmind/mujoco/issues/965>`_) `#965 <https://github.com/deepmind/mujoco/issues/965>`_)
- Fix GLFW crash when Python exits while the passive viewer is running.
(`#790 <https://github.com/deepmind/mujoco/issues/790>`_)
Models Models
^^^^^^ ^^^^^^
+1
View File
@@ -405,6 +405,7 @@ target_link_libraries(
_simulate _simulate
PRIVATE mujoco PRIVATE mujoco
mujoco::libsimulate mujoco::libsimulate
errors_header
raw raw
structs_header structs_header
) )
+175 -105
View File
@@ -13,14 +13,17 @@
// limitations under the License. // limitations under the License.
#include <atomic> #include <atomic>
#include <chrono>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <string> #include <string>
#include <thread>
#include <utility> #include <utility>
#include <glfw_adapter.h> #include <glfw_adapter.h>
#include <glfw_dispatch.h> #include <glfw_dispatch.h>
#include <simulate.h> #include <simulate.h>
#include "errors.h"
#include "structs.h" #include "structs.h"
#include <pybind11/gil.h> #include <pybind11/gil.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
@@ -31,21 +34,20 @@ namespace {
namespace py = ::pybind11; namespace py = ::pybind11;
template <typename T, int N> template <typename T, int N>
constexpr inline std::size_t sizeof_arr(const T(&arr)[N]) { constexpr inline std::size_t sizeof_arr(const T (&arr)[N]) {
return sizeof(arr); return sizeof(arr);
} }
class SimulateWrapper : public mujoco::Simulate { class SimulateWrapper {
public: public:
SimulateWrapper(std::unique_ptr<PlatformUIAdapter> platform_ui_adapter, SimulateWrapper(std::unique_ptr<PlatformUIAdapter> platform_ui_adapter,
py::object scn, py::object cam, py::object scn, py::object cam, py::object opt,
py::object opt, py::object pert, bool fully_managed) py::object pert, bool fully_managed)
: Simulate(std::move(platform_ui_adapter), : simulate_(new mujoco::Simulate(
scn.cast<MjvSceneWrapper&>().get(), std::move(platform_ui_adapter), scn.cast<MjvSceneWrapper&>().get(),
cam.cast<MjvCameraWrapper&>().get(), cam.cast<MjvCameraWrapper&>().get(),
opt.cast<MjvOptionWrapper&>().get(), opt.cast<MjvOptionWrapper&>().get(),
pert.cast<MjvPerturbWrapper&>().get(), pert.cast<MjvPerturbWrapper&>().get(), fully_managed)),
fully_managed),
m_(py::none()), m_(py::none()),
d_(py::none()), d_(py::none()),
scn_(scn), scn_(scn),
@@ -53,12 +55,33 @@ class SimulateWrapper : public mujoco::Simulate {
opt_(opt), opt_(opt),
pert_(pert) {} pert_(pert) {}
~SimulateWrapper() { Destroy(); }
void Destroy() {
if (simulate_) {
delete simulate_;
simulate_ = nullptr;
destroyed_.store(1);
}
}
void WaitUntilDestroyed() {
// TODO: replace with atomic wait when we migrate to C++20
while (!destroyed_.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void Load(py::object m, py::object d, const std::string& path) { void Load(py::object m, py::object d, const std::string& path) {
if (!simulate_) {
return;
}
mjModel* m_raw = m.cast<MjModelWrapper&>().get(); mjModel* m_raw = m.cast<MjModelWrapper&>().get();
mjData* d_raw = d.cast<MjDataWrapper&>().get(); mjData* d_raw = d.cast<MjDataWrapper&>().get();
{ {
py::gil_scoped_release no_gil; py::gil_scoped_release no_gil;
Simulate::Load(m_raw, d_raw, path.c_str()); simulate_->Load(m_raw, d_raw, path.c_str());
} }
m_ = m; m_ = m;
d_ = d; d_ = d;
@@ -66,7 +89,12 @@ class SimulateWrapper : public mujoco::Simulate {
d_raw_ = d_raw; d_raw_ = d_raw;
} }
mujoco::Simulate* simulate() { return simulate_; }
private: private:
mujoco::Simulate* simulate_;
std::atomic_int destroyed_ = 0;
// Hold references to keep these Python objects alive for as long as the // Hold references to keep these Python objects alive for as long as the
// simulate object. // simulate object.
py::object m_; py::object m_;
@@ -80,6 +108,49 @@ class SimulateWrapper : public mujoco::Simulate {
mjData* d_raw_ = nullptr; mjData* d_raw_ = nullptr;
}; };
inline mujoco::Simulate& SimulateRefOrThrow(SimulateWrapper& wrapper) {
auto* sim = wrapper.simulate();
if (!sim) {
throw UnexpectedError("simulate object is already deleted");
}
return *sim;
}
template <typename T, typename... Args>
inline auto CallIfNotNull(T (*func)(mujoco::Simulate&, Args...)) {
return [func](SimulateWrapper& wrapper, Args&&... args) {
return func(SimulateRefOrThrow(wrapper), std::forward<Args>(args)...);
};
}
template <typename... Args>
inline auto CallIfNotNull(void (*func)(mujoco::Simulate&, Args...)) {
return [func](SimulateWrapper& wrapper, Args&&... args) -> void {
func(SimulateRefOrThrow(wrapper), std::forward<Args>(args)...);
};
}
template <typename... Args>
inline auto CallIfNotNull(void (mujoco::Simulate::*func)(Args...)) {
return [func](SimulateWrapper& wrapper, Args&&... args) -> void {
(SimulateRefOrThrow(wrapper).*func)(std::forward<Args>(args)...);
};
}
template <typename T>
inline auto GetIfNotNull(T mujoco::Simulate::*member) {
return [member](SimulateWrapper& wrapper) -> T& {
return SimulateRefOrThrow(wrapper).*member;
};
}
template <typename T, typename... Args>
inline auto SetIfNotNull(T mujoco::Simulate::*member) {
return [member](SimulateWrapper& wrapper, const T& value) -> void {
SimulateRefOrThrow(wrapper).*member = value;
};
}
PYBIND11_MODULE(_simulate, pymodule) { PYBIND11_MODULE(_simulate, pymodule) {
py::class_<SimulateMutex>(pymodule, "Mutex") py::class_<SimulateMutex>(pymodule, "Mutex")
.def( .def(
@@ -100,110 +171,109 @@ PYBIND11_MODULE(_simulate, pymodule) {
std::make_unique<mujoco::GlfwAdapter>(), scn, cam, opt, pert, std::make_unique<mujoco::GlfwAdapter>(), scn, cam, opt, pert,
fully_managed); fully_managed);
})) }))
.def("destroy", &SimulateWrapper::Destroy,
py::call_guard<py::gil_scoped_release>())
.def("load", &SimulateWrapper::Load) .def("load", &SimulateWrapper::Load)
.def("sync", &mujoco::Simulate::Sync, .def("sync", CallIfNotNull(&mujoco::Simulate::Sync),
py::call_guard<py::gil_scoped_release>()) py::call_guard<py::gil_scoped_release>())
.def("render_loop", CallIfNotNull(&mujoco::Simulate::RenderLoop),
py::call_guard<py::gil_scoped_release>())
.def("lock", GetIfNotNull(&mujoco::Simulate::mtx),
py::call_guard<py::gil_scoped_release>(),
py::return_value_policy::reference_internal)
.def_property_readonly("ctrl_noise_std",
GetIfNotNull(&mujoco::Simulate::ctrl_noise_std),
py::call_guard<py::gil_scoped_release>())
.def_property_readonly("ctrl_noise_rate",
GetIfNotNull(&mujoco::Simulate::ctrl_noise_rate),
py::call_guard<py::gil_scoped_release>())
.def_property_readonly("real_time_index",
GetIfNotNull(&mujoco::Simulate::real_time_index),
py::call_guard<py::gil_scoped_release>())
.def_property("speed_changed",
GetIfNotNull(&mujoco::Simulate::speed_changed),
SetIfNotNull(&mujoco::Simulate::speed_changed),
py::call_guard<py::gil_scoped_release>())
.def_property("measured_slowdown",
GetIfNotNull(&mujoco::Simulate::measured_slowdown),
SetIfNotNull(&mujoco::Simulate::measured_slowdown),
py::call_guard<py::gil_scoped_release>())
.def_property_readonly("refresh_rate",
GetIfNotNull(&mujoco::Simulate::refresh_rate),
py::call_guard<py::gil_scoped_release>())
.def_property_readonly("busywait",
GetIfNotNull(&mujoco::Simulate::busywait),
py::call_guard<py::gil_scoped_release>())
.def_property_readonly("run", GetIfNotNull(&mujoco::Simulate::run),
py::call_guard<py::gil_scoped_release>())
.def_property_readonly("exitrequest",
CallIfNotNull(+[](mujoco::Simulate& sim) {
return sim.exitrequest.load();
}),
py::call_guard<py::gil_scoped_release>())
.def( .def(
"render_loop", "exit",
[](SimulateWrapper& simulate) { simulate.RenderLoop(); }, [](SimulateWrapper& wrapper) {
py::call_guard<py::gil_scoped_release>()) mujoco::Simulate* sim = wrapper.simulate();
.def( if (!sim) {
"lock", return;
[](SimulateWrapper& simulate) -> SimulateMutex& { }
return simulate.mtx;
int value = 0;
sim->exitrequest.compare_exchange_strong(value, 1);
wrapper.WaitUntilDestroyed();
}, },
py::call_guard<py::gil_scoped_release>(), py::call_guard<py::gil_scoped_release>())
py::return_value_policy::reference_internal)
.def_readonly("ctrl_noise_std", &mujoco::Simulate::ctrl_noise_std,
py::call_guard<py::gil_scoped_release>())
.def_readonly("ctrl_noise_rate", &mujoco::Simulate::ctrl_noise_rate,
py::call_guard<py::gil_scoped_release>())
.def_readonly("real_time_index", &mujoco::Simulate::real_time_index, .def_property_readonly("uiloadrequest",
py::call_guard<py::gil_scoped_release>()) CallIfNotNull(+[](mujoco::Simulate& sim) {
.def_readwrite("speed_changed", &mujoco::Simulate::speed_changed, return sim.uiloadrequest.load();
py::call_guard<py::gil_scoped_release>()) }),
.def_readwrite("measured_slowdown", &mujoco::Simulate::measured_slowdown, py::call_guard<py::gil_scoped_release>())
py::call_guard<py::gil_scoped_release>()) .def("uiloadrequest_decrement", CallIfNotNull(+[](mujoco::Simulate& sim) {
.def_readonly("refresh_rate", &mujoco::Simulate::refresh_rate, sim.uiloadrequest.fetch_sub(1);
py::call_guard<py::gil_scoped_release>()) }),
py::call_guard<py::gil_scoped_release>())
.def_readonly("busywait", &mujoco::Simulate::busywait, .def("update_hfield",
py::call_guard<py::gil_scoped_release>()) CallIfNotNull(+[](mujoco::Simulate& sim, int hfieldid) {
.def_readonly("run", &mujoco::Simulate::run, sim.UpdateHField(hfieldid);
py::call_guard<py::gil_scoped_release>()) }),
py::call_guard<py::gil_scoped_release>())
.def("update_mesh", CallIfNotNull(+[](mujoco::Simulate& sim, int meshid) {
sim.UpdateMesh(meshid);
}),
py::call_guard<py::gil_scoped_release>())
.def("update_texture",
CallIfNotNull(+[](mujoco::Simulate& sim, int texid) {
sim.UpdateTexture(texid);
}),
py::call_guard<py::gil_scoped_release>())
.def_property( .def_property(
"exitrequest", "droploadrequest", CallIfNotNull(+[](mujoco::Simulate& sim) {
[](SimulateWrapper& simulate) { return simulate.exitrequest.load(); }, return sim.droploadrequest.load();
[](SimulateWrapper& simulate, int exitrequest) { }),
simulate.exitrequest.store(exitrequest); CallIfNotNull(+[](mujoco::Simulate& sim, bool droploadrequest) {
}, sim.droploadrequest.store(droploadrequest);
}),
py::call_guard<py::gil_scoped_release>()) py::call_guard<py::gil_scoped_release>())
.def_property_readonly("dropfilename",
.def_property_readonly( GetIfNotNull(&mujoco::Simulate::dropfilename),
"uiloadrequest", py::call_guard<py::gil_scoped_release>())
[](SimulateWrapper& simulate) { .def_property_readonly("filename",
return simulate.uiloadrequest.load(); GetIfNotNull(&mujoco::Simulate::filename),
}, py::call_guard<py::gil_scoped_release>())
py::call_guard<py::gil_scoped_release>())
.def(
"uiloadrequest_decrement",
[](SimulateWrapper& simulate) {
simulate.uiloadrequest.fetch_sub(1);
},
py::call_guard<py::gil_scoped_release>())
.def(
"update_hfield",
[](SimulateWrapper& simulate, int hfieldid) {
simulate.UpdateHField(hfieldid);
},
py::call_guard<py::gil_scoped_release>())
.def(
"update_mesh",
[](SimulateWrapper& simulate, int meshid) {
simulate.UpdateMesh(meshid);
},
py::call_guard<py::gil_scoped_release>())
.def(
"update_texture",
[](SimulateWrapper& simulate, int texid) {
simulate.UpdateTexture(texid);
},
py::call_guard<py::gil_scoped_release>())
.def_property( .def_property(
"droploadrequest", "load_error", GetIfNotNull(&mujoco::Simulate::load_error),
[](SimulateWrapper& simulate) { CallIfNotNull(+[](mujoco::Simulate& sim, const std::string& error) {
return simulate.droploadrequest.load(); const auto max_length = sizeof_arr(sim.load_error);
}, std::strncpy(sim.load_error, error.c_str(), max_length - 1);
[](SimulateWrapper& simulate, bool droploadrequest) { sim.load_error[max_length - 1] = '\0';
simulate.droploadrequest.store(droploadrequest); }));
},
py::call_guard<py::gil_scoped_release>())
.def_property_readonly(
"dropfilename",
[](SimulateWrapper& simulate) -> std::string {
return simulate.dropfilename;
},
py::call_guard<py::gil_scoped_release>())
.def_property_readonly(
"filename",
[](SimulateWrapper& simulate) -> std::string {
return simulate.filename;
},
py::call_guard<py::gil_scoped_release>())
.def_property(
"load_error",
[](SimulateWrapper& simulate) -> std::string {
return simulate.load_error;
},
[](SimulateWrapper& simulate, const std::string& error) {
const auto max_length = sizeof_arr(simulate.load_error);
std::strncpy(simulate.load_error, error.c_str(), max_length - 1);
simulate.load_error[max_length - 1] = '\0';
});
pymodule.def("set_glfw_dlhandle", [](std::uintptr_t dlhandle) { pymodule.def("set_glfw_dlhandle", [](std::uintptr_t dlhandle) {
mujoco::Glfw(reinterpret_cast<void*>(dlhandle)); mujoco::Glfw(reinterpret_cast<void*>(dlhandle));
+9 -8
View File
@@ -96,7 +96,7 @@ class Handle:
def close(self): def close(self):
sim = self._sim() sim = self._sim()
if sim is not None: if sim is not None:
sim.exitrequest = 1 sim.exit()
def is_running(self) -> bool: def is_running(self) -> bool:
sim = self._sim() sim = self._sim()
@@ -348,18 +348,19 @@ def _launch_internal(
side_thread = threading.Thread( side_thread = threading.Thread(
target=_reload, args=(simulate, loader, notify_loaded)) target=_reload, args=(simulate, loader, notify_loaded))
def make_exit_requester(simulate): def make_exit(simulate):
def exit_requester(): def exit_simulate():
simulate.exitrequest = True simulate.exit()
return exit_requester return exit_simulate
exit_requester = make_exit_requester(simulate) exit_simulate = make_exit(simulate)
atexit.register(exit_requester) atexit.register(exit_simulate)
side_thread.start() side_thread.start()
simulate.render_loop() simulate.render_loop()
atexit.unregister(exit_requester) atexit.unregister(exit_simulate)
side_thread.join() side_thread.join()
simulate.destroy()
def launch(model: Optional[mujoco.MjModel] = None, def launch(model: Optional[mujoco.MjModel] = None,
+1
View File
@@ -110,6 +110,7 @@ GlfwAdapter::GlfwAdapter() {
} }
GlfwAdapter::~GlfwAdapter() { GlfwAdapter::~GlfwAdapter() {
FreeMjrContext();
Glfw().glfwMakeContextCurrent(nullptr); Glfw().glfwMakeContextCurrent(nullptr);
Glfw().glfwDestroyWindow(window_); Glfw().glfwDestroyWindow(window_);
} }
+1 -1
View File
@@ -21,7 +21,7 @@ PlatformUIAdapter::PlatformUIAdapter() {
mjr_defaultContext(&con_); mjr_defaultContext(&con_);
} }
PlatformUIAdapter::~PlatformUIAdapter() { void PlatformUIAdapter::FreeMjrContext() {
mjr_freeContext(&con_); mjr_freeContext(&con_);
} }
+3 -2
View File
@@ -22,7 +22,7 @@
namespace mujoco { namespace mujoco {
class PlatformUIAdapter { class PlatformUIAdapter {
public: public:
virtual ~PlatformUIAdapter(); virtual ~PlatformUIAdapter() = default;
inline mjuiState& state() { return state_; } inline mjuiState& state() { return state_; }
inline const mjuiState& state() const { return state_; } inline const mjuiState& state() const { return state_; }
@@ -38,7 +38,7 @@ class PlatformUIAdapter {
layout_callback_ = layout_callback; layout_callback_ = layout_callback;
} }
// Optionally overrideable function to (re)create an mjrContext for an mjModel // Optionally overridable function to (re)create an mjrContext for an mjModel
virtual bool RefreshMjrContext(const mjModel* m, int fontscale); virtual bool RefreshMjrContext(const mjModel* m, int fontscale);
virtual bool EnsureContextSize(); virtual bool EnsureContextSize();
@@ -73,6 +73,7 @@ class PlatformUIAdapter {
protected: protected:
PlatformUIAdapter(); PlatformUIAdapter();
void FreeMjrContext();
// Event handlers // Event handlers
void OnFilesDrop(int count, const char** paths); void OnFilesDrop(int count, const char** paths);