Files
Mujoco_WASM/simulate/main.cc
T
Yuval Tassa 834e8dd506 Improvements to time synchronisation in simulate.
- Improved time-sync logic readability in `PhysicsLoop`.
- Fixed unattainable condition for breaking from stepping loop if data was reset.
- Added measurement of actual real-time tracking.
- Moved slowdown overlay to top left.
- Report slowdown as % of real-time, rather than fraction.
- Report actual slowdown if different than requested (very small timestep, PhysicsLoop cannot keep up) by more than 10%.
- Replaced slowdown increments of 2 with decimal-rounded increments of 10^(1/10). This is a finer-grained scale (approximately 3x as fine) and well suited for decimal percentage representation.
- Maximum slowdown increased to 1000.
- Added `Simulate.refreshrate` with a default of 60Hz, in case videomode refreshrate is 0 (can occur e.g. when forwarding over X11).
- Remove realtime reporting from regular info overlay.
- Various cleanups.

PiperOrigin-RevId: 471547481
Change-Id: Idd2514a4defb1dc431617f3a515cc747c4fd8971
2022-09-01 09:09:48 -07:00

315 lines
9.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <chrono>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <mujoco/mujoco.h>
#include "glfw_dispatch.h"
#include "simulate.h"
#include "array_safety.h"
namespace {
namespace mj = ::mujoco;
namespace mju = ::mujoco::sample_util;
using ::mujoco::Glfw;
// constants
const double syncMisalign = 0.1; // maximum mis-alignment before re-sync (simulation seconds)
const double simRefreshFraction = 0.7; // fraction of refresh available for simulation
const int kErrorLength = 1024; // load error string length
// model and data
mjModel* m = nullptr;
mjData* d = nullptr;
// control noise variables
mjtNum* ctrlnoise = nullptr;
//------------------------------------------- simulation -------------------------------------------
mjModel* LoadModel(const char* file, mj::Simulate& sim) {
// this copy is needed so that the mju::strlen call below compiles
char filename[mj::Simulate::kMaxFilenameLength];
mju::strcpy_arr(filename, file);
// make sure filename is not empty
if (!filename[0]) {
return nullptr;
}
// load and compile
char loadError[kErrorLength] = "";
mjModel* mnew = 0;
if (mju::strlen_arr(filename)>4 &&
!std::strncmp(filename + mju::strlen_arr(filename) - 4, ".mjb",
mju::sizeof_arr(filename) - mju::strlen_arr(filename)+4)) {
mnew = mj_loadModel(filename, nullptr);
if (!mnew) {
mju::strcpy_arr(loadError, "could not load binary model");
}
} else {
mnew = mj_loadXML(filename, nullptr, loadError, mj::Simulate::kMaxFilenameLength);
// remove trailing newline character from loadError
if (loadError[0]) {
int error_length = mju::strlen_arr(loadError);
if (loadError[error_length-1] == '\n') {
loadError[error_length-1] = '\0';
}
}
}
mju::strcpy_arr(sim.loadError, loadError);
if (!mnew) {
std::printf("%s\n", loadError);
return nullptr;
}
// compiler warning: print and pause
if (loadError[0]) {
// mj_forward() below will print the warning message
std::printf("Model compiled, but simulation warning (paused):\n %s\n", loadError);
sim.run = 0;
}
return mnew;
}
// simulate in background thread (while rendering in main thread)
void PhysicsLoop(mj::Simulate& sim) {
// cpu-sim syncronization point
double syncCPU = 0;
mjtNum syncSim = 0;
// run until asked to exit
while (!sim.exitrequest.load()) {
if (sim.droploadrequest.load()) {
mjModel* mnew = LoadModel(sim.dropfilename, sim);
sim.droploadrequest.store(false);
mjData* dnew = nullptr;
if (mnew) dnew = mj_makeData(mnew);
if (dnew) {
sim.load(sim.dropfilename, mnew, dnew, true);
m = mnew;
d = dnew;
mj_forward(m, d);
// allocate ctrlnoise
free(ctrlnoise);
ctrlnoise = (mjtNum*) malloc(sizeof(mjtNum)*m->nu);
mju_zero(ctrlnoise, m->nu);
}
}
if (sim.uiloadrequest.load()) {
sim.uiloadrequest.fetch_sub(1);
mjModel* mnew = LoadModel(sim.filename, sim);
mjData* dnew = nullptr;
if (mnew) dnew = mj_makeData(mnew);
if (dnew) {
sim.load(sim.filename, mnew, dnew, true);
m = mnew;
d = dnew;
mj_forward(m, d);
// allocate ctrlnoise
free(ctrlnoise);
ctrlnoise = static_cast<mjtNum*>(malloc(sizeof(mjtNum)*m->nu));
mju_zero(ctrlnoise, m->nu);
}
}
// sleep for 1 ms or yield, to let main thread run
// yield results in busy wait - which has better timing but kills battery life
if (sim.run && sim.busywait) {
std::this_thread::yield();
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
{
// lock the sim mutex
const std::lock_guard<std::mutex> lock(sim.mtx);
// run only if model is present
if (m) {
// running
if (sim.run) {
// record cpu time at start of iteration
double startCPU = Glfw().glfwGetTime();
// elapsed CPU and simulation time since last sync
double elapsedCPU = startCPU - syncCPU;
double elapsedSim = d->time - syncSim;
// inject noise
if (sim.ctrlnoisestd) {
// convert rate and scale to discrete time (OrnsteinUhlenbeck)
mjtNum rate = mju_exp(-m->opt.timestep / sim.ctrlnoiserate);
mjtNum scale = sim.ctrlnoisestd * mju_sqrt(1-rate*rate);
for (int i=0; i<m->nu; i++) {
// update noise
ctrlnoise[i] = rate * ctrlnoise[i] + scale * mju_standardNormal(nullptr);
// apply noise
d->ctrl[i] = ctrlnoise[i];
}
}
// requested slow-down factor
double slowdown = 100 / sim.percentRealTime[sim.realTimeIndex];
// misalignment condition: distance from target sim time is bigger than syncmisalign
bool misaligned = mju_abs(elapsedCPU/slowdown - elapsedSim) > syncMisalign;
// out-of-sync (for any reason): reset sync times, step
if (elapsedSim < 0 || elapsedCPU < 0 || syncCPU == 0 || misaligned || sim.speedChanged) {
// re-sync
syncCPU = startCPU;
syncSim = d->time;
sim.speedChanged = false;
// clear old perturbations, apply new
mju_zero(d->xfrc_applied, 6*m->nbody);
sim.applyposepertubations(0); // move mocap bodies only
sim.applyforceperturbations();
// run single step, let next iteration deal with timing
mj_step(m, d);
}
// in-sync: step until ahead of cpu
else {
bool measured = false;
mjtNum prevSim = d->time;
double refreshTime = simRefreshFraction/sim.refreshRate;
// step while sim lags behind cpu and within refreshTime
while ((d->time - syncSim)*slowdown < (Glfw().glfwGetTime()-syncCPU) &&
(Glfw().glfwGetTime()-startCPU) < refreshTime) {
// measure slowdown before first step
if (!measured && elapsedSim) {
sim.measuredSlowdown = elapsedCPU / elapsedSim;
measured = true;
}
// clear old perturbations, apply new
mju_zero(d->xfrc_applied, 6*m->nbody);
sim.applyposepertubations(0); // move mocap bodies only
sim.applyforceperturbations();
// call mj_step
mj_step(m, d);
// break if reset
if (d->time < prevSim) {
break;
}
}
}
}
// paused
else {
// apply pose perturbation
sim.applyposepertubations(1); // move mocap and dynamic bodies
// run mj_forward, to update rendering and joint sliders
mj_forward(m, d);
}
}
} // release std::lock_guard<std::mutex>
}
}
} // namespace
//-------------------------------------- physics_thread --------------------------------------------
void PhysicsThread(mj::Simulate* sim, const char* filename) {
// request loadmodel if file given (otherwise drag-and-drop)
if (filename != nullptr) {
m = LoadModel(filename, *sim);
if (m) d = mj_makeData(m);
if (d) {
sim->load(filename, m, d, true);
mj_forward(m, d);
// allocate ctrlnoise
free(ctrlnoise);
ctrlnoise = static_cast<mjtNum*>(malloc(sizeof(mjtNum)*m->nu));
mju_zero(ctrlnoise, m->nu);
}
}
PhysicsLoop(*sim);
// delete everything we allocated
free(ctrlnoise);
mj_deleteData(d);
mj_deleteModel(m);
}
//------------------------------------------ main --------------------------------------------------
// run event loop
int main(int argc, const char** argv) {
// print version, check compatibility
std::printf("MuJoCo version %s\n", mj_versionString());
if (mjVERSION_HEADER!=mj_version()) {
mju_error("Headers and library have different versions");
}
// simulate object encapsulates the UI
auto sim = std::make_unique<mj::Simulate>();
// init GLFW
if (!Glfw().glfwInit()) {
mju_error("could not initialize GLFW");
}
const char* filename = nullptr;
if (argc > 1) {
filename = argv[1];
}
// start physics thread
std::thread physicsthreadhandle = std::thread(&PhysicsThread, sim.get(), filename);
// start simulation UI loop (blocking call)
sim->renderloop();
physicsthreadhandle.join();
// terminate GLFW (crashes with Linux NVidia drivers)
#if defined(__APPLE__) || defined(_WIN32)
Glfw().glfwTerminate();
#endif
return 0;
}