google-deepmind / mujoco

Multi-Joint dynamics with Contact. A general purpose physics simulator.
https://mujoco.org
Apache License 2.0
8.25k stars 823 forks source link

ROS 2 Integration: Rendering on Separate Threads #1402

Closed peterdavidfagan closed 9 months ago

peterdavidfagan commented 9 months ago

Hi MuJoCo Developers,

I'm a PhD student and I'm trying to use MuJoCo for developing/testing ROS infrastructure.

I'm looking for some help with integrating MuJoCo with ROS 2.

I've created a ROS 2 node that manages a mujoco simulation instance. I have a version of this Node working, but it handles rendering and physics timestepping in a single thread. I now wish to handle rendering on a separate thread in order to speed up the rate at which I can reliably simulate physics. When attempting to do so through copying the simulation model and data I encounter segmentation faults in rendering. I have attempted to make the window context current within the callback to prevent this error but still no luck with this. Below is my current implementation (please ignore joint callback as this is less relevant for this debug task):

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include <rclcpp/rclcpp.hpp>
#include "rclcpp/qos.hpp"
#include <rclcpp/node.hpp>
#include <rclcpp/publisher.hpp>
#include <rclcpp/subscription.hpp>
#include <rmw/types.h>

#include <std_msgs/msg/string.hpp>
#include <sensor_msgs/msg/joint_state.hpp>
#include <sensor_msgs/msg/image.hpp>
#include <rosgraph_msgs/msg/clock.hpp>

#include <mujoco/mujoco.h>

#include <GLFW/glfw3.h>
#include <opencv2/opencv.hpp>

using namespace std::chrono_literals;

class MJSimulation : public rclcpp::Node
{
  public:
    MJSimulation()
    : Node("mujoco_simulation"), count_(0)
    { 

      // init GLFW
      if (!glfwInit()) {
        mju_error("Could not initialize GLFW");
        }
      glfwWindowHint(GLFW_VISIBLE, 0);
      glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_FALSE);
      GLFWwindow* window = glfwCreateWindow(640, 480, "Invisible window", NULL, NULL);
      if (!window) {
        mju_error("Could not create GLFW window");
      }
      glfwMakeContextCurrent(window);

      // read mujoco model from file and check for errors
      char error[1000] = "Could not load binary model";
      const char* filename = "/home/peter/Code/research_projects/mujoco_debug/src/mujoco_ros_sim/models/colour_splitter.mjb";
      if (std::strlen(filename)>4 && !std::strcmp(filename+std::strlen(filename)-4, ".mjb")) {
          m = mj_loadModel(filename, 0);
      } else {
          m = mj_loadXML(filename, 0, error, 1000);
      }
      if (!m) {
        mju_error("Could not load model");
      }

      // setup mujoco simulation and rendering
      d = mj_makeData(m);
      mj_forward(m, d);
      mju_copy(d->qpos, hold_qpos, 7);

      m_render = mj_copyModel(m_render, m);
      d_render = mj_copyData(d_render, m_render, d);

      overhead_cam.type = mjCAMERA_FIXED;
      overhead_cam.fixedcamid = 1;    
      front_cam.type = mjCAMERA_FIXED;
      front_cam.fixedcamid = 2;

      mjv_defaultOption(&opt); 
      mjv_defaultScene(&scn);  
      mjr_defaultContext(&con);

      mjv_makeScene(m_render, &scn, 2000);
      mjr_makeContext(m_render, &con, mjFONTSCALE_150);

      mjr_setBuffer(mjFB_OFFSCREEN, &con);    
      if (con.currentBuffer!=mjFB_OFFSCREEN) {
        std::printf("Warning: offscreen rendering not supported, using default/window framebuffer\n");
      }

      viewport =  mjr_maxViewport(&con);
      int W = viewport.width; 
      int H = viewport.height;
      overhead_rgb = (unsigned char*)std::malloc(3*W*H);
      front_rgb = (unsigned char*)std::malloc(3*W*H);

