How to connect your UI to your C++ engine with JSON

How to connect your UI to your C++ engine with JSON

Implementation of a JSON interface between the RealTimeParticles UI and its physics engine. Source code available here.

  1. Introduction
  2. An unsubstainable UI/engine interface
  3. JSON and the golden fleece
    1. What’s JSON?
    2. How do you represent something in JSON?
    3. JSON in C++
  4. Implementing a JSON interface in RealTimeParticles
    1. “I have a concept of a plan.”
    2. Storing inputs via JSON on engine side
    3. Generating UI by parsing JSON
      1. High-level data transfer in PhysicsWidget
      2. The core JSON drawer
      3. Drawing boolean checkboxes
      4. Drawing numerical sliders
    4. Updating model and GPU kernels by parsing JSON
      1. An intermediary OpenCLModel layer
      2. Passing the parameters values to the GPU
  5. Results
    1. Pros
      1. Less boilerplate code
      2. Generic and automated UI
      3. Separation of concern and encapsulation
      4. Next model can be added in an instant
    2. Cons
      1. Automatic UI means less customization
      2. No explicit type means more risks
  6. Conclusion

Introduction

Last year, I posted an in-depth technical blog post on periodic boundary conditions in a Position Based Dynamics (PBD) framework. This was part of my work implementing a PBD clouds model based on this scientific paper. I then promised that my next post would be about this topic, but it didn’t happen. This year (2024), my professional life has been hectic, and diving back into the complexity of clouds physics is too time-consuming. Also, I’m not very happy with the results. I got nice real-time dynamics but the visuals are lacking. I need to implement a volume renderer to properly showcase the simulation.

So let’s instead focus on a problem that has become increasingly important with my RealTimeParticles application: every time a model is added, many UI/engine connections have to be made manually, and the complexity increases unsustainably.

How can I connect the UI to my various physics models in a simpler and generic way?

As you probably guessed from the title of this blog post, the answer is JSON, a powerful data interchange format coming from web development that can help us a lot in software development.

Regarding the structure of this blog post, I will first present the current problematic state of my UI/engine interface, then we will see the basics of JSON and finally how we can use it to reduce the boilerplate and improve the UI/engine situation. No fancy theory here, and no specific knowledge is required aside from a classic computer science background. Let’s get right into it!

An unsubstainable UI/engine interface

For those who have played around with my RealTimeParticles (RTP) application, it’s probably obvious that UI/UX isn’t very high on my priority list… I’ve only implemented the bare minimum to play with the physics models and to adjust their rendering. So the UI is very basic, just 3 widgets on top of DearImgui: MainWidget is used for high-level features selection, GraphicsWidget contains all graphics options and PhysicsWidget is a debug widget providing all the model parameters, this last one is model-specific.

RtpUI RealTimeParticles 1.0.1 UI is made of 3 widgets and the simulation view

For each frame, these widgets get data from the physics and graphics engines via getters, render it, and update via setters any value modified by the users.

hierarchyUI UI hierarchy and different levels of interactions, strongest one being at PhysicsWidget level

The connection to the UI is very similar for both engines. However, it is very limited on the graphics engine side as it doesn’t require many parameters, so let’s focus on the physics engine side where we continue adding models. Here are some details of the implementation on UI and engine sides for the clouds model:

  // PhysicsWidget.cpp

  ImGui::Spacing();
  ImGui::Text("Clouds Parameters");
  ImGui::Spacing();

  bool isTempSmoothingEnabled = cloudsEngine->isTempSmoothingEnabled();
  if (ImGui::Checkbox("Enable Temperature Smoothing", &isTempSmoothingEnabled))
  {
    cloudsEngine->enableTempSmoothing(isTempSmoothingEnabled);
  }

  float groundHeatCoeff = cloudsEngine->getGroundHeatCoeff();
  if (ImGui::SliderFloat("Ground Heat Coefficient", &groundHeatCoeff, 0.0f, 1000.0f, "%.4f"))
  {
    cloudsEngine->setGroundHeatCoeff(groundHeatCoeff);
  }

  float buoyancyCoeff = cloudsEngine->getBuoyancyCoeff();
  if (ImGui::SliderFloat("Buoyancy Coefficient", &buoyancyCoeff, 0.0f, 5.0f, "%.4f"))
  {
    cloudsEngine->setBuoyancyCoeff(buoyancyCoeff);
  }

  float gravCoeff = cloudsEngine->getGravCoeff();
  if (ImGui::SliderFloat("Gravity Coefficient", &gravCoeff, 0.0f, 0.1f, "%.4f"))
  {
    cloudsEngine->setGravCoeff(gravCoeff);
  }

