Разбор главного цикла симуляции: что, где, когда...

Раздел посвящен обсуждению вопросов разработки DLL-модулей подвижного состава
Ответить
Аватара пользователя
maisvendoo
Модератор
Сообщения: 339
Зарегистрирован: 13 авг 2019, 10:25
Город: Ростов-на-Дону
Настоящее имя: Дмитрий
VK: https://vk.com/maisvendoo
Контактная информация:

Разбор главного цикла симуляции: что, где, когда...

Сообщение maisvendoo » 29 фев 2020, 22:28

Коль скоро у некоторых пользователей проснулся интерес к API симулятора, думаю будет правильно немного осветить вопросы, связанные с его работой. В частности, пользователя andreykod интересовала механика вызова метода Vehicle::keyProcess(). Для того чтобы пояснить этот момент, думаю стоит пояснить как работает движок игры на основном цикле моделирования.

Итак, за симуляцию физики поезда в RRS отвечает процесс simulator. Его точка входа, функция main() содержит следующий код (здесь и далее пути буду указывать относительно корня дерева исходников)

simulator/simulator/main.cpp

Код: Выделить всё

int main(int argc, char *argv[])
{
    init_journal();
    register_handlers();

    AppCore app(argc, argv);

    if (!app.init())
        return -1;
    else
        return app.exec();
}
На вход эта функция, как известно принимает аргументы командной строки. Далее: вызов init_journal() выполняет инициализацию подсистемы логирования (та, что пишет файл logs/Journal.log); вызов regist_handler() инициализирует обработчики структурных исключений - это используется для отлова ошибок.

Потом создается экземпляр класса AppCore, который наследует от QCoreApplication. Объекту app передаются аргументы командной строки, а потом вызывается метод AppCore::init() выполняющий стартовую реализацию. При успешной инициализации запускается основной цикл обработки сигналов Qt вызовом AppCore::exec().

Что происходит при инициализации. А вот что

simulator/simulator/app.cpp

Код: Выделить всё

bool AppCore::init()
{
    // Common application data settings
    this->setApplicationName(APPLICATION_NAME);
    this->setApplicationVersion(APPLICATION_VERSION);

    QString errorMessage = "";

    QString cmd_line = "";
    for (QString s : this->arguments())
        cmd_line += " " + s;

    Journal::instance()->info("Process " + APPLICATION_NAME + " started with command line: " + cmd_line);
    Journal::instance()->info("Started " + APPLICATION_NAME + " initialization");

    switch (parseCommandLine(parser, command_line, errorMessage))
    {
    case CommandLineOk:

        // Creation and initialization of train model
        model = new Model();
        Journal::instance()->info(QString("Created Model object at address: 0x%1").arg(reinterpret_cast<quint64>(model), 0, 16));
        return model->init(command_line);

    case CommandLineError:

        fputs(qPrintable(errorMessage), stderr);
        fputs("\n", stderr);
        return false;

    case CommandLineVersionRequired:

        printf("%s %s\n",
               qPrintable(this->applicationName()),
               qPrintable(this->applicationVersion()));

        return false;

    case CommandLineHelpRequired:

        parser.showHelp();
        return false;
    }

    return false;
}
Принципиальным тут является вызов parseCommandLine(), который выполняет разбор параметров командной строки, и в зависимости от его результата возвращает либо код ошибки, либо код успешного завершения (CommandLineOk). При успешном разборе аргументов создается экземпляр класса Model, для которого вызывается его инициализация, с передачей распарсенных аргументов командной строки.

Класс Model отвечает за реализацию физической модели поезда, организует её счет в реальном времени, а так же обеспечивает обмен данными между другими процессами, в частности отправляют данные для viewer-ра и принимают от него коды клавиш. При инициализации этого класса происходит:
  1. Применение аргументов командной строки.
  2. Создание и настройка решателя дифференциальных уравнений движения поезда
  3. Загружается профиль пути из выбранного маршрута
  4. Создается и инициализируется экземпляр класса Train - физическая модель поезда. При инициализации этого класса происходит загрузка и настройка модулей DLL всего подвижного состава, входящего в поезд.
  5. Создается и инициализируется общая память для обмена с вьювером.
  6. Инициализируется внешний пульт управления