      // configure ROS communication
      static const rmw_qos_profile_t rmw_qos_profile_reliable =
      {
        RMW_QOS_POLICY_HISTORY_KEEP_LAST,
        10,
        RMW_QOS_POLICY_RELIABILITY_RELIABLE,
        RMW_QOS_POLICY_DURABILITY_VOLATILE,
        RMW_QOS_DEADLINE_DEFAULT,
        RMW_QOS_LIFESPAN_DEFAULT,                 
        RMW_QOS_POLICY_LIVELINESS_SYSTEM_DEFAULT, 
        RMW_QOS_LIVELINESS_LEASE_DURATION_DEFAULT,
        false
      };
      auto qos_reliable = rclcpp::QoS(
                      rclcpp::QoSInitialization::from_rmw(rmw_qos_profile_reliable),
                      rmw_qos_profile_reliable
                      );

      sim_step_callback_group_ = this->create_callback_group(
                      rclcpp::CallbackGroupType::MututallyExclusive
                      );

      sim_render_callback_group_ = this->create_callback_group(
                      rclcpp::CallbackGroupType::MutuallyExclusive
                      );

      //joint_command_subscription_ = this->create_subscription<sensor_msgs::msg::JointState>(
      //                      "/mujoco_joint_commands", 
      //                      qos_reliable,
      //                      std::bind(&MJSimulation::joint_command_callback, this, std::placeholders::_1)
      //                      );

      // publishers
      rclcpp::PublisherOptions sim_publisher_options;
      sim_publisher_options.callback_group = sim_step_callback_group_;
      joint_state_publisher_ = this->create_publisher<sensor_msgs::msg::JointState>(
                      "/mujoco_joint_states",
                      qos_reliable,
                      sim_publisher_options
                      );

      rclcpp::PublisherOptions clock_publisher_options;                    
      clock_publisher_options.callback_group = sim_step_callback_group_;   
      clock_publisher_ = this->create_publisher<rosgraph_msgs::msg::Clock>(
                      "/clock",    
                      qos_reliable,
                      clock_publisher_options
                      );

      // create a publisher for the overhead camera
      rclcpp::PublisherOptions camera_publisher_options;
      camera_publisher_options.callback_group = sim_render_callback_group_;
      overhead_camera_publisher_ = this->create_publisher<sensor_msgs::msg::Image>(
                      "/overhead_camera",
                      qos_reliable,
                      camera_publisher_options
                      );
      front_camera_publisher_ = this->create_publisher<sensor_msgs::msg::Image>(
                      "/front_camera",
                      qos_reliable,
                      camera_publisher_options
                      );

      // simulation stepping timer
      timer_ = this->create_wall_timer(
        10ms, 
        std::bind(&MJSimulation::update_sim, this),
        sim_step_callback_group_
        );