At the beginning of the clouds section of PhysicsWidget

  // CloudsModel.hpp

  void enableTempSmoothing(bool enable);
  bool isTempSmoothingEnabled() const;
  //
  void setGroundHeatCoeff(float coeff);
  float getGroundHeatCoeff() const;
  //
  void setBuoyancyCoeff(float coeff);
  float getBuoyancyCoeff() const;
  //
  void setGravCoeff(float coeff);
  float getGravCoeff() const;

Half of CloudsModel header file looks like this, not great

As I said, this is a very basic implementation, everything has to be declared/defined and connected manually. Also, you can note that the value range for each parameter is hardcoded in the UI. This is bad, this should be encapsulated in the physics model definition.

Yet, I think that the famous approach Keep It Simple, Stupid is always a good start, and this setup was totally fine at first. When there was only the boids model, it only required a couple of getter/setters and limited boilerplate on widget side, all good. Then, I added the fluids models with a lot more parameters to tweak and I had to add one or two hundred lines of additional boilerplate, a bit more annoying but still manageable. However, the recent clouds model with 19 additional parameters clearly showed that this simplistic approach was not tenable. Connected to the three models via their parameters, PhysicsWidget implementation is now 500 lines long and the header file of my clouds model has more than 100 lines of getters and setters. Before adding another model to the mix, something needs to be done to simplify the interface between PhysicsWidget and the physics models.

Hmm… it would be nice if I could have some generic data structure storing all my model params despite their different types, regardless of the model, and then UI would iterate over this data structure, display the values and return the modified ones to the engine. It is surely doable with a standard container and some template magic. Let’s try to design this. First of all, let me just… Waaait! Folks, this is 2024 and almost everything already exists on the internet, open source and MIT licensed if lucky. What if you tried to reuse someone’s else magic instead of reinventing the wheel? That’s where JSON comes in!

JSON and the golden fleece

What’s JSON?

JSON, also known as JavaScript Object Notation. is a very popular data-interchange format that was initially implemented in JavaScript 25 years ago. It has since spread to dozens of different programming languages, being widely praised for its simplicity to read as a human and parse as a machine. Data-interchange concept refers to the process of exchanging data between different systems so that the information sent by one system can be understood and used by another, even if those systems are built using different technologies, languages or platforms. In our case, the data-interchange process is relatively straightforward, the physics engine sends the names and values of its model parameters to the UI layer and in return receives values modified by the user.

How do you represent something in JSON?

A JSON object is an ordered list of key-value pairs where values can be strings, numbers, booleans, null values, arrays and also… JSON objects! This makes it versatile and ideal for representing complex data structures. Also, JSON objects can be serialized into a JSON file. In our UI/engine case, we won’t need to serialize/deserialize anything, since both systems exchanging data are in the same C++ program, so we can keep everything in the program’s memory. We could use serialization to save custom model parameters values into a JSON file and share it with other RealTimeParticles users for example. I won’t go too much into details about JSON syntax since hundreds of websites already do it really well like the official JSON page, but here is an example:

{
  "name": "Santa Claus",      // string
  "age": null,                // null value, Santa's exact age is unknown or timeless
  "address": "North Pole", // string
  "has_chimney_entry_skills": true, // bool
  "sleigh": {                   // Another JSON object
    "mileage": 34000,
    "weight": 344,
    "plate_number": null
  }
}

