Как стать автором
Обновить

Комментарии 20

Спасибо за статью, идея интересная! Вопрос, были ли мысли как обрабатывать 2*2!=2^2, x^0=nan и т.д. Степень, ясное дело, через exp(b*ln(a)), я про общий принцип подхода. Второй вопрос, как отслеживаете различную работу модификаторов точности и работу частных случаев на разных девайсах? Я использую паралельный вывод стадий работы шейдера на квад, но это не всегда удобно.
Общий принцип подхода: если где-то встречается какая-то неоднозначность, то лучше про нее посигналить на всякий случай, пусть разработчик смотрит. Скажем, если вдруг встретилось бы выражение вроде `pow(x, [0,1])`, то я бы посчитал результат для случая `pow(x, (0,1])`, а про 0 выдал бы предупреждение (я использую сослагательное наклонение т.к. прямо сейчас в коде не так).

Работу модификаторов точности не использую, возможно напрасно. Особенности разных девайсов тут тоже не учитываются. Можно сказать, что такая проверка ближе к юнит-тестам, чем к интеграционным/приемочным. Тестирование на «железе» статический анализ все равно не заменит.

А может логичнее попытаться с символьными вычислениями? Т.е. вместо диапазонов вы будете хранить набор уравнений и неравенств, определяющих как диапазон входных данных (и только их, диапазоны зависимых переменных вами не вычисляются), так и всё, что происходило с разными переменными. А при нахождении строчки, на которой можно навернуться, добавлять недопустимое условие и скармливать какой‐нибудь CAS (computer algebra system). Если она успешно решает данный набор, то могут быть проблемы и можно доложить пользователю. Если говорит решений нет — то проблем нет. Если зависает или выдаёт ошибку — то либо у вас ошибка в программе, либо система уравнений и неравенств просто слишком сложная для CAS. В обоих случаях нужно использовать запасной план — либо игнорирование (т.е. «в анализируемом коде нет ошибки»), либо уточнение условий (не у пользователя, а на основании эвристики) и перезапуск CAS, либо использование другого алгоритма без символьных вычислений.


Я не думаю, что такой вариант бы сработал с языком общего назначения. Но с GLSL может и зайдёт.

Спасибо за мысль.
В целом, похоже на то, что в этом направлении и надо думать. Явно надо отслеживать весь «маршрут» преобразований диапазонов, а это как раз и приведет к набору уравнений и неравенств.
Если будет время, попробую.
Вы как минимум не учли ещё одну проблему. В общем случае диапазон выходных значений цвета вообще ничем не ограничен. Как пример — любой многоступенчатый рендеринг (HDRI, PBR и т.д.). В этом случае диапазон 0..1 должен быть на выходе только последнего в цепочке шейдера. Да и то не уверен, вроде уже есть HDR мониторы, которые, возможно, принимают сразу более широкий диапазон.

Что, кстати, автоматически означает, что и на входе значения из текстуры могут быть вне этого диапазона.

Но идея интересная. Отладка сложных шейдеров иногда занятие весёлое.
Ну, это не проблема ) Анализатору все равно, просто ему заданы некоторые умолчания. Можно для каждого случая указывать произвольные входные/выходные допустимые диапазоны.
И проверка выходных значений — только одно из применений. Меня больше волновали выходы за границы при вызове всяких `pow`, `mix`, `step` — разные видеокарты по-разному обрабатывают такие случаи.

Отладка сложных шейдеров иногда занятие весёлое.

Да даже простые шейдеры отлаживать — сплошное веселье )
НЛО прилетело и опубликовало эту надпись здесь
Ну ок, в следующий раз напишу «чаще всего новички сталкиваются с двумя видами шейдеров».
Получается абстрактная интерпретация с нереляционным интервальным доменом. Если захочется улучшать реализацию — смотрите в этот раздел, там даются ответы на некоторые из вопросов.
. Юнит-тесты на GLSL не напишешь.

Дилетантский вопрос… собственно почему не напишешь?!
Ну, я просто не знаю таких инструментов. GLSL выполняется на видео-карте, единственное, что мы получаем в результате выполнения шейдера — пиксель на экране. Никаких промежуточных значений, никаких `console.log` выполнить не получится. Смотри на пиксель на экране и угадывай — это у тебя шейдер правильный, или твоя видеокарта сгладила твои ляпы :)

Если бы был транслятор GLSL в какой-нибудь другой язык, то можно было бы на транслируемую версию писать юнит-тесты. Но опять же — я такого не встречал…
Пиксель это тоже информация и её скорее всего можно взять из видеобуфера. И проанализировать на корректность.
Можно. Но вот пример из начала статьи — у меня на машине такой юнит-тест прошел бы. А юнит-тест, который то проходит, то не проходит в зависимости от окружения — это плохой юнит-тест.