      // render timer
      render_timer_ = this->create_wall_timer(
        100ms,
        std::bind(&MJSimulation::render, this),
        sim_render_callback_group_
        );
    }

  private:
    void update_sim()
    {
      std::unique_lock<std::mutex> lock(render_mutex);        
      if (hold) {
        d->ctrl = hold_qpos;
      }

      // step simulation
      for (int i=0; i<10; i++) {
         mj_step(m, d);
      }
      lock.unlock();

      // publish clock and joint states
      auto message = rosgraph_msgs::msg::Clock();
      int64 current_time = d->time * 1e9;
      message.clock.sec = rclcpp::Time(current_time).seconds();
      message.clock.nanosec = rclcpp::Time(current_time).nanoseconds();
      clock_publisher_->publish(message);

      // publish joint state of franka emika panda
      auto joint_state = sensor_msgs::msg::JointState();
      joint_state.header.stamp.sec = rclcpp::Time(current_time).seconds();
      joint_state.header.stamp.nanosec = rclcpp::Time(current_time).nanoseconds();
      for (int i=0; i<8; i++) {
        joint_state.name.push_back("panda_joint" + std::to_string(i+1));
        joint_state.position.push_back(d->qpos[i]);
        joint_state.velocity.push_back(d->qvel[i]);
      }
      joint_state_publisher_->publish(joint_state);
    }

    void render()
    {
        // copy data from physics sim
        std::unique_lock<std::mutex> lock(render_mutex);
        d_render = mj_copyData(d_render, m, d);
        lock.unlock();
        int64 current_time = d_render->time * 1e9;

        // render offscreen     
        mjv_updateScene(m_render, d_render, &opt, NULL, &front_cam, mjCAT_ALL, &scn);
        mjr_render(viewport, &scn, &con);
        mjr_readPixels(front_rgb, NULL, viewport, &con);

        // create front camera image message
        auto front_image = sensor_msgs::msg::Image();
        front_image.header.stamp.sec = rclcpp::Time(current_time).seconds();
        front_image.header.stamp.nanosec = rclcpp::Time(current_time).nanoseconds();
        front_image.height = 480;
        front_image.width = 640;
        front_image.encoding = "rgb8";
        front_image.is_bigendian = 0;
        front_image.step = front_image.width * 3;
        size_t front_data_size = front_image.width * front_image.height * 3;
        front_image.data.resize(front_data_size);
        std::memcpy(&front_image.data[0], front_rgb, front_data_size);

        // publish image
        front_camera_publisher_->publish(front_image);
    }

    void joint_command_callback(const sensor_msgs::msg::JointState::SharedPtr msg)
    {
      for (int i=0; i<8; i++) {
        d->ctrl[i] = msg->position[i];
      }
      hold = false;
    }

    // ROS 2 
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::TimerBase::SharedPtr render_timer_;
    rclcpp::Subscription<sensor_msgs::msg::JointState>::SharedPtr joint_command_subscription_;
    rclcpp::Publisher<sensor_msgs::msg::JointState>::SharedPtr joint_state_publisher_;
    rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr overhead_camera_publisher_;
    rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr front_camera_publisher_;
    rclcpp::Publisher<rosgraph_msgs::msg::Clock>::SharedPtr clock_publisher_;
    rclcpp::CallbackGroup::SharedPtr sim_step_callback_group_;  
    rclcpp::CallbackGroup::SharedPtr sim_render_callback_group_;
    std::mutex render_mutex;     // mutex for copying data from sim to render buffer
    size_t count_;   
    bool hold = true;

    // MuJoCo variables
    mjModel* m;                  // MuJoCo model
    mjData* d;                   // MuJoCo data
    mjModel* m_render;           // MuJoCo model for rendering
    mjData* d_render;            // MuJoCo data for rendering
    mjvCamera overhead_cam;      // abstract camera
    mjvCamera front_cam;         // abstract camera
    mjvOption opt;               // visualization options
    mjvScene scn;                // abstract scene    
    mjrContext con;              // custom GPU context
    mjtNum* hold_qpos = new mjtNum[8]{0, -0.785, 0, -2.356, 0, 1.571, 0.785, 0};

    // GLFW variables  
    GLFWwindow* window;
    unsigned char* overhead_rgb;
    unsigned char* front_rgb;
    mjrRect viewport;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  std::shared_ptr<MJSimulation> node = std::make_shared<MJSimulation>();
  //rclcpp::executors::SingleThreadedExecutor exec;
  rclcpp::executors::MultiThreadedExecutor exec;
  exec.add_node(node);
  exec.spin();
  rclcpp::shutdown();
  return 0;
}

I am consistently getting segfaults when attempting to render, even after copying data to a separate pointer and including mutex guards for the copy operation.

Backtrace:

(gdb) bt
#0  0x00007ffff40e3b89 in ?? () from /lib/x86_64-linux-gnu/libGLdispatch.so.0
#1  0x00007ffff7b0e649 in mjr_render ()
   from /home/peter/Code/research_projects/mujoco_debug/src/mujoco_ros_sim/mujoco-3.1.1/lib/libmujoco.so.3.1.1
#2  0x00005555555d663f in MJSimulation::render (this=0x55555567b450)
    at /home/peter/Code/research_projects/mujoco_debug/src/mujoco_ros_sim/src/franka_block.cpp:210
#3  0x00005555556014bd in std::__invoke_impl<void, void (MJSimulation::*&)(), MJSimulation*&> (
    __f=@0x555556334968: (void (MJSimulation::*)(MJSimulation * const)) 0x5555555d654a <MJSimulation::render()>, 
    __t=@0x555556334978: 0x55555567b450) at /usr/include/c++/11/bits/invoke.h:74
#4  0x0000555555601399 in std::__invoke<void (MJSimulation::*&)(), MJSimulation*&> (
    __fn=@0x555556334968: (void (MJSimulation::*)(MJSimulation * const)) 0x5555555d654a <MJSimulation::render()>)
    at /usr/include/c++/11/bits/invoke.h:96
