Introduce new logging API, fixes #858

PiperOrigin-RevId: 930744288
Change-Id: I6ec1203b55c031390f3eef23192e2337508ce886
This commit is contained in:
Yuval Tassa
2026-06-11 14:36:17 -07:00
committed by Copybara-Service
parent a2abaf7aef
commit 58f6d52491
45 changed files with 2586 additions and 422 deletions
+16 -9
View File
@@ -32,10 +32,14 @@ you are simulating multiple models in parallel, they use the same set of callbac
mju_user_error
~~~~~~~~~~~~~~
This is called from within the main error function :ref:`mju_error`. When installed, this function overrides the default
error processing. Once it prints error messages (or whatever else the user wants to do), it must **exit** the program.
MuJoCo is written with the assumption that mju_error will not return. If it does, the behavior of the software is
undefined.
.. deprecated::
Use :ref:`mju_setLogHandler` instead. See :ref:`siLogHandler`.
Called by the default log handler when a fatal error occurs. If installed, this function overrides the default error
processing. It may ``longjmp`` out or return. MuJoCo is written with the assumption that error handlers will not
return; if they do, the behavior of the software is undefined.
If a custom log handler is installed via :ref:`mju_setLogHandler`, this callback is not consulted.
.. code-block:: C
@@ -47,8 +51,11 @@ undefined.
mju_user_warning
~~~~~~~~~~~~~~~~
This is called from within the main warning function :ref:`mju_warning`. It is similar to the error handler, but instead
it must return without exiting the program.
.. deprecated::
Use :ref:`mju_setLogHandler` instead. See :ref:`siLogHandler`.
Called by the default log handler when a warning occurs. If a custom log handler is installed via
:ref:`mju_setLogHandler`, this callback is not consulted.
.. code-block:: C
@@ -185,9 +192,9 @@ mjcb_time
~~~~~~~~~
Installing this callback enables the built-in profiler, and keeps timing statistics in ``mjData.timer``. The return type
is mjtNum, while the time units are up to the user. :ref:`simulate.cc <saSimulate>` assumes the unit is 1 millisecond.
In order to be useful, the callback should use high-resolution timers with at least microsecond precision. This is
because the computations being timed are very fast.
is mjtNum, while the time units are up to the user. Both :ref:`simulate.cc <saSimulate>` and the ``mjTOPIC_TIME_STP``
informational :ref:`topic <mjtLogTopic>` assume the unit is 1 millisecond. In order to be useful, the callback should
use high-resolution timers with at least microsecond precision.
.. code-block:: C
+77
View File
@@ -540,6 +540,33 @@ Sleep state of an object.
.. mujoco-include:: mjtSleepState
.. _tyLogEnums:
Logging
~~~~~~~
.. _mjtLogLevel:
mjtLogLevel
"""""""""""
Log message severity level.
.. mujoco-include:: mjtLogLevel
.. _mjtLogTopic:
mjtLogTopic
"""""""""""
Topic identifiers for informational messages. Used with :ref:`mju_info` for topic-based filtering.
Topic 0 (``mjTOPIC_NONE``) always passes through the default handler's filter. Other topics must be enabled in
the :ref:`mjLogConfig` bitmask. Since topics are 1-indexed, the bitmask for topic ``t`` is ``(1 << (t - 1))``.
.. mujoco-include:: mjtLogTopic
.. _tyVisEnums:
Visualization
@@ -1059,6 +1086,36 @@ Asset cache used by the compiler to avoid repeated slow recompilation. See :ref:
.. mujoco-include:: mjCache
.. _tyLogStructure:
Logging
^^^^^^^
.. _mjLogMessage:
mjLogMessage
~~~~~~~~~~~~
Structured log message passed to :ref:`mjfLogHandler` callbacks. Contains the severity level, optional topic for
info messages, a one-line subject, an optional multi-line body, and optional source location (function name, file
name, line number).
.. mujoco-include:: mjLogMessage
.. _mjLogConfig:
mjLogConfig
~~~~~~~~~~~
Configuration for the default log handler. Controls whether messages are printed to the console and/or written to
a log file (default: ``MUJOCO_LOG.TXT``). The ``logto_file`` field enables file logging, while ``logfile`` specifies
the file path. The ``topics`` field is a bitmask of :ref:`mjtLogTopic` values: bit ``(topic - 1)`` enables
that topic. Topic 0 (``mjTOPIC_NONE``) always passes through.
.. mujoco-include:: mjLogConfig
.. _tyStatStructure:
Sim statistics
@@ -1836,6 +1893,26 @@ mjfCollision
This is the function type of the callbacks in the collision table :ref:`mjCOLLISIONFUNC`.
.. _tyLogCallbacks:
Log Callbacks
^^^^^^^^^^^^^
.. _mjfLogHandler:
mjfLogHandler
~~~~~~~~~~~~~
.. code-block:: C
typedef void (*mjfLogHandler)(const mjLogMessage*);
This is the function type of the log handler callback installed via :ref:`mju_setLogHandler`. The handler receives
all errors, warnings and informational messages as structured :ref:`mjLogMessage` data. It must be thread-safe.
It must not call :ref:`mju_error` from within the callback.
.. _tyUICallbacks:
UI Callbacks
+112 -3
View File
@@ -1971,7 +1971,9 @@ Error and memory
.. mujoco-include:: mju_error
Main error function; does not return to caller.
Main error function. The error message is dispatched to the active log handler (see :ref:`mju_setLogHandler`).
Errors are always fatal: if the handler returns, the process is terminated with ``exit(EXIT_FAILURE)``. Handlers
wishing to recover must ``longjmp`` or otherwise transfer control before returning.
.. _mju_warning:
@@ -1980,7 +1982,7 @@ Main error function; does not return to caller.
.. mujoco-include:: mju_warning
Main warning function; returns to caller.
Main warning function; returns to caller. The warning message is dispatched to the active log handler.
.. _mju_clearHandlers:
@@ -1989,7 +1991,114 @@ Main warning function; returns to caller.
.. mujoco-include:: mju_clearHandlers
Clear user error and memory handlers.
Clear all user handlers and restore defaults. Resets the legacy error/warning/memory callbacks to ``NULL``, restores
the default log handler, and resets the log configuration to its defaults (console and file output enabled, all info
topics disabled).
.. _mju_setLogHandler:
`mju_setLogHandler <#mju_setLogHandler>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mju_setLogHandler
Set the active global log handler. Returns the previous handler (which is never ``NULL``), intended for save/restore
or callback chaining. If ``handler`` is ``NULL``, the default handler is restored. The handler receives all errors,
warnings and informational messages as a structured :ref:`mjLogMessage`. See :ref:`siLogHandler` for usage examples.
.. _mju_getLogConfig:
`mju_getLogConfig <#mju_getLogConfig>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mju_getLogConfig
Get the current default handler configuration. See :ref:`mjLogConfig`.
.. _mju_setLogConfig:
`mju_setLogConfig <#mju_setLogConfig>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mju_setLogConfig
Set the default handler configuration. Controls console output, file output, and info topic filtering.
See :ref:`mjLogConfig`.
Example usage (disabling file output):
.. code-block:: C
mjLogConfig config = mju_getLogConfig();
config.logto_file = false;
mju_setLogConfig(config);
.. _mju_info:
`mju_info <#mju_info>`__
~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mju_info
Log an informational message with optional topic filtering. The ``topic`` argument is a :ref:`mjtLogTopic` value.
Topic 0 (``mjTOPIC_NONE``) always passes through. Other topics must be enabled in the default handler configuration
via :ref:`mju_setLogConfig`. Note that topic filtering is implemented in the default handler; custom handlers
receive all info messages regardless.
.. _mju_message:
`mju_message <#mju_message>`__
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. mujoco-include:: mju_message
Dispatch a structured :ref:`mjLogMessage` to the active log handler. This is the primary entry point for emitting
log messages with full control over all fields. The convenience functions :ref:`mju_error`, :ref:`mju_warning`, and
:ref:`mju_info` are thin wrappers that populate an ``mjLogMessage`` and call this function.
The ``subject`` field is a one-line summary (up to 1024 bytes, inline in the struct). The ``body`` field is an
optional ``const char*`` pointer to multi-line detail text, owned by the caller. When ``body`` is ``NULL``, only the
subject line is printed.
The default handler formats the output as follows:
.. code-block:: text
LEVEL FUNC (FILE:LINE) TIME: SUBJECT
BODY
where:
- ``LEVEL`` is ``ERROR``, ``WARNING``, ``INFO``, or ``DEBUG``.
- ``FUNC`` is present when the ``func`` field is set.
- ``(FILE:LINE)`` is present when the ``file`` and ``line`` fields are set.
- ``TIME`` is present when the ``timestamp`` field is set or file logging is active.
- ``SUBJECT`` is the contents of the ``subject`` field.
- ``BODY`` follows on the next line(s), printed raw without indentation or separators, only if non-NULL.
The default handler appends a trailing blank line after ``ERROR``, ``WARNING``, and ``INFO`` messages for visual
separation. ``DEBUG`` messages are printed compactly without a trailing blank line.
Example usage:
.. code-block:: C
mjLogMessage msg = {
.level = mjLOG_INFO,
.timestamp = true,
.body = " height: 0.001 m\n velocity: 0.000 m/s\n bounces: 47",
};
snprintf(msg.subject, sizeof(msg.subject), "The ball has come to rest");
mju_message(&msg);
This produces:
.. code-block:: text
INFO Mon Jun 9 15:04:05 2026: The ball has come to rest
height: 0.001 m
velocity: 0.000 m/s
bounces: 47
.. _mju_malloc:
+96
View File
@@ -463,6 +463,102 @@ time, while :ref:`mjui_update` is called only when changes in the UI take place.
.. _Errorandmemory:
.. _mju_error:
Main error function. The error message is dispatched to the active log handler (see :ref:`mju_setLogHandler`).
Errors are always fatal: if the handler returns, the process is terminated with ``exit(EXIT_FAILURE)``. Handlers
wishing to recover must ``longjmp`` or otherwise transfer control before returning.
.. _mju_warning:
Main warning function; returns to caller. The warning message is dispatched to the active log handler.
.. _mju_clearHandlers:
Clear all user handlers and restore defaults. Resets the legacy error/warning/memory callbacks to ``NULL``, restores
the default log handler, and resets the log configuration to its defaults (console and file output enabled, all info
topics disabled).
.. _mju_setLogHandler:
Set the active global log handler. Returns the previous handler (which is never ``NULL``), intended for save/restore
or callback chaining. If ``handler`` is ``NULL``, the default handler is restored. The handler receives all errors,
warnings and informational messages as a structured :ref:`mjLogMessage`. See :ref:`siLogHandler` for usage examples.
.. _mju_getLogConfig:
Get the current default handler configuration. See :ref:`mjLogConfig`.
.. _mju_setLogConfig:
Set the default handler configuration. Controls console output, file output, and info topic filtering.
See :ref:`mjLogConfig`.
Example usage (disabling file output):
.. code-block:: C
mjLogConfig config = mju_getLogConfig();
config.logto_file = false;
mju_setLogConfig(config);
.. _mju_info:
Log an informational message with optional topic filtering. The ``topic`` argument is a :ref:`mjtLogTopic` value.
Topic 0 (``mjTOPIC_NONE``) always passes through. Other topics must be enabled in the default handler configuration
via :ref:`mju_setLogConfig`. Note that topic filtering is implemented in the default handler; custom handlers
receive all info messages regardless.
.. _mju_message:
Dispatch a structured :ref:`mjLogMessage` to the active log handler. This is the primary entry point for emitting
log messages with full control over all fields. The convenience functions :ref:`mju_error`, :ref:`mju_warning`, and
:ref:`mju_info` are thin wrappers that populate an ``mjLogMessage`` and call this function.
The ``subject`` field is a one-line summary (up to 1024 bytes, inline in the struct). The ``body`` field is an
optional ``const char*`` pointer to multi-line detail text, owned by the caller. When ``body`` is ``NULL``, only the
subject line is printed.
The default handler formats the output as follows:
.. code-block:: text
LEVEL FUNC (FILE:LINE) TIME: SUBJECT
BODY
where:
- ``LEVEL`` is ``ERROR``, ``WARNING``, ``INFO``, or ``DEBUG``.
- ``FUNC`` is present when the ``func`` field is set.
- ``(FILE:LINE)`` is present when the ``file`` and ``line`` fields are set.
- ``TIME`` is present when the ``timestamp`` field is set or file logging is active.
- ``SUBJECT`` is the contents of the ``subject`` field.
- ``BODY`` follows on the next line(s), printed raw without indentation or separators, only if non-NULL.
The default handler appends a trailing blank line after ``ERROR``, ``WARNING``, and ``INFO`` messages for visual
separation. ``DEBUG`` messages are printed compactly without a trailing blank line.
Example usage:
.. code-block:: C
mjLogMessage msg = {
.level = mjLOG_INFO,
.timestamp = true,
.body = " height: 0.001 m\n velocity: 0.000 m/s\n bounces: 47",
};
snprintf(msg.subject, sizeof(msg.subject), "The ball has come to rest");
mju_message(&msg);
This produces:
.. code-block:: text
INFO Mon Jun 9 15:04:05 2026: The ball has come to rest
height: 0.001 m
velocity: 0.000 m/s
bounces: 47
.. _Standardmath:
The "functions" in this section are preprocessor macros replaced with the corresponding C standard library math
+10
View File
@@ -10,6 +10,16 @@ General
- Added :ref:`mju_threadpool`, a new function for creating a thread pool on an ``mjData`` instance. When a thread pool
is initialized, parts of the simulation pipeline, such as collision detection and constraint solving across islands,
are parallelized. The thread pool is automatically destroyed when the ``mjData`` is freed.
- Added a unified :ref:`logging API<siError>`:
- All errors, warnings, and informational messages are now routed through a single :ref:`mjfLogHandler` callback
receiving a structured :ref:`mjLogMessage`.
- Users can install a custom handler via :ref:`mju_setLogHandler`,
configure the default handler's behavior (console/file output, topic filtering) via :ref:`mju_setLogConfig`.
- Messages can be emitted via :ref:`mju_info` and :ref:`mju_message`.
- New types: :ref:`mjtLogLevel`, :ref:`mjtLogTopic`, :ref:`mjLogMessage`, :ref:`mjLogConfig`.
- The legacy callbacks :ref:`mju_user_error` and :ref:`mju_user_warning` are deprecated but remain functional.
- Improved primal solver convergence under float32. Improvements initially proposed by :github:user:`n3b` in
:issue:`2313` and :github:user:`denzeler-nvidia` in :doc:`MJWarp <mjwarp/index>` pull request
`1374 <https://github.com/google-deepmind/mujoco_warp/pull/1374>`__.
+37
View File
@@ -2619,6 +2619,38 @@ typedef enum mjtSleepState_ { // sleep state of an object
mjS_ASLEEP = 0, // object is asleep
mjS_AWAKE = 1 // object is awake
} mjtSleepState;
typedef enum mjtLogLevel_ { // log message severity
mjLOG_DEBUG = 0, // internal engine debug trace (opt-in via topic filtering)
mjLOG_INFO, // informational (opt-in via topic filtering)
mjLOG_WARNING, // warning
mjLOG_ERROR, // error
} mjtLogLevel;
typedef enum mjtLogTopic_ { // log topic identifiers
mjTOPIC_NONE = 0, // no topic (always passes filtering)
// INFO topics:
mjTOPIC_TIME_STP = 1, // timing diagnostics (step)
mjTOPIC_TIME_CMP = 2, // timing diagnostics (compile)
// DEBUG topics:
mjTOPIC_SLEEP = 3, // sleep/wake events
mjNTOPIC = 3 // number of filterable topics
} mjtLogTopic;
typedef struct mjLogMessage_ { // structured log message
int level; // mjtLogLevel
int topic; // mjtLogTopic (0 for error/warning/user)
char subject[1024]; // message subject (one-liner, printf-formatted)
const char* body; // message body (multi-line detail, or NULL)
const char* func; // __func__ or NULL
const char* file; // __FILE__ or NULL
int line; // __LINE__ or 0
mjtBool timestamp; // prepend timestamp to output
} mjLogMessage;
typedef struct mjLogConfig_ { // log handler default configuration
mjtBool logto_console; // print to console (default: true)
mjtBool logto_file; // print to log file (default: true)
char logfile[1024]; // log file path (default: "MUJOCO_LOG.TXT")
int topics; // enabled info topic bitmask (default: 0)
} mjLogConfig;
typedef enum mjtButton_ { // mouse button
mjBUTTON_NONE = 0, // no button
mjBUTTON_LEFT, // left button
@@ -3495,6 +3527,11 @@ void mjui_render(mjUI* ui, const mjuiState* state, const mjrContext* con);
void mju_error(const char* msg, ...) mjPRINTFLIKE(1, 2);
void mju_warning(const char* msg, ...) mjPRINTFLIKE(1, 2);
void mju_clearHandlers(void);
mjfLogHandler mju_setLogHandler(mjfLogHandler handler);
mjLogConfig mju_getLogConfig(void);
void mju_setLogConfig(mjLogConfig config);
void mju_info(int topic, const char* msg, ...) mjPRINTFLIKE(2, 3);
void mju_message(const mjLogMessage* msg);
void* mju_malloc(size_t size);
void mju_free(void* ptr);
void mj_warning(mjData* d, int warning, int info);
+161 -29
View File
@@ -886,20 +886,79 @@ and :ref:`mj_stackAllocByte` is provided for allocation of arbitrary number of b
.. _siError:
Errors and warnings
~~~~~~~~~~~~~~~~~~~
Errors, warnings, logging
~~~~~~~~~~~~~~~~~~~~~~~~~
When a terminal error occurs, MuJoCo calls the function :ref:`mju_error` internally. Here is what mju_error does:
MuJoCo has a unified logging system for errors, warnings and informational messages. All log output is routed
through a single callback of type :ref:`mjfLogHandler`, which receives a structured :ref:`mjLogMessage`
containing the severity level, message text, and optional source location. Errors are fatal and terminate the
program by default. Warnings indicate problematic but non-fatal conditions. Informational messages provide
optional diagnostic output.
#. Append the error message at the end of the file MUJOCO_LOG.TXT in the program directory (create the file if it does
not exist). Also write the date and time along with the error message.
#. If the user error callback :ref:`mju_user_error` is installed, call that function with the error message as
argument. Otherwise, print the error message and "Press Enter to exit..." to standard output. Then wait for any
keyboard input, and then terminate the simulator with failure.
.. _siLogHandler:
If a user error callback is installed, it must **not** return, otherwise the behavior of the simulator is undefined.
The idea here is that if mju_error is called, the simulation cannot continue and the user is expected to make some
change such that the error condition is avoided. The error messages are self-explanatory.
Installing a handler
^^^^^^^^^^^^^^^^^^^^
Users who want to intercept and process MuJoCo's log output should install a log handler using
:ref:`mju_setLogHandler`. The handler receives all errors, warnings and info messages as a single structured
callback:
.. code-block:: C
void my_handler(const mjLogMessage* msg) {
// do something with msg, for example:
printf("%s\n", msg->subject);
}
// install handler, save previous
mjfLogHandler prev = mju_setLogHandler(my_handler);
// ... do work ...
// restore previous handler
mju_setLogHandler(prev);
:ref:`mju_setLogHandler` returns the previously installed handler (which is never ``NULL``; passing ``NULL`` restores
the default handler). The previous handler can be used in two ways:
* **Save/restore**: A library or subsystem can temporarily install its own handler and later restore the previous one.
* **Chaining**: A custom handler can act as a pure observer by calling the previous handler at the end of its callback
to preserve existing behavior. Conversely, handlers intended to intercept and recover from errors (e.g., via
``longjmp``) should not chain to the previous handler.
When the handler is called with ``level == mjLOG_ERROR``, the error is always fatal: the :ref:`default handler
<siDefaultHandler>` terminates the process with ``exit(EXIT_FAILURE)`` (unless a legacy error handler is installed).
Handlers that wish to recover from errors (e.g., to throw a C++ exception or convert to a Python exception) must not
return — they should ``longjmp`` to a previously established recovery point or otherwise transfer control before
returning. This is how the compiler and Python bindings handle errors. MuJoCo is written with the assumption that
error handlers will not return; if they do, the behavior of the software is undefined.
.. warning::
Log handlers must not call :ref:`mju_error` from within the callback; this will cause infinite recursion.
.. _siDefaultHandler:
Default handler
^^^^^^^^^^^^^^^
If no custom handler is installed (or if ``NULL`` is passed to :ref:`mju_setLogHandler`), MuJoCo uses a default
handler that provides the following behavior:
#. If a legacy handler (:ref:`mju_user_error` or :ref:`mju_user_warning`) is installed, it is called with the
formatted message text. This provides backward compatibility with existing code.
#. Otherwise, the message is written to the log file (default: ``MUJOCO_LOG.TXT``) and printed to the
console (``stderr`` for errors and warnings, ``stdout`` for info).
#. For errors, the program is terminated with ``exit(EXIT_FAILURE)`` (unless a legacy error handler is installed).
The default handler's behavior can be configured using :ref:`mju_setLogConfig` and :ref:`mju_getLogConfig`,
which control whether output goes to the console, the log file path (or disabling file logging by setting it to
an empty string), and which info topics are enabled.
.. _siErrorRecovery:
Error recovery
^^^^^^^^^^^^^^
One situation where it is desirable to continue even after an error is an interactive simulator that fails to load a
model file. This could be because the user provided the wrong file name, or because model compilation failed. This is
@@ -909,26 +968,99 @@ operation fails, and there is no need to exit the program. In the case of mj_loa
containing the parser or compiler error that caused the failure, while mj_loadModel generates corresponding warnings
(see below).
Internally mj_loadXML actually uses the mju_error mechanism, by temporarily installing a "user" handler that triggers
a C++ exception, which is then intercepted. This is possible because the parser, compiler and runtime are compiled and
linked together, and use the same copy of the C/C++ memory manager and standard library. If the user implements an
error callback that triggers a C++ exception, this will be in their workspace which is not necessarily the same as the
MuJoCo library workspace, and so it is not clear what will happen; the outcome probably depends on the compiler and
platform. It is better to avoid this approach and simply exit when mju_error is called (which is the default behavior
in the absence of a user handler).
Internally mj_loadXML actually uses the mju_error mechanism, by temporarily installing a thread-local handler
(using the internal ``_mjPRIVATE_setTlsLogHandler``) that triggers a C++ exception, which is then intercepted.
This thread-local override takes priority over the global handler and affects only the calling thread.
MuJoCo can also generate warnings. They indicate conditions that are likely to cause numerical inaccuracies, but can
also indicate problems with loading a model and other problematic situations where the simulator is nevertheless able
to continue normal operation. The warning mechanism has two levels. The high-level is implemented with the function
:ref:`mj_warning`. It registers a warning in mjData as explained in more detail in the :ref:`diagnostics
<siDiagnostics>` section below, and also calls the low-level function :ref:`mju_warning`. Alternatively, the low-level
function may be called directly (from within mj_loadModel for example) without registering a warning in mjData. This
is done in places where mjData is not available.
.. _siInfoMessages:
mju_warning does the following: if the user callback :ref:`mju_user_warning` is installed, it calls that callback.
Otherwise it appends the warning message to MUJOCO_LOG.TXT and also does a printf, similar to mju_error but without
exiting. When MuJoCo wrappers are developed for environments such as MATLAB, it makes sense to install a user callback
which prints warnings in the command window (with mexPrintf).
Informational messages
^^^^^^^^^^^^^^^^^^^^^^
MuJoCo provides two :ref:`levels<mjtLogLevel>` of opt-in diagnostic logging: informational messages (``mjLOG_INFO``) and
debug traces (``mjLOG_DEBUG``). Both use topic identifiers from the :ref:`mjtLogTopic` enum, but have a subtle
architectural distinction in how filtering is applied:
* **INFO messages**: Emitted unconditionally by the engine. Filtering happens on the **consumer side** inside the
default handler. Custom handlers installed via :ref:`mju_setLogHandler` receive all INFO messages and
can implement their own filtering logic.
* **DEBUG messages**: Designed for tight, high-frequency simulation loops where constructing strings would be a
performance bottleneck. Therefore, filtering happens on the **producer side** via :ref:`mju_isTopicEnabled`. If a
topic is disabled, the message is never constructed or dispatched. Consequently, custom handlers will only receive
DEBUG messages if the topic is explicitly enabled in the active :ref:`mjLogConfig`.
In the default handler, INFO messages are followed by a blank line for readability, whereas high-frequency DEBUG traces
are printed compactly without trailing blank lines.
To enable topics in the default handler configuration:
.. code-block:: C
// enable sleep/wake messages
mjLogConfig config = mju_getLogConfig();
config.topics |= (1 << (mjTOPIC_SLEEP - 1));
mju_setLogConfig(config);
Topic 0 (``mjTOPIC_NONE``) always passes through, regardless of the topic configuration.
Note that topics are 1-indexed, so the bitmask for topic ``t`` is ``(1 << (t - 1))``. This is also how the
``topics`` field of :ref:`mjLogConfig` is encoded.
Topics can also be enabled via the environment variable ``MUJOCO_LOG_TOPICS``, which is read once at startup.
The value is a comma-separated list of topic names (case-insensitive), derived from the :ref:`mjtLogTopic` enum
by removing the ``mjTOPIC_`` prefix and lowercasing (e.g., ``mjTOPIC_SLEEP`` becomes ``sleep``).
For example:
.. code-block:: shell
export MUJOCO_LOG_TOPICS=sleep,time_stp
This is equivalent to programmatically enabling the corresponding topic bits via :ref:`mju_setLogConfig`, and is
useful for enabling diagnostics without modifying code.
.. _siLogFrameworks:
Frameworks and wrappers
^^^^^^^^^^^^^^^^^^^^^^^
Framework authors (e.g., those building Python bindings, MATLAB wrappers, or game engine integrations) should
install a custom log handler to route MuJoCo's output to their environment's logging system:
.. code-block:: C
// example: route to a framework's logging API
void framework_handler(const mjLogMessage* msg) {
if (msg->level == mjLOG_ERROR) {
framework_log_error(msg->subject);
framework_abort(); // must not return
} else if (msg->level == mjLOG_WARNING) {
framework_log_warning(msg->subject);
} else {
framework_log_info(msg->subject);
}
}
mju_setLogHandler(framework_handler);
The :ref:`mjLogMessage` struct also provides source location information (``func``, ``file``, ``line``) when
available, which can be useful for debugging.
.. _siLogLegacy:
Legacy handlers
^^^^^^^^^^^^^^^
The global function pointers :ref:`mju_user_error` and :ref:`mju_user_warning` are still supported for backward
compatibility, but are deprecated in favor of :ref:`mju_setLogHandler`. When both a custom log handler and legacy
handlers are installed, the custom log handler takes precedence. The legacy handlers are only consulted by the
*default* handler when no custom handler has been installed.
.. _siLogMemory:
Memory handlers
^^^^^^^^^^^^^^^
When MuJoCo allocates and frees memory on the heap, it always uses the functions :ref:`mju_malloc` and
:ref:`mju_free`. These functions call the user callbacks :ref:`mju_user_malloc` and :ref:`mju_user_free` when
+43
View File
@@ -576,4 +576,47 @@ typedef enum mjtSleepState_ { // sleep state of an object
mjS_AWAKE = 1 // object is awake
} mjtSleepState;
//---------------------------------- logging -------------------------------------------------------
typedef enum mjtLogLevel_ { // log message severity
mjLOG_DEBUG = 0, // internal engine debug trace (opt-in via topic filtering)
mjLOG_INFO, // informational (opt-in via topic filtering)
mjLOG_WARNING, // warning
mjLOG_ERROR, // error
} mjtLogLevel;
typedef enum mjtLogTopic_ { // log topic identifiers
mjTOPIC_NONE = 0, // no topic (always passes filtering)
// INFO topics:
mjTOPIC_TIME_STP = 1, // timing diagnostics (step)
mjTOPIC_TIME_CMP = 2, // timing diagnostics (compile)
// DEBUG topics:
mjTOPIC_SLEEP = 3, // sleep/wake events
mjNTOPIC = 3 // number of filterable topics
} mjtLogTopic;
typedef struct mjLogMessage_ { // structured log message
int level; // mjtLogLevel
int topic; // mjtLogTopic (0 for error/warning/user)
char subject[1024]; // message subject (one-liner, printf-formatted)
const char* body; // message body (multi-line detail, or NULL)
const char* func; // __func__ or NULL
const char* file; // __FILE__ or NULL
int line; // __LINE__ or 0
mjtBool timestamp; // prepend timestamp to output
} mjLogMessage;
typedef struct mjLogConfig_ { // log handler default configuration
mjtBool logto_console; // print to console (default: true)
mjtBool logto_file; // print to log file (default: true)
char logfile[1024]; // log file path (default: "MUJOCO_LOG.TXT")
int topics; // enabled info topic bitmask (default: 0)
} mjLogConfig;
// function type for log handler callback; must be thread-safe, must not call mju_error
typedef void (*mjfLogHandler)(const mjLogMessage*);
#endif // MUJOCO_INCLUDE_MJTYPE_H_
+22 -3
View File
@@ -41,12 +41,14 @@
extern "C" {
#endif
// user error and memory handlers
MJAPI extern void (*mju_user_error)(const char*);
MJAPI extern void (*mju_user_warning)(const char*);
// user memory handlers
MJAPI extern void* (*mju_user_malloc)(size_t);
MJAPI extern void (*mju_user_free)(void*);
// legacy error/warning handlers (deprecated: prefer mju_setLogHandler)
MJAPI extern void (*mju_user_error)(const char*);
MJAPI extern void (*mju_user_warning)(const char*);
// callbacks extending computation pipeline
MJAPI extern mjfGeneric mjcb_passive;
@@ -71,6 +73,7 @@ MJAPI extern const char* mjLABELSTRING[mjNLABEL];
MJAPI extern const char* mjFRAMESTRING[mjNFRAME];
MJAPI extern const char* mjVISSTRING[mjNVISFLAG][3];
MJAPI extern const char* mjRNDSTRING[mjNRNDFLAG][3];
MJAPI extern const char* mjTOPICSTRING[mjNTOPIC];
//---------------------------------- Virtual file system -------------------------------------------
@@ -978,6 +981,22 @@ MJAPI void mju_warning(const char* msg, ...) mjPRINTFLIKE(1, 2);
// Clear user error and memory handlers.
MJAPI void mju_clearHandlers(void);
// Set the active log handler; return the previous handler.
// If handler is NULL, restore the default handler.
MJAPI mjfLogHandler mju_setLogHandler(mjfLogHandler handler);
// Get default handler configuration.
MJAPI mjLogConfig mju_getLogConfig(void);
// Set default handler configuration.
MJAPI void mju_setLogConfig(mjLogConfig config);
// Log an info message with optional topic filtering.
MJAPI void mju_info(int topic, const char* msg, ...) mjPRINTFLIKE(2, 3);
// Dispatch a structured log message to the active handler.
MJAPI void mju_message(const mjLogMessage* msg);
// Allocate memory; byte-align on 64; pad size to multiple of 64.
MJAPI void* mju_malloc(size_t size);
+49
View File
@@ -17,6 +17,7 @@
import contextlib
import copy
from etils import epath
import os
import pickle
import sys
@@ -511,6 +512,54 @@ class MuJoCoBindingsTest(parameterized.TestCase):
mujoco.mj_checkPos(self.model, self.data)
self.assertEqual(warnings[mujoco.mjtWarning.mjWARN_BADQPOS].number, 1)
def test_mju_user_warning_callback_receives_warnings(self):
"""Regression test: C warnings must reach Python mju_user_warning callbacks.
The unified logging API routes all messages through a TLS log handler.
This test verifies that non-error messages are forwarded to the global
handler chain, where legacy mju_user_warning callbacks are invoked.
"""
warning_messages = []
def warning_cb(msg):
warning_messages.append(msg)
old_cb = mujoco.get_mju_user_warning()
try:
mujoco.set_mju_user_warning(warning_cb)
# Trigger a C-level warning by setting qpos to NaN and calling mj_step.
model = mujoco.MjModel.from_xml_string(TEST_XML)
data = mujoco.MjData(model)
data.qpos[0] = float('NaN')
mujoco.mj_checkPos(model, data)
self.assertNotEmpty(warning_messages)
# The warning message should mention the bad QPOS value.
self.assertTrue(
any('QPOS' in msg for msg in warning_messages), warning_messages
)
finally:
mujoco.set_mju_user_warning(old_cb)
def test_mjtopic_time_cmp_logs_compile_time(self):
"""Verifies MjLogConfig.get/set and info topic logging."""
old_cfg = mujoco.MjLogConfig.get()
log_path = os.path.join(
absltest.get_default_test_tmpdir(), 'test_compile.log'
)
os.makedirs(os.path.dirname(log_path), exist_ok=True)
try:
cfg = mujoco.MjLogConfig.get()
cfg.logto_file = True
cfg.logfile = log_path
cfg.topics |= (1 << (mujoco.mjtLogTopic.mjTOPIC_TIME_CMP - 1))
cfg.set()
mujoco.MjModel.from_xml_string(TEST_XML)
with open(log_path, 'r') as f:
output = f.read()
self.assertIn('compile time', output)
finally:
old_cfg.set()
def test_mjcontact_can_copy(self):
mujoco.mj_forward(self.model, self.data)
@@ -192,6 +192,7 @@ _ALLOWED_FIXED_ARRAYS = {
_FUNCTION_POINTER_TYPES = {
'mjfItemEnable',
'mjfLogHandler',
}
_STRUCT_NAME_OVERRIDES = {}
@@ -37,10 +37,8 @@ def main(argv: Sequence[str]) -> None:
struct_decls = []
for func in FUNCTIONS.values():
# Skip mju_error_{i,s} and mju_warning_{i,s} as these are not
# supported in the Python bindings, and Introspect currently
# doesn't support variadic functions.
if func.name.startswith('mju_error') or func.name == 'mju_warning':
# Skip variadic functions as Introspect currently doesn't support them.
if func.name in ('mju_error', 'mju_warning', 'mju_info'):
continue
# Modify some parameter types.
+43 -8
View File
@@ -15,12 +15,15 @@
#ifndef MUJOCO_PYTHON_ERRORS_H_
#define MUJOCO_PYTHON_ERRORS_H_
#include <array>
#include <csetjmp>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <mujoco/mjexport.h>
#include <mujoco/mjtype.h>
#include "private.h"
#include "util/crossplatform.h"
#include "util/func_wrap.h"
@@ -104,9 +107,28 @@ class ErrorBase : public pybind11::builtin_exception {
static thread_local std::jmp_buf mju_error_jmp_buf;
static thread_local std::array<char, 1024> mju_error_msg{0};
static inline void MjErrorHandler(const char* msg) {
std::strncpy(mju_error_msg.data(), msg, mju_error_msg.size() - 1);
mju_error_msg.data()[mju_error_msg.size() - 1] = '\0';
// The handler to forward non-error messages to. Set by WrapFunc before each
// call into MuJoCo C code, pointing to either the previously installed TLS
// handler or the active global handler.
static thread_local mjfLogHandler mju_forward_handler = nullptr;
static inline void MjErrorHandler(const mjLogMessage* msg) {
if (msg->level != mjLOG_ERROR) {
// Forward warnings, info, and debug messages to the previous handler so
// that legacy mju_user_warning callbacks and console output continue to
// work.
if (mju_forward_handler != nullptr) {
mju_forward_handler(msg);
}
return;
}
if (msg->func != nullptr) {
std::snprintf(mju_error_msg.data(), mju_error_msg.size(), "%s: %s",
msg->func, msg->subject);
} else {
std::strncpy(mju_error_msg.data(), msg->subject, mju_error_msg.size() - 1);
mju_error_msg.data()[mju_error_msg.size() - 1] = '\0';
}
std::longjmp(mju_error_jmp_buf, 1);
}
@@ -121,7 +143,17 @@ struct MjErrorIntercepter {
#else
return [callable](Args... args) MUJOCO_ALWAYS_INLINE_LAMBDA_MUTABLE {
#endif
_mjPRIVATE__set_tls_error_fn(&MjErrorHandler);
mjfLogHandler prev_handler = _mjPRIVATE_setTlsLogHandler(&MjErrorHandler);
// Determine the handler to forward non-error messages to.
// If there was a previously installed TLS handler, forward to it.
// Otherwise, probe the global handler via mju_setLogHandler.
mjfLogHandler old_forward = mju_forward_handler;
if (prev_handler != nullptr) {
mju_forward_handler = prev_handler;
} else {
mju_forward_handler = _mjPRIVATE_getGlobalLogHandler();
}
// DON'T MIX RAII WITH SETJMP!
// From https://en.cppreference.com/w/cpp/utility/program/longjmp:
@@ -131,16 +163,19 @@ struct MjErrorIntercepter {
if (setjmp(mju_error_jmp_buf) == 0) {
if constexpr (std::is_void_v<decltype(callable(args...))>) {
callable(args...);
_mjPRIVATE__set_tls_error_fn(nullptr);
mju_forward_handler = old_forward;
_mjPRIVATE_setTlsLogHandler(prev_handler);
} else {
auto ret = callable(args...);
static_assert(std::is_trivially_destructible_v<decltype(ret)>);
_mjPRIVATE__set_tls_error_fn(nullptr);
mju_forward_handler = old_forward;
_mjPRIVATE_setTlsLogHandler(prev_handler);
return ret;
}
} else {
// This branch is entered via a longjmp back from our mju_error handler.
_mjPRIVATE__set_tls_error_fn(nullptr);
mju_forward_handler = old_forward;
_mjPRIVATE_setTlsLogHandler(prev_handler);
{
// Check if a Python callback has thrown an exception.
// We cannot use a py::gil_scoped_acquire here: on Windows its
+23
View File
@@ -609,6 +609,29 @@ ENUMS: Mapping[str, EnumDecl] = dict([
('mjS_AWAKE', 1),
]),
)),
('mjtLogLevel',
EnumDecl(
name='mjtLogLevel',
declname='enum mjtLogLevel_',
values=dict([
('mjLOG_DEBUG', 0),
('mjLOG_INFO', 1),
('mjLOG_WARNING', 2),
('mjLOG_ERROR', 3),
]),
)),
('mjtLogTopic',
EnumDecl(
name='mjtLogTopic',
declname='enum mjtLogTopic_',
values=dict([
('mjTOPIC_NONE', 0),
('mjTOPIC_TIME_STP', 1),
('mjTOPIC_TIME_CMP', 2),
('mjTOPIC_SLEEP', 3),
('mjNTOPIC', 3),
]),
)),
('mjtGeomInertia',
EnumDecl(
name='mjtGeomInertia',
+63
View File
@@ -6309,6 +6309,69 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
parameters=(),
doc='Clear user error and memory handlers.',
)),
('mju_setLogHandler',
FunctionDecl(
name='mju_setLogHandler',
return_type=ValueType(name='mjfLogHandler'),
parameters=(
FunctionParameterDecl(
name='handler',
type=ValueType(name='mjfLogHandler'),
),
),
doc='Set the active log handler; return the previous handler. If handler is NULL, restore the default handler.', # pylint: disable=line-too-long
)),
('mju_getLogConfig',
FunctionDecl(
name='mju_getLogConfig',
return_type=ValueType(name='mjLogConfig'),
parameters=(),
doc='Get default handler configuration.',
)),
('mju_setLogConfig',
FunctionDecl(
name='mju_setLogConfig',
return_type=ValueType(name='void'),
parameters=(
FunctionParameterDecl(
name='config',
type=ValueType(name='mjLogConfig'),
),
),
doc='Set default handler configuration.',
)),
('mju_info',
FunctionDecl(
name='mju_info',
return_type=ValueType(name='void'),
parameters=(
FunctionParameterDecl(
name='topic',
type=ValueType(name='int'),
),
FunctionParameterDecl(
name='msg',
type=PointerType(
inner_type=ValueType(name='char', is_const=True),
),
),
),
doc='Log an info message with optional topic filtering.',
)),
('mju_message',
FunctionDecl(
name='mju_message',
return_type=ValueType(name='void'),
parameters=(
FunctionParameterDecl(
name='msg',
type=PointerType(
inner_type=ValueType(name='mjLogMessage', is_const=True),
),
),
),
doc='Dispatch a structured log message to the active handler.',
)),
('mju_malloc',
FunctionDecl(
name='mju_malloc',
+86
View File
@@ -28,6 +28,92 @@ from .ast_nodes import StructFieldDecl
from .ast_nodes import ValueType
STRUCTS: Mapping[str, StructDecl] = dict([
('mjLogMessage',
StructDecl(
name='mjLogMessage',
declname='struct mjLogMessage_',
fields=(
StructFieldDecl(
name='level',
type=ValueType(name='int'),
doc='mjtLogLevel',
),
StructFieldDecl(
name='topic',
type=ValueType(name='int'),
doc='mjtLogTopic (0 for error/warning/user)',
),
StructFieldDecl(
name='subject',
type=ArrayType(
inner_type=ValueType(name='char'),
extents=(1024,),
),
doc='message subject (one-liner, printf-formatted)',
),
StructFieldDecl(
name='body',
type=PointerType(
inner_type=ValueType(name='char', is_const=True),
),
doc='message body (multi-line detail, or NULL)',
),
StructFieldDecl(
name='func',
type=PointerType(
inner_type=ValueType(name='char', is_const=True),
),
doc='__func__ or NULL',
),
StructFieldDecl(
name='file',
type=PointerType(
inner_type=ValueType(name='char', is_const=True),
),
doc='__FILE__ or NULL',
),
StructFieldDecl(
name='line',
type=ValueType(name='int'),
doc='__LINE__ or 0',
),
StructFieldDecl(
name='timestamp',
type=ValueType(name='mjtBool'),
doc='prepend timestamp to output',
),
),
)),
('mjLogConfig',
StructDecl(
name='mjLogConfig',
declname='struct mjLogConfig_',
fields=(
StructFieldDecl(
name='logto_console',
type=ValueType(name='mjtBool'),
doc='print to console (default: true)',
),
StructFieldDecl(
name='logto_file',
type=ValueType(name='mjtBool'),
doc='print to log file (default: true)',
),
StructFieldDecl(
name='logfile',
type=ArrayType(
inner_type=ValueType(name='char'),
extents=(1024,),
),
doc='log file path (default: "MUJOCO_LOG.TXT")',
),
StructFieldDecl(
name='topics',
type=ValueType(name='int'),
doc='enabled info topic bitmask (default: 0)',
),
),
)),
('mjLROpt',
StructDecl(
name='mjLROpt',
+3 -1
View File
@@ -17,11 +17,13 @@
#include <mujoco/mjdata.h>
#include <mujoco/mjexport.h>
#include <mujoco/mjtype.h>
// DO NOT USE THESE FUNCTIONS ELSEWHERE.
// They should be regarded as part of MuJoCo's internal implementation detail.
extern "C" {
MJAPI void _mjPRIVATE__set_tls_error_fn(void (*h)(const char*));
MJAPI mjfLogHandler _mjPRIVATE_setTlsLogHandler(mjfLogHandler handler);
MJAPI mjfLogHandler _mjPRIVATE_getGlobalLogHandler(void);
MJAPI void* mj_arenaAllocByte(mjData* d, int bytes, int alignment);
}
+3
View File
@@ -19,6 +19,7 @@
#include <mujoco/mjmodel.h>
#include <mujoco/mjrender.h>
#include <mujoco/mjspec.h>
#include <mujoco/mjtype.h>
#include <mujoco/mjvisualize.h>
// Type aliases for MuJoCo C structs to allow us refer to consistently refer
@@ -72,6 +73,8 @@ using MjVisualMap = decltype(::mjVisual::map);
using MjVisualScale = decltype(::mjVisual::scale);
using MjVisualRgba = decltype(::mjVisual::rgba);
using MjWarningStat = ::mjWarningStat;
using MjLogConfig = ::mjLogConfig;
using MjLogMessage = ::mjLogMessage;
// From mjrender.h
using MjrRect = ::mjrRect;
+99 -5
View File
@@ -539,6 +539,96 @@ This is useful for example when the MJB is not available as a file on disk.)"));
X(int, number);
#undef X
// ==================== MJLOGCONFIG ==========================================
py::class_<MjLogConfigWrapper> mjLogConfig(m, "MjLogConfig");
mjLogConfig.def(py::init<>());
mjLogConfig.def("__copy__", [](const MjLogConfigWrapper& other) {
return MjLogConfigWrapper(other);
});
mjLogConfig.def("__deepcopy__",
[](const MjLogConfigWrapper& other, py::dict) {
return MjLogConfigWrapper(other);
});
DefineStructFunctions(mjLogConfig);
mjLogConfig.def_property(
"logto_console",
[](const MjLogConfigWrapper& d) { return d.get()->logto_console; },
[](MjLogConfigWrapper& d, bool rhs) { d.get()->logto_console = rhs; });
mjLogConfig.def_property(
"logto_file",
[](const MjLogConfigWrapper& d) { return d.get()->logto_file; },
[](MjLogConfigWrapper& d, bool rhs) { d.get()->logto_file = rhs; });
mjLogConfig.def_property(
"logfile",
[](const MjLogConfigWrapper& d) { return std::string(d.get()->logfile); },
[](MjLogConfigWrapper& d, const std::string& rhs) {
std::strncpy(d.get()->logfile, rhs.c_str(), 1023);
d.get()->logfile[1023] = '\0';
});
mjLogConfig.def_property(
"topics", [](const MjLogConfigWrapper& d) { return d.get()->topics; },
[](MjLogConfigWrapper& d, int rhs) { d.get()->topics = rhs; });
mjLogConfig.def_static("get", []() {
MjLogConfigWrapper wrapper;
*wrapper.get() = mju_getLogConfig();
return wrapper;
});
mjLogConfig.def("set", [](const MjLogConfigWrapper& self) {
mju_setLogConfig(*self.get());
});
// ==================== MJLOGMESSAGE =========================================
py::class_<MjLogMessageWrapper> mjLogMessage(m, "MjLogMessage");
mjLogMessage.def(py::init<>());
mjLogMessage.def("__copy__", [](const MjLogMessageWrapper& other) {
return MjLogMessageWrapper(other);
});
mjLogMessage.def("__deepcopy__",
[](const MjLogMessageWrapper& other, py::dict) {
return MjLogMessageWrapper(other);
});
DefineStructFunctions(mjLogMessage);
mjLogMessage.def_property(
"level", [](const MjLogMessageWrapper& d) { return d.get()->level; },
[](MjLogMessageWrapper& d, int rhs) { d.get()->level = rhs; });
mjLogMessage.def_property(
"topic", [](const MjLogMessageWrapper& d) { return d.get()->topic; },
[](MjLogMessageWrapper& d, int rhs) { d.get()->topic = rhs; });
mjLogMessage.def_property(
"subject",
[](const MjLogMessageWrapper& d) {
return std::string(d.get()->subject);
},
[](MjLogMessageWrapper& d, const std::string& rhs) {
std::strncpy(d.get()->subject, rhs.c_str(), 1023);
d.get()->subject[1023] = '\0';
});
mjLogMessage.def_property_readonly(
"body",
[](const MjLogMessageWrapper& d) -> py::object {
if (d.get()->body) return py::str(d.get()->body);
return py::none();
});
mjLogMessage.def_property_readonly(
"func",
[](const MjLogMessageWrapper& d) -> py::object {
if (d.get()->func) return py::str(d.get()->func);
return py::none();
});
mjLogMessage.def_property_readonly(
"file",
[](const MjLogMessageWrapper& d) -> py::object {
if (d.get()->file) return py::str(d.get()->file);
return py::none();
});
mjLogMessage.def_property(
"line", [](const MjLogMessageWrapper& d) { return d.get()->line; },
[](MjLogMessageWrapper& d, int rhs) { d.get()->line = rhs; });
mjLogMessage.def_property(
"timestamp",
[](const MjLogMessageWrapper& d) { return d.get()->timestamp; },
[](MjLogMessageWrapper& d, bool rhs) { d.get()->timestamp = rhs; });
// ==================== MJTIMERSTAT ==========================================
py::class_<MjTimerStatWrapper> mjTimerStat(m, "MjTimerStat");
mjTimerStat.def(py::init<>());
@@ -960,18 +1050,22 @@ This is useful for example when the MJB is not available as a file on disk.)"));
#undef X
// ==================== MJRVERTEXATTRIBUTE ===================================
py::class_<raw::MjrVertexAttribute> mjrVertexAttribute(m, "MjrVertexAttribute");
py::class_<raw::MjrVertexAttribute> mjrVertexAttribute(m,
"MjrVertexAttribute");
mjrVertexAttribute.def(py::init([](int usage, int type) {
return raw::MjrVertexAttribute{nullptr, usage, type};
}),
py::arg("usage") = 0, py::arg("type") = 0);
mjrVertexAttribute.def("__copy__",
[](const raw::MjrVertexAttribute& other) { return raw::MjrVertexAttribute(other); });
mjrVertexAttribute.def("__deepcopy__", [](const raw::MjrVertexAttribute& other, py::dict) {
mjrVertexAttribute.def("__copy__", [](const raw::MjrVertexAttribute& other) {
return raw::MjrVertexAttribute(other);
});
mjrVertexAttribute.def("__deepcopy__",
[](const raw::MjrVertexAttribute& other, py::dict) {
return raw::MjrVertexAttribute(other);
});
DefineStructFunctions(mjrVertexAttribute);
#define X(var) mjrVertexAttribute.def_readwrite(#var, &raw::MjrVertexAttribute::var)
#define X(var) \
mjrVertexAttribute.def_readwrite(#var, &raw::MjrVertexAttribute::var)
X(usage);
X(type);
#undef X
+34
View File
@@ -377,6 +377,38 @@ struct is_mj_struct_list<raw::MjWarningStat> {
static constexpr bool value = true;
};
// ==================== MJLOGCONFIG ============================================
template <>
class MjWrapper<raw::MjLogConfig> : public WrapperBase<raw::MjLogConfig> {
public:
MjWrapper();
MjWrapper(const MjWrapper&);
MjWrapper(MjWrapper&&) = default;
MjWrapper(raw::MjLogConfig* ptr, pybind11::handle owner);
~MjWrapper() = default;
};
using MjLogConfigWrapper = MjWrapper<raw::MjLogConfig>;
template <>
struct enable_if_mj_struct<raw::MjLogConfig> { using type = void; };
// ==================== MJLOGMESSAGE ===========================================
template <>
class MjWrapper<raw::MjLogMessage> : public WrapperBase<raw::MjLogMessage> {
public:
MjWrapper();
MjWrapper(const MjWrapper&);
MjWrapper(MjWrapper&&) = default;
MjWrapper(raw::MjLogMessage* ptr, pybind11::handle owner);
~MjWrapper() = default;
};
using MjLogMessageWrapper = MjWrapper<raw::MjLogMessage>;
template <>
struct enable_if_mj_struct<raw::MjLogMessage> { using type = void; };
// ==================== MJTIMERSTAT ============================================
template <>
class MjWrapper<raw::MjTimerStat> : public WrapperBase<raw::MjTimerStat> {
@@ -977,6 +1009,8 @@ using _impl::MjVisualRgbaWrapper;
using _impl::MjVisualWrapper;
using _impl::MjStatisticWrapper;
using _impl::MjWarningStatWrapper;
using _impl::MjLogConfigWrapper;
using _impl::MjLogMessageWrapper;
using _impl::MjTimerStatWrapper;
using _impl::MjSolverStatWrapper;
using _impl::MjModelWrapper;
+22
View File
@@ -1019,6 +1019,28 @@ MjWarningStatList::MjStructList(MjWarningStatList& other, py::slice slice)
: StructListBase(other, slice), X(int, lastinfo), X(int, number) {}
#undef X
// ==================== MJLOGCONFIG ============================================
MjLogConfigWrapper::MjWrapper() : WrapperBase(new raw::MjLogConfig{}) {}
MjLogConfigWrapper::MjWrapper(raw::MjLogConfig* ptr, py::handle owner)
: WrapperBase(ptr, owner) {}
MjLogConfigWrapper::MjWrapper(const MjLogConfigWrapper& other)
: MjLogConfigWrapper() {
*this->ptr_ = *other.ptr_;
}
// ==================== MJLOGMESSAGE ===========================================
MjLogMessageWrapper::MjWrapper() : WrapperBase(new raw::MjLogMessage{}) {}
MjLogMessageWrapper::MjWrapper(raw::MjLogMessage* ptr, py::handle owner)
: WrapperBase(ptr, owner) {}
MjLogMessageWrapper::MjWrapper(const MjLogMessageWrapper& other)
: MjLogMessageWrapper() {
*this->ptr_ = *other.ptr_;
}
// ==================== MJTIMERSTAT ============================================
MjTimerStatWrapper::MjWrapper() : WrapperBase(new raw::MjTimerStat{}) {}
+12 -22
View File
@@ -137,22 +137,13 @@ int main(int argc, char** argv) {
}
}
// print compiler timing diagnostics
auto print_timers = [](const mjSpec* s, const char* label) {
const double* timer = mjs_getTimer(const_cast<mjSpec*>(s));
std::printf("\n%s:\n", label);
std::printf(" total: %8.1f ms\n", 1e3 * timer[mjCTIMER_TOTAL]);
std::printf(" assets: %8.1f ms (wall clock)\n", 1e3 * timer[mjCTIMER_ASSETS]);
std::printf(" load: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_LOAD]);
std::printf(" hull: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_HULL]);
std::printf(" poly: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_POLYGON]);
std::printf(" inert: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_INERTIA]);
std::printf(" bvh: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_BVH]);
std::printf(" octr: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_OCTREE]);
std::printf(" tex: %8.1f ms\n", 1e3 * timer[mjCTIMER_TEXTURE]);
std::printf(" other: %8.1f ms\n",
1e3 * (timer[mjCTIMER_TOTAL] - timer[mjCTIMER_ASSETS]));
};
// enable compile timing diagnostics
if (type2 == typeNONE) {
mjLogConfig config = mju_getLogConfig();
config.logfile[0] = '\0';
config.topics |= (1 << (mjTOPIC_TIME_CMP - 1));
mju_setLogConfig(config);
}
// load model
mjSpec* s = nullptr;
@@ -162,20 +153,19 @@ int main(int argc, char** argv) {
return finish(error, EXIT_FAILURE);
}
if (type2 == typeNONE) {
std::cout << "Compile 1 (cold cache)\n";
}
m = mj_compile(s, 0);
if (!m) {
mj_deleteSpec(s);
return finish("Could not compile model", EXIT_FAILURE);
}
print_timers(s, "Compile 1 (cold cache)");
if (type2 == typeNONE) {
mj_deleteModel(m);
std::cout << "Compile 2 (warm cache)\n";
m = mj_compile(s, 0);
if (m) {
print_timers(s, "Compile 2 (warm cache)");
}
}
} else {
m = mj_loadModel(argv[1], 0);
@@ -201,5 +191,5 @@ int main(int argc, char** argv) {
// finalize
if (s) mj_deleteSpec(s);
return finish("\nDone.", EXIT_SUCCESS, m);
return finish("Done.", EXIT_SUCCESS, m);
}
+96 -27
View File
@@ -107,6 +107,7 @@ enum {
SECT_RENDERING,
SECT_VISUALIZATION,
SECT_GROUP,
SECT_LOGGING,
NSECT0,
// right ui
@@ -367,22 +368,27 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) {
}
// get timers: total, collision, prepare, solve, other
mjtNum total = d->timer[mjTIMER_STEP].duration;
int number = d->timer[mjTIMER_STEP].number;
mjtNum total = d->timer[mjTIMER_STEP].duration - sim->timer_prev_[mjTIMER_STEP].duration;
int number = d->timer[mjTIMER_STEP].number - sim->timer_prev_[mjTIMER_STEP].number;
int prev_forward_number = sim->timer_prev_[mjTIMER_FORWARD].number;
mjtNum prev_forward_duration = sim->timer_prev_[mjTIMER_FORWARD].duration;
if (!number) {
total = d->timer[mjTIMER_FORWARD].duration;
number = d->timer[mjTIMER_FORWARD].number;
total = d->timer[mjTIMER_FORWARD].duration - prev_forward_duration;
number = d->timer[mjTIMER_FORWARD].number - prev_forward_number;
}
if (number) { // skip update if no measurements
if (number > 0) { // skip update if no measurements
float tdata[5] = {
static_cast<float>(total/number),
static_cast<float>(d->timer[mjTIMER_POS_COLLISION].duration/number),
static_cast<float>(d->timer[mjTIMER_POS_MAKE].duration/number) +
static_cast<float>(d->timer[mjTIMER_POS_PROJECT].duration/number),
static_cast<float>(d->timer[mjTIMER_CONSTRAINT].duration/number),
0
};
static_cast<float>(total / number),
static_cast<float>((d->timer[mjTIMER_POS_COLLISION].duration -
sim->timer_prev_[mjTIMER_POS_COLLISION].duration) / number),
static_cast<float>((d->timer[mjTIMER_POS_MAKE].duration -
sim->timer_prev_[mjTIMER_POS_MAKE].duration +
d->timer[mjTIMER_POS_PROJECT].duration -
sim->timer_prev_[mjTIMER_POS_PROJECT].duration) / number),
static_cast<float>((d->timer[mjTIMER_CONSTRAINT].duration -
sim->timer_prev_[mjTIMER_CONSTRAINT].duration) / number),
0};
tdata[4] = tdata[0] - tdata[1] - tdata[2] - tdata[3];
// update figtimer
@@ -399,6 +405,10 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) {
}
}
for (int i = 0; i < mjNTIMER; i++) {
sim->timer_prev_[i] = d->timer[i];
}
// get total number of iterations and nonzeros
mjtNum sqrt_nnz = 0;
int solver_niter = 0;
@@ -1154,6 +1164,35 @@ void MakeGroupSection(mj::Simulate* sim) {
mjui_add(&sim->ui0, defGroup);
}
// make logging section of UI
void MakeLoggingSection(mj::Simulate* sim) {
mjLogConfig cfg = mju_getLogConfig();
sim->log_console = cfg.logto_console;
sim->log_file = cfg.logto_file;
for (int i = 0; i < mjNTOPIC; i++) {
sim->log_topics[i] = ((cfg.topics & (1 << i)) != 0);
}
mjuiDef defLogging[] = {
{mjITEM_SECTION, "Logging", mjPRESERVE, nullptr, "AL"},
{mjITEM_CHECKBYTE, "Console", 2, &sim->log_console, ""},
{mjITEM_CHECKBYTE, "File", 2, &sim->log_file, ""},
{mjITEM_SEPARATOR, "Info topics", 1},
{mjITEM_END}
};
mjui_add(&sim->ui0, defLogging);
mjuiDef defTopic[] = {
{mjITEM_CHECKBYTE, "", 2, nullptr, ""},
{mjITEM_END}
};
for (int i = 0; i < mjNTOPIC; i++) {
mju::strcpy_arr(defTopic[0].name, mjTOPICSTRING[i]);
defTopic[0].pdata = sim->log_topics + i;
mjui_add(&sim->ui0, defTopic);
}
}
// make joint section of UI
void MakeJointSection(mj::Simulate* sim) {
mjuiDef defJoint[] = {
@@ -1304,6 +1343,7 @@ void MakeUiSections(mj::Simulate* sim, const mjModel* m, const mjData* d) {
MakeRenderingSection(sim, m);
MakeVisualizationSection(sim, m);
MakeGroupSection(sim);
MakeLoggingSection(sim);
MakeJointSection(sim);
MakeControlSection(sim);
MakeEqualitySection(sim);
@@ -1406,14 +1446,6 @@ mjtNum Timer() {
return elapsed.count();
}
// clear all times
void ClearTimers(mjData* d) {
for (int i=0; i<mjNTIMER; i++) {
d->timer[i].duration = 0;
d->timer[i].number = 0;
}
}
// copy current camera to clipboard as MJCF specification
void CopyCamera(mj::Simulate* sim) {
mjvGLCamera* camera = sim->scn.camera;
@@ -1479,6 +1511,28 @@ void UpdateSettings(mj::Simulate* sim, const mjModel* m) {
if (old_camera != sim->camera) {
sim->pending_.ui_update_rendering = true;
}
// logging flags
mjLogConfig cfg = mju_getLogConfig();
bool logging_changed = false;
if (sim->log_console != cfg.logto_console) {
sim->log_console = cfg.logto_console;
logging_changed = true;
}
if (sim->log_file != cfg.logto_file) {
sim->log_file = cfg.logto_file;
logging_changed = true;
}
for (int i = 0; i < mjNTOPIC; i++) {
int enabled = ((cfg.topics & (1 << i)) != 0);
if (sim->log_topics[i] != enabled) {
sim->log_topics[i] = enabled;
logging_changed = true;
}
}
if (logging_changed) {
sim->pending_.ui_update_logging = true;
}
}
// Compute suitable font scale.
@@ -1722,7 +1776,6 @@ void UiEvent(mjuiState* state) {
// rendering section
else if (it && it->sectionid==SECT_RENDERING) {
// only update the camera when the camera itself changed
if (it->pdata == &sim->camera) {
if (sim->camera==0) {
@@ -1772,6 +1825,20 @@ void UiEvent(mjuiState* state) {
}
}
// logging section
else if (it && it->sectionid==SECT_LOGGING) {
mjLogConfig cfg = mju_getLogConfig();
cfg.logto_console = sim->log_console;
cfg.logto_file = sim->log_file;
cfg.topics = 0;
for (int i = 0; i < mjNTOPIC; i++) {
if (sim->log_topics[i]) {
cfg.topics |= (1 << i);
}
}
mju_setLogConfig(cfg);
}
// stop if UI processed event
if (it!=nullptr || (state->type==mjEVENT_KEY && state->key==0)) {
return;
@@ -1815,8 +1882,6 @@ void UiEvent(mjuiState* state) {
case mjKEY_RIGHT: // step forward
if (!sim->is_passive_ && sim->m_ && !sim->run) {
ClearTimers(sim->d_);
// currently in scrubber: increment scrub, load state, update slider UI
if (sim->scrub_index < 0) {
sim->scrub_index++;
@@ -1839,7 +1904,6 @@ void UiEvent(mjuiState* state) {
case mjKEY_LEFT: // step backward
if (!sim->is_passive_ && sim->m_) {
sim->run = 0;
ClearTimers(sim->d_);
// decrement scrub, load state
sim->scrub_index = mjMAX(sim->scrub_index - 1, 1 - sim->nhistory_);
@@ -2184,6 +2248,7 @@ void Simulate::Sync(bool state_only) {
if (pending_.reset) {
mj_resetData(m_, d_);
memset(timer_prev_, 0, sizeof(timer_prev_));
mj_forward(m_, d_);
load_error[0] = '\0';
update_profiler = true;
@@ -2366,9 +2431,6 @@ void Simulate::Sync(bool state_only) {
UpdateSensorImage(this, m_, d_);
}
// clear timers once profiler info has been copied
ClearTimers(d_);
if (this->run || this->is_passive_) {
// clear old perturbations, apply new
mju_zero(d_->xfrc_applied, 6*m_->nbody);
@@ -2689,6 +2751,13 @@ void Simulate::Render() {
pending_.ui_update_visualization = false;
}
if (pending_.ui_update_logging) {
if (this->ui0_enable && this->ui0.sect[SECT_LOGGING].state) {
mjui0_update_section(this, SECT_LOGGING);
}
pending_.ui_update_logging = false;
}
if (is_passive_) {
if (this->ui0_enable && this->ui0.sect[SECT_RENDERING].state &&
(cam_prev_.type != cam.type ||
+7
View File
@@ -172,6 +172,7 @@ class Simulate {
bool ui_update_joint;
bool ui_update_ctrl;
bool ui_update_equality;
bool ui_update_logging;
bool ui_remake_ctrl;
} pending_ = {};
@@ -257,6 +258,12 @@ class Simulate {
int enable[mjNENABLE] = {0};
int enableactuator[mjNGROUP] = {0};
// logging: need sync
mjtByte log_console = 0;
mjtByte log_file = 0;
mjtByte log_topics[mjNTOPIC] = {0};
mjTimerStat timer_prev_[mjNTIMER] = {};
// rendering: need sync
int camera = 0;
+6
View File
@@ -33,6 +33,12 @@
#include <strings.h>
#endif
// Environment variable handling.
#ifdef _WIN32
#define setenv(name, value, overwrite) _putenv_s(name, value)
#define unsetenv(name) _putenv_s(name, "")
#endif
// Switch-case fallthrough annotation.
#if defined(__cplusplus)
#define mjFALLTHROUGH [[fallthrough]]
+76
View File
@@ -1069,6 +1069,9 @@ void mj_makeRawData(mjData** dest, const mjModel* m) {
mjERROR("could not allocate mjData");
}
// prevent spurious timing print from mj_resetData before _resetData zeroes the struct
d->timer[mjTIMER_STEP].number = 0;
// compute buffer size
d->nbuffer = 0;
d->buffer = d->arena = NULL;
@@ -1563,12 +1566,85 @@ static void _resetData(const mjModel* m, mjData* d, unsigned char debug_value) {
}
// emit step timing diagnostics
static void mj_logTimingDiagnostics(const mjData* d) {
int nstep = d->timer[mjTIMER_STEP].number;
if (nstep <= 0) {
return;
}
mjtNum tstep = d->timer[mjTIMER_STEP].duration / nstep;
if (tstep <= 0) {
return;
}
char buf[2048];
int pos = 0;
mjtNum components = 0;
for (int i = mjTIMER_POSITION; i <= mjTIMER_ADVANCE; i++) {
if (d->timer[i].number > 0) {
mjtNum istep = d->timer[i].duration / d->timer[i].number;
components += istep;
pos += snprintf(buf + pos, sizeof(buf) - pos,
"%s %-15s %8.1f (%5.1f%%)",
pos > 0 ? "\n" : "",
mjTIMERSTRING[i], istep * 1000, 100 * istep / tstep);
// position sub-breakdown
if (i == mjTIMER_POSITION) {
for (int p = mjTIMER_POS_KINEMATICS; p <= mjTIMER_POS_PROJECT; p++) {
if (d->timer[p].number > 0) {
mjtNum pstep = d->timer[p].duration / d->timer[p].number;
pos += snprintf(buf + pos, sizeof(buf) - pos,
"\n %-13s %8.1f (%5.1f%%)",
mjTIMERSTRING[p] + 4, pstep * 1000, 100 * pstep / tstep);
// collision sub-breakdown
if (p == mjTIMER_POS_COLLISION) {
for (int c = mjTIMER_COL_BROAD; c <= mjTIMER_COL_NARROW; c++) {
if (d->timer[c].number > 0) {
mjtNum cstep = d->timer[c].duration / d->timer[c].number;
pos += snprintf(buf + pos, sizeof(buf) - pos,
"\n %-11s %8.1f (%5.1f%%)",
mjTIMERSTRING[c] + 4, cstep * 1000, 100 * cstep / tstep);
}
}
}
}
}
}
}
}
mjtNum other = tstep - components;
pos += snprintf(buf + pos, sizeof(buf) - pos,
"%s %-15s %8.1f (%5.1f%%)",
pos > 0 ? "\n" : "",
"other", other * 1000, 100 * other / tstep);
pos += snprintf(buf + pos, sizeof(buf) - pos,
"%s %-15s %8.1f",
pos > 0 ? "\n" : "",
"total", tstep * 1000);
mjLogMessage msg = {.level = mjLOG_INFO, .topic = mjTOPIC_TIME_STP, .body = buf};
snprintf(msg.subject, sizeof(msg.subject),
"average time per step (%d steps, units: \u00B5s)", nstep);
mju_message(&msg);
}
// clear data, set data->qpos = model->qpos0
void mj_resetData(const mjModel* m, mjData* d) {
// emit step timing diagnostics before timers are cleared
mj_logTimingDiagnostics(d);
_resetData(m, d, 0);
}
// clear data, set data->qpos = model->qpos0, fill with debug_value
void mj_resetDataDebug(const mjModel* m, mjData* d, unsigned char debug_value) {
_resetData(m, d, debug_value);
+46 -53
View File
@@ -24,8 +24,6 @@
#include "engine/engine_util_errmem.h"
#include "engine/engine_util_misc.h"
// uncomment to print sleep/wake events
// #define MJ_DEBUG_SLEEP
//-------------------------------- update ----------------------------------------------------------
@@ -187,8 +185,14 @@ int mj_sleepCycle(const int* tree_asleep, int ntree, int i) {
//-------------------------------- wake ------------------------------------------------------------
// helper for pluralizing in log messages
static inline const char* plural(int n) {
return n > 1 ? "s" : "";
}
// wake tree i and its associated cycle, return number of woke trees
int mj_wakeTree(int* tree_asleep, int ntree, int i, int wakeval) {
int mj_wakeIsland(int* tree_asleep, int ntree, int i, int wakeval, const char* reason, mjtNum time) {
int nwoke = 0;
// i is invalid; SHOULD NOT OCCUR
@@ -207,6 +211,7 @@ int mj_wakeTree(int* tree_asleep, int ntree, int i, int wakeval) {
// tree i asleep: wake up tree and its island cycle
else {
int current = i;
int woke_trees[1024]; // buffer for woke tree indices
do {
// get the index of the next tree in the cycle
int next = tree_asleep[current];
@@ -217,8 +222,9 @@ int mj_wakeTree(int* tree_asleep, int ntree, int i, int wakeval) {
return 0;
}
// wake the current tree, increment count, advance to next
// wake the current tree, record index, increment and advance to next
tree_asleep[current] = wakeval;
if (nwoke < 1024) woke_trees[nwoke] = current;
nwoke++;
current = next;
} while (current != i && nwoke < ntree);
@@ -228,6 +234,21 @@ int mj_wakeTree(int* tree_asleep, int ntree, int i, int wakeval) {
mjERROR("tree %d is not in a cycle", i);
return 0;
}
#ifndef MJ_DISABLE_DEBUG_TRACING
if (reason && mju_isTopicEnabled(mjTOPIC_SLEEP)) {
int nprint = mjMIN(nwoke, 1024);
char buf[1024];
int pos = snprintf(buf, sizeof(buf), "t=%6.3g, woke due to %s tree%s ", time, reason, plural(nprint));
for (int j = 0; j < nprint; j++) {
pos += snprintf(buf + pos, sizeof(buf) - pos, "%d%s", woke_trees[j],
(j == nprint - 1) ? "" : " ");
}
mjLogMessage msg = {.level = mjLOG_DEBUG, .topic = mjTOPIC_SLEEP, .func = __func__};
mju_strncpy(msg.subject, buf, sizeof(msg.subject));
mju_message(&msg);
}
#endif
}
return nwoke;
@@ -260,14 +281,7 @@ int mj_wake(const mjModel* m, mjData* d) {
// if qpos mismatch or cannot sleep: wake up
if (d->tree_awake[i] || !treeCanSleep(m, d, i, 0)) {
int woke = mj_wakeTree(d->tree_asleep, ntree, i, kAwake);
if (woke) {
nwoke += woke;
#ifdef MJ_DEBUG_SLEEP
printf("woke tree %d due to perturbation at t=%g\n", i, d->time);
#endif
}
nwoke += mj_wakeIsland(d->tree_asleep, ntree, i, kAwake, "perturbation", d->time);
}
}
@@ -340,11 +354,7 @@ int mj_wakeCollision(const mjModel* m, mjData* d) {
// wake sleeping tree
int sleeping_tree = awake1 ? tree2 : tree1;
int wakeval = awake1 ? d->tree_asleep[tree1] : d->tree_asleep[tree2];
nwoke += mj_wakeTree(d->tree_asleep, ntree, sleeping_tree, wakeval);
#ifdef MJ_DEBUG_SLEEP
printf("woke tree %d due to contact at t=%g\n", sleeping_tree, d->time);
#endif
nwoke += mj_wakeIsland(d->tree_asleep, ntree, sleeping_tree, wakeval, "contact", d->time);
}
return nwoke;
@@ -372,11 +382,8 @@ int mj_wakeTendon(const mjModel* m, mjData* d) {
if (awake1 != awake2) {
int sleeping_tree = awake1 ? tree2 : tree1;
int wakeval = awake1 ? d->tree_asleep[tree1] : d->tree_asleep[tree2];
nwoke += mj_wakeTree(d->tree_asleep, m->ntree, sleeping_tree, wakeval);
#ifdef MJ_DEBUG_SLEEP
printf("woke tree %d due to tendon constraint at t=%g\n", sleeping_tree, d->time);
#endif
nwoke += mj_wakeIsland(d->tree_asleep, m->ntree, sleeping_tree, wakeval,
"tendon constraint", d->time);
}
}
@@ -455,10 +462,8 @@ int mj_wakeEquality(const mjModel* m, mjData* d) {
for (int j = 0; j < num; j++) {
int treeid = m->body_treeid[bodyid[adr+j]];
if (treeid >= 0 && !d->tree_awake[treeid]) {
nwoke += mj_wakeTree(d->tree_asleep, m->ntree, treeid, wakeval);
#ifdef MJ_DEBUG_SLEEP
printf("woke tree %d due to flex equality %d at t=%g\n", treeid, i, d->time);
#endif
nwoke += mj_wakeIsland(d->tree_asleep, m->ntree, treeid, wakeval,
"flex equality", d->time);
break;
}
}
@@ -494,12 +499,8 @@ int mj_wakeEquality(const mjModel* m, mjData* d) {
int cycle1 = mj_sleepCycle(d->tree_asleep, m->ntree, tree1);
int cycle2 = mj_sleepCycle(d->tree_asleep, m->ntree, tree2);
if (cycle1 != cycle2) {
int nwoke1 = mj_wakeTree(d->tree_asleep, m->ntree, tree1, kAwake);
int nwoke2 = mj_wakeTree(d->tree_asleep, m->ntree, tree2, kAwake);
#ifdef MJ_DEBUG_SLEEP
printf("woke trees %d, %d due to equality %d at t=%g\n", tree1, tree2, i, d->time);
#endif
int nwoke1 = mj_wakeIsland(d->tree_asleep, m->ntree, tree1, kAwake, "equality", d->time);
int nwoke2 = mj_wakeIsland(d->tree_asleep, m->ntree, tree2, kAwake, "equality", d->time);
nwoke += nwoke1 + nwoke2;
}
@@ -508,11 +509,7 @@ int mj_wakeEquality(const mjModel* m, mjData* d) {
// one is asleep and one is awake, wake the sleeping tree
int sleeping_tree = s1 == mjS_ASLEEP ? tree1 : tree2;
nwoke += mj_wakeTree(d->tree_asleep, m->ntree, sleeping_tree, kAwake);
#ifdef MJ_DEBUG_SLEEP
printf("woke tree %d due to equality %d at t=%g\n", sleeping_tree, i, d->time);
#endif
nwoke += mj_wakeIsland(d->tree_asleep, m->ntree, sleeping_tree, kAwake, "equality", d->time);
}
return nwoke;
@@ -522,7 +519,7 @@ int mj_wakeEquality(const mjModel* m, mjData* d) {
//-------------------------------- sleep -----------------------------------------------------------
// put n trees to sleep (create cycle), set their velocity and acceleration to zero
static inline void sleepTrees(const mjModel* m, mjData* d, const int* tree, int n) {
static inline void mj_sleepTrees(const mjModel* m, mjData* d, const int* tree, int n) {
for (int i=0; i < n; i++) {
// create cycle
int current = tree[i];
@@ -545,17 +542,18 @@ static inline void sleepTrees(const mjModel* m, mjData* d, const int* tree, int
mju_zero(d->qacc+adr, num);
}
#ifdef MJ_DEBUG_SLEEP
if (n == 1) {
printf("tree %d put to sleep at t=%g\n", tree[0], d->time);
} else if (n > 1) {
printf("trees ");
#ifndef MJ_DISABLE_DEBUG_TRACING
if (mju_isTopicEnabled(mjTOPIC_SLEEP)) {
char buf[1024];
int pos = snprintf(buf, sizeof(buf), "t=%6.2g, slept tree%s ", d->time, plural(n));
for (int i = 0; i < n; i++) {
printf("%d%s", tree[i], (i == n - 1) ? "" : ", ");
pos += snprintf(buf + pos, sizeof(buf) - pos, "%d%s", tree[i], (i == n - 1) ? "" : " ");
}
printf(" put to sleep at t=%g\n", d->time);
mjLogMessage msg = {.level = mjLOG_DEBUG, .topic = mjTOPIC_SLEEP, .func = __func__};
mju_strncpy(msg.subject, buf, sizeof(msg.subject));
mju_message(&msg);
}
#endif
#endif
}
@@ -611,7 +609,7 @@ int mj_sleep(const mjModel* m, mjData* d) {
if (can_sleep) {
const int* tree = d->map_itree2tree + start;
int n = d->island_ntree[i];
sleepTrees(m, d, tree, n);
mj_sleepTrees(m, d, tree, n);
nslept += n;
}
}
@@ -621,7 +619,7 @@ int mj_sleep(const mjModel* m, mjData* d) {
for (int j=start; j < ntree; j++) {
int i = nisland ? d->map_itree2tree[j] : j;
if (d->tree_asleep[i] == -1) {
sleepTrees(m, d, &i, 1);
mj_sleepTrees(m, d, &i, 1);
nslept++;
}
}
@@ -853,8 +851,3 @@ mjtSleepState mj_sleepState(const mjModel* m, const mjData* d, mjtObj type, int
return mjS_AWAKE;
}
}
#ifdef MJ_DEBUG_SLEEP
#undef MJ_DEBUG_SLEEP
#endif
+3 -4
View File
@@ -18,6 +18,7 @@
#include <mujoco/mjdata.h>
#include <mujoco/mjexport.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjtype.h>
#ifdef __cplusplus
extern "C" {
@@ -32,11 +33,9 @@ MJAPI void mj_updateSleep(const mjModel* m, mjData* d);
// return the first tree in the sleep cycle that starts at i, -1 if error
int mj_sleepCycle(const int* tree_asleep, int ntree, int i);
// return the first tree in the sleep cycle that starts at i, -1 if error
int mj_sleepCycle(const int* tree_asleep, int ntree, int i);
// wake tree i and its related island cycle, return number of woke trees
MJAPI int mj_wakeTree(int* tree_asleep, int ntree, int i, int wakeval);
MJAPI int mj_wakeIsland(int* tree_asleep, int ntree, int i, int wakeval,
const char* reason, mjtNum time);
// wake trees with nonzero velocity or external forces, return number of woke trees
int mj_wake(const mjModel* m, mjData* d);
+8
View File
@@ -102,6 +102,14 @@ const char* mjTIMERSTRING[mjNTIMER]= {
};
// names of log topics (index i corresponds to topic i+1)
const char* mjTOPICSTRING[mjNTOPIC] = {
"Step timing",
"Compile timing",
"Sleep/wake"
};
// size of contact data fields
const int mjCONDATA_SIZE[mjNCONDATA] = {
1, // mjCONDATA_FOUND
+1
View File
@@ -28,6 +28,7 @@ extern "C" {
MJAPI extern const char* mjDISABLESTRING[mjNDISABLE];
MJAPI extern const char* mjENABLESTRING[mjNENABLE];
MJAPI extern const char* mjTIMERSTRING[mjNTIMER];
MJAPI extern const char* mjTOPICSTRING[mjNTOPIC];
// arrays
MJAPI extern const int mjCONDATA_SIZE[mjNCONDATA]; // TODO(tassa): expose in public header?
+268 -114
View File
@@ -18,17 +18,29 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
#include <unistd.h>
#endif
#include "engine/engine_array_safety.h"
#include "engine/engine_crossplatform.h" // IWYU pragma: keep
#include "engine/engine_macro.h"
//------------------------- cross-platform aligned malloc/free -------------------------------------
// forward declaration for active log handler dispatch
static void mju_defaultLogHandler(const mjLogMessage* msg);
//------------------------------ malloc and free ---------------------------------------------------
// user memory handlers
void* (*mju_user_malloc) (size_t) = 0;
void (*mju_user_free) (void*) = 0;
// cross-platform aligned malloc
static inline void* mju_alignedMalloc(size_t size, size_t align) {
#ifdef _WIN32
return _aligned_malloc(size, align);
@@ -37,6 +49,7 @@ static inline void* mju_alignedMalloc(size_t size, size_t align) {
#endif
}
// cross-platform aligned free
static inline void mju_alignedFree(void* ptr) {
#ifdef _WIN32
_aligned_free(ptr);
@@ -45,98 +58,262 @@ static inline void mju_alignedFree(void* ptr) {
#endif
}
// allocate memory; byte-align on 64; pad size to multiple of 64
void* mju_malloc(size_t size) {
void* ptr = 0;
//------------------------- default user handlers --------------------------------------------------
if (mju_user_malloc) {
ptr = mju_user_malloc(size);
} else {
if (size > 0 && (size % 64)) {
size += 64 - (size % 64);
}
if (size > 0) {
ptr = mju_alignedMalloc(size, 64);
}
}
// define and clear handlers
if (!ptr && size > 0) {
mju_error("Could not allocate memory");
}
return ptr;
}
// free memory
void mju_free(void* ptr) {
if (!ptr) return;
if (mju_user_free) {
mju_user_free(ptr);
} else {
mju_alignedFree(ptr);
}
}
//------------------------------ logging configuration and handlers --------------------------------
// global log handler
static mjfLogHandler global_log_handler = mju_defaultLogHandler;
// legacy error/warning handlers (deprecated)
void (*mju_user_error) (const char*) = 0;
void (*mju_user_warning) (const char*) = 0;
void* (*mju_user_malloc) (size_t) = 0;
void (*mju_user_free) (void*) = 0;
// default handler configuration
static mjLogConfig log_config = {.logto_console = true,
.logto_file = true,
.logfile = "MUJOCO_LOG.TXT",
.topics = 0};
static mjtBool env_checked = 0;
// parse MUJOCO_LOG_TOPICS env var to seed initial topic bitmask
// example: MUJOCO_LOG_TOPICS="time_stp,sleep"
static void mju_initLogTopicsFromEnv(void) {
const char* env = getenv("MUJOCO_LOG_TOPICS");
if (!env) return;
// mjTOPIC_X enum names with the mjTOPIC_ prefix stripped and lowercased; keep in sync with mjtLogTopic
static const char* topic_names[mjNTOPIC] = {"time_stp", "time_cmp", "sleep"};
char buf[256];
strncpy(buf, env, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char* token = buf;
while (*token) {
while (*token == ' ' || *token == ',') token++;
if (!*token) break;
char* end = token;
while (*end && *end != ',') end++;
bool trailing_comma = (*end == ',');
char* tEnd = end - 1;
while (tEnd > token && *tEnd == ' ') tEnd--;
*(tEnd + 1) = '\0';
for (int i = 0; i < mjNTOPIC; i++) {
if (strcasecmp(token, topic_names[i]) == 0) {
log_config.topics |= (1 << i);
break;
}
}
token = trailing_comma ? end + 1 : end;
}
}
// private pointer getter encapsulates lazy init with zero copy overhead
static const mjLogConfig* mju_getLogConfigPtr(void) {
if (!env_checked) {
mju_initLogTopicsFromEnv();
env_checked = 1;
}
return &log_config;
}
// check whether an info topic is enabled
mjtBool mju_isTopicEnabled(int topic) {
if (!topic) return 1;
const mjLogConfig* cfg = mju_getLogConfigPtr();
return ((cfg->topics & (1 << (topic - 1))) != 0);
}
// set the active log handler; return the previous handler
mjfLogHandler mju_setLogHandler(mjfLogHandler handler) {
mjfLogHandler prev = global_log_handler;
global_log_handler = handler ? handler : mju_defaultLogHandler;
return prev;
}
// get default handler configuration
mjLogConfig mju_getLogConfig(void) {
return *mju_getLogConfigPtr();
}
// set default handler configuration
void mju_setLogConfig(mjLogConfig config) {
env_checked = 1;
log_config = config;
}
// restore default processing
void mju_clearHandlers(void) {
global_log_handler = mju_defaultLogHandler;
log_config = (mjLogConfig){.logto_console = true,
.logto_file = true,
.logfile = "MUJOCO_LOG.TXT",
.topics = 0};
env_checked = 1;
mju_initLogTopicsFromEnv();
mju_user_error = 0;
mju_user_warning = 0;
mju_user_malloc = 0;
mju_user_free = 0;
}
//------------------------- internal-only handlers -------------------------------------------------
typedef void (*callback_fn)(const char*);
static mjTHREADLOCAL callback_fn _mjPRIVATE_tls_error_fn = NULL;
static mjTHREADLOCAL callback_fn _mjPRIVATE_tls_warning_fn = NULL;
callback_fn _mjPRIVATE__get_tls_error_fn(void) {
return _mjPRIVATE_tls_error_fn;
}
void _mjPRIVATE__set_tls_error_fn(callback_fn h) {
_mjPRIVATE_tls_error_fn = h;
}
callback_fn _mjPRIVATE__get_tls_warning_fn(void) {
return _mjPRIVATE_tls_warning_fn;
}
void _mjPRIVATE__set_tls_warning_fn(callback_fn h) {
_mjPRIVATE_tls_warning_fn = h;
}
//------------------------------ error hadling -----------------------------------------------------
// write datetime, type: message to MUJOCO_LOG.TXT
void mju_writeLog(const char* type, const char* msg) {
// fill buffer with formatted local time string (thread-safe)
static void mju_localTimeStr(char* buf, int buf_sz) {
time_t rawtime;
struct tm timeinfo;
FILE* fp = fopen("MUJOCO_LOG.TXT", "a+t");
if (fp) {
// get time
time(&rawtime);
time(&rawtime);
#if defined(_POSIX_C_SOURCE) || defined(__APPLE__) || defined(__STDC_VERSION_TIME_H__) || defined(__EMSCRIPTEN__)
localtime_r(&rawtime, &timeinfo);
localtime_r(&rawtime, &timeinfo);
#elif defined(_WIN32)
localtime_s(&timeinfo, &rawtime);
localtime_s(&timeinfo, &rawtime);
#elif __STDC_LIB_EXT1__
localtime_s(&rawtime, &timeinfo);
localtime_s(&rawtime, &timeinfo);
#else
#error "Thread-safe version of `localtime` is not present in the standard C library"
#error "Thread-safe version of `localtime` is not present in the standard C library"
#endif
// write to log file
fprintf(fp, "%s%s: %s\n\n", asctime(&timeinfo), type, msg);
fclose(fp);
}
strftime(buf, buf_sz, "%c", &timeinfo);
}
// write formatted message to stream
static void mju_fprint_message(FILE* stream, const char* timestr,
const mjLogMessage* msg) {
const char* type = msg->level == mjLOG_ERROR ? "ERROR" :
msg->level == mjLOG_WARNING ? "WARNING" :
msg->level == mjLOG_INFO ? "INFO" : "DEBUG";
fprintf(stream, "%s", type);
if (msg->func) fprintf(stream, " %s", msg->func);
if (msg->file && msg->line) fprintf(stream, " (%s:%d)", BaseName(msg->file), msg->line);
if (timestr[0]) fprintf(stream, " %s", timestr);
fprintf(stream, ": %s\n", msg->subject);
if (msg->body) fprintf(stream, "%s\n", msg->body);
void mju_error_raw(const char* msg) {
if (_mjPRIVATE_tls_error_fn) {
_mjPRIVATE_tls_error_fn(msg);
} else if (mju_user_error) {
mju_user_error(msg);
} else {
// write to log and console
mju_writeLog("ERROR", msg);
printf("ERROR: %s\n\n", msg);
// add blank line after message except for DEBUG, for compactness
if (msg->level != mjLOG_DEBUG) fprintf(stream, "\n");
}
// exit
// format legacy adapter string: "func: subject" or just "subject"
static const char* mju_legacy_text(const mjLogMessage* msg, char* buf, int bufsz) {
if (msg->func) {
snprintf(buf, bufsz, "%s: %s", msg->func, msg->subject);
return buf;
}
return msg->subject;
}
// default log handler: topic filtering, console/file output, legacy compat, exit on error
static void mju_defaultLogHandler(const mjLogMessage* msg) {
const mjLogConfig* cfg = mju_getLogConfigPtr();
if ((msg->level == mjLOG_INFO || msg->level == mjLOG_DEBUG) && !mju_isTopicEnabled(msg->topic)) {
return;
}
if (msg->level == mjLOG_ERROR && mju_user_error) {
char buf[1024];
mju_user_error(mju_legacy_text(msg, buf, sizeof(buf)));
return;
}
if (msg->level == mjLOG_WARNING && mju_user_warning) {
char buf[1024];
mju_user_warning(mju_legacy_text(msg, buf, sizeof(buf)));
return;
}
char timestr[64] = "";
if (msg->timestamp || (cfg->logto_file && cfg->logfile[0])) {
mju_localTimeStr(timestr, sizeof(timestr));
}
if (cfg->logto_file && cfg->logfile[0]) {
FILE* fp = fopen(cfg->logfile, "a+t");
if (fp) {
mju_fprint_message(fp, timestr, msg);
fclose(fp);
}
}
if (cfg->logto_console) {
FILE* stream = (msg->level >= mjLOG_WARNING) ? stderr : stdout;
mju_fprint_message(stream, msg->timestamp ? timestr : "", msg);
}
if (msg->level == mjLOG_ERROR) {
exit(EXIT_FAILURE);
}
}
void mju_error_v(const char* msg, va_list args) {
// Format msg into errmsg
char errmsg[1024];
vsnprintf(errmsg, mjSIZEOFARRAY(errmsg), msg, args);
mju_error_raw(errmsg);
//------------------------------ public message logging --------------------------------------------
// thread-local log handler override
static mjTHREADLOCAL mjfLogHandler _mjPRIVATE_tls_log_handler = NULL;
// recursion guard for log handler
static mjTHREADLOCAL bool in_log = false;
// dispatch to active handler (TLS > global)
static inline mjfLogHandler mju_activeHandler(void) {
return _mjPRIVATE_tls_log_handler ? _mjPRIVATE_tls_log_handler : global_log_handler;
}
// dispatch a structured log message to the active handler
void mju_message(const mjLogMessage* msg) {
// recursion guard: silently drop messages dispatched from within a handler
if (in_log) return;
// error handlers are expected to not return (longjmp or exit); we cannot set in_log
// around the call because longjmp would leave it permanently true on this thread
if (msg->level != mjLOG_ERROR) {
in_log = true;
mju_activeHandler()(msg);
in_log = false;
} else {
mju_activeHandler()(msg);
}
}
void mju_error_v(const char* msg, va_list args) {
mjLogMessage m = {.level = mjLOG_ERROR};
vsnprintf(m.subject, sizeof(m.subject), msg, args);
mju_message(&m);
}
// write message to logfile and console, pause and exit
void mju_error(const char* msg, ...) {
@@ -146,73 +323,50 @@ void mju_error(const char* msg, ...) {
va_end(args);
}
// write message to logfile and console
void mju_warning(const char* msg, ...) {
char wrnmsg[1024];
// Format msg into wrnmsg
mjLogMessage m = {.level = mjLOG_WARNING};
va_list args;
va_start(args, msg);
vsnprintf(wrnmsg, mjSIZEOFARRAY(wrnmsg), msg, args);
vsnprintf(m.subject, sizeof(m.subject), msg, args);
va_end(args);
mju_message(&m);
}
if (_mjPRIVATE_tls_warning_fn) {
_mjPRIVATE_tls_warning_fn(wrnmsg);
} else if (mju_user_warning) {
mju_user_warning(wrnmsg);
} else {
// write to log file and console
mju_writeLog("WARNING", wrnmsg);
printf("WARNING: %s\n\n", wrnmsg);
// log an info message
void mju_info(int topic, const char* msg, ...) {
mjLogMessage m = {.level = mjLOG_INFO, .topic = topic};
va_list args;
va_start(args, msg);
vsnprintf(m.subject, sizeof(m.subject), msg, args);
va_end(args);
mju_message(&m);
}
// (deprecated) write datetime, type: message to MUJOCO_LOG.TXT
void mju_writeLog(const char* type, const char* msg) {
char timestr[64];
mju_localTimeStr(timestr, sizeof(timestr));
FILE* fp = fopen("MUJOCO_LOG.TXT", "a+t");
if (fp) {
fprintf(fp, "%s\n%s: %s\n\n", timestr, type, msg);
fclose(fp);
}
}
//------------------------------ malloc and free ---------------------------------------------------
//------------------------------ internal helpers --------------------------------------------------
// allocate memory; byte-align on 64; pad size to multiple of 64
void* mju_malloc(size_t size) {
void* ptr = 0;
// user allocator
if (mju_user_malloc) {
ptr = mju_user_malloc(size);
}
// default allocator
else {
// pad size to multiple of 64
if (size > 0 && (size % 64)) {
size += 64 - (size % 64);
}
// allocate
if (size > 0) {
ptr = mju_alignedMalloc(size, 64);
}
}
// error if null pointer
if (!ptr && size > 0) {
mju_error("Could not allocate memory");
}
return ptr;
// set thread-local log handler; return previous
mjfLogHandler _mjPRIVATE_setTlsLogHandler(mjfLogHandler handler) {
mjfLogHandler prev = _mjPRIVATE_tls_log_handler;
_mjPRIVATE_tls_log_handler = handler;
return prev;
}
// free memory
void mju_free(void* ptr) {
// return if null
if (!ptr) {
return;
}
// free with user or built-in function
if (mju_user_free) {
mju_user_free(ptr);
} else {
mju_alignedFree(ptr);
}
// get the currently active global log handler (read-only, no modification)
mjfLogHandler _mjPRIVATE_getGlobalLogHandler(void) {
return global_log_handler;
}
+89 -42
View File
@@ -22,6 +22,7 @@
#include <mujoco/mjexport.h>
#include <mujoco/mjmacro.h>
#include <mujoco/mjtype.h>
#ifdef __cplusplus
extern "C" {
@@ -36,48 +37,6 @@ extern "C" {
#endif // mjPRINTFLIKE
//------------------------------ user handlers -----------------------------------------------------
MJAPI extern void (*mju_user_error)(const char*);
MJAPI extern void (*mju_user_warning)(const char*);
MJAPI extern void* (*mju_user_malloc)(size_t);
MJAPI extern void (*mju_user_free)(void*);
// clear user handlers; restore default processing
MJAPI void mju_clearHandlers(void);
// gets/sets thread-local error/warning handlers for internal use
MJAPI void (*_mjPRIVATE__get_tls_error_fn(void))(const char*);
MJAPI void _mjPRIVATE__set_tls_error_fn(void (*h)(const char*));
MJAPI void (*_mjPRIVATE__get_tls_warning_fn(void))(const char*);
MJAPI void _mjPRIVATE__set_tls_warning_fn(void (*h)(const char*));
//------------------------------ errors and warnings -----------------------------------------------
// errors
MJAPI void mju_error_raw(const char* msg);
MJAPI void mju_error(const char* msg, ...) mjPRINTFLIKE(1, 2);
MJAPI void mju_error_v(const char* msg, va_list args);
// warnings
MJAPI void mju_warning(const char* msg, ...) mjPRINTFLIKE(1, 2);
// write [datetime, type: message] to MUJOCO_LOG.TXT
MJAPI void mju_writeLog(const char* type, const char* msg);
//------------------------------ internal error macros --------------------------------------------
// internal macro to prepend the calling function name to the error message
#pragma warning(disable : 4996) // needed to use strncpy with Visual Studio
#define mjERROR(...) \
{ \
char _errbuf[1024]; \
size_t _funclen = strlen(__func__); \
strncpy(_errbuf, __func__, sizeof(_errbuf)); \
snprintf(_errbuf + _funclen, sizeof(_errbuf) - _funclen, ": " __VA_ARGS__); \
mju_error_raw(_errbuf); \
}
//------------------------------ malloc and free ---------------------------------------------------
// allocate memory; byte-align on 8; pad size to multiple of 8
@@ -86,6 +45,94 @@ MJAPI void* mju_malloc(size_t size);
// free memory with free() by default
MJAPI void mju_free(void* ptr);
// user memory handlers
MJAPI extern void* (*mju_user_malloc)(size_t);
MJAPI extern void (*mju_user_free)(void*);
//------------------------------ logging configuration and handlers --------------------------------
// set the active log handler, return the previous handler
// if handler is NULL, restore the default handler
MJAPI mjfLogHandler mju_setLogHandler(mjfLogHandler handler);
// set/get default handler configuration
MJAPI mjLogConfig mju_getLogConfig(void);
MJAPI void mju_setLogConfig(mjLogConfig config);
// clear user handlers; restore default processing
MJAPI void mju_clearHandlers(void);
// legacy error/warning handlers (deprecated: prefer mju_setLogHandler)
MJAPI extern void (*mju_user_error)(const char*);
MJAPI extern void (*mju_user_warning)(const char*);
//------------------------------ public message logging --------------------------------------------
// log a fatal error message, write to logfile and console, pause and exit
MJAPI void mju_error(const char* msg, ...) mjPRINTFLIKE(1, 2);
MJAPI void mju_error_v(const char* msg, va_list args);
// log a warning message, write to logfile and console
MJAPI void mju_warning(const char* msg, ...) mjPRINTFLIKE(1, 2);
// log an info message with optional topic filtering
MJAPI void mju_info(int topic, const char* msg, ...) mjPRINTFLIKE(2, 3);
// dispatch a structured log message to the active handler
MJAPI void mju_message(const mjLogMessage* msg);
// (deprecated) write [datetime, type: message] to MUJOCO_LOG.TXT
MJAPI void mju_writeLog(const char* type, const char* msg);
//------------------------------ internal helpers and macros ---------------------------------------
// set thread-local log handler; return previous thread-local handler
MJAPI mjfLogHandler _mjPRIVATE_setTlsLogHandler(mjfLogHandler handler);
// get the currently active global log handler (read-only, no modification)
MJAPI mjfLogHandler _mjPRIVATE_getGlobalLogHandler(void);
// check whether an info topic is enabled
MJAPI mjtBool mju_isTopicEnabled(int topic);
// strip directory from __FILE__ (cross-platform)
static inline const char* BaseName(const char* path) {
const char* slash = strrchr(path, '/');
const char* bslash = strrchr(path, '\\');
if (slash && bslash) return (slash > bslash ? slash : bslash) + 1;
if (slash) return slash + 1;
if (bslash) return bslash + 1;
return path;
}
// internal macro to emit a structured error with source location
#define mjERROR(...) \
{ \
mjLogMessage _msg = {.level = mjLOG_ERROR, \
.func = __func__, \
.file = __FILE__, \
.line = __LINE__}; \
snprintf(_msg.subject, sizeof(_msg.subject), __VA_ARGS__); \
mju_message(&_msg); \
}
// internal macro to emit a structured debug trace with fast producer-side topic filtering
#ifndef MJ_DISABLE_DEBUG_TRACING
#define mjDEBUG(_topic, ...) \
if (mju_isTopicEnabled(_topic)) { \
mjLogMessage _msg = {.level = mjLOG_DEBUG, \
.topic = _topic, \
.func = __func__}; \
snprintf(_msg.subject, sizeof(_msg.subject), __VA_ARGS__); \
mju_message(&_msg); \
}
#else
#define mjDEBUG(_topic, ...) ((void)0)
#endif
#ifdef __cplusplus
}
#endif
+45 -1
View File
@@ -32,6 +32,7 @@
#include <mujoco/mujoco.h>
#include "engine/engine_support.h"
#include "engine/engine_util_errmem.h"
#include "user/user_cache.h"
#include "user/user_flexcomp.h"
#include "user/user_model.h"
@@ -204,10 +205,53 @@ int mj_encode(const mjSpec* s, const mjModel* m, const char* filename,
return nbytes;
}
// helper function to log compile time diagnostics
static void LogCompileTime(const double* t) {
std::string body(1024, '\0');
int n = std::snprintf(body.data(), body.size(),
" total: %8.1f (wall clock)\n"
" assets: %8.1f -\n"
" load: %8.1f (CPU time)\n"
" hull: %8.1f -\n"
" polygon: %8.1f -\n"
" inertia: %8.1f -\n"
" bvh: %8.1f -\n"
" octree: %8.1f -\n"
" texture: %8.1f -\n"
" other: %8.1f (wall clock)",
1e3 * t[mjCTIMER_TOTAL],
1e3 * t[mjCTIMER_ASSETS],
1e3 * t[mjCTIMER_MESH_LOAD],
1e3 * t[mjCTIMER_MESH_HULL],
1e3 * t[mjCTIMER_MESH_POLYGON],
1e3 * t[mjCTIMER_MESH_INERTIA],
1e3 * t[mjCTIMER_MESH_BVH],
1e3 * t[mjCTIMER_MESH_OCTREE],
1e3 * t[mjCTIMER_TEXTURE],
1e3 * (t[mjCTIMER_TOTAL] - t[mjCTIMER_ASSETS]));
if (n > 0 && n < body.size()) {
body.resize(n);
}
// send log message
mjLogMessage msg = {.level = mjLOG_INFO,
.topic = mjTOPIC_TIME_CMP,
.subject = "compile time (ms)",
.body = body.c_str()};
mju_message(&msg);
}
// compile model
mjModel* mj_compile(mjSpec* s, const mjVFS* vfs) {
mjCModel* modelC = static_cast<mjCModel*>(s->element);
return modelC->Compile(vfs);
mjModel* m = modelC->Compile(vfs);
// log compile time if model was compiled successfully
if (m) {
LogCompileTime(modelC->timer);
}
return m;
}
+20 -28
View File
@@ -4623,20 +4623,21 @@ void mjCModel::CheckRepeat(mjtObj type) {
constexpr int kErrorBufferSize = 500;
static thread_local std::jmp_buf error_jmp_buf;
static thread_local char errortext[kErrorBufferSize] = "";
static void errorhandler(const char* msg) {
mju::strcpy_arr(errortext, msg);
std::longjmp(error_jmp_buf, 1);
}
// warning handler for low-level engine
static thread_local char warningtext[kErrorBufferSize] = ""; // top-level warning buffer
static thread_local std::string* local_warningtext_ptr = nullptr; // sub-thread warning buffer
static void warninghandler(const char* msg) {
if (local_warningtext_ptr) {
*local_warningtext_ptr = msg;
} else {
mju::strcpy_arr(warningtext, msg);
static void compilerLogHandler(const mjLogMessage* msg) {
if (msg->level == mjLOG_ERROR) {
mju::strcpy_arr(errortext, msg->subject);
std::longjmp(error_jmp_buf, 1);
} else if (msg->level == mjLOG_WARNING) {
if (local_warningtext_ptr) {
*local_warningtext_ptr = msg->subject;
} else {
mju::strcpy_arr(warningtext, msg->subject);
}
}
}
@@ -4660,13 +4661,8 @@ mjModel* mjCModel::Compile(const mjVFS* vfs, mjModel** m) {
mjModel* volatile model = (m && *m) ? *m : nullptr;
mjData* volatile data = nullptr;
// save error and warning handlers
void (*save_error)(const char*) = _mjPRIVATE__get_tls_error_fn();
void (*save_warning)(const char*) = _mjPRIVATE__get_tls_warning_fn();
// install error and warning handlers, clear error and warning
_mjPRIVATE__set_tls_error_fn(errorhandler);
_mjPRIVATE__set_tls_warning_fn(warninghandler);
// save log handler
mjfLogHandler save_handler = _mjPRIVATE_setTlsLogHandler(compilerLogHandler);
errInfo = mjCError();
warningtext[0] = 0;
@@ -4705,14 +4701,12 @@ mjModel* mjCModel::Compile(const mjVFS* vfs, mjModel** m) {
}
// restore handler, return 0
_mjPRIVATE__set_tls_error_fn(save_error);
_mjPRIVATE__set_tls_warning_fn(save_warning);
_mjPRIVATE_setTlsLogHandler(save_handler);
return nullptr;
}
// restore error handler, mark as compiled, return mjModel
_mjPRIVATE__set_tls_error_fn(save_error);
_mjPRIVATE__set_tls_warning_fn(save_warning);
// restore log handler, mark as compiled, return mjModel
_mjPRIVATE_setTlsLogHandler(save_handler);
compiled = true;
return model;
}
@@ -4723,8 +4717,7 @@ static void CompileMesh(mjCMesh* mesh, const mjVFS* vfs,
std::exception_ptr& exception, std::mutex& exception_mutex,
std::string* warningtext) {
local_warningtext_ptr = warningtext;
auto previous_handler = _mjPRIVATE__get_tls_warning_fn();
_mjPRIVATE__set_tls_warning_fn(warninghandler);
auto previous_handler = _mjPRIVATE_setTlsLogHandler(compilerLogHandler);
try {
mesh->Compile(vfs);
@@ -4735,7 +4728,7 @@ static void CompileMesh(mjCMesh* mesh, const mjVFS* vfs,
}
}
_mjPRIVATE__set_tls_warning_fn(previous_handler);
_mjPRIVATE_setTlsLogHandler(previous_handler);
local_warningtext_ptr = nullptr;
}
@@ -4746,8 +4739,7 @@ static void CompileTexture(mjCTexture* texture, const mjVFS* vfs,
using Clock = std::chrono::steady_clock;
using Seconds = std::chrono::duration<double>;
local_warningtext_ptr = warningtext;
auto previous_handler = _mjPRIVATE__get_tls_warning_fn();
_mjPRIVATE__set_tls_warning_fn(warninghandler);
auto previous_handler = _mjPRIVATE_setTlsLogHandler(compilerLogHandler);
Clock::time_point t0 = Clock::now();
try {
@@ -4760,7 +4752,7 @@ static void CompileTexture(mjCTexture* texture, const mjVFS* vfs,
}
texture->texture_time_ = Seconds(Clock::now() - t0).count();
_mjPRIVATE__set_tls_warning_fn(previous_handler);
_mjPRIVATE_setTlsLogHandler(previous_handler);
local_warningtext_ptr = nullptr;
}
@@ -4822,7 +4814,7 @@ void mjCModel::CompileMeshesAndTextures(const mjVFS* vfs) {
for (int i = 0; i < nmesh; i++) {
if (!mesh_warningtext[i].empty()) {
if (has_warning) {
concatenated_warnings += "\n";
concatenated_warnings += '\n';
}
concatenated_warnings += mesh_warningtext[i];
has_warning = true;
+8 -8
View File
@@ -185,38 +185,38 @@ TEST_F(SleepTest, MjSleepUpdate) {
mj_deleteModel(m);
}
TEST_F(SleepTest, MjWakeTree) {
TEST_F(SleepTest, MjWakeIsland) {
// one awake tree and two cycles
int asleep[] = {kAwake, 2, 1, 3};
EXPECT_EQ(mj_wakeTree(asleep, 4, 0, kAwake), 0);
EXPECT_EQ(mj_wakeIsland(asleep, 4, 0, kAwake, nullptr, 0), 0);
EXPECT_THAT(AsVector(asleep, 4), ElementsAre(kAwake, 2, 1, 3));
EXPECT_EQ(mj_wakeTree(asleep, 4, 1, kAwake), 2);
EXPECT_EQ(mj_wakeIsland(asleep, 4, 1, kAwake, nullptr, 0), 2);
EXPECT_THAT(AsVector(asleep, 4),
ElementsAre(kAwake, kAwake, kAwake, 3));
EXPECT_EQ(mj_wakeTree(asleep, 4, 3, kAwake), 1);
EXPECT_EQ(mj_wakeIsland(asleep, 4, 3, kAwake, nullptr, 0), 1);
EXPECT_THAT(AsVector(asleep, 4),
ElementsAre(kAwake, kAwake, kAwake, kAwake));
}
TEST_F(SleepTest, BadWakeTree) {
TEST_F(SleepTest, BadWakeIsland) {
EXPECT_FATAL_FAILURE(
([] {
int asleep_bad1[] = {-1, 0};
mj_wakeTree(asleep_bad1, 2, 1, kAwake);
mj_wakeIsland(asleep_bad1, 2, 1, kAwake, nullptr, 0);
}()),
"invalid sleep state index -1 when waking tree 1");
EXPECT_FATAL_FAILURE(
([] {
int asleep_bad2[] = {-1, 2};
mj_wakeTree(asleep_bad2, 2, 1, kAwake);
mj_wakeIsland(asleep_bad2, 2, 1, kAwake, nullptr, 0);
}()),
"invalid sleep state index 2 when waking tree 1");
EXPECT_FATAL_FAILURE(
([] {
int asleep_bad3[] = {1, 2, 1};
mj_wakeTree(asleep_bad3, 3, 0, kAwake);
mj_wakeIsland(asleep_bad3, 3, 0, kAwake, nullptr, 0);
}()),
"tree 0 is not in a cycle");
}
+585 -48
View File
@@ -14,66 +14,603 @@
// Tests for engine/engine_util_errmem.c.
#include <csetjmp>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include <mujoco/mujoco.h>
#include "src/engine/engine_crossplatform.h" // IWYU pragma: keep
#include "src/engine/engine_util_errmem.h"
extern "C" {
MJAPI mjfLogHandler _mjPRIVATE_setTlsLogHandler(mjfLogHandler handler);
}
namespace mujoco {
namespace {
constexpr int kBufferSize = 1024;
// ========================= test infrastructure ==============================
char* ErrorMessageBuffer() {
static char error_message[kBufferSize] = "";
return error_message;
}
char* WarningMessageBuffer() {
static char warning_message[kBufferSize] = "";
return warning_message;
}
void MjErrorHandler(const char* msg) {
if (strnlen(msg, kBufferSize) == kBufferSize) {
FAIL() << "mju_user_error message exceeds maximum length of "
<< kBufferSize;
}
strncpy(ErrorMessageBuffer(), msg, kBufferSize);
}
void MjWarningHandler(const char* msg) {
if (strnlen(msg, kBufferSize) == kBufferSize) {
FAIL() << "mju_user_warning message exceeds maximum length of "
<< kBufferSize;
}
strncpy(WarningMessageBuffer(), msg, kBufferSize);
}
void ClearErrorMessage() { ErrorMessageBuffer()[0] = '\0'; }
void ClearWarningMessage() { WarningMessageBuffer()[0] = '\0'; }
class MujocoErrorAndWarningTest : public ::testing::Test {
public:
MujocoErrorAndWarningTest() {
mju_user_error = MjErrorHandler;
mju_user_warning = MjWarningHandler;
}
~MujocoErrorAndWarningTest() {
mju_user_error = nullptr;
mju_user_warning = nullptr;
}
// captured log message
struct CapturedMsg {
int level;
int topic;
std::string subject;
std::string func;
std::string file;
int line;
std::string body;
};
TEST_F(MujocoErrorAndWarningTest, MjuErrorInternal) {
ClearErrorMessage();
mjERROR("foobar %d", 123);
std::string funcname(__func__);
ASSERT_TRUE(funcname.length());
EXPECT_EQ(std::string(ErrorMessageBuffer()), funcname + ": foobar 123");
// thread-local capture state
static thread_local std::vector<CapturedMsg> captured_msgs;
static thread_local bool capture_longjmp = false;
static thread_local std::jmp_buf capture_jmp_buf;
// log handler that captures messages
void CapturingHandler(const mjLogMessage* msg) {
captured_msgs.push_back({
.level = msg->level,
.topic = msg->topic,
.subject = msg->subject,
.func = msg->func ? msg->func : "",
.file = msg->file ? msg->file : "",
.line = msg->line,
.body = msg->body ? msg->body : "",
});
// longjmp on error to prevent exit()
if (msg->level == mjLOG_ERROR && capture_longjmp) {
std::longjmp(capture_jmp_buf, 1);
}
}
// RAII guard: installs capturing handler, restores previous on destruction
class ScopedCapture {
public:
ScopedCapture() {
captured_msgs.clear();
capture_longjmp = true;
prev_ = _mjPRIVATE_setTlsLogHandler(CapturingHandler);
}
~ScopedCapture() {
_mjPRIVATE_setTlsLogHandler(prev_);
capture_longjmp = false;
}
const std::vector<CapturedMsg>& msgs() const { return captured_msgs; }
private:
mjfLogHandler prev_;
};
// ========================= mju_setLogHandler tests ==========================
TEST(LogHandlerTest, SetLogHandlerReturnsOldHandler) {
mjfLogHandler old = mju_setLogHandler(CapturingHandler);
EXPECT_NE(old, nullptr); // default handler is non-null
mjfLogHandler prev = mju_setLogHandler(nullptr);
EXPECT_EQ(prev, CapturingHandler);
// NULL restores default (non-null)
mjfLogHandler restored = mju_setLogHandler(nullptr);
EXPECT_NE(restored, nullptr);
}
TEST(LogHandlerTest, CustomHandlerReceivesWarning) {
ScopedCapture cap;
mju_warning("test warning %d", 42);
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].level, mjLOG_WARNING);
EXPECT_EQ(cap.msgs()[0].subject, "test warning 42");
}
TEST(LogHandlerTest, CustomHandlerReceivesError) {
ScopedCapture cap;
if (setjmp(capture_jmp_buf) == 0) {
mju_error("test error %s", "foo");
}
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].level, mjLOG_ERROR);
EXPECT_EQ(cap.msgs()[0].subject, "test error foo");
}
TEST(LogHandlerTest, WarningRecursionGuard) {
static thread_local int call_count = 0;
call_count = 0;
auto recursive_handler = +[](const mjLogMessage* msg) {
call_count++;
mju_warning("recursive warning");
};
auto old_tls = _mjPRIVATE_setTlsLogHandler(recursive_handler);
mju_warning("initial warning");
_mjPRIVATE_setTlsLogHandler(old_tls);
EXPECT_EQ(call_count, 1);
}
// ========================= TLS handler override tests =======================
TEST(TlsHandlerTest, TlsOverridesGlobal) {
// install a global handler
auto global_prev = mju_setLogHandler(CapturingHandler);
// install a different TLS handler
static thread_local bool tls_called = false;
auto tls_handler = +[](const mjLogMessage* msg) {
tls_called = true;
if (msg->level == mjLOG_ERROR) {
std::longjmp(capture_jmp_buf, 1);
}
};
tls_called = false;
captured_msgs.clear();
auto old_tls = _mjPRIVATE_setTlsLogHandler(tls_handler);
if (setjmp(capture_jmp_buf) == 0) {
mju_error("tls test");
}
// TLS handler should have been called, not the global one
EXPECT_TRUE(tls_called);
EXPECT_EQ(captured_msgs.size(), 0);
_mjPRIVATE_setTlsLogHandler(old_tls);
mju_setLogHandler(global_prev);
}
TEST(TlsHandlerTest, NullTlsFallsBackToGlobal) {
auto global_prev = mju_setLogHandler(CapturingHandler);
captured_msgs.clear();
// ensure TLS is null
auto old_tls = _mjPRIVATE_setTlsLogHandler(nullptr);
mju_warning("fallback test");
// global capturing handler should have been called
ASSERT_EQ(captured_msgs.size(), 1);
EXPECT_EQ(captured_msgs[0].subject, "fallback test");
_mjPRIVATE_setTlsLogHandler(old_tls);
mju_setLogHandler(global_prev);
}
TEST(TlsHandlerTest, SetTlsReturnsPrevious) {
auto h1 = _mjPRIVATE_setTlsLogHandler(CapturingHandler);
auto h2 = _mjPRIVATE_setTlsLogHandler(nullptr);
EXPECT_EQ(h2, CapturingHandler);
_mjPRIVATE_setTlsLogHandler(h1);
}
// ========================= mju_info and topic filtering =====================
TEST(InfoTest, InfoMessageReachesHandler) {
ScopedCapture cap;
mju_info(mjTOPIC_NONE, "info message %d", 7);
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].level, mjLOG_INFO);
EXPECT_EQ(cap.msgs()[0].topic, mjTOPIC_NONE);
EXPECT_EQ(cap.msgs()[0].subject, "info message 7");
}
TEST(InfoTest, TopicZeroAlwaysPasses) {
// topic 0 (NONE) should pass regardless of config
ScopedCapture cap;
mju_info(mjTOPIC_NONE, "always passes");
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].subject, "always passes");
}
// ========================= mjLogConfig tests ================================
TEST(LogConfigTest, GetSetRoundtrip) {
mjLogConfig orig = mju_getLogConfig();
mjLogConfig custom = {.logto_console = false,
.logto_file = false,
.logfile = "",
.topics = (1 << 0) | (1 << 1)};
mju_setLogConfig(custom);
mjLogConfig readback = mju_getLogConfig();
EXPECT_EQ(readback.logto_console, false);
EXPECT_EQ(readback.logto_file, false);
EXPECT_STREQ(readback.logfile, "");
EXPECT_EQ(readback.topics, (1 << 0) | (1 << 1));
// restore
mju_setLogConfig(orig);
}
TEST(LogConfigTest, InitFromEnv) {
mjLogConfig orig = mju_getLogConfig();
setenv("MUJOCO_LOG_TOPICS", "sleep,time_cmp", 1);
mju_clearHandlers();
mjLogConfig config = mju_getLogConfig();
EXPECT_EQ(config.topics, (1 << 1) | (1 << 2));
unsetenv("MUJOCO_LOG_TOPICS");
mju_clearHandlers();
mju_setLogConfig(orig);
}
// ========================= topic filtering in default handler ===============
TEST(TopicFilterTest, DefaultHandlerFiltersDisabledTopic) {
// save and configure: disable all topics, route to capture
mjLogConfig orig = mju_getLogConfig();
mjLogConfig no_topics = {
.logto_console = false, .logto_file = false, .logfile = "", .topics = 0};
mju_setLogConfig(no_topics);
auto global_prev = mju_setLogHandler(nullptr); // restore default handler
// send info with a topic — should be filtered by default handler
// we need to observe absence of output, so we'll use a custom global handler
mju_setLogHandler(CapturingHandler);
captured_msgs.clear();
// make sure TLS is null so global handler is used
auto old_tls = _mjPRIVATE_setTlsLogHandler(nullptr);
mju_info(mjTOPIC_SLEEP, "should be filtered");
// TLS handler (none) -> global handler (CapturingHandler)
// But topic filtering happens inside the handler dispatch, not in mju_info
// mju_info just dispatches; the *default* handler filters.
// Since we installed CapturingHandler (not default), it won't filter.
// To test default handler filtering, we'd need the default handler.
// Instead, test that the topic field is correctly set.
ASSERT_EQ(captured_msgs.size(), 1);
EXPECT_EQ(captured_msgs[0].topic, mjTOPIC_SLEEP);
_mjPRIVATE_setTlsLogHandler(old_tls);
mju_setLogHandler(global_prev);
mju_setLogConfig(orig);
}
// ========================= mjERROR macro tests ==============================
TEST(MjErrorMacroTest, HasSourceLocation) {
ScopedCapture cap;
if (setjmp(capture_jmp_buf) == 0) {
mjERROR("macro error %d", 99);
}
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].level, mjLOG_ERROR);
EXPECT_EQ(cap.msgs()[0].subject, "macro error 99");
EXPECT_FALSE(cap.msgs()[0].func.empty());
EXPECT_FALSE(cap.msgs()[0].file.empty());
EXPECT_GT(cap.msgs()[0].line, 0);
}
TEST(MjErrorMacroTest, FileIsPath) {
ScopedCapture cap;
if (setjmp(capture_jmp_buf) == 0) {
mjERROR("path test");
}
ASSERT_EQ(cap.msgs().size(), 1);
// file should be a path containing the filename
EXPECT_NE(cap.msgs()[0].file.find("engine_util_errmem_test.cc"),
std::string::npos);
}
// ========================= mju_clearHandlers ================================
TEST(ClearHandlersTest, RestoresDefaults) {
// set some custom state
mju_user_error = [](const char*) {};
mju_user_warning = [](const char*) {};
mju_clearHandlers();
EXPECT_EQ(mju_user_error, nullptr);
EXPECT_EQ(mju_user_warning, nullptr);
EXPECT_EQ(mju_user_malloc, nullptr);
EXPECT_EQ(mju_user_free, nullptr);
// log config should be restored to defaults
mjLogConfig config = mju_getLogConfig();
EXPECT_TRUE(config.logto_console);
EXPECT_STREQ(config.logfile, "MUJOCO_LOG.TXT");
}
// ========================= mju_message ======================================
TEST(MessageTest, DispatchesStructuredMessage) {
ScopedCapture cap;
mjLogMessage msg = {};
msg.level = mjLOG_WARNING;
snprintf(msg.subject, sizeof(msg.subject), "raw warning");
msg.func = "TestFunc";
msg.file = "test.c";
msg.line = 42;
mju_message(&msg);
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].level, mjLOG_WARNING);
EXPECT_EQ(cap.msgs()[0].subject, "raw warning");
EXPECT_EQ(cap.msgs()[0].func, "TestFunc");
EXPECT_EQ(cap.msgs()[0].file, "test.c");
EXPECT_EQ(cap.msgs()[0].line, 42);
}
TEST(MessageTest, DispatchesError) {
ScopedCapture cap;
mjLogMessage msg = {};
msg.level = mjLOG_ERROR;
snprintf(msg.subject, sizeof(msg.subject), "raw error");
if (setjmp(capture_jmp_buf) == 0) {
mju_message(&msg);
}
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].level, mjLOG_ERROR);
EXPECT_EQ(cap.msgs()[0].subject, "raw error");
}
// ========================= legacy handler compatibility =====================
TEST(LegacyCompatTest, LegacyErrorHandlerViaDefault) {
static thread_local std::string legacy_msg;
legacy_msg.clear();
auto old_tls = _mjPRIVATE_setTlsLogHandler(nullptr);
auto old_global = mju_setLogHandler(nullptr); // default handler
mju_user_error = [](const char* msg) {
legacy_msg = msg;
// legacy error handlers are expected to not return; longjmp to simulate
std::longjmp(capture_jmp_buf, 1);
};
if (setjmp(capture_jmp_buf) == 0) {
mju_error("legacy test %d", 1);
}
EXPECT_EQ(legacy_msg, "legacy test 1");
mju_user_error = nullptr;
mju_setLogHandler(old_global);
_mjPRIVATE_setTlsLogHandler(old_tls);
}
TEST(LegacyCompatTest, LegacyWarningHandlerViaDefault) {
static thread_local std::string legacy_msg;
legacy_msg.clear();
auto old_tls = _mjPRIVATE_setTlsLogHandler(nullptr);
auto old_global = mju_setLogHandler(nullptr); // default handler
mju_user_warning = [](const char* msg) {
legacy_msg = msg;
};
mju_warning("legacy warn %s", "bar");
EXPECT_EQ(legacy_msg, "legacy warn bar");
mju_user_warning = nullptr;
mju_setLogHandler(old_global);
_mjPRIVATE_setTlsLogHandler(old_tls);
}
// ========================= truncation tests (from original) =================
TEST(TruncationTest, MjuErrorInternal) {
ScopedCapture cap;
if (setjmp(capture_jmp_buf) == 0) {
mjERROR("foobar %d", 123);
}
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].subject, "foobar 123");
// func field should contain the test function name
EXPECT_FALSE(cap.msgs()[0].func.empty());
}
// ========================= mjDEBUG macro tests ==============================
TEST(MjDebugMacroTest, EmitsDebugMessage) {
mjLogConfig orig = mju_getLogConfig();
mjLogConfig cfg = orig;
cfg.topics = (1 << (mjTOPIC_SLEEP - 1));
mju_setLogConfig(cfg);
ScopedCapture cap;
mjDEBUG(mjTOPIC_SLEEP, "debug msg %d", 77);
ASSERT_EQ(cap.msgs().size(), 1);
EXPECT_EQ(cap.msgs()[0].level, mjLOG_DEBUG);
EXPECT_EQ(cap.msgs()[0].topic, mjTOPIC_SLEEP);
EXPECT_EQ(cap.msgs()[0].subject, "debug msg 77");
EXPECT_FALSE(cap.msgs()[0].func.empty());
mju_setLogConfig(orig);
}
TEST(MjDebugMacroTest, FilteredByProducerSideCheck) {
mjLogConfig orig = mju_getLogConfig();
mjLogConfig cfg = orig;
cfg.topics = 0;
mju_setLogConfig(cfg);
ScopedCapture cap;
mjDEBUG(mjTOPIC_SLEEP, "should be filtered");
EXPECT_EQ(cap.msgs().size(), 0);
mju_setLogConfig(orig);
}
// ========================= BaseName tests ===================================
TEST(BaseNameTest, UnixPath) {
EXPECT_STREQ(BaseName("/path/to/file.c"), "file.c");
}
TEST(BaseNameTest, WindowsPath) {
EXPECT_STREQ(BaseName("C:\\path\\to\\file.c"), "file.c");
}
TEST(BaseNameTest, NoSeparator) {
EXPECT_STREQ(BaseName("file.c"), "file.c");
}
TEST(BaseNameTest, MixedSeparators) {
EXPECT_STREQ(BaseName("/mixed\\path/file.c"), "file.c");
EXPECT_STREQ(BaseName("C:\\mixed/path\\file.c"), "file.c");
}
TEST(BaseNameTest, EmptyString) {
EXPECT_STREQ(BaseName(""), "");
}
// ========================= default handler topic filtering ==================
TEST(DefaultHandlerFilterTest, DisabledTopicNotWrittenToFile) {
mjLogConfig orig = mju_getLogConfig();
auto old_tls = _mjPRIVATE_setTlsLogHandler(nullptr);
auto old_global = mju_setLogHandler(nullptr);
std::string logpath = std::string(::testing::TempDir()) + "/filter_test.txt";
std::remove(logpath.c_str());
mjLogConfig cfg = {};
cfg.logto_console = false;
cfg.logto_file = true;
cfg.topics = 0;
snprintf(cfg.logfile, sizeof(cfg.logfile), "%s", logpath.c_str());
mju_setLogConfig(cfg);
// disabled topic: should be filtered by default handler
mju_info(mjTOPIC_SLEEP, "filtered message");
std::FILE* fp = std::fopen(logpath.c_str(), "r");
bool file_empty = true;
if (fp) {
std::fseek(fp, 0, SEEK_END);
file_empty = (std::ftell(fp) == 0);
std::fclose(fp);
}
EXPECT_TRUE(file_empty) << "disabled topic should not write to logfile";
// enable the topic: should pass through
cfg.topics = (1 << (mjTOPIC_SLEEP - 1));
mju_setLogConfig(cfg);
mju_info(mjTOPIC_SLEEP, "passed message");
fp = std::fopen(logpath.c_str(), "r");
ASSERT_NE(fp, nullptr);
std::fseek(fp, 0, SEEK_END);
EXPECT_GT(std::ftell(fp), 0) << "enabled topic should write to logfile";
std::fclose(fp);
std::remove(logpath.c_str());
mju_setLogConfig(orig);
mju_setLogHandler(old_global);
_mjPRIVATE_setTlsLogHandler(old_tls);
}
// ========================= timing diagnostics ===============================
TEST(TimingDiagnosticsTest, EmitsOnReset) {
const char xml[] =
"<mujoco><worldbody><body><geom size=\"1\"/></body></worldbody></mujoco>";
char error[1024] = "";
mjVFS vfs;
mj_defaultVFS(&vfs);
mj_addBufferVFS(&vfs, "m.xml", xml, static_cast<int>(std::strlen(xml)));
mjModel* m = mj_loadXML("m.xml", &vfs, error, sizeof(error));
mj_deleteVFS(&vfs);
ASSERT_NE(m, nullptr) << error;
mjData* d = mj_makeData(m);
ASSERT_NE(d, nullptr);
// populate timers manually
d->timer[mjTIMER_STEP].number = 100;
d->timer[mjTIMER_STEP].duration = 0.01;
d->timer[mjTIMER_POSITION].number = 100;
d->timer[mjTIMER_POSITION].duration = 0.005;
// capture log messages during reset
ScopedCapture cap;
mj_resetData(m, d);
// find the timing diagnostics message
const CapturedMsg* tmsg = nullptr;
for (const auto& msg : cap.msgs()) {
if (msg.level == mjLOG_INFO && msg.topic == mjTOPIC_TIME_STP) {
tmsg = &msg;
break;
}
}
ASSERT_NE(tmsg, nullptr) << "expected timing diagnostics message";
EXPECT_NE(tmsg->subject.find("100 steps"), std::string::npos);
EXPECT_FALSE(tmsg->body.empty());
EXPECT_NE(tmsg->body.find("total"), std::string::npos);
mj_deleteData(d);
mj_deleteModel(m);
mj_freeLastXML();
}
// ========================= env var edge cases ===============================
TEST(InitFromEnvEdgeTest, EmptyString) {
mjLogConfig orig = mju_getLogConfig();
setenv("MUJOCO_LOG_TOPICS", "", 1);
mju_clearHandlers();
EXPECT_EQ(mju_getLogConfig().topics, 0);
unsetenv("MUJOCO_LOG_TOPICS");
mju_clearHandlers();
mju_setLogConfig(orig);
}
TEST(InitFromEnvEdgeTest, UnknownTopic) {
mjLogConfig orig = mju_getLogConfig();
setenv("MUJOCO_LOG_TOPICS", "nonexistent", 1);
mju_clearHandlers();
EXPECT_EQ(mju_getLogConfig().topics, 0);
unsetenv("MUJOCO_LOG_TOPICS");
mju_clearHandlers();
mju_setLogConfig(orig);
}
TEST(InitFromEnvEdgeTest, ExtraCommas) {
mjLogConfig orig = mju_getLogConfig();
setenv("MUJOCO_LOG_TOPICS", ",,sleep,,", 1);
mju_clearHandlers();
EXPECT_EQ(mju_getLogConfig().topics, (1 << 2));
unsetenv("MUJOCO_LOG_TOPICS");
mju_clearHandlers();
mju_setLogConfig(orig);
}
TEST(InitFromEnvEdgeTest, WhitespaceHandling) {
mjLogConfig orig = mju_getLogConfig();
setenv("MUJOCO_LOG_TOPICS", " sleep , time_cmp ", 1);
mju_clearHandlers();
EXPECT_EQ(mju_getLogConfig().topics, (1 << 1) | (1 << 2));
unsetenv("MUJOCO_LOG_TOPICS");
mju_clearHandlers();
mju_setLogConfig(orig);
}
} // namespace
-3
View File
@@ -21,12 +21,10 @@
#include <cstring>
#include <filesystem> // NOLINT
#include <fstream>
#include <limits>
#include <sstream>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
@@ -39,7 +37,6 @@
#include <absl/strings/str_join.h>
#include <absl/synchronization/mutex.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjxmacro.h>
#include <mujoco/mujoco.h>
#include "src/xml/xml_global.h"
+7 -9
View File
@@ -34,8 +34,7 @@
#include <mujoco/mujoco.h>
extern "C" {
MJAPI void _mjPRIVATE__set_tls_error_fn(decltype(mju_user_error));
MJAPI decltype(mju_user_error) _mjPRIVATE__get_tls_error_fn();
MJAPI mjfLogHandler _mjPRIVATE_setTlsLogHandler(mjfLogHandler handler);
}
namespace mujoco {
@@ -138,21 +137,20 @@ auto MjuErrorMessageFrom(Return (*func)(Args...)) {
thread_local std::jmp_buf current_jmp_buf;
thread_local char err_msg[1000];
auto* old_error_handler = _mjPRIVATE__get_tls_error_fn();
auto* new_error_handler = +[](const char* msg) -> void {
std::strncpy(err_msg, msg, sizeof(err_msg));
auto new_error_handler = +[](const mjLogMessage* msg) -> void {
if (msg->level != mjLOG_ERROR) return;
std::strncpy(err_msg, msg->subject, sizeof(err_msg));
std::longjmp(current_jmp_buf, 1);
};
return [func, old_error_handler,
new_error_handler](Args... args) -> std::string {
return [func, new_error_handler](Args... args) -> std::string {
auto old_handler = _mjPRIVATE_setTlsLogHandler(new_error_handler);
if (setjmp(current_jmp_buf) == 0) {
err_msg[0] = '\0';
_mjPRIVATE__set_tls_error_fn(new_error_handler);
func(args...);
}
_mjPRIVATE__set_tls_error_fn(old_error_handler);
_mjPRIVATE_setTlsLogHandler(old_handler);
return err_msg;
};
}
+48
View File
@@ -516,6 +516,19 @@ public enum mjtSleepState : int{
mjS_ASLEEP = 0,
mjS_AWAKE = 1,
}
public enum mjtLogLevel : int{
mjLOG_DEBUG = 0,
mjLOG_INFO = 1,
mjLOG_WARNING = 2,
mjLOG_ERROR = 3,
}
public enum mjtLogTopic : int{
mjTOPIC_NONE = 0,
mjTOPIC_TIME_STP = 1,
mjTOPIC_TIME_CMP = 2,
mjTOPIC_SLEEP = 3,
mjNTOPIC = 3,
}
public enum mjtGeomInertia : int{
mjINERTIA_VOLUME = 0,
mjINERTIA_SHELL = 1,
@@ -803,6 +816,26 @@ public enum mjtSection : int{
// -------------------------------struct declarations---------------------------
[StructLayout(LayoutKind.Sequential)]
public unsafe struct mjLogMessage_ {
public int level;
public int topic;
public fixed char subject[1024];
public char* body;
public char* func;
public char* file;
public int line;
public byte timestamp;
}
[StructLayout(LayoutKind.Sequential)]
public unsafe struct mjLogConfig_ {
public byte logto_console;
public byte logto_file;
public fixed char logfile[1024];
public int topics;
}
[StructLayout(LayoutKind.Sequential)]
public unsafe struct mjLROpt_ {
public int mode;
@@ -7267,6 +7300,21 @@ public static unsafe extern void mju_warning([MarshalAs(UnmanagedType.LPStr)]str
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern void mju_clearHandlers();
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern IntPtr mju_setLogHandler(IntPtr handler);
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern mjLogConfig_ mju_getLogConfig();
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern void mju_setLogConfig(mjLogConfig_ config);
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern void mju_info(int topic, [MarshalAs(UnmanagedType.LPStr)]string msg);
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern void mju_message(mjLogMessage_* msg);
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
public static unsafe extern void* mju_malloc(UIntPtr size);
+180
View File
@@ -597,6 +597,90 @@ struct MjLROpt {
bool owned_ = false;
};
struct MjLogConfig {
~MjLogConfig();
MjLogConfig();
explicit MjLogConfig(mjLogConfig *ptr);
MjLogConfig(const MjLogConfig &);
MjLogConfig &operator=(const MjLogConfig &);
std::unique_ptr<MjLogConfig> copy();
mjLogConfig* get() const;
void set(mjLogConfig* ptr);
mjtBool logto_console() const {
return ptr_->logto_console;
}
void set_logto_console(mjtBool value) {
ptr_->logto_console = value;
}
mjtBool logto_file() const {
return ptr_->logto_file;
}
void set_logto_file(mjtBool value) {
ptr_->logto_file = value;
}
emscripten::val logfile() const {
return emscripten::val(emscripten::typed_memory_view(1024, ptr_->logfile));
}
int topics() const {
return ptr_->topics;
}
void set_topics(int value) {
ptr_->topics = value;
}
private:
mjLogConfig* ptr_;
bool owned_ = false;
};
struct MjLogMessage {
~MjLogMessage();
MjLogMessage();
explicit MjLogMessage(mjLogMessage *ptr);
mjLogMessage* get() const;
void set(mjLogMessage* ptr);
int level() const {
return ptr_->level;
}
void set_level(int value) {
ptr_->level = value;
}
int topic() const {
return ptr_->topic;
}
void set_topic(int value) {
ptr_->topic = value;
}
emscripten::val subject() const {
return emscripten::val(emscripten::typed_memory_view(1024, ptr_->subject));
}
std::string body() const {
return ptr_->body ? std::string(ptr_->body) : "";
}
std::string func() const {
return ptr_->func ? std::string(ptr_->func) : "";
}
std::string file() const {
return ptr_->file ? std::string(ptr_->file) : "";
}
int line() const {
return ptr_->line;
}
void set_line(int value) {
ptr_->line = value;
}
mjtBool timestamp() const {
return ptr_->timestamp;
}
void set_timestamp(mjtBool value) {
ptr_->timestamp = value;
}
private:
mjLogMessage* ptr_;
bool owned_ = false;
};
struct MjOption {
~MjOption();
MjOption();
@@ -7194,6 +7278,51 @@ void MjLROpt::set(mjLROpt* ptr) {
ptr_ = ptr;
}
MjLogConfig::MjLogConfig(mjLogConfig *ptr) : ptr_(ptr) {}
MjLogConfig::~MjLogConfig() {
if (owned_ && ptr_) {
delete ptr_;
}
}
MjLogConfig::MjLogConfig() : ptr_(new mjLogConfig()) {
owned_ = true;
}
MjLogConfig::MjLogConfig(const MjLogConfig &other) : MjLogConfig() {
*ptr_ = *other.get();
}
MjLogConfig& MjLogConfig::operator=(const MjLogConfig &other) {
if (this == &other) {
return *this;
}
*ptr_ = *other.get();
return *this;
}
std::unique_ptr<MjLogConfig> MjLogConfig::copy() {
return std::make_unique<MjLogConfig>(*this);
}
mjLogConfig* MjLogConfig::get() const {
return ptr_;
}
void MjLogConfig::set(mjLogConfig* ptr) {
ptr_ = ptr;
}
MjLogMessage::MjLogMessage(mjLogMessage *ptr) : ptr_(ptr) {}
MjLogMessage::~MjLogMessage() {
if (owned_ && ptr_) {
delete ptr_;
}
}
MjLogMessage::MjLogMessage() : ptr_(new mjLogMessage()) {
owned_ = true;
}
mjLogMessage* MjLogMessage::get() const {
return ptr_;
}
void MjLogMessage::set(mjLogMessage* ptr) {
ptr_ = ptr;
}
MjOption::MjOption(mjOption *ptr) : ptr_(ptr) {}
MjOption::~MjOption() {
if (owned_ && ptr_) {
@@ -8552,6 +8681,11 @@ int mj_setLengthRange_wrapper(const MjModel& m, const MjData& d, int index, cons
return result;
}
void mju_info_wrapper(int topic, const String& msg) {
CHECK_VAL(msg);
mju_info(topic, "%s", msg.as<const std::string>().data());
}
void mj_Euler_wrapper(const MjModel& m, MjData& d) {
mj_Euler(m.get(), d.get());
}
@@ -10364,6 +10498,12 @@ void mju_fill_wrapper(const val& res, mjtNum val) {
mju_fill(res_.data(), val, n);
}
MjLogConfig mju_getLogConfig_wrapper() {
MjLogConfig result;
*result.get() = mju_getLogConfig();
return result;
}
void mju_insertionSort_wrapper(const val& list) {
UNPACK_VALUE(mjtNum, list);
int n = list_.size();
@@ -10393,6 +10533,10 @@ int mju_mat2Rot_wrapper(const val& quat, const NumberArray& mat) {
return mju_mat2Rot(quat_.data(), mat_.data());
}
void mju_message_wrapper(const MjLogMessage& msg) {
mju_message(msg.get());
}
void mju_mulMatMat_wrapper(const val& res, const NumberArray& mat1, const NumberArray& mat2, int r1, int c1, int c2) {
UNPACK_VALUE(mjtNum, res);
UNPACK_ARRAY(mjtNum, mat1);
@@ -10642,6 +10786,10 @@ void mju_scl3_wrapper(const val& res, const NumberArray& vec, mjtNum scl) {
mju_scl3(res_.data(), vec_.data(), scl);
}
void mju_setLogConfig_wrapper(const MjLogConfig& config) {
mju_setLogConfig(*config.get());
}
void mju_sparse2dense_wrapper(const val& res, const NumberArray& mat, int nr, int nc, const NumberArray& rownnz, const NumberArray& rowadr, const NumberArray& colind) {
UNPACK_VALUE(mjtNum, res);
UNPACK_ARRAY(mjtNum, mat);
@@ -11258,6 +11406,17 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.value("mjLIMITED_FALSE", mjLIMITED_FALSE)
.value("mjLIMITED_TRUE", mjLIMITED_TRUE)
.value("mjLIMITED_AUTO", mjLIMITED_AUTO);
enum_<mjtLogLevel>("mjtLogLevel")
.value("mjLOG_DEBUG", mjLOG_DEBUG)
.value("mjLOG_INFO", mjLOG_INFO)
.value("mjLOG_WARNING", mjLOG_WARNING)
.value("mjLOG_ERROR", mjLOG_ERROR);
enum_<mjtLogTopic>("mjtLogTopic")
.value("mjTOPIC_NONE", mjTOPIC_NONE)
.value("mjTOPIC_TIME_STP", mjTOPIC_TIME_STP)
.value("mjTOPIC_TIME_CMP", mjTOPIC_TIME_CMP)
.value("mjTOPIC_SLEEP", mjTOPIC_SLEEP)
.value("mjNTOPIC", mjNTOPIC);
enum_<mjtMark>("mjtMark")
.value("mjMARK_NONE", mjMARK_NONE)
.value("mjMARK_EDGE", mjMARK_EDGE)
@@ -11817,6 +11976,23 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.property("tolrange", &MjLROpt::tolrange, &MjLROpt::set_tolrange, reference())
.property("useexisting", &MjLROpt::useexisting, &MjLROpt::set_useexisting, reference())
.property("uselimit", &MjLROpt::uselimit, &MjLROpt::set_uselimit, reference());
emscripten::class_<MjLogConfig>("MjLogConfig")
.constructor<>()
.function("copy", &MjLogConfig::copy, take_ownership())
.property("logfile", &MjLogConfig::logfile)
.property("logto_console", &MjLogConfig::logto_console, &MjLogConfig::set_logto_console, reference())
.property("logto_file", &MjLogConfig::logto_file, &MjLogConfig::set_logto_file, reference())
.property("topics", &MjLogConfig::topics, &MjLogConfig::set_topics, reference());
emscripten::class_<MjLogMessage>("MjLogMessage")
.constructor<>()
.property("body", &MjLogMessage::body, reference())
.property("file", &MjLogMessage::file, reference())
.property("func", &MjLogMessage::func, reference())
.property("level", &MjLogMessage::level, &MjLogMessage::set_level, reference())
.property("line", &MjLogMessage::line, &MjLogMessage::set_line, reference())
.property("subject", &MjLogMessage::subject)
.property("timestamp", &MjLogMessage::timestamp, &MjLogMessage::set_timestamp, reference())
.property("topic", &MjLogMessage::topic, &MjLogMessage::set_topic, reference());
emscripten::class_<MjModel>("MjModel")
// mj_loadXML is deprecated and will be removed in a future release
.class_function("mj_loadXML", emscripten::select_overload<std::unique_ptr<MjModel>(std::string)>(&mj_loadXML_wrapper_1))
@@ -13534,6 +13710,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mju_eye", &mju_eye_wrapper);
function("mju_f2n", &mju_f2n_wrapper);
function("mju_fill", &mju_fill_wrapper);
function("mju_getLogConfig", &mju_getLogConfig_wrapper);
function("mju_insertionSort", &mju_insertionSort_wrapper);
function("mju_insertionSortInt", &mju_insertionSortInt_wrapper);
function("mju_isBad", &mju_isBad);
@@ -13541,6 +13718,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mju_mat2Quat", &mju_mat2Quat_wrapper);
function("mju_mat2Rot", &mju_mat2Rot_wrapper);
function("mju_max", &mju_max);
function("mju_message", &mju_message_wrapper);
function("mju_min", &mju_min);
function("mju_mulMatMat", &mju_mulMatMat_wrapper);
function("mju_mulMatMatT", &mju_mulMatMatT_wrapper);
@@ -13577,6 +13755,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mju_round", &mju_round);
function("mju_scl", &mju_scl_wrapper);
function("mju_scl3", &mju_scl3_wrapper);
function("mju_setLogConfig", &mju_setLogConfig_wrapper);
function("mju_sigmoid", &mju_sigmoid);
function("mju_sign", &mju_sign);
function("mju_sparse2dense", &mju_sparse2dense_wrapper);
@@ -13636,6 +13815,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mj_saveModel", &mj_saveModel_wrapper);
function("mj_saveLastXML", &mj_saveLastXML_wrapper);
function("mj_setLengthRange", &mj_setLengthRange_wrapper);
function("mju_info", &mju_info_wrapper);
// mj_compile is bound using two overloads to handle the optional MjVFS argument,
// as using std::optional<MjVFS> caused memory errors due to missing copy/move constructors.
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&)>(&mj_compile_wrapper_1));
+18 -1
View File
@@ -15,7 +15,9 @@
"""Utility functions for code generation."""
import os
from typing import Union
from introspect import ast_nodes
from wasm.codegen.generators import constants
def write_to_file(filepath: str, content: str) -> None:
@@ -87,12 +89,27 @@ def get_inner_value_type(
return param.type.inner_type
def is_struct_value_type(
t: Union[ast_nodes.ValueType, ast_nodes.ArrayType, ast_nodes.PointerType],
) -> bool:
"""Checks if a type is a struct passed by value."""
if isinstance(t, ast_nodes.ValueType):
return (
t.name not in constants.PRIMITIVE_TYPES
and t.name != "void"
and not t.name.startswith("mjf")
)
return False
def should_be_wrapped(func: ast_nodes.FunctionDecl) -> bool:
"""Checks if a MuJoCo function needs a wrapper function."""
if get_pointer_return_inner_value_type(func):
return True
if is_struct_value_type(func.return_type):
return True
for param in func.parameters:
if get_inner_value_type(param):
if get_inner_value_type(param) or is_struct_value_type(param.type):
return True
return False
+4
View File
@@ -149,6 +149,7 @@ _SKIPPED_MEMORY_FUNCTIONS: tuple[str, ...] = (
"mju_error",
"mju_free",
"mju_malloc",
"mju_setLogHandler",
"mju_strncpy",
"mju_warning",
# go/keep-sorted end
@@ -230,6 +231,7 @@ MANUAL_WRAPPER_FUNCTIONS: tuple[str, ...] = (
"mj_saveModel",
"mj_setLengthRange",
"mju_error",
"mju_info",
# go/keep-sorted end
)
@@ -276,6 +278,8 @@ STRUCTS_TO_BIND: list[str] = list(
NO_DEFAULT_CONSTRUCTORS: tuple[str, ...] = (
# go/keep-sorted start
"mjContact",
"mjLogConfig",
"mjLogMessage",
"mjPreContact",
"mjSolverStat",
"mjStatistic",
+21
View File
@@ -154,6 +154,14 @@ def get_param_string(p: ast_nodes.FunctionParameterDecl) -> str:
return f"const NumberArray& {p.name}"
else:
return f"const val& {p.name}"
elif common.is_struct_value_type(p.type):
# Struct by value parameters
value_type = cast(ast_nodes.ValueType, p.type)
const_qualifier = "const " if value_type.is_const else "const "
return (
f"{const_qualifier}{common.capitalize(value_type.name)}&"
f" {p.name}"
)
else:
# This case should ideally not be reached if AST is well-formed
# and types are categorized by the helper booleans correctly.
@@ -183,6 +191,8 @@ def get_params_string_maybe_with_conversion(
native_params.append(f"{p.name}.get()")
elif param_is_primitive_value(p):
native_params.append(p.name)
elif common.is_struct_value_type(p.type):
native_params.append(f"*{p.name}.get()")
else:
raise TypeError(
f"Unhandled parameter type for conversion: {p.type} for param"
@@ -201,6 +211,14 @@ def get_compatible_return_code(func: ast_nodes.FunctionDecl) -> str:
return f"{c_call};"
if func.return_type.name in constants.PRIMITIVE_TYPES:
return f"return {c_call};"
if common.is_struct_value_type(func.return_type):
struct_name = func.return_type.name
w = common.wrapped_struct_name(struct_name)
builder = code_builder.CodeBuilder()
builder.line(f"{w} result;")
builder.line(f"*result.get() = {c_call};")
builder.line("return result;")
return builder.to_string()
if inner_type := common.get_pointer_return_inner_value_type(func):
if inner_type.name == "char":
@@ -230,6 +248,9 @@ def get_compatible_return_type(func: ast_nodes.FunctionDecl) -> str:
and func.return_type.name in constants.PRIMITIVE_TYPES
):
return f"{func.return_type.name}"
if common.is_struct_value_type(func.return_type):
return_type = cast(ast_nodes.ValueType, func.return_type)
return common.wrapped_struct_name(return_type.name)
return "val"
+30
View File
@@ -229,6 +229,36 @@ def _generate_field_data(
binding=_get_property_binding(f, w, setter=False, reference=True),
)
# Case 2.5: const char* without array_extent (C strings like body, func).
elif (
inner_type_name == "char"
and not is_dynamically_sized
):
builder = code_builder.CodeBuilder()
if f.type.inner_type.is_const:
with builder.function(f"std::string {f.name}() const"):
builder.line(
f'return ptr_->{f.name} ? std::string(ptr_->{f.name}) : "";'
)
else:
with builder.function(f"std::string {f.name}() const"):
builder.line(
f'return ptr_->{f.name} ? std::string(ptr_->{f.name}) : "";'
)
with builder.function(f"void set_{f.name}(const std::string& value)"):
with builder.block(f"if (ptr_->{f.name})"):
builder.line(
f"std::strncpy(ptr_->{f.name}, value.c_str(),"
f" sizeof(ptr_->{f.name}));"
)
return WrappedFieldData(
declaration=builder.to_string(),
typename=_get_field_struct_type(f, s),
binding=_get_property_binding(
f, w, setter=not f.type.inner_type.is_const, reference=True
),
)
# Case 3: Non-dynamically sized pointer fields to other structs.
elif (
not is_dynamically_sized
+6
View File
@@ -833,6 +833,11 @@ int mj_setLengthRange_wrapper(const MjModel& m, const MjData& d, int index, cons
return result;
}
void mju_info_wrapper(int topic, const String& msg) {
CHECK_VAL(msg);
mju_info(topic, "%s", msg.as<const std::string>().data());
}
// {{ WRAPPER_FUNCTIONS }}
EMSCRIPTEN_BINDINGS(mujoco_bindings) {
@@ -879,6 +884,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
function("mj_saveModel", &mj_saveModel_wrapper);
function("mj_saveLastXML", &mj_saveLastXML_wrapper);
function("mj_setLengthRange", &mj_setLengthRange_wrapper);
function("mju_info", &mju_info_wrapper);
// mj_compile is bound using two overloads to handle the optional MjVFS argument,
// as using std::optional<MjVFS> caused memory errors due to missing copy/move constructors.
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&)>(&mj_compile_wrapper_1));