Или скажем ваш шейдер записывает в выходной цвет в красный канал значение 2, при допустимом диапазоне от 0 до 1. На экран такое все равно не выведется, т.е. ошибку мы не увидим. А увидим что-то в зависимости от конкретной видеокарты — может быть максимально яркий красный цвет. А может черный. Т.е. выходной информации может просто не хватать для анализа.

Однако если вам надо убедиться что в таких-то ситуациях в таких-то точках должен получиться такой-то цвет — то ваш способ идеально подходит. Просто это ближе к интеграиционным/приемочным тестам, чем к юнит-тестами.
Однако это явный false alarm, а для статического анализатора нет ничего хуже ложных срабатываний — если его не отключат после первого крика «волки», то после десятого точно.

Порой это лучше, чем ничего, особенно если есть возможность поставить исключения после первого просмотра.

При этом на других языках можно хотя бы юнит-тесты писать, а тут — никак.

В своём небольшом проекте мы директивой #include включали код шейдеров в код на c++, и с помощью нескольких дефайнов и обёрток превращали шейдеры в обычные функции, которые возможно вызвать.
В своём небольшом проекте мы директивой #include включали код шейдеров в код на c++, и с помощью нескольких дефайнов и обёрток превращали шейдеры в обычные функции, которые возможно вызвать.

Пример не приведёте?
Было бы здорово в виде статьи.
Например, для шейдера из статьи
uniform float value; //-> [0,1];
void main() {
    float val2 = value - 1.;
    gl_FragColor = vec4(value - val2);
}


Можно сделать так.
#include <iostream>

#define uniform
typedef float vec4;

struct shader_test
{
	vec4   gl_FragColor;
	shader_test():
		gl_FragColor(0) {}
#include "shader.glsl"
};

int main(int, char**)
{
	shader_test tst;
	tst.value = 0.5;
	tst.main();
	std::cerr << "gl_FragColor = " << tst.gl_FragColor << std::endl;
	return 0;
}

Спасибо!

А как вы собираетесь разбираться с доступами к элементам вектора в таком коде?


vec4 color = vec4(sin(coord.yzx),0)
Нехитрая с++ магия
template<class T>
struct v2
{
	T& x;
	T& y;
	v2(T& _x, T& _y) : x(_x), y(_y) {}
	template<class X>
	v2(const X& other) : x(other.x), y(other.y) {}

	template<class X>
	v2<T>& operator = (const X& other)
	{
		T   temp_x(other.x);
		T   temp_y(other.y);
		x = temp_x;
		y = temp_y;
		return *this;
	}
	v2<T>& operator = (const v2<T>& other)
	{
		T   temp_x(other.x);
		T   temp_y(other.y);
		x = temp_x;
		y = temp_y;
		return *this;
	}
};

template<class T>
struct vec2
{
	T       x;
	T       y;
	v2<T>   xx;
	v2<T>   yy;
	v2<T>   xy;
	v2<T>   yx;

	vec2(T _x, T _y) : x(_x), y(_y), xx(x, x), yy(y, y), xy(x, y), yx(y, x) {}

	template<class X>
	vec2(const X& other) : x(other.x), y(other.y) {}

	template<class X>
	vec2<T>& operator = (const X& other)
	{
		x = other.x;
		y = other.y;
		return *this;
	}
};


Позволяет творить невообразимые вещи
void test_swap()
{
	{
		vec2<float> point1(0, 0);
		vec2<float> point2(1, 2);

		point1.xy = point2;
		std::cerr << "point = " << point1.x << " " << point1.y << std::endl;

		point1.yx = point2;
		std::cerr << "point = " << point1.x << " " << point1.y << std::endl;
	}
	{
		vec2<float> point1(0, 0);
		vec2<float> point2(1, 2);

		point1 = point2.xy;
		std::cerr << "point = " << point1.x << " " << point1.y << std::endl;

		point1 = point2.yx;
		std::cerr << "point = " << point1.x << " " << point1.y << std::endl;
	}
	{
		vec2<float> point1(0, 0);
		vec2<float> point2(1, 2);

		point1.yx = point2.xx;
		std::cerr << "point = " << point1.x << " " << point1.y << std::endl;

		point1.xy = point2.yy;
		std::cerr << "point = " << point1.x << " " << point1.y << std::endl;
	}
	{
		vec2<float> point1(3, 4);
		point1.xy = point1.yx;
		std::cerr << "point = " << point1.x << " " << point1.y << std::endl;
	}
}

Я не успел плюсануть этот комментарий. Интересный способ.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации