Доброго времени суток всем хабровчанам!

Как-то пришлось работать с медленным промышленным ARM с кастомным линуксом на борту. Было там одно слабое ядро и 512 мегабайт оперативной памяти, которая выделялась на низком уровне частично для видеопамяти. Был там огромный графический интерфейс полностью на QML, внутри которого был и осциллограф. В то время он был реализован на QChart, и показывал максимум 20 FPS, поскольку рисовался полностью слабым процессором. Мне это жутко не нравилось. Я привык видеть глазами минимум 60 FPS, так что я серьёзно занялся этой проблемой.

Текущий вид осциллографа на 6 датчиков
Текущий вид осциллографа на 6 датчиков

Для начала я натолкнулся на проблему, что нельзя передать вектор переменных в ShaderEffect. Можно передавать всякие одиночные переменные, которые потом будет обрабатывать шейдер, но речь идёт о std::vector измерений отображаемого датчика, который надо обновлять каждую итерацию. Мне пришла идея закодировать массив данных в текстуре, которую потом будет уже разбирать шейдер. В QT есть замечательный класс QQuickImageProvider, который позволяет передавать любое изображение из С++ в QML/Image. Он автоматически синхронизируется с потоком QML, и QML/Image обновляется только после того как текстура будет лежать уже в потоке QML. Т.е. если ты будешь часто менять картинку на бэкэнде она не будет у тебя моргать на экране. С помощью него я и кодировал изображение для моего будущего шейдера. Делал я это так:

Я создал пустое изображение размером 110х6 пикселей (картинка ниже). С каждым замером датчиков я прохожу по следующему столбику пикселей в моём изображении и заполняю пиксель текущим значением датчика в виде цвета от 0 до 256. Если ряд не чётный - я кодирую значение красным цветом, если чётный - то зелёным. Сделано это для того, чтобы при увеличении размера текстуры на большом экране шейдер не брал часть цвета с соседнего ряда пикселей из-за диффузии. В стартовый пиксель я кладу текущий тайм код замера, который рисует параллельную синюю линию на осциллографе.

Строение кодированного изображения
Строение кодированного изображения

Теперь, когда у нас готово кодированное изображение переходим к самому простому - написанию шейдера.

Можно сделать двумя разными способами - написать один шейдер на весь экран и внутри него написать логику для разных уровней высоты, или написать 6 шейдеров, которые будут брать нужный себе ряд информации из этого изображения. В моём случае для гибкости UI был выбран вариант с 6-тью разными шейдерами. Код шейдера в QML ниже:

Image { //В QML можно изображение использоавть напрямую как текстура для шейдеров
        id: sensorValues
        visible: false
        cache: false
        function reload(){ //Для обновления изображения
            var old = source
            source = ""
            source = old
        }
        source: "image://calculateImage/1000" //класс нашего QQuickImageProvider
}

Repeater{ //Не писать же 6 почти одинаковых шейдеров вручную)
model: 6
Rectangle{
  property var imageSource: sensorValues // id Image, который ссылается на QQuickImageProvider
  property var borderSensivity: (index*2+1)/12 //Ряд пикселей, которые будет смотреть текущий шейдер
  property var trigerColor: index%2===0 ? "r" : "g" //Цвет, которым закодирован текущий ряд
  property var sensivity: 0.001 //Допустимая погрешность при сравнении двух float в шейдере (по сути qFuzzyCompare)
  fragmentShader:"
    uniform highp float borderSensivity;
    uniform highp float sensivity;
    uniform highp sampler2D imageSource;
    varying highp vec2 qt_TexCoord0;
    void main(){
        highp vec2 coordImage = vec2(qt_TexCoord0.x,borderSensivity);
        highp vec4 tempColor = texture2D(imageSource,coordImage);
        highp vec4 resultColor;
        if(qt_TexCoord0.y <= tempColor." + trigerColor + " && qt_TexCoord0.y >= tempColor." + trigerColor + " - sensivity){
            resultColor = vec4(redColor,greenColor,blueColor,1.0); //Значит я попал в значение датчика и надо раскрасить себя жёлтым
        }else{
            resultColor = vec4(0.0,0.0,0.0,0.0); //Не попал в значение, так что просто чёрный пиксель
        }
        tempColor = texture2D(imageSource,vec2(0.0,0.0));
        if(qt_TexCoord0.x <=tempColor.b + 0.001 && qt_TexCoord0.x >= tempColor.b - 0.001){
            resultColor += vec4(0.0,0.0,0.8,0.8); //Попал в таймкод - значит добавляем к себе синий цвет
        }
        gl_FragColor = resultColor;
    }"
  }
}

По сути всё, вот мы закодировали большой массив данных в изображение и передали в QML, обойдя отсутствие возможности передать QVector в shader. Как было сказано в заголовке - такой способ позволил мне сделать осциллограф со скоростью 140 FPS, который просто летает на старом железе. Этот же способ кодирования вообще можно очень гибко использовать при работе с большими данными, когда допустим у вас 4000 вершины высоты, и передавать это как вертексные вершины в QSGNode на слабом железе не представляется возможным. Этот же способ даст вам 60 фпс. Проверено на собственном опыте)

Комментарии (4)


  1. shares-caisson
    08.08.2024 08:36

    Ожидал что "огромный массив данных" будет чем-то вроде гигабайта-другого, а тут аж 1980 байт передаётся.


    1. drakkonne Автор
      08.08.2024 08:36

      Максимально передавал 12 изображений для отрисовки разреза поверхности с интересующими объектами внутри. Передавал высотное закодированное 512 на 512 пикселей и к нему 11 изображений 40 на 40. Итого получается 2 114 752 байта. Обновлял каждые 10 секунд, а сам шейдер поворачивался 60 раз в секунду. Фризов нигде не было. QSGNode сдох бы сразу. А как передать гигабайт в 100 мегабайт видеопамяти не знаю)


      1. drakkonne Автор
        08.08.2024 08:36

        Пример более сложный, но принцип тот же, один в один. Решил для статьи взять более простую вещь для понимания


  1. Sdima1357
    08.08.2024 08:36

    Одно слабое ядро

    В пару гигагерц?

    140 fps

    Прям вот на экране? Вы уверены?