• Как использовать потоки в современном C++ в приложении на основе цикла событий?

    @OnePride
    Делаю красиво
    Чтобы не плодить велосипедов с std::async, не наводить хаос с количеством одновременно живущих потоков, и в будущем сразу иметь возможность строить граф задача - присмотритесь к taskflow
    Ответ написан
    Комментировать
  • Как в ировом движке на C++ распаралерить функции Update и Render?

    @OnePride
    Делаю красиво
    Не очевидно почему физика должна стать "плавнее". Начинать нужно с профайлинга, и понять сколько времени выполняются функции Update() и Render(), и уже от этого решать как параллелить, что параллелить, и нужно ли вообще. Так же нужно определиться как обновляется игровое состояние - всегда ли с фиксированным шагом dt (православно), или позволительно обновляться с плавающим dt (не православно) - в этом случае результаты симуляции не будут сходиться у игроков с разным ФПС, и это может сильно мешать прохождению игры.

    Случай 1: время выполнения одного Update() много больше Render()
    Тогда вынос Update() в отдельный поток профита практически не даст, т.к. почти весь кадр он же и съедает, а оверхэд связанный с многопоточностью перекроет весь теоретический прирост ФПС с вероятностью 146%. В этом случае надо распараллеливать внутри вызова Update() - обновлять игровой стейт в несколько потоков. Если каждый объект модифицирует только собственный стейт, то все параллелится элементарно.

    Случай 2: время выполнения одного Render() много больше Update()
    Здесь уже два варианта:
    1. можно попробовать обойтись без многопоточности, просто в начале кадра провернуть цикл с несколькими вызовами Update(), разбивая dt кадра на несколько шагов
    2. вынести Update() в отдельный поток, и там уже крутить цикл с фиксированным dt или плавающим. Там же ограничивать логический ФПС, чтобы не считать игровой стейт, например, с частотой 2000 Гц, и не жарить процессор юзера

    В общем случае идеальная многопоточность - это когда потоки не конкурируют за данные, которые одновременно могут быть читаемы и модифицируемы, т.е. точек синхронизации практически нет, и нет блокировок потоков. А мьютекс - это как раз блокировка в случае одновременного доступа двумя и более потоками. Чем больше кусок кода под мьютексом и чем больше потоков за него конкурируют, тем больше ущерб перфомансу, и тем печальнее вся многопоточность работает, и такой результат получить очень легко. Планировать использование мьютексов на раннем этапе игрового движка - это тупик, на такой поточности далеко не уехать. Мьютекс годится для редких операций, либо очень коротких вычислений под ним, либо ожидание происходит в каких-то вторичных потока, которые не приведут к фризу на экране.

    В случае игрового движка, оптимальная схема будет выглядеть так:
    Есть два независимых стейта - логический и графический.
    Логический стейт - это вся физика, скрипты, эвенты, и тд - то что сейчас и есть.
    Графический стейт - это минимально достаточный набор данных по каждому объекту логики, плюс состояние окружения, погоды, эффектов, и т.п. Этот стейт полностью описывает графический кадр, передается как аргумент в Render() и позволяет нарисовать весь кадр с нуля без обращения к логическому стейту. Этот стейт должен быть сериализуем, чтобы его можно было отложить в сторонку и через какое-то время нарисовать.
    Так же у нас есть два потока - основной логический и основной рендерящий. В логическом потоке мы обновляем логический стейт, по необходимости по нему формируем графический стейт, после чего через тройную буферизацию передаем графический стейт в поток графики. Поток графики же всегда рисует самый актуальный графический стейт (предыдущие рисовать не имеет смысла), либо ждет, когда появится новый, если логика сильно тормозит.
    В итоге имеет два потока работающих независимо, и единственное место синхронизации - это свап указателей/индексов в triple buffer, который сделан через атомики, что практически бесплатно (плюс ожидание на семафоре или спинлоке в потоке графики, если логика не успевает). В зависимости от задачи можно сделать чуть более жесткую синхронизацию, чтобы логический поток также ждал поток график, как сделано в большинстве движков, когда логический поток считает N кадр, а графический поток рисует (N-1), в остальных случаях один из потоков будет висеть на семафоре...

    Псевдокод:
    LogicObject { position, velocity, script, health, ammo,... }
    //соответствующий LogicObject графический объект:
    GraphicObject { position, shape, color, opacity, animationPhase...}
    
    LogicState { time, vector<LogicObject > entites,....}
    
    GraphicState { logicTime, camerPosition, cameraFOV, cameraAspect, ... , skyBoxID,  vector<GraphicObject> objects,...}
    
    
    LogicState gameState;
    TrippleBuffer<GraphicState> graphicStateBuffer;//3 графических стейта
    
    //логический поток:
    {    
        while(true)
        {
            Update(dt, gameState);
    
            GraphicState graphState = buildGraphState(gameState);
            graphicStateBuffer.write(graphState);
        }
    }
    
    //графический поток:
    {    
        while(true)
        {
            //для простоты считаем что метод блокирующий, когда нет нового стейта
            bool bNewFrame = graphicStateBuffer.update();
    
            if(bNewFrame)
                Render(graphicStateBuffer.read());
        }
    }


    Если делать прям совсем на красоту, то можно сделать экстраполяцию позиций графических объектов. У каждого GraphState мы знаем время логического стейта, для которого он был запечен, и это время всегда в прошлом. Соответственно позиции графических объектов в начале кадра графики экстраполируются на разницу между реальным временем начала рендеринга и временем, когда этот графический стейт был сформирован. В этом случае к графическому стейту прилипают скорости всех объектов...
    Ответ написан
    Комментировать