Code structure - DigitalHolography/Holovibes GitHub Wiki

API

It's the back-end interface used by the UI and the back.

Overview

The definition of the API is split into multiple modules. The main API is a singleton with sub-modules as members. Modules are:

Module Responsible for
composite RGB and HSV composite mode and all their settings
compute image type, computation mode and start/stop of computation.
contrast contrast, log and reticle display.
filter2d filter 2D
global_pp global post processing computation: registration, renormalization and convolution.
information credits, benchmark mode, boundary, documentation URL, cuda version, logger and statistics (hardware, queues usage).
input camera, file loading and the input queue.
record start and stop record, record mode, record file, chart record and record queue.
transform space (ST) and time transformation (TT) algorithm: batch size, time stride, TT size, ST algorithm, TT algotirhm, lambda, z distance, frequencies/eigen values ranges, fft shift, unwrapping 2d and cuts output buffer.
view load/unload computation of views (raw, filter 2D, cuts, lens, chart). Display rate.
window_pp per window post processing: flip, rotation, accumulation level and bitshift.
settings load/save compute settings as json.

API functions can be called at any moment. For some the behavior may differ whether computation as started or not. For example for set_batch_size:

  • If computation has stopped, the setting is updated.
  • If computation has started, the setting is updated, the input queue is rebuild and the pipe is refreshed.

Error handling

Getters functions returns directly a value.

Most setters return a ApiCode indicating if the operation succeed or an error code otherwise. Each time something went wrong a warning log is printed in order to gave more insight.

Usage

Important

  • When using the API you must only include API.hh. Sub-modules headers are internal.
  • API setters MUST NEVER be called outside of the API EXCEPTED for the UI.

Here a small example:

#include "API.hh"

// ...

auto& api = API; // Macro that expands to holovibes::api::Api::instance()
api.input.import_file("test.holo");
api.compute.start();

// ...

api.compute.stop();

Contributing

When adding functions think as follow:

  • Does my function really a back-end one ? If not put it in GUI.cc/hh.
  • Does another API function do the same thing ? Can i try to merge both if they are similar ?
  • Does my function is more related to a sub-module ? If not create a new one (try to have the fewest number of them)
  • Is my function useful for the user or it's an internal one ? If it's internal put it as private. If you had to access it from another module make it friend of it.

Warning

  • The API is solely for back-end logic, DO NOT put UI related logic inside.
  • Keep the fewest sub-modules possibles while maintaining consistency.
  • Exposes the fewest possible function to the user make internals one private and use friend if necessary.
  • Create the fewest possible functions in the API, it must remain logical and easy to understand.
  • Always think from the dev user point of view (the one that will use the API).

Request system

Some logic must be done only once in the pipe (like allocation/deallocation of buffers when changing batch size or the accumulation window size).

Requests are declared in the enum ICompute::Setting. A macro ICS (stands for ICompute Settings) is used to access request easily: ICS::YourRequest.

Usage:

auto pipe = ... // Get an instance of the pipe

// Request YourRequest and request a pipe_refresh
pipe.request(ICS::YourRequest);

// Set YourRequest to false but do not discard the pipe_refresh
pipe.clear_request(ICS::YourRequest);

// Request (or discard if false) YourRequest without requesting a pipe_refresh
pipe.set_requested(ICS::YourRequest, true);

pipe.is_requested(ICS::YourRequest); // true

Add a request

Add a field in ICompute::Setting, it muse be before Count:

enum class Setting
{
	...
	YourRequest,
	...
	Count
};

Add the logic in Pipe::make_requests. A macro can be used for one line request:

HANDLE_REQUEST(ICS::YourRequest, "Log String", ...);

// Will be expanded to
if (is_requested(ICS::YourRequest))
{
	LOG_DEBUG("Log String");
	...
	clear_request(ICS::YourRequest);
}

Note

  • Requests are handled at the beginning of each pipe refresh (if requested)
  • There are internally handled with an array of atomic bool.

Settings

Each settings is a struct holding a single variable value. There are defined in settings.hh using a macro. Usage:

// Declare a new settings of type NewSetting that will hold a size_t
DECLARE_SETTING(NewSetting, size_t);

Settings container

