Files
Mujoco_WASM/python/mujoco/mjpython/mjpython.mm
T
Saran Tunyasuvunakool b362cb4972 Expose a handle for the Python viewer.
This change also requires user scripts to explicitly synchronize changes to physics state to the viewer. The Simulate class was reconfigured so that certain UI events are handled during this sync operation, outside of the render loop on the main thread. These correspond to operations that require access to the full mjModel/mjData.

To support other, more interactive operations (e.g. camera movements), a new mjvSceneState struct is introduced which captures only the portion of the physics state required for scene re-rendering. The mjvSceneState is updated from mjModel/mjData during the viewer sync operation, and is significantly cheaper than a full mj_copyModel and mj_copyData.

Fixes https://github.com/deepmind/mujoco/issues/796

PiperOrigin-RevId: 525723636
Change-Id: Id08d0210a2c067d5afe85e2bf104f276aeddd75e
2023-04-20 05:59:19 -07:00

361 lines
12 KiB
Plaintext

// Copyright 2023 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.
#import <algorithm>
#import <atomic>
#import <cstdint>
#import <cstdlib>
#import <cstring>
#import <iostream>
#import <vector>
#import <dlfcn.h>
#import <mach-o/dyld.h>
#import <pthread.h>
#import <sys/resource.h>
#import <unistd.h>
#import <Cocoa/Cocoa.h>
#import <Python.h>
extern "C" {
extern char **environ; // for execve
// Wrap Objective-C Cocoa calls into C-style functions with default visibility,
// so that we can dlsym and call them from Python via ctypes.
__attribute__((used)) void mjpython_hide_dock_icon() {
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
}
__attribute__((used)) void mjpython_show_dock_icon() {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}
} // extern "C"
// TODO(b/273744079): Remove Python 3.7 code after end-of-life (27 Jun 2023).
namespace {
// 16MiB is the default Python thread stack size on macOS as of Python 3.11
// https://bugs.python.org/issue18075
constexpr rlim_t kThreadStackSize = 0x1000000;
struct {
#define CPYTHON_FN(fname) decltype(&::fname) fname
#if PY_MINOR_VERSION >= 8
// go/keep-sorted start
CPYTHON_FN(PyConfig_Clear);
CPYTHON_FN(PyConfig_InitPythonConfig);
CPYTHON_FN(PyConfig_SetBytesArgv);
CPYTHON_FN(Py_InitializeFromConfig);
CPYTHON_FN(Py_RunMain);
// go/keep-sorted end
#else
// go/keep-sorted start
CPYTHON_FN(PyMem_RawFree);
CPYTHON_FN(Py_DecodeLocale);
CPYTHON_FN(Py_Initialize);
CPYTHON_FN(Py_Main);
CPYTHON_FN(Py_SetProgramName);
// go/keep-sorted end
#endif
// go/keep-sorted start
CPYTHON_FN(PyGILState_Ensure);
CPYTHON_FN(PyGILState_Release);
CPYTHON_FN(PyRun_SimpleStringFlags);
CPYTHON_FN(Py_FinalizeEx);
// go/keep-sorted end
#undef CPYTHON_FN
} cpython;
std::atomic_bool py_initialized = false;
struct Args {
int argc;
char** argv;
};
// The Python main thread (distinct from the macOS main thread, which executes the main function).
void* mjpython_pymain(void* vargs) {
Args* args = static_cast<Args*>(vargs);
PyGILState_STATE gil;
// Initialize the Python interpreter.
#if PY_MINOR_VERSION >= 8
PyConfig config;
cpython.PyConfig_InitPythonConfig(&config);
cpython.PyConfig_SetBytesArgv(&config, args->argc, args->argv);
cpython.Py_InitializeFromConfig(&config);
cpython.PyConfig_Clear(&config);
#else
// Convert each argv to wchar_t* (needed for Py_Main).
wchar_t** wargv = static_cast<wchar_t**>(std::calloc(args->argc, sizeof(wchar_t*)));
for (int i = 0; i < args->argc; ++i) {
wargv[i] = cpython.Py_DecodeLocale(args->argv[i], nullptr);
}
cpython.Py_SetProgramName(wargv[0]);
cpython.Py_Initialize();
#endif
// Set up the condition variable to pass control back to the macOS main thread.
gil = cpython.PyGILState_Ensure();
cpython.PyRun_SimpleStringFlags("import threading; cond = threading.Condition()", nullptr);
py_initialized.store(true);
// Wait until GLFW is initialized on macOS main thread, set up the queue and an atexit hook
// to enqueue a termination flag upon exit.
cpython.PyRun_SimpleStringFlags(R"(
import atexit
# The mujoco.viewer module should only be imported here after glfw.init() in the macOS main thread.
with cond:
cond.wait()
import mujoco.viewer
# Similar to a queue.Queue(maxsize=1), but where only one active task is allowed at a time.
# With queue.Queue(1), another item is allowed to be enqueued before task_done is called.
class _MjPythonImpl(mujoco.viewer._MjPythonBase):
# Termination statuses
NOT_TERMINATED = 0
TERMINATION_REQUESTED = 1
TERMINATION_ACCEPTED = 2
TERMINATED = 3
def __init__(self):
self._cond = threading.Condition()
self._task = None
self._termination = self.__class__.NOT_TERMINATED
self._busy = False
def launch_on_ui_thread(self, model, data, handle_return):
with self._cond:
if self._busy or self._task is not None:
raise RuntimeError('another MuJoCo viewer is already open')
else:
self._task = (model, data, handle_return)
self._cond.notify()
def terminate(self):
with self._cond:
self._termination = self.__class__.TERMINATION_REQUESTED
self._cond.notify()
self._cond.wait_for(
lambda: self._termination == self.__class__.TERMINATED)
def get(self):
with self._cond:
self._cond.wait_for(
lambda: self._task is not None or self._termination)
if self._termination:
if self._termination == self.__class__.TERMINATION_REQUESTED:
self._termination = self.__class__.TERMINATION_ACCEPTED
return None
task = self._task
self._busy = True
self._task = None
return task
def done(self):
with self._cond:
self._busy = False
if self._termination == self.__class__.TERMINATION_ACCEPTED:
self._termination = self.__class__.TERMINATED
self._cond.notify()
mujoco.viewer._MJPYTHON = _MjPythonImpl()
atexit.register(mujoco.viewer._MJPYTHON.terminate)
del _MjPythonImpl # Don't pollute globals for user script.
with cond:
cond.notify()
del cond # Don't pollute globals for user script.
)", nullptr);
// Run the Python interpreter main loop.
#if PY_MINOR_VERSION >= 8
cpython.Py_RunMain();
#else
cpython.Py_Main(args->argc, wargv);
#endif
// Tear down the interpreter.
cpython.Py_FinalizeEx();
#if PY_MINOR_VERSION < 8
for (int i = 0; i < args->argc; ++i) {
cpython.PyMem_RawFree(wargv[i]);
wargv[i] = nullptr;
}
std::free(wargv);
wargv = nullptr;
#endif
return nullptr;
}
} // namespace
int main(int argc, char** argv) {
const char* libpython_path = getenv("MJPYTHON_LIBPYTHON");
if (!libpython_path || !libpython_path[0]) {
std::cerr << "This binary must be launched via the mjpython.py script.\n";
return EXIT_FAILURE;
}
// Enlarge the stack if necessary to match what Python normally expects to have when launching
// a new thread.
rlimit stack;
if (getrlimit(RLIMIT_STACK, &stack)) {
std::cerr << "getrlimit failed to query stack size with error code " << errno << " ("
<< std::strerror(errno) << ")\n";
std::cerr << "continuing anyway but crashes may occur if the stack is too small\n";
} else if (stack.rlim_cur < kThreadStackSize && stack.rlim_cur < stack.rlim_max) {
auto rlim_old = stack.rlim_cur;
stack.rlim_cur = std::min(kThreadStackSize, stack.rlim_max);
if (setrlimit(RLIMIT_STACK, &stack)) {
std::cerr << "setrlimit failed to increase stack size with error code " << errno << " ("
<< std::strerror(errno) << ")\n";
std::cerr << "continuing anyway with stack size " << rlim_old << " but crashes may occur\n";
} else {
// re-exec the binary so that the new stack size takes effect
std::uint32_t path_size = 0;
_NSGetExecutablePath(nullptr, &path_size);
std::vector<char> path(path_size);
if (_NSGetExecutablePath(path.data(), &path_size)) {
std::cerr << "unexpected error from _NSGetExecutablePath, continuing anyway\n";
} else {
execve(path.data(), argv, environ);
}
}
}
// Resolve libpython at runtime to prevent linking against the wrong dylib. The correct libpython
// path is passed from a Python trampoline script, which ran inside the desired interpreter and
// exec'd this binary.
void* libpython = dlopen(libpython_path, RTLD_NOW | RTLD_GLOBAL);
if (!libpython) {
std::cerr << "failed to dlopen path '" << libpython_path << "': " << dlerror() << "\n";
return EXIT_FAILURE;
}
// Look up required CPython API functions from table of symbols already loaded into the process.
#define CPYTHON_INITFN(fname) \
{ \
cpython.fname = reinterpret_cast<decltype(cpython.fname)>(dlsym(libpython, #fname)); \
if (!cpython.fname) { \
std::cerr << "failed to dlsym '" << #fname << "': " << dlerror() << "\n"; \
return EXIT_FAILURE; \
} \
}
#if PY_MINOR_VERSION >= 8
// go/keep-sorted start
CPYTHON_INITFN(PyConfig_Clear);
CPYTHON_INITFN(PyConfig_InitPythonConfig);
CPYTHON_INITFN(PyConfig_SetBytesArgv);
CPYTHON_INITFN(Py_InitializeFromConfig);
CPYTHON_INITFN(Py_RunMain);
// go/keep-sorted end
#else
// go/keep-sorted start
CPYTHON_INITFN(PyMem_RawFree);
CPYTHON_INITFN(Py_DecodeLocale);
CPYTHON_INITFN(Py_Initialize);
CPYTHON_INITFN(Py_Main);
CPYTHON_INITFN(Py_SetProgramName);
// go/keep-sorted end
#endif
// go/keep-sorted start
CPYTHON_INITFN(PyGILState_Ensure);
CPYTHON_INITFN(PyGILState_Release);
CPYTHON_INITFN(PyRun_SimpleStringFlags);
CPYTHON_INITFN(Py_FinalizeEx);
// go/keep-sorted end
#undef CPYTHON_INITFN
// Package up argc and argv together to pass to pthread_create.
Args args{argc, argv};
#define PTHREAD_CHECKED(func, ...) \
{ \
int result = func(__VA_ARGS__); \
if (result) { \
std::cerr << #func << " failed with " << result << "(" << std::strerror(result) << ")\n"; \
return EXIT_FAILURE; \
} \
}
// Configure the new thread with the correct stack size;
pthread_attr_t pthread_attr;
PTHREAD_CHECKED(pthread_attr_init, &pthread_attr);
PTHREAD_CHECKED(pthread_attr_setstacksize, &pthread_attr, stack.rlim_cur);
// Create a thread to be used as the "Python main thread".
pthread_t pymain_thread;
PTHREAD_CHECKED(pthread_create, &pymain_thread, &pthread_attr, &mjpython_pymain, &args);
pthread_attr_destroy(&pthread_attr);
#undef PTHREAD_CHECKED
// Busy-wait until Python interpreter is initialized.
while (!py_initialized.load()) {}
// Initialize GLFW on the macOS main thread, yield control to Python main thread and wait for it
// to finish setting up _MJPYTHON, then serve incoming viewer launch requests.
PyGILState_STATE gil = cpython.PyGILState_Ensure();
cpython.PyRun_SimpleStringFlags(R"(
import ctypes
# GLFW must be initialized on the OS main thread (i.e. here).
import glfw
import mujoco.viewer
glfw.init()
glfw.poll_events()
ctypes.CDLL(None).mjpython_hide_dock_icon()
# Wait for Python main thread to finish setting up _MJPYTHON
with cond:
cond.notify()
cond.wait()
while True:
try:
# Wait for an incoming payload.
task = mujoco.viewer._MJPYTHON.get()
# None means that we are exiting.
if task is None:
glfw.terminate()
break
# Otherwise, launch the viewer.
model, data, handle_return = task
ctypes.CDLL(None).mjpython_show_dock_icon()
mujoco.viewer._launch_internal(
model, data, run_physics_thread=False, handle_return=handle_return)
ctypes.CDLL(None).mjpython_hide_dock_icon()
finally:
mujoco.viewer._MJPYTHON.done()
)", nullptr);
cpython.PyGILState_Release(gil);
// Tear everything down.
pthread_join(pymain_thread, nullptr);
dlclose(libpython);
}