#5  0x0000555555601271 in std::_Bind<void (MJSimulation::*(MJSimulation*))()>::__call<void, , 0ul>(std::tuple<>&&, std::_Index_tuple<0ul>) (this=0x555556334968, __args=...) at /usr/include/c++/11/functional:420
#6  0x00005555556010b7 in std::_Bind<void (MJSimulation::*(MJSimulation*))()>::operator()<, void>() (this=0x555556334968)
    at /usr/include/c++/11/functional:503
#7  0x0000555555600c5a in rclcpp::GenericTimer<std::_Bind<void (MJSimulation::*(MJSimulation*))()>, (void*)0>::execute_callback_delegate<std::_Bind<void (MJSimulation::*(MJSimulation*))()>, (void*)0>() (this=0x5555563348f0)
    at /opt/ros/rolling/include/rclcpp/rclcpp/timer.hpp:293
#8  0x0000555555600351 in rclcpp::GenericTimer<std::_Bind<void (MJSimulation::*(MJSimulation*))()>, (void*)0>::execute_callback() (this=0x5555563348f0) at /opt/ros/rolling/include/rclcpp/rclcpp/timer.hpp:279
#9  0x00007ffff7d01a21 in rclcpp::Executor::execute_any_executable(rclcpp::AnyExecutable&) ()
   from /opt/ros/rolling/lib/librclcpp.so
#10 0x00007ffff7d115ba in rclcpp::executors::MultiThreadedExecutor::run(unsigned long) ()
   from /opt/ros/rolling/lib/librclcpp.so
#11 0x00007ffff74dc253 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#12 0x00007ffff7094ac3 in start_thread (arg=<optimised out>) at ./nptl/pthread_create.c:442
#13 0x00007ffff7126850 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81
peterdavidfagan commented 9 months ago

From initial reading it looks like OpenGL contexts and threading requires careful management. I guess that ROS multi-threaded executors don't play well with OpenGL contexts out of the box as its challenging to assign a single thread to a given callback function.

I think if it were possible to manage callback threads this would be solved through dedicating a thread for rendering contexts.

peterdavidfagan commented 9 months ago

This now seems to be solved thanks to advice from Levi on Discord. I will post updated code here but essentially I use std::thread to create a rendering thread.

peterdavidfagan commented 9 months ago

Updated Code:

#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include <thread>

#include <rclcpp/rclcpp.hpp>
#include "rclcpp/qos.hpp"
#include <rclcpp/node.hpp>
#include <rclcpp/publisher.hpp>
#include <rclcpp/subscription.hpp>
#include <rmw/types.h>

#include <std_msgs/msg/string.hpp>
#include <sensor_msgs/msg/joint_state.hpp>
#include <sensor_msgs/msg/image.hpp>
#include <rosgraph_msgs/msg/clock.hpp>

#include <mujoco/mujoco.h>

#include <GLFW/glfw3.h>
#include <opencv2/opencv.hpp>

using namespace std::chrono_literals;