These settings are then stored into a settings container (defined in settings_container.hh. There are two containers:

  • RealtimeSettingsContainer: used to store settings that will be updated immediately (for real time).
  • DelayedSettingsContainer: used to store initial settings value and keep track of what should be updated. The modification are delayed (for pipe refresh).

Usage:

  • To create a new container pass a std::tuple of the initial values to the constructor and all the settings as template parameters (remember a setting is a type). It's recommended to declare the list of type as a macro (see icompute.hh).
  • To update settings in real time, use RealtimeSettingsContainer::update_setting.
  • To update settings with a delay, use DelayedSettingsContainer::update_setting and apply delayed updates with DelayedSettingsContainer::apply_updates.
  • To get the value of a setting, use SettingsContainer::get.
  • To check if a setting is in a container, use the trait has_setting or holovibes::has_setting_v.

Code example:

// Create a RealtimeSettingsContainer
RealtimeSettingsContainer settings<std::string, int, float>(std::make_tuple("Hello", 9, 3.14));

settings.update_setting(42);
settings.get<int>(); // 42

// Check if a setting is in the container
has_setting_v<int, settings> // returns true
has_setting<int, settings>::value == has_setting_v<int, settings>; // True

// Create a DelayedSettingsContainer
DelayedSettingsContainer delayed_settings<std::string, int, float>(std::make_tuple("Hello", 9, 3.14));

delayed_settings.update_setting(42);
delayed_settings.get<int>(); // 9

has_setting_v<int, delayed_settings> // returns true

delayed_settings.apply_updates();
delayed_settings.get<int>(); // 42

Location

All settings are stored in a RealtimeSettingsContainer inside holovibes.hh. Default values are given to them in the ctor.

The pipe stores some of these settings in three containers (defined in icompute.hh):

All settings inside submodules of the pipe (fourrier, converts, post-process, ...) has also been updated.

  • realtime_settings_: settings that needs to be updated directly and do not trigger pipe refresh or clear the accumulation queue.
  • pipe_cycle_settings_: settings that are updated at the end of a pipe cycle. When updated, the accumulation queue will be cleared and the auto-contrast reapplied. Typically settings inside lambda
  • pipe_refresh_settings_: settings that are updated at the end of a pipe cycle and trigger a pipe refresh. Typically settings outside lambda that are used to build the pipe (add/remove a computation step, allocate/deallocate buffers).
  • onrestart_settings_: settings that does not change during the entire life time of the object. (like the record queue location or the output buffer size).

Other computation steps (Analysis, Post process, Rendering, ...) also have containers. They MUST respect the ordering defined in icompute.hh.

Access (get, set)

  • If you are in the pipe (pipe.cc or other computation step) to get the value of a setting you MUST call this->setting<T>() and you MUST never call this->update_setting(T value) nor api::get_xxx and api::set_xxx.
  • In all other places you MUST call the appropriate function of the API (api::get_xxx and api::set_xxx).
  • You MUST never modify directly settings stored in holovibes nor call GET_SETTING, UPDATE_SETTING and SET_SETTING (these macros get and set settings stored in holovibes) except if you are in API.hxx and in all_struct.cc.

When a setting T is updated in the API, this will call Holovibes::update_setting then if ICompute has T call ICompute::update_setting(T), then call update_setting for each computation step if they contain T.

Tools

Logging

We have 5 levels of log:

  • Trace (LOG_TRACE)
  • Debug (LOG_DEBUG)
  • Infos (LOG_INFO)
  • Warnings (LOG_WARN)
  • Errors (LOG_ERROR)

And 7 loggers that log on std::cerr and in a log file in appdata:

  • main
  • setup
  • cuda
  • information_worker
  • record_worker
  • compute_worker
  • frame_read_worker

Usage:

LOG_ERROR(logger_name, formated_string [,args]);

Format:

[${log_level}] [${timestamp}] [${Thread ID}] ${logger_name} >> ${message}

Assertions

Usage:

CHECK(condition)
CHECK(condition, formated_string [,args]); // Up to 14 varags

Format:

[${log_level}] [${timestamp}] [${Thread ID}] ${logger_name} >> ${filename}:${line} ${message}

Versioning

When loading a holo file or a compute settings, Holovibes will apply patch to it in order to convert them to their latest version. This is done inside Backend/includes/version/. They are two converters, one for holo file (done by the class HoloFileConverter) and one for the compute settings (done by ComputeSettingsConverter).

Both have a single public member latest_version and a single static function for converting. They have an array of all patches build in th init() function. Each path has a json patch attached to it.

Json patch

They are small file written in json that specifies how to change a json (RFC). Operations are: add, remove, replace, move, copy and test.

Holo file version

The version of the file is stored in the header. When you change the format of the file (header, add new entry to the footer) you need to:

  1. Update the latest version inside the HoloFileConverter struct inside Backend/includes/version/holo_file_converter.hh
  2. Add a new entry in the array inside the HoloFileConverter::init(). The object takes three arguments: the source version, the relative path of the json patch (they are stored inside Backend/assets/json_patches_holofile/) or empty string if none to apply and the converting function to call.

Here an example:

void convert_v3_to_v4(io_files::InputHoloFile& input, json& footer, const json& json_patch)
{
    // Do stuff
}

void HoloFileConverter::init()
{
    converters_ = {
        {2, "patch_v2_to_v3.json", convert_default}, // Apply solely the json patch in order to convert from v2 to v3
        {3, "", convert_v3_to_v4}, // Does not apply any json patch but will call convert_v3_to_v4
    };
}

Compute settings version

When adding, renaming or deleting a compute setting you need to:

  1. Add a new entry in the enum ComputeSettingsVersion (inside enum_compute_settings_version.hh) with a small comment describing what you have changed.
  2. Change the member function ComputeSettingsConverter::latest_version (inside compute_settings_converter.hh)
  3. Add a new entry in the patch list inside ComputeSettingsConverter::init(). The object takes three argument, the compute setting source version, the relative path of the json patch (they are stored inside Backend/assets/json_patches_settings/) or empty string if none to apply

Example:

static void convert_v1_to_v2(json& compute_setting)
{
    // Do stuff on the json
}

void ComputeSettingsConverter::init()
{
    converters_ = {
        {ComputeSettingsVersion::NONE, "patch_v1.json", convert_default}, // Apply solely the patch
        {ComputeSettingsVersion::V1, "", convert_v1_to_v2}, // Apply no patch and run the function
    };
}
⚠️ **GitHub.com Fallback** ⚠️