From 071af3b0155c14fd1d890ee206a8da9752e4b51a Mon Sep 17 00:00:00 2001 From: Matthew Bennice Date: Thu, 5 Oct 2023 21:04:39 -0700 Subject: [PATCH] Implement threading for island constraint solving. Humanoids22 No threads: Benchmark Execution Time: 9.475905812s Humanoids22 with 10 Threads: Benchmark Execution Time: 4.871352s PiperOrigin-RevId: 571214307 Change-Id: I1f4f2c761b4ae6bc8fac1f28c6c695f6c499f339 --- doc/APIreference/functions.rst | 9 + doc/changelog.rst | 12 +- doc/includes/references.h | 1 + include/mujoco/mujoco.h | 3 + introspect/functions.py | 20 + sample/testspeed.cc | 18 +- src/engine/engine_collision_driver.c | 7 +- src/engine/engine_forward.c | 60 +- src/engine/engine_io.c | 4 +- src/thread/thread_pool.cc | 7 +- src/thread/thread_pool.h | 2 +- src/thread/thread_task.h | 7 +- test/benchmark/CMakeLists.txt | 8 + test/benchmark/step_benchmark_test.cc | 16 +- test/benchmark/testdata/22_humanoids.xml | 2278 +++++++++++++++++++++ test/benchmark/testdata/humanoid200.xml | 913 +++++++++ test/benchmark/testdata/humanoid_body.xml | 158 ++ test/benchmark/thread_performance_test.cc | 110 + test/engine/engine_island_test.cc | 61 + unity/Runtime/Bindings/MjBindings.cs | 3 + 20 files changed, 3667 insertions(+), 30 deletions(-) create mode 100644 test/benchmark/testdata/22_humanoids.xml create mode 100644 test/benchmark/testdata/humanoid200.xml create mode 100644 test/benchmark/testdata/humanoid_body.xml create mode 100644 test/benchmark/thread_performance_test.cc diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 79bb77ea..07012a15 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -3446,6 +3446,15 @@ mju_threadPoolCreate Create a thread pool with the specified number of threads running. +.. _mju_bindThreadPool: + +mju_bindThreadPool +~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mju_bindThreadPool + +Adds a thread pool to :ref:`mjData` and configures it for multi-threaded use. + .. _mju_threadPoolEnqueue: mju_threadPoolEnqueue diff --git a/doc/changelog.rst b/doc/changelog.rst index 05c88c99..dd40f3c4 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -122,11 +122,13 @@ General attributes are specified. See the following `example model `__. - Note that these attributes only take effect for offline rendering and do not affect interactive visualisation. - +22. Added multi-threaded constraint solving via :ref:`mj_island` and :ref:`mjThreadPool` to :ref:`testspeed` + exposed via npoolthread flag. The `22 humanoids `__ + model shows a 3x speedup compared to the single threaded simulation. Python bindings ^^^^^^^^^^^^^^^ -22. Fixed `#870 `__ where calling ``update_scene`` with an invalid +23. Fixed `#870 `__ where calling ``update_scene`` with an invalid camera name used the default camera. Simulate @@ -136,18 +138,18 @@ Simulate :align: right :width: 240px -23. Added **state history** mechanism to :ref:`simulate` and the managed +24. Added **state history** mechanism to :ref:`simulate` and the managed :ref:`Python viewer`. State history can be viewed by scrubbing the History slider and (more precisely) with the left and right arrow keys. See screen capture: -24. The ``LOADING...`` label is now shown correctly. +25. The ``LOADING...`` label is now shown correctly. `Contribution `__ by `Levi Burner `__. Bug fixes ^^^^^^^^^ -25. Fixed a bug that was causing :ref:`geom margin` to be ignored during the construction of +26. Fixed a bug that was causing :ref:`geom margin` to be ignored during the construction of midphase collision trees. Version 2.3.7 (July 20, 2023) diff --git a/doc/includes/references.h b/doc/includes/references.h index 8cc0691f..3a447e74 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2613,6 +2613,7 @@ int mjp_resourceProviderCount(void); const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name); const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot); mjThreadPool* mju_threadPoolCreate(size_t number_of_threads); +void mju_bindThreadPool(mjData* d, void* thread_pool); void mju_threadPoolEnqueue(mjThreadPool* thread_pool, mjTask* task); void mju_threadPoolDestroy(mjThreadPool* thread_pool); void mju_defaultTask(mjTask* task); diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 26bf0567..85e8ec5b 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1310,6 +1310,9 @@ MJAPI const mjpResourceProvider* mjp_getResourceProviderAtSlot(int slot); // Create a thread pool with the specified number of threads running. MJAPI mjThreadPool* mju_threadPoolCreate(size_t number_of_threads); +// Adds a thread pool to mjData and configures it for multi-threaded use. +MJAPI void mju_bindThreadPool(mjData* d, void* thread_pool); + // Enqueue a task in a thread pool. MJAPI void mju_threadPoolEnqueue(mjThreadPool* thread_pool, mjTask* task); diff --git a/introspect/functions.py b/introspect/functions.py index 69b74de7..eb2d1d46 100644 --- a/introspect/functions.py +++ b/introspect/functions.py @@ -8271,6 +8271,26 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Create a thread pool with the specified number of threads running.', # pylint: disable=line-too-long )), + ('mju_bindThreadPool', + FunctionDecl( + name='mju_bindThreadPool', + return_type=ValueType(name='void'), + parameters=( + FunctionParameterDecl( + name='d', + type=PointerType( + inner_type=ValueType(name='mjData'), + ), + ), + FunctionParameterDecl( + name='thread_pool', + type=PointerType( + inner_type=ValueType(name='void'), + ), + ), + ), + doc='Adds a thread pool to mjData and configures it for multi-threaded use.', # pylint: disable=line-too-long + )), ('mju_threadPoolEnqueue', FunctionDecl( name='mju_threadPoolEnqueue', diff --git a/sample/testspeed.cc b/sample/testspeed.cc index 7baf267a..8f7932ce 100644 --- a/sample/testspeed.cc +++ b/sample/testspeed.cc @@ -119,12 +119,12 @@ void simulate(int id, int nstep, mjtNum* ctrl) { int main(int argc, char** argv) { // print help if arguments are missing - if (argc < 2 || argc > 6) { - return finish("\n Usage: testspeed modelfile [nstep nthread ctrlnoise profile]\n"); + if (argc < 2 || argc > 7) { + return finish("\n Usage: testspeed modelfile [nstep nthread ctrlnoise profile npoolthread]\n"); } // read arguments - int nstep = 10000, nthread = 0, profile = 1; + int nstep = 10000, nthread = 0, profile = 1, npoolthread = 0; // inject small noise by default, to avoid fixed contact state mjtNum ctrlnoise = 0.01; if (argc > 2 && (std::sscanf(argv[2], "%d", &nstep) != 1 || nstep <= 0)) { @@ -139,12 +139,16 @@ int main(int argc, char** argv) { if (argc > 5 && std::sscanf(argv[5], "%d", &profile) != 1) { return finish("Invalid profile argument"); } + if (argc > 6 && std::sscanf(argv[6], "%d", &npoolthread) != 1) { + return finish("Invalid npoolthread argument"); + } // clamp ctrlnoise to [0.0, 1.0] ctrlnoise = mjMAX(0.0, mjMIN(ctrlnoise, 1.0)); // clamp nthread to [1, maxthread] nthread = mjMAX(1, mjMIN(maxthread, nthread)); + npoolthread = mjMAX(0, mjMIN(maxthread, npoolthread)); // get filename, determine file type std::string filename(argv[1]); @@ -169,6 +173,10 @@ int main(int argc, char** argv) { return finish("Could not allocate mjData", m); } + if (npoolthread > 0) { + mjThreadPool* threadpool = mju_threadPoolCreate(npoolthread); + mju_bindThreadPool(d[id], threadpool); + } // init to keyframe "test" if present if (testkey>=0) { mju_copy(d[id]->qpos, m->key_qpos + testkey*m->nq, m->nq); @@ -254,7 +262,11 @@ int main(int argc, char** argv) { // free per-thread data for (int id=0; idthreadpool; mj_deleteData(d[id]); + if (threadpool) { + mju_threadPoolDestroy(threadpool); + } } // finalize diff --git a/src/engine/engine_collision_driver.c b/src/engine/engine_collision_driver.c index 9e784a98..6bfd3ab2 100644 --- a/src/engine/engine_collision_driver.c +++ b/src/engine/engine_collision_driver.c @@ -14,6 +14,7 @@ #include "engine/engine_collision_driver.h" +#include #include #include @@ -61,8 +62,10 @@ static void collideGeoms(const mjModel* m, mjData* d, static inline void resetArena(mjData* d) { d->parena = d->ncon * sizeof(mjContact); #ifdef ADDRESS_SANITIZER - ASAN_POISON_MEMORY_REGION( - (char*)d->arena + d->parena, d->narena - d->pstack - d->parena); + if (!d->threadpool) { + ASAN_POISON_MEMORY_REGION( + (char*)d->arena + d->parena, d->narena - d->pstack - d->parena); + } #endif } diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 22bda474..a099dd6f 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -40,6 +40,8 @@ #include "engine/engine_util_misc.h" #include "engine/engine_util_solve.h" #include "engine/engine_util_sparse.h" +#include "thread/thread_pool.h" +#include "thread/thread_task.h" @@ -492,6 +494,52 @@ static void warmstart(const mjModel* m, mjData* d) { +// struct encapsulating arguments to thread task +struct mjSolIslandArgs_ { + const mjModel* m; + mjData* d; + int island; +}; +typedef struct mjSolIslandArgs_ mjSolIslandArgs; + +// extract arguments, pass to solver +void* mj_solCG_island_wrapper(void* args) { + mjSolIslandArgs* solargs = (mjSolIslandArgs*) args; + mj_solCG_island(solargs->m, solargs->d, solargs->island, solargs->m->opt.iterations); + return NULL; +} + + + + +// CG solver, multi-threaded over islands +void mj_solCG_island_multithreaded(const mjModel* m, mjData* d) { + mj_markStack(d); + // allocate array of arguments to be passed to threads + mjSolIslandArgs* sol_cg_island_args = + mj_stackAllocByte(d, sizeof(mjSolIslandArgs) * d->nisland, _Alignof(mjSolIslandArgs)); + mjTask* tasks = mj_stackAllocByte(d, sizeof(mjTask) * d->nisland, _Alignof(mjTask)); + + for (int island = 0; island < d->nisland; ++island) { + sol_cg_island_args[island].m = m; + sol_cg_island_args[island].d = d; + sol_cg_island_args[island].island = island; + + mju_defaultTask(&tasks[island]); + tasks[island].func = mj_solCG_island_wrapper; + tasks[island].args = &sol_cg_island_args[island]; + mju_threadPoolEnqueue((mjThreadPool*)d->threadpool, &tasks[island]); + } + + for (int island = 0; island < d->nisland; ++island) { + mju_taskJoin(&tasks[island]); + } + + mj_freeStack(d); +} + + + // compute efc_b, efc_force, qfrc_constraint; update qacc void mj_fwdConstraint(const mjModel* m, mjData* d) { TM_START; @@ -524,9 +572,15 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { // run solver over constraint islands if (islands_supported) { - // loop over islands - for (int island=0; island < nisland; island++) { - mj_solCG_island(m, d, island, m->opt.iterations); + // no threadpool, loop over islands + if (!d->threadpool) { + for (int island=0; island < nisland; island++) { + mj_solCG_island(m, d, island, m->opt.iterations); + } + } + else { + // solve using threads + mj_solCG_island_multithreaded(m, d); } d->solver_nisland = nisland; } diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 49798389..19c716df 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -1470,7 +1470,9 @@ static void _resetData(const mjModel* m, mjData* d, unsigned char debug_value) { //------------------------------ clear header // clear stack pointer - d->pstack = 0; + if (!d->threadpool) { + d->pstack = 0; + } d->pbase = 0; // clear arena pointers diff --git a/src/thread/thread_pool.cc b/src/thread/thread_pool.cc index c36097fa..6c32c2b8 100644 --- a/src/thread/thread_pool.cc +++ b/src/thread/thread_pool.cc @@ -206,7 +206,10 @@ mjStackInfo* mju_getStackInfoForThread(mjData* d, size_t thread_id) { // align the end of the shard to be mjStackInfo. misalignment = result % alignof(mjStackInfo); result -= misalignment; - +#ifdef ADDRESS_SANITIZER + // Ensure StackInfo is always accessible + ASAN_UNPOISON_MEMORY_REGION((void*)result, sizeof(mjStackInfo)); +#endif return (mjStackInfo*) result; } @@ -253,7 +256,7 @@ static void ConfigureMultiThreadedStack(mjData* d) { } // adds a thread pool to mjData and configures it for multi-threaded use. -void mju_bindThreadPool(mjData* d, mjThreadPool* thread_pool) { +void mju_bindThreadPool(mjData* d, void* thread_pool) { if (d->threadpool) { mju_error("Thread Pool already bound to mjData"); } diff --git a/src/thread/thread_pool.h b/src/thread/thread_pool.h index fb6920de..54d852d9 100644 --- a/src/thread/thread_pool.h +++ b/src/thread/thread_pool.h @@ -54,7 +54,7 @@ MJAPI mjThreadPool* mju_threadPoolCreate(size_t number_of_threads); mjStackInfo* mju_getStackInfoForThread(mjData* d, size_t thread_id); // Adds a thread pool to mjData and configures it for multi-threaded use. -MJAPI void mju_bindThreadPool(mjData* d, mjThreadPool* thread_pool); +MJAPI void mju_bindThreadPool(mjData* d, void* thread_pool); // Gets the number of running threads in the thread pool. MJAPI size_t mju_threadPoolNumberOfThreads(mjThreadPool* thread_pool); diff --git a/src/thread/thread_task.h b/src/thread/thread_task.h index 81c63dd7..0ed6f468 100644 --- a/src/thread/thread_task.h +++ b/src/thread/thread_task.h @@ -15,14 +15,13 @@ #ifndef MUJOCO_SRC_THREAD_THREAD_TASK_H_ #define MUJOCO_SRC_THREAD_THREAD_TASK_H_ -#include -#include -#include - #include #include #ifdef __cplusplus +#include +#include +#include namespace mujoco { extern "C" { #endif diff --git a/test/benchmark/CMakeLists.txt b/test/benchmark/CMakeLists.txt index 9c132c33..65969e18 100644 --- a/test/benchmark/CMakeLists.txt +++ b/test/benchmark/CMakeLists.txt @@ -48,6 +48,14 @@ target_link_libraries( benchmark::benchmark ) +mujoco_benchmark_test(thread_performance_test) +target_link_libraries( + thread_performance_test + fixture + gmock + benchmark::benchmark +) + mujoco_benchmark_test(parse_benchmark_test) target_link_libraries( parse_benchmark_test diff --git a/test/benchmark/step_benchmark_test.cc b/test/benchmark/step_benchmark_test.cc index 945479f8..6a3d5e9e 100644 --- a/test/benchmark/step_benchmark_test.cc +++ b/test/benchmark/step_benchmark_test.cc @@ -46,23 +46,21 @@ static void run_step_benchmark(const mjModel* model, benchmark::State& state) { int nsteps = kNumWarmupSteps+kNumBenchmarkSteps; std::vector ctrl = GetCtrlNoise(model, nsteps); - // warm-up rollout to get a typcal state + // warm-up rollout to get a typical state for (int i=0; i < kNumWarmupSteps; i++) { mju_copy(data->ctrl, ctrl.data()+model->nu*i, model->nu); mj_step(model, data); } + // save state - std::vector qpos = AsVector(data->qpos, model->nq); - std::vector qvel = AsVector(data->qvel, model->nv); - std::vector act = AsVector(data->act, model->na); - std::vector warmstart = AsVector(data->qacc_warmstart, model->nv); + int spec = mjSTATE_INTEGRATION; + int size = mj_stateSize(model, spec); + std::vector initial_state(size); + mj_getState(model, data, initial_state.data(), spec); // reset state, benchmark subsequent kNumBenchmarkSteps steps while (state.KeepRunningBatch(kNumBenchmarkSteps)) { - mju_copy(data->qpos, qpos.data(), model->nq); - mju_copy(data->qvel, qvel.data(), model->nv); - mju_copy(data->act, act.data(), model->na); - mju_copy(data->qacc_warmstart, warmstart.data(), model->nv); + mj_setState(model, data, initial_state.data(), spec); for (int i=kNumWarmupSteps; i < nsteps; i++) { mju_copy(data->ctrl, ctrl.data()+model->nu*i, model->nu); diff --git a/test/benchmark/testdata/22_humanoids.xml b/test/benchmark/testdata/22_humanoids.xml new file mode 100644 index 00000000..63bff4d6 --- /dev/null +++ b/test/benchmark/testdata/22_humanoids.xml @@ -0,0 +1,2278 @@ + + + + diff --git a/test/benchmark/testdata/humanoid200.xml b/test/benchmark/testdata/humanoid200.xml new file mode 100644 index 00000000..aebfcd47 --- /dev/null +++ b/test/benchmark/testdata/humanoid200.xml @@ -0,0 +1,913 @@ + + + + + + + diff --git a/test/benchmark/testdata/humanoid_body.xml b/test/benchmark/testdata/humanoid_body.xml new file mode 100644 index 00000000..c3cc25c9 --- /dev/null +++ b/test/benchmark/testdata/humanoid_body.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/benchmark/thread_performance_test.cc b/test/benchmark/thread_performance_test.cc new file mode 100644 index 00000000..d0fb153a --- /dev/null +++ b/test/benchmark/thread_performance_test.cc @@ -0,0 +1,110 @@ +// Copyright 2021 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include +#include + +#include +#include +#include +#include +#include "test/fixture.h" + +namespace mujoco { +namespace { + +// number of steps to roll out before benchmarking +static const int kNumWarmupSteps = 500; + +void BM_StepHumanoid200(benchmark::State& state) { + auto model_path = GetTestDataFilePath("benchmark/testdata/humanoid200.xml"); + std::array error; + mjModel* model = + mj_loadXML(model_path.c_str(), nullptr, error.data(), error.size()); + + model->opt.solver = mjSOL_CG; // use CG solver + model->opt.enableflags |= mjENBL_ISLAND; // enable islands + + mjData* data = mj_makeData(model); + mjThreadPool* threadpool = mju_threadPoolCreate(10); + mju_bindThreadPool(data, threadpool); + + // warm-up rollout to get a steady state + for (int i = 0; i < kNumWarmupSteps; i++) { + mj_step(model, data); + } + + // save the initial state and step + int spec = mjSTATE_INTEGRATION; + int size = mj_stateSize(model, spec); + std::vector initial_state(size); + mj_getState(model, data, initial_state.data(), spec); + + // note: this tests resetting and stepping + for (int i = 0; i < 10; ++i) { + // reset to the saved state, step again, get the resulting state + mj_setState(model, data, initial_state.data(), spec); + for (int i = 0; i < kNumWarmupSteps; i++) { + mj_step(model, data); + } + } + + state.SetItemsProcessed(state.iterations()); + mj_deleteData(data); + mju_threadPoolDestroy(threadpool); +} + +void BM_Step22Humanoids(benchmark::State& state) { + auto model_path = GetTestDataFilePath("benchmark/testdata/22_humanoids.xml"); + std::array error; + mjModel* model = + mj_loadXML(model_path.c_str(), nullptr, error.data(), error.size()); + model->opt.solver = mjSOL_CG; // use CG solver + model->opt.enableflags |= mjENBL_ISLAND; // enable islands + + mjData* data = mj_makeData(model); + mjThreadPool* threadpool = mju_threadPoolCreate(10); + mju_bindThreadPool(data, threadpool); + + // warm-up rollout to get a steady state + for (int i = 0; i < kNumWarmupSteps; i++) { + mj_step(model, data); + } + + // save the initial state and step + int spec = mjSTATE_INTEGRATION; + int size = mj_stateSize(model, spec); + std::vector initial_state(size); + mj_getState(model, data, initial_state.data(), spec); + + std::vector ctrl = GetCtrlNoise(model, kNumWarmupSteps); + + // note: this tests resetting and stepping + for (int i = 0; i < 10; ++i) { + // reset to the saved state, step again, get the resulting state + mj_setState(model, data, initial_state.data(), spec); + for (int i = 0; i < kNumWarmupSteps; i++) { + mju_copy(data->ctrl, ctrl.data()+model->nu*i, model->nu); + mj_step(model, data); + } + } + + state.SetItemsProcessed(state.iterations()); + mj_deleteData(data); + mju_threadPoolDestroy(threadpool); +} + +BENCHMARK(BM_StepHumanoid200); +BENCHMARK(BM_Step22Humanoids); +} // namespace +} // namespace mujoco diff --git a/test/engine/engine_island_test.cc b/test/engine/engine_island_test.cc index 58027551..758dcc89 100644 --- a/test/engine/engine_island_test.cc +++ b/test/engine/engine_island_test.cc @@ -14,15 +14,20 @@ // Tests for engine/engine_island.c. +#include +#include #include #include #include #include #include +#include +#include #include #include "src/engine/engine_island.h" #include "src/engine/engine_util_sparse.h" +#include "src/thread/thread_pool.h" #include "test/fixture.h" namespace mujoco { @@ -382,5 +387,61 @@ TEST_F(IslandTest, IslandEfcElliptic) { mj_deleteModel(model); } +TEST_F(IslandTest, IslandSingleAndMultiThreadedMatch) { + auto model_path = GetTestDataFilePath("benchmark/testdata/22_humanoids.xml"); + std::array error; + mjModel* model = + mj_loadXML(model_path.c_str(), nullptr, error.data(), error.size()); + model->opt.solver = mjSOL_CG; // use CG solver + model->opt.enableflags |= mjENBL_ISLAND; // enable islands + + mjModel* model_threaded = + mj_loadXML(model_path.c_str(), nullptr, error.data(), error.size()); + model_threaded->opt.solver = mjSOL_CG; // use CG solver + model_threaded->opt.enableflags |= mjENBL_ISLAND; // enable islands + + mjData* data = mj_makeData(model); + mjData* data_threaded = mj_makeData(model_threaded); + + // warm-up rollout to get a steady state + static constexpr int kNumWarmupSteps = 100; + for (int i = 0; i < kNumWarmupSteps; i++) { + mj_step(model, data); + } + + // sync the models and data + int spec = mjSTATE_INTEGRATION; + int size = mj_stateSize(model, spec); + std::vector initial_state(size); + mj_getState(model, data, initial_state.data(), spec); + mj_setState(model_threaded, data_threaded, initial_state.data(), spec); + + // bind a threadpool to the data_threaded + mjThreadPool* threadpool = mju_threadPoolCreate(10); + mju_bindThreadPool(data_threaded, threadpool); + + for (int i = 0; i < 10; ++i) { + mj_step(model, data); + mj_step(model_threaded, data_threaded); + } + + // compare the mjData's. + { + MJDATA_POINTERS_PREAMBLE((model)) + #define X(type, name, nr, nc) \ + EXPECT_EQ(std::memcmp(data->name, data_threaded->name, \ + sizeof(type)*(model->nr)*(nc)), \ + 0) << "mjData::" #name " differs"; + MJDATA_POINTERS + #undef X + } + + mj_deleteData(data); + mj_deleteModel(model); + mj_deleteData(data_threaded); + mj_deleteModel(model_threaded); + mju_threadPoolDestroy(threadpool); +} + } // namespace } // namespace mujoco diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index f2f395bd..718c5167 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -7088,5 +7088,8 @@ public static unsafe extern void mjd_subQuat(double* qa, double* qb, double* Da, [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mjd_quatIntegrate(double* vel, double scale, double* Dquat, double* Dvel, double* Dscale); + +[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] +public static unsafe extern void mju_bindThreadPool(mjData_* d, void* thread_pool); } }