class MJSimulation : public rclcpp::Node
{
  public:
    MJSimulation()
    : Node("mujoco_simulation"), count_(0)
    {
      // read mujoco model from file and check for errors
      char error[1000] = "Could not load binary model";
      const char* filename = "/home/peter/Code/research_projects/mujoco_debug/src/mujoco_ros_sim/models/colour_splitter.mjb";
      if (std::strlen(filename)>4 && !std::strcmp(filename+std::strlen(filename)-4, ".mjb")) {
          m = mj_loadModel(filename, 0);
      } else {
          m = mj_loadXML(filename, 0, error, 1000);
      }
      if (!m) {
        mju_error("Could not load model");
      }

      // setup mujoco simulation and rendering
      d = mj_makeData(m);
      mj_forward(m, d);
      mju_copy(d->qpos, hold_qpos, 7);

      overhead_cam.type = mjCAMERA_FIXED;
      overhead_cam.fixedcamid = 1;
      front_cam.type = mjCAMERA_FIXED;
      front_cam.fixedcamid = 2;

      // configure ROS communication
      static const rmw_qos_profile_t rmw_qos_profile_reliable =
      {
    RMW_QOS_POLICY_HISTORY_KEEP_LAST,
    10,
    RMW_QOS_POLICY_RELIABILITY_RELIABLE,
    RMW_QOS_POLICY_DURABILITY_VOLATILE,
    RMW_QOS_DEADLINE_DEFAULT,
    RMW_QOS_LIFESPAN_DEFAULT,
    RMW_QOS_POLICY_LIVELINESS_SYSTEM_DEFAULT,
    RMW_QOS_LIVELINESS_LEASE_DURATION_DEFAULT,
    false
      };

      // quality of service and callback groups
      auto qos_reliable = rclcpp::QoS(
              rclcpp::QoSInitialization::from_rmw(rmw_qos_profile_reliable),
              rmw_qos_profile_reliable
              );

      physics_step_callback_group_ = this->create_callback_group(
              rclcpp::CallbackGroupType::MutuallyExclusive
              );

      render_callback_group_ = this->create_callback_group(
              rclcpp::CallbackGroupType::MutuallyExclusive
              );

      // subscriptions
      joint_command_subscription_ = this->create_subscription<sensor_msgs::msg::JointState>(
                  "/mujoco_joint_commands", 
                  qos_reliable,
                  std::bind(&MJSimulation::joint_command_callback, this, std::placeholders::_1)
                  );

      // publishers
      rclcpp::PublisherOptions physics_publisher_options;
      physics_publisher_options.callback_group = physics_step_callback_group_;
      joint_state_publisher_ = this->create_publisher<sensor_msgs::msg::JointState>(
              "/mujoco_joint_states", 
              qos_reliable,
              physics_publisher_options
              );

      clock_publisher_ = this->create_publisher<rosgraph_msgs::msg::Clock>(
              "/clock", 
              qos_reliable,
              physics_publisher_options
              );

      // create a publisher for the overhead camera
      rclcpp::PublisherOptions camera_publisher_options;
      camera_publisher_options.callback_group = render_callback_group_;
      overhead_camera_publisher_ = this->create_publisher<sensor_msgs::msg::Image>(
              "/overhead_camera",
              qos_reliable,
              camera_publisher_options
              );
      front_camera_publisher_ = this->create_publisher<sensor_msgs::msg::Image>(
              "/front_camera",
              qos_reliable,
              camera_publisher_options
              );

      // simulation stepping timer
      timer_ = this->create_wall_timer(
    10ms, 
    std::bind(&MJSimulation::update_sim, this),
    physics_step_callback_group_
    );

      // start a separate thread for rendering
      m_render = mj_copyModel(m_render, m);
      d_render = mj_copyData(d_render, m, d);
      this->start_render_thread();

    } 

  private:
    void update_sim()
    {
      std::unique_lock<std::mutex> lock(render_mutex);  
      if (hold) {
    d->ctrl = hold_qpos;
      } 
      for (int i=0; i<10; i++) {
         mj_step(m, d);
      }
      lock.unlock();

      // publish clock and joint states
      auto message = rosgraph_msgs::msg::Clock();
      int64 current_time = d->time * 1e9;
      message.clock.sec = rclcpp::Time(current_time).seconds();
      message.clock.nanosec = rclcpp::Time(current_time).nanoseconds();
      clock_publisher_->publish(message);

      // publish joint state of franka emika panda
      auto joint_state = sensor_msgs::msg::JointState();
      joint_state.header.stamp.sec = rclcpp::Time(current_time).seconds();
      joint_state.header.stamp.nanosec = rclcpp::Time(current_time).nanoseconds();
      for (int i=0; i<8; i++) {
    joint_state.name.push_back("panda_joint" + std::to_string(i+1));
    joint_state.position.push_back(d->qpos[i]);
    joint_state.velocity.push_back(d->qvel[i]);
      }
      joint_state_publisher_->publish(joint_state);
    }

