Where this package is going, and why it is going in this order. The short version: the physics first, because that is what Monty needs and what a NIF is good at; the scene second, because building a model at run time is a data-structure problem; rendering last, because a GL context is a platform problem.
v0.1 — physics (this release)
What is here:
- a model compiled from MJCF, from a path or from memory;
- a data block, stepping,
mj_forward, and the integration state as a binary; - every field of
mjModelandmjData, one function each, generated from the headers; - every MuJoCo enum, resolved from the headers;
- the C functions in
c_src/manifest.exs, bound 1:1 (ExMujoco.Api), plus a dozen hand-written ones where a call needs a decision; - MuJoCo's fatal error caught and re-raised, so a bad model cannot abort the VM.
What is deliberately not here: mjSpec, rendering. Both are below.
What "later replaces the Python wrapper" needs
ex.tbp.monty/priv/monty_py/world.py imports MjData, MjSpec, Renderer, mj_forward, mjtGeom, mjtJoint, mjtLightType, mjtTexture, mjtTextureRole. Three of those four are covered
here. The fourth — and the one that builds the scene — is mjSpec.
v0.2 — mjSpec: a model you build, not only one you read
The object graph MuJoCo compiles from: mjSpec, mjsBody, mjsGeom, mjsJoint,
mjsLight, mjsCamera, mjsSite, mjsTexture, mjsMesh, plus mj_compile.
The shape of the work:
mjsBody* body = mjs_addBody(parent, spec)returns a pointer into a tree owned by the spec, so the resource is themjSpecand the Elixir side holds a path to an element, not a pointer — an element handle that outlives a re-parent is a use-after-free waiting to be written. The alternative, one resource per element, holds the spec alive per element and is easier to reason about; that is the decision to make first.- The
mjsXxxstructs are plain C, so their fields generate the same waymjModel's do. mj_compilereturns anmjModelthat the spec still owns;mj_recompileis the loop that matters for editing a scene between episodes.
Monty's world builder needs one more thing: meshes from memory. mjs_addMesh takes a name
and fills vert/face; MuJoCo also has mj_addBufferVFS for assets that are not files. That
is the piece that replaces mesh_data handling in the Python wrapper.
v0.3 — rendering
mjrContext, mjrRender, mjvScene, mjvCamera, mjvUpdateGeom, and an offscreen frame to
depth / rgb / segmentation.
Why it is last: a GL context belongs to a thread and to a platform. Linux wants EGL or
OSMesa, macOS wants CGL, Windows wants WGL, and MuJoCo's own Renderer picks between them
with mjr_resolveGLContext. Two consequences:
- one NIF per frame and a context pinned to one scheduler thread, or a dirty-IO NIF with
its own thread; the Python wrapper already had to solve this (
_on_one_thread), and the reason it did is worth reading before writing the Elixir one; - the context must be created and destroyed by the same thread, so the resource holds the thread, not just the context.
Until then, depth and segmentation come from Python, or from whatever else already renders.
Beyond
mj_stepon a worker. A step is one call into MuJoCo, which is the point of the NIF — but on a big model it is milliseconds, and milliseconds inside a normal NIF block a scheduler.step/1is documented as such; astep_async/1backed by a dirty-IO NIF or aTask-per-episode pool is the next thing a caller will want.Nx.ExMujoco.Tensoris{type, shape, binary}, which isNx.from_binary/2plus a shape. A dependency on Nx in a physics package would be a dependency on a numerical ecosystem the caller may not have, so the conversion is theirs to make and one line long.- Batched rollouts.
mjbatchexists in MuJoCo 4 and is a genuine match for a BEAM process per environment; it is not in v0.1 because it is a large API and wantsmjSpecfirst. - A
MuJoCoplugin surface.mj_loadPluginLibraryand the decoder plugins (libstl_decoder,libobj_decoder) ship with the release; loading them from a path inpriv/is a small addition once someone needs a format the built-in decoder does not read.