A basic JSON example inspired by a famous character

JSON in C++

There is no built-in support for JSON in C++. The language was invented before the format, and the latter has not yet made its way to the standard library. Nevertheless, many third-party open source libraries bring JSON support to C++. Personally, I really like nohlmann::json for its convenience and accessibility. It is heavily templated and requires only a few headers via a single #include<nlohmann/json.hpp>. It also provides many features for an nice integration in C++: implicit conversions, arbitrary type conversions, JSON Merge Patch, STL-container support, type checkers… Another nicety is its availability via the third-party package manager Conan already used in the RealTimeParticles application. With Conan, we can get the latest version of nlohmann::json header library and integrate it into the application very easily.

using json = nlohmann::json;

namespace ns
{
    // a simple struct to model a person
    struct person
    {
        std::string name;
        std::string address;
        bool has_chimney_entry_skills;
    };

    // operator to convert a person object to a JSON object
    void to_json(json& j, const person& p)
    {
        j = json{ {"name", p.name}, {"address", p.address}, {"hasChimneyEntrySkills", p.has_chimney_entry_skills}};
    }

    // operator to convert a JSON object to a person object
    void from_json(const json& j, person& p)
    {
        j.at("name").get_to(p.name);
        j.at("address").get_to(p.address);
        j.at("hasChimneyEntrySkills").get_to(p.has_chimney_entry_skills);
    }
}

// create a person
ns::person p {"Santa Claus", "North Pole", true};

// conversion: person -> json
json j = p;

// list is ordered and will be printed accordingly
std::cout << j << std::endl;
// {"address":"North Pole", "hasChimneyEntrySkills":true,"name":"Santa Claus"}

// conversion: json -> person
auto p2 = j.template get<ns::person>();

// that's it
assert(p == p2);

Adding JSON support to an arbitrary type with nlohmann::json

Implementing a JSON interface in RealTimeParticles

“I have a concept of a plan.”

Alright, now that we have presented our current UI/engine issue and briefly introduced JSON, let’s see how we can exploit it in this situation that is getting out of hands. In order to move toward this ideal of a generic and automatic interface between UI and physics engine, the focus is to get rid of all the parameters’ getters/setters boilerplate on the engine side and automate the generation of the physics widget on the UI side. By doing so, adding a new physics parameter or model will be straightforward. Also keep in mind that the data transfer must be done in both directions since we need to update in the engine the physics parameters values modified by the user.

To achieve this, the proposal is to store all physics parameters data of a given physics model into a JSON object owned by the model. The engine will then pass this generic JSON object to the physics UI widget which will parse it and draw UI items for each key-value pair in the JSON. At the end of the frame, if the user has modified some values, we can then simply send the updated JSON back to the engine which will parse it and update the model. Then, the round trip data transfer is complete.

newImplJSON Using JSON as the data inter-change format between UI and physics engine

Storing inputs via JSON on engine side

In order to store all our physics data into a JSON object, we need to do quite a bit of refactoring in the physics engine. All parameters data defining a model is now placed in the JSON object m_inputJson owned by the base class Model. The OpenCL implementation of this base class is called oclModel, and the different existing models Boids/Fluids/Clouds are implemented on top of it. When refactoring, I initially focus on the fluids model and deactivate the other ones to iterate faster.

// Fluids.cpp

// Initial state of the fluids model parameters
static const json initFluidsJson // clang-format off
{ 
  {"Fluids", {
      { "Rest Density", { 450.0f, 10.0f, 1000.0f } },
      { "Relax CFM", { 600.0f, 100.0f, 1000.0f } },
      { "Time Step", { 0.010f, 0.0001f, 0.020f } },
      { "Nb Jacobi Iterations", { 2, 1, 6 } },
      { "Artificial Pressure",
          { { "Enable##Pressure", true },
            { "Coefficient##Pressure", { 0.001f, 0.0f, 0.001f} },
            { "Radius", {0.006f, 0.001f, 0.015f}},
            { "Exp", {4, 1, 6}}
          }
      },
      { "Vorticity Confinement",
          { { "Enable##Vorticity", true },
            { "Coefficient##Vorticity", {0.0004f, 0.0f, 0.001f}},
            { "xSPH Viscosity Coefficient", {0.0001f, 0.0f, 0.001f}}
          }
      }
    }
  }
}; // clang-format on

Fluids::Fluids(ModelParams params)
    : OclModel<FluidKernelInputs>(params, FluidKernelInputs {}, json(initFluidsJson)) // Initialize Fluids with initFluidsJson
    //...

All fluids model parameters are stored in a JSON object with their default values

As shown above, the fluids model parameters with their initial values are stored in a single JSON object initFluidsJson. For numerics values, we have the following layout: name - {default val, min val, max val}, using an array for the values. Compared to the original design, the encapsulation is much better with the range value of each parameter stored in the model itself in addition to its current value, no more leakage at UI level. Also note the two sub-JSON objects Artifical Pressure and Vorticity Confinement used for conditional parameters that can enable other parameters if they are active.

// Model.hpp

public:
Model(ModelParams params, json js = {})

json getInputJson() const
{
  return m_inputJson;
}

void updateInputJson(const json& newJson)
{
  // No modification, no update
  if (json::diff(m_inputJson, newJson).empty())
    return;

  m_inputJson.merge_patch(newJson);
  updateModelWithInputJson(m_inputJson);
}

protected:
// Derived classes must implement their JSON parser
virtual void updateModelWithInputJson(json& inputJson) = 0;

private:
// Private to prevent derived classes to modify/use it outside updateModelWithInputJson()
// Once parsed, for safeness, derived classes must only rely on their member vars
// Not perfect (extra copy and memory) but safe
json m_inputJson;

JSON input integration in the physics engine at Model level

The implementation at Model level turns out to be quite simple. The JSON input is logically passed as a constructor parameter. Instead of having a getter for each parameter, we have now greatly reduced in granularity with a single getInputJson() API. updateInputJson() is the setter to update the model JSON, inside it we use the JSON Merge Patch functionality to update changed parameter values of the model. Also, note the abstract function updateModelWithInputJson() that must be defined for each model type, this is where the model parses m_inputJson to get its different parameter values. Finally, we can notice that m_inputJson is private and therefore not accessible to derived model classes. This design choice prioritizes safeness over performance, it allows us to clearly define where the parsing happens as it can be a risky operation. We will come back to this later (link cons).

Generating UI by parsing JSON

High-level data transfer in PhysicsWidget

We already introduced the two physics engine APIs getInputJson()/updateInputJson() that allow the data round trip. Now we can call them in PhysicsWidget to retrieve the JSON object and return it to the engine.

// PhysicsWidget.cpp

void UI::PhysicsWidget::display()
{
  auto physicsEngine = m_physicsEngine.lock();

  //...

  // Retrieve input json from the physics engine with all available parameters
  json js = physicsEngine->getInputJson();
  // Draw all items from input json
  drawImguiObjectFromJson(js);
  // Update physics engine with new parameters values if any
  physicsEngine->updateInputJson(js);
}

New generic JSON workflow between PhysicsWidget and the physics engine

Notice how generic the new workflow is, the physics widget doesn’t know which model is used anymore, it just gets a JSON object, draws it, updates it and returns it. We don’t even include the different model headers anymore, only the base Model.hpp. Previously we were calling the parameter getters from the leaf model type, because the parameters depend on the model itself. To do this we had to dynamically cast the physics engine with the different leaf model types and add model specific logic to handle it, all of this is gone, the UI logic is much more generic.

The core JSON drawer

// PhysicsWidget.cpp