    void start_render_thread()
    {
      render_thread_ = std::thread([this]() {
               // init GLFW
                if (!glfwInit()) {
                  mju_error("Could not initialize GLFW");
            }
                glfwWindowHint(GLFW_VISIBLE, 0);
                glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_FALSE);
                GLFWwindow* window = glfwCreateWindow(640, 480, "Invisible window", NULL, NULL);
                if (!window) {
                  mju_error("Could not create GLFW window");
                }
            glfwMakeContextCurrent(window);
                mjv_defaultOption(&opt);
                mjv_defaultScene(&scn);
                mjv_makeScene(m_render, &scn, 2000);
            //mjrContext con;
            mjr_defaultContext(&con);
                mjr_makeContext(m_render, &con, mjFONTSCALE_150);      
                mjr_setBuffer(mjFB_OFFSCREEN, &con);
            //mjrRect viewport =  mjr_maxViewport(&con);
                viewport =  mjr_maxViewport(&con);
                int W = viewport.width;
                int H = viewport.height;
                front_rgb = (unsigned char*)std::malloc(3*W*H);
        while (running_) {
                this->render();
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
        });
    }

    void render()
    {
        // copy data from sim to render buffer
    std::unique_lock<std::mutex> lock(render_mutex);
    d_render = mj_copyData(d_render, m, d);
    lock.unlock();
    int64 current_time = d_render->time * 1e9;

    // render offscreen
    mjv_updateScene(m_render, d_render, &opt, NULL, &front_cam, mjCAT_ALL, &scn);
    mjr_render(viewport, &scn, &con);
    mjr_readPixels(front_rgb, NULL, viewport, &con);

        // create front camera image message
        auto front_image = sensor_msgs::msg::Image();
        front_image.header.stamp.sec = rclcpp::Time(current_time).seconds();
        front_image.header.stamp.nanosec = rclcpp::Time(current_time).nanoseconds();
        front_image.height = 480;
        front_image.width = 640;
        front_image.encoding = "rgb8";
        front_image.is_bigendian = 0;
        front_image.step = front_image.width * 3;
        size_t front_data_size = front_image.width * front_image.height * 3;
        front_image.data.resize(front_data_size);
        std::memcpy(&front_image.data[0], front_rgb, front_data_size);

        // publish image
        front_camera_publisher_->publish(front_image);
    }

    void joint_command_callback(const sensor_msgs::msg::JointState::SharedPtr msg)
    {
      std::unique_lock<std::mutex> lock(render_mutex);
      for (int i=0; i<8; i++) {
    d->ctrl[i] = msg->position[i];
      }
      hold = false;
      lock.unlock();
    }

    // ROS 2 
    rclcpp::TimerBase::SharedPtr timer_;

    rclcpp::Subscription<sensor_msgs::msg::JointState>::SharedPtr joint_command_subscription_;

    rclcpp::Publisher<rosgraph_msgs::msg::Clock>::SharedPtr clock_publisher_;
    rclcpp::Publisher<sensor_msgs::msg::JointState>::SharedPtr joint_state_publisher_;
    rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr overhead_camera_publisher_;
    rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr front_camera_publisher_;

    rclcpp::CallbackGroup::SharedPtr physics_step_callback_group_;
    rclcpp::CallbackGroup::SharedPtr render_callback_group_;

    size_t count_;
    std::mutex render_mutex;
    std::thread render_thread_;
    bool running_ = true;
    bool hold = false;  

    // MuJoCo variables
    mjModel* m;                  // MuJoCo model
    mjData* d;                   // MuJoCo data
    mjModel* m_render;           // MuJoCo model for rendering
    mjData* d_render;            // MuJoCo data for rendering
    mjvCamera overhead_cam;      // abstract camera
    mjvCamera front_cam;         // abstract camera
    mjvOption opt;               // visualization options
    mjvScene scn;                // abstract scene
    mjrContext con;              // custom GPU context
    mjtNum* hold_qpos = new mjtNum[8]{0, -0.785, 0, -2.356, 0, 1.571, 0.785, 0};

    // GLFW variables
    GLFWwindow* window;
    unsigned char* overhead_rgb;
    unsigned char* front_rgb;
    mjrRect viewport;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  std::shared_ptr<MJSimulation> node = std::make_shared<MJSimulation>();
  //rclcpp::executors::SingleThreadedExecutor exec;
  rclcpp::executors::MultiThreadedExecutor exec(rclcpp::ExecutorOptions(), 2);
  exec.add_node(node);
  exec.spin();
  rclcpp::shutdown();
  return 0;
}