Доброго времени суток всем хабровчанам!
Как-то пришлось работать с медленным промышленным ARM с кастомным линуксом на борту. Было там одно слабое ядро и 512 мегабайт оперативной памяти, которая выделялась на низком уровне частично для видеопамяти. Был там огромный графический интерфейс полностью на QML, внутри которого был и осциллограф. В то время он был реализован на QChart
, и показывал максимум 20 FPS, поскольку рисовался полностью слабым процессором. Мне это жутко не нравилось. Я привык видеть глазами минимум 60 FPS, так что я серьёзно занялся этой проблемой.
Для начала я натолкнулся на проблему, что нельзя передать вектор переменных в 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 фпс. Проверено на собственном опыте)
shares-caisson
Ожидал что "огромный массив данных" будет чем-то вроде гигабайта-другого, а тут аж 1980 байт передаётся.
drakkonne Автор
Максимально передавал 12 изображений для отрисовки разреза поверхности с интересующими объектами внутри. Передавал высотное закодированное 512 на 512 пикселей и к нему 11 изображений 40 на 40. Итого получается 2 114 752 байта. Обновлял каждые 10 секунд, а сам шейдер поворачивался 60 раз в секунду. Фризов нигде не было. QSGNode сдох бы сразу. А как передать гигабайт в 100 мегабайт видеопамяти не знаю)
drakkonne Автор
Пример более сложный, но принцип тот же, один в один. Решил для статьи взять более простую вещь для понимания