Если ни на каком из этапов не происходит критический сбой, то метод Model::init() а вслед за ним и AppCore::init() возвращает "истину" и далее вызывается переопределенный в классе AppCore метод AppCore::exec()

simulator/simulator/app.cpp

Код: Выделить всё

int AppCore::exec()
{
    if (model != Q_NULLPTR)
        model->start();

    return QCoreApplication::exec();
}
Тут вызывается метод Model::start(), который активирует симуляцию и запускается основной цикл всего приложения, где происходит обработка событий. Внимание: симуляция стартует только после вызова QCoreApplication::exec() и крутится в основном цикле обработки событий по таймеру!

Таймер, обрабатывающий симуляцию создается в классе Model и работает с интервалом, который определяется параметром IntegrationTimeInterval, задаваемым в конфиге cfg/init-data.xml. По-умолчанию его значение равно 20 миллисекундам. Таймер работает в отдельном потоке, дабы как можно четче выдерживать этот самый временной интервал.

Итак, один раз в 20 мс таймер вызывает метод (слот!) Model::process(), реализованный в классе Model так

simulator/model/model.cpp

Код: Выделить всё

void Model::process()
{
    double tau = 0;
    double integration_time = static_cast<double>(integration_time_interval) / 1000.0;    

    // Integrate all ODE in train motion model
    while ( (tau <= integration_time) &&
            is_step_correct)
    {
        preStep(t);

        // Feedback to viewer
        sharedMemoryFeedback();

        controlStep(control_time, control_delay);

        is_step_correct = step(t, dt);

        tau += dt;
        t += dt;

        postStep(t);
    }

    train->inputProcess();    

    // Debug print, is allowed
    if (is_debug_print)
        debugPrint();    
}
В этом методе выполняется прежде всего интегрирование дифференциальных уравнений, описывающих физику поезда. Выполняется это в цикле while, где, с как можно большей скоростью просчитывается 20 мс "жизни" поезда. Но не только, разберем по полкам
  1. preStep() - действия, которые требуется выполнить перед шагом интегрирования дифуров.
  2. sharedMemoryFeedback() - обмен данными с вьювером (визуализацией).
  3. controlStepControl() - чтение из общей памяти состояния клавиатуры и передача его в модель поезда
  4. step() - шаг решения дифуров.
  5. Наращивание счетчиков модельного времени.
  6. postStep() - действия, которые надо выполнить после шага интегрирования.
Главное узкое место тут - очень желательно, чтобы этот цикл выполнялся менее чем за 20 мс. Иначе будет виден эффект "отставания" времени, физика выпадет из реалтайма. После этого цикла, когда физика на 20 мс посчитана вызывается

Код: Выделить всё

train->inputProcess(); 
код которого выглядит так

simulator/train/train.cpp

Код: Выделить всё

void Train::inputProcess()
{
    auto end = vehicles.end();
    auto begin = vehicles.begin();

    for (auto i = begin; i != end; ++i)
    {
        Vehicle *vehicle = *i;
        vehicle->keyProcess();
        vehicle->hardwareProcess();
    }
}
Здесь мы и видим вызовы Vehicle::keyProcess() и Vehicle::hardwareProcess(), выполняемые для каждой единицы подвижного состава в поезде. Первый призван обрабатывать ввод с клавиатуры, второй - прием данных с органов управления аппаратного пульта.

Отсюда становится очевиден ответ - keyProcess() вызывается раз в IntegrationTimeInterval, то есть по-умолчанию раз в 20 миллисекунд. Поэтому да, наблюдается звонковая работа клавиш, в коде обработки необходимы специальные "противозвонковые" меры, применяемые на всех реализованных для сима локомотивах.

Так работает основной цикл симуляции. Этой его реализации присущ ряд проблем, поскольку в алгоритме есть достаточно тяжелая математика, например продольная динамика поезда. Многие процессы в тормозном оборудовании требуют малого шага интегрирования, исчисляемого 1-2 мс, поэтому, а общем, цикл симуляции и организован так хитро.

Что же, начало бесед о коде RRS положено, надеюсь это будет интересно, рассчитываю на полезные замечания и советы.
Возврата к деспотии Ситхов не будет!

Ответить

Вернуться в «Программирование модулей подвижного состава и оборудования (C++ API)»