void drawImguiObjectFromJson(json& js)
{
  for (auto& pair : js.items())
  {
    const auto& name = pair.key();
    auto& val = pair.value();

    if (val.is_object())
    {
      ImGui::Spacing();
      ImGui::Text(name.c_str());
      ImGui::Indent(15.0f);
      // Recursive call
      drawImguiObjectFromJson(val);
      ImGui::Unindent(15.0f);
      ImGui::Spacing();
    }
    else if (val.is_boolean())
    {
      // accessing json bool value by reference, directly modifying the value within the json
      drawImguiCheckBoxFromJson(name, val.get_ref<bool&>());

      // special case where we skip the rest of the items if "Enable" param is false
      bool skipRestOfItems = name.find("Enable##") != std::string::npos && val == false;

      if (skipRestOfItems)
        return;
    }
    else if (val.is_array() && val.size() == 3)
    {
      if (val[0].is_number_integer())
      {
        // cannot directly access json array items by reference, one copy needed
        drawImguiSliderInt(name, val);
      }
      else if (val[0].is_number_float())
      {
        // cannot directly access json array items by reference, one copy needed
        drawImguiSliderFloat(name, val);
      }
    }
    else
    {
      LOG_ERROR("{} type is not supported by PhysicsWidget drawer", name);
    }
  }
}

Core drawer function iterating on all items of the model’s JSON input

The function drawImguiObjectFromJson() handles the drawing of the whole JSON object, it iterates through the different pair of key-value and acts on them. If the value is another JSON object, it calls itself recursively with the subobject. For the other supported types: float/int/bool, it calls helper functions on top of DearImgui to draw the UI items. The list of supported types is hardcoded and limited, it could probably be improved but the DearImgui APIs are hard-typed anyway and we are able to implement the three existing models with this set of types.

You can notice the hardcoded logic with skipRestOfItems for boolean parameters. This is a limited hack allowing us to generate conditional sub-blocks of parameters in our UI, if feature A is disabled, we don’t need to draw all its sub parameters to tweak it. This only works if we implement a feature with sub-parameters as a JSON sub-object in our model’s JSON input (see above inputFluidsJson).

Drawing boolean checkboxes

For boolean parameters, we pass directly the JSON value reference to drawImguiCheckBoxFromJson() using val.get_ref<bool&>(), this is safe as we ensure val.is_boolean() before doing so. It allows us to let the user modifying the value in place within the JSON object, which is pretty cool.

// PhysicsWidget.cpp

void drawImguiCheckBoxFromJson(const std::string& name, bool& enable)
{
  ImGui::Checkbox(name.c_str(), &enable);
}

drawImguiCheckBoxFromJson() is just a wrapper around DearImGui API

Drawing numerical sliders

For numerical parameters, we store arrays instead of single values in order to pass the min/max range with the current parameter value. This allows us to keep everything encapsulated and close in the model, but prevents us from getting direct references to the value. Indeed, currently nlohmann::json doesn’t support direct reference access for array elements. Nevermind, we can make a local copy of the array.

// PhysicsWidget.cpp

void drawImguiSliderFloat(const std::string& name, json& js)
{
  float floatVal = js.at(0);
  float minVal = js.at(1);
  float maxVal = js.at(2);
  std::string precision = floatVal <= 0.1f ? "%.4f" : "%.2f";
  if (ImGui::SliderFloat(name.c_str(), &floatVal, minVal, maxVal, precision.c_str()))
  {
    js.at(0) = floatVal;
  }
}

Precision varies based on the value.

Updating model and GPU kernels by parsing JSON

An intermediary OpenCLModel layer

I described how the model parameters are sent to the UI layer and rendered there. As already mentioned, they are returned to the simulation side with updated values through physicsEngine->updateInputJson(js);. Now, I need to implement updateModelWithInputJson(json& inputJson) to define how the model parses the JSON object to update its parameters values. In our OpenCL-based physics engine based, this mainly boils down to passing the information to the GPU kernels that run the simulation. So I decide to implement an intermediary class OclModel between Model and Boids/Fluids/Clouds defining this in a clean and generic way through two new functions transferJsonInputsToModel(json& inputJson) and transferKernelInputsToGPU():

// OclModel.hpp

// OclModel is a variadic template inheriting from Model
// KernelInputs are structs used to pass model parameters values as args to GPU kernels
// Fluids model uses a single kernel input struct
// Clouds has two kernel input structs as it uses most of the fluids kernels and some clouds-specific ones 
template <typename... KernelInputs>
class OclModel : public Model
{
  public:
  OclModel(ModelParams params, KernelInputs... kernelInputs, json inputJson = {})
      : Model(params, inputJson)
  {
    // Adding all inputs to kernel inputs for GPU-CPU interaction
    (m_kernelInputs.push_back(kernelInputs), ...);
  };

  //...

  // This override is called within Model::updateInputJson(const json& newJson)
  void updateModelWithInputJson(json& inputJson) override
  {
    // First transfer inputs from json to model and kernel inputs
    transferJsonInputsToModel(inputJson);
    // Then transfer kernel inputs from CPU to GPU
    transferKernelInputsToGPU();
  }

  // Model-specific logic, from json to model
  virtual void transferJsonInputsToModel(json& inputJson) = 0;
  // Model-specific logic, from model to gpu kernels
  virtual void transferKernelInputsToGPU() = 0;

  // KernelInputs logic...
}

A new intermediary class OclModel.hpp (simplified version)

Passing the parameters values to the GPU

Now we just need to define transferJsonInputsToModel(json& inputJson)/transferKernelInputsToGPU() in our various models to complete our round trip data transfer from UI to the physics model with JSON.

// Fluid.cpp

void Fluids::transferJsonInputsToModel(json& inputJson)
{
  if (!m_init)
    return;

  // Wrong parameter path will trigger an exception
  try
  {
    const auto& fluidsJson = inputJson["Fluids"];

    // Caching this parameter for easier use in the physics loop - not perfect
    m_nbJacobiIters = fluidsJson["Nb Jacobi Iterations"][0];

    auto& kernelInputs = getKernelInput<FluidKernelInputs>(0);
    // Updating the fluids kernel input struct
    kernelInputs.restDensity = (cl_float)(fluidsJson["Rest Density"][0]);
    kernelInputs.relaxCFM = (cl_float)(fluidsJson.at("Relax CFM")[0]);
    kernelInputs.timeStep = (cl_float)(fluidsJson["Time Step"][0]);
    kernelInputs.dim = (cl_uint)((m_dimension == Geometry::Dimension::dim2D) ? 2 : 3);
    //...
  }
  catch (...)
  {
    LOG_ERROR("Fluids Input Json parsing is incorrect, did you use a wrong path for a parameter?");

    throw std::runtime_error("Wrong Json parsing");
  }
};

Passing data to the model

Being the first one called, transferJsonInputsToModel() parses the incoming JSON object to update model’s internal data used in the simulation loop. We are now passing model parameters values to GPU kernels through KernelInputs structs, but I won’t go into details as it is not directly related to JSON. tranferJsonInputsToModel() is mostly about filling these structs.

// Fluids.cpp

void Fluids::transferKernelInputsToGPU()
{
  if (!m_init)
    return;

  assert(getNbKernelInputs() == 1);
  const auto& kernelInputs = getKernelInput<FluidKernelInputs>(0);

  CL::Context& clContext = CL::Context::Get();
  clContext.setKernelArg(KERNEL_PREDICT_POS, 2, sizeof(FluidKernelInputs), &kernelInputs);
  clContext.setKernelArg(KERNEL_UPDATE_VEL, 2, sizeof(FluidKernelInputs), &kernelInputs);
  //...
}

Passing data to the GPU

Once the kernel input struct is updated, in transferKernelInputsToGPU() we set it as an argument to the GPU kernels that needs it. And voila, the round-trip data transfer between the UI and the physics engine with JSON is complete!

Results

Here is the final result, as you can see the new generic physics widget is very similar to the initial handmade one:

NewRtpUI New RealTimeParticles UI, a few items have been shuffled but it is very similar to the previous one

Now that the system is up and running, let’s do a quick comparison of the pros and cons of our new JSON approach. I was aware of most of its advantages as they are the reasons I started this project. As expected, some inconvenients also appeared along the way, nothing is perfect, especially not my initial JSON proposal or my implementation. So let’s take a step back and see what survives the reality test.

Pros

Less boilerplate code

Having less boilerplate to do the plumbing on both UI and engine side was the main reason I started this project. In that respect, it has been a real success. I have been able to remove about 100 lines of code (LOC) in the various model header files, about 350 LOC in the model implementations and 500 LOC in PhysicsWidget, that’s about 1000 LOC gone!

Generic and automated UI

Most of the UI for the physics engine is now automated, which is another great improvement. I don’t need to spend time adjusting the different parameter widgets, how they interact or even their layout. Everything is automatic. I can now focus only on the core drawer loop and a single change will impact any parameter in any model.

Separation of concern and encapsulation

With the new approach, the UI does UI stuff and the physics engine does… physics stuff. The UI doesn’t know anymore anything about the physics models. It just receives a JSON object, displays it and updates it if necessary. On the other side, the physics engine now contains in a single data structure all the parameters provided to the users with their range and default values. This is much better than hard-coding the parameter range within the UI layer.

Next model can be added in an instant

This is the biggest win for me, thanks to all the points mentioned above, I can now add a new model and connect it to the existing UI in a matter of minutes. This is perfect for prototyping and focusing on what matters to me: implementing physics simulations!

Cons

Automatic UI means less customization

If you automatize a system, you accept loosing tweaks for the sake of uniformity. Nothing surprising here, this was expected. However, it became an small issue with some bool parameters enabling subsystems, previously I would just hide those subsystems parameters if the upper condition was disabled. With the automatic UI generation, I had to implement additional logic and find a proper JSON layout to maintain the UX and ensure no regression on the UI side. Adding these small adjustments was acceptable for me in order to cover these corner cases, but it could get tricky if I have more constraints.

No explicit type means more risks

One serious drawback is the lack of explicit typing for the parameters in the JSON input. This comes inherently with JSON generic storage. Because of this, in the model implementation I have to be careful when parsing the information from the JSON object: missing parameter, wrong path or mistmatched type can lead to nasty surprises. For now, to counter this, I prefer to use the JSON input mostly for transfer, and store the models data in different more granular member variables once transmitted. It adds a bit of overhead but creates a clear data transfer cascade from the JSON input to the kernel inputs. With KernelInputs, I have to make a copy anyway in order to pass the data to the GPU.

So the JSON input is parsed once per update in transferJsonInputsToModel(json& inputJson) and then kept out of the physics processing loop. This approach is not optimal as it generates an extra copy and means duplicated data. At the scale of the current system, I am totally fine with the trade-off as it comes with many benefits. But for a bigger system, I would probably implement a safety layer to make it more robust, using type checking and default values if a parameter is not found at runtime. This would allow me to store the data only in the JSON and keep a single source of truth. Again, no deal-breaker here, just a few drawbacks to be aware of, but I consider the tradeoff is completely worth it in my situation.

Conclusion

Alright, we’ve come to the end of this article which presented one way to use JSON to connect your UI to your C++ engine. I hope you enjoyed it. All the code presented above can be found on GitHub. Personally, I’m very happy with the changes and I’m looking forward to implementing the next model and seeing how much time is won with the new system. Like any approach, it has its pros and cons, but I think the pros far outweight the cons in my case. I encourage everyone to take a look at JSON and see if you can use it in your project! Happy coding!


© 2021. All rights reserved.

Powered by Hydejack v9.2.1