Асинхронные запросы в OpenGL. Расширения ARB_occlusion_query2 и ARB_timer_query.

Поскольку OpenGL построен исходя из модели клиент-сервер, то возвращение управления из очередной команды OpenGL (например, glDraw*) еще не говорит ни о чем, кроме того, что Ваша команды была добавлена в командный буфер (вполне возможно она еще даже не начала выполняться на GPU). Поэтому большинство команд являются асинхронными, т.е. они просто добавляют команды в буфер команд и немедленно возвращают управление клиенту.

Поэтому возникает необходимость в некотором асинхронном механизме получения информации от сервера, который бы не обязательно блокировал клиентскую часть приложения в ожидании пока запрос дойдет до GPU, будет готов ответ и этот ответ будет получен клиентской частью. Подобная функциональность предоставляется через так называемые асинхронные запросы (asynchroneous queries), позволяющие отправлять запросы на GPU и отслеживать их состояние.

В OpenGL существует специальный механизм асинхронных запросов (впервые появившиеся в расширении ARB_occlusion_query) позволяющий отправлять на GPU определенные типы запросов, отслеживать их состояние и получать требуемую информацию. В каждый момент времени можно узнать готов ли результат данного запроса (без ожидания) и получить результат запроса (ожидая готовности, если этот результат еще не готов).

На данный момент (версия OpenGL 4.1) поддерживаются следующие типы запросов:

Каждому асинхронному запросу соответствует свой объект OpenGL, который идентифицируется при помощи беззнакового целого числа, не равного нулю. Для выделения массива таких чисел и их освобождения служат следующие функции:

void glGenQueries    ( GLsizei n, GLuint * ids );
void glDeleteQueries ( GLsizei n, const GLuint * ids );

Узнать, является ли заданное целое число idидентификатором какого-либо асинхронного запроса, можно при помощи функции glIsQuery:

GLboolean glIsQuery ( GLuint id );

Простейшим (и чаще всего используемым) способом создания запроса с заданным идентификатором по группе команд OpenGL является помещение этих команд внутри блока glBeginQuery/glEndQuery:

void glBeginQuery ( GLenum target, GLuint id );
void glEndQuery   ( GLenum target );

В этих запросах параметр id является идентификатором запроса, а параметр target задает тип запроса и принимает одно из следующих значений - GL_SAMPLES_PASSED, GL_ANY_SAMPLES_PASSED, GL_TIME_ELAPSED, GL_PRIMITIVES_GENERATED и GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN.

Также существует возможность создания запроса с индексом (на данный момент это применяется только для transform feedback'а с несколькими вершинными потоками, индекс отвечает за выбор вершинного потока). Для создания индексированных запросов служат команды glBeginQueryIndexed и glEndQueryIndexed.

void glBeginQueryIndexed ( GLenum target, GLuint index, GLuint id );
void glEndQueryIndexed   ( GLenum target, GLuint index );

При этом обычный вызов glBeginQuery ( target, id ) эквивалентен вызову glBeginQueryIndexed ( target, 0, id ), а вызов glEndQuery ( target ) - эквивалентен вызову glEndQueryIndexed ( target, 0 ).

При помощи функции glQueryiv можно получать информацию об асинхронных запросах:

void glGetQueryiv ( GLenum target, GLenum pname, int * params );

Запрос с параметром pname равным GL_CURRENT_QUERY позволяет узнать идентификатор активного в данный момент запроса типа target. Запрос с pname равным GL_QUERY_COUNTER_BITS позволяет узнать количество бит счетчика, используемого для хранения результатов запросов данного типа (это значение может зависеть от реализации).

Следующие функции позволяют получить информацию о конкретном запросе по его идентификатору.

void glGetQueryObjectiv    ( GLuint id, GLenum pname, int      * params );
void glGetQueryObjectuiv   ( GLuint id, GLenum pname, GLuint   * params );
void glGetQueryObjecti64v  ( GLuint id, GLenum pname, GLint64  * params );
void glGetQueryObjectui64v ( GLuint id, GLenum pname, GLuint64 * params );

Параметр id задает идентификатор существующего запроса, а параметр pname определяет какое именно значение, связанное с данным запросом следует вернуть.

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

GLint ready;

glGetQueryObjectiv ( id, GL_QUERY_RESULT_READY, &ready );

if ( ready )
{
    // результат готов можно сразу же читать

    GLuint result;

    glGetQueryObjectiv ( id, GL_QUERY_RESULT, &result );
	
    // результат получен, можно использовать
	
}

Для того, чтобы получить результат запроса используется значение параметра pname равное GL_QUERY_RESULT. При этом если результат запроса еще не готов, то происходит блокировка приложения и явное ожидание готовности результата.

Значение однажды полученного идентификатора можно переиспользовать, в том числе и для других типов запросов. Обратите внимание, что поддержка 64-битовых результатов обеспечивается расширением ARB_timer_query.

Расширения ARB_occlusion_query и ARB_occlusion_query2

Оба эти расширения предназначены для отслеживания фрагментов, полученных в результате выполнения команд из соответствующего блока glBeginQuery*/glEndQuery*, успешно прошедших тест глубины. Расширение ARB_occlusion_query вводит типа запроса GL_SAMPLES_PASSED, а расширение ARB_occlusion_query2 - тип запроса GL_ANY_SAMPLES_PASSED.

Первый тип запроса считает сколько именно фрагментов успешно прошло тест глубины (считаются только те фрагменты, которые получены в результате выполнения команд, заключенных внутри соответствующего блока glBeginQuery*/glEndQuery*). Значением данного запроса является именно точное количество фрагментов, прошедших тест глубины (подробнее об этом запросе смотрите в статье о расширении ARB_occlusion_query.

Запрос с типом GL_ANY_SAMPLES_PASSED позволяет узнать прошел хотя бы один фрагмент тест глубины, т.е. данный запрос является упрощенной (и, как правило, более быстрой) версией запроса GL_SAMPLES_PASSED. Значением данного запроса является GL_FALSE или GL_TRUE.

Расширение ARB_occlusion_query вошло в OpenGL 3.3 core.

Расширение ARB_timer_query

Данное расширение вводит еще один асинхронного запроса - GL_TIME_ELAPSED. Запросы данного типа позволяют узнать время, затраченное GPU на выполнение команд из соответствующего блока glBeginQuery*/glEndQuery*. Результатом запроса данного типа является затраченное время в наносекундах (наносекунда - 10-9 секунды).

Обратите внимание, что не всегда 32-битового счетчика хватает для представления необходимого времени, поэтому данное расширение вводит функции, позволяющие получать 64-битовое значение запроса - glGetQueryObject*64v.

Следующий фрагмент кода демонстрирует применение данного запроса для получения времени, затраченного на выполнение заданного блока кода.

GLint    query;                    // query id
GLuint64 elapsedTime;              // затраченное время в наносекундах

glGenQueries ( 1, &query );

// . . . 
 
 // Начинаем запрос
glBeginQuery ( GL_ELAPSED_TIME, query );
 
 // Команды, время выполнения которых нас интересует
 
glEndQuery ( GL_ELAPSED_TIME );
 
 // . . .
 
 // получаем затраченное время (ожидая готовности результата)
glGetQueryObjectui64v ( query, GL_QUERY_RESULT, &elapsedTime );

Также расширение ARB_timer_query вводит еще один тип запроса - GL_TIMESTAMP. Для работы с этим типом запроса служит специальная функция glQueryCounter:

void glQueryCounter ( GLuint id, GLenum target );

В качестве параметра target этой функции может выступать только константа GL_TIMESTAMP, а параметр id, как и ранее, является идентификатором асинхронного запроса.

Данная функция позволяет отправить запрос к внутреннему таймеру GPU и получить его время (в наносекундах). При этом сама команда просто добавляет запрос на получение времени от таймера в поток команд и когда все команды, предшествующие glQueryCounter будут полностью выполнены, то текущее значение внутреннего таймера GPU будет записано в соответствующий объект-запрос и станет доступным.

Обратите внимание, что вызовы glQueryCounter можно использовать в том числе и внутри блока glBeginQuery*/glEndQuery*. Еще одним важным моментом является то, что GPU может использовать разные таймеры для запросов GL_TIMESTAMP и GL_TIME_ELAPSED.

За счет использования вызова glQueryCounter можно переписать предыдущий пример с определением времени выполнения заданного фрагмента кода следующим образом:

GLint    startQuery, endQuery;     // query id
GLuint64 startTime, endTime;       // время начала выполнения фрагмента кода и время окончания выполнения
GLuint64 elapsedTime;              // затраченное время в наносекундах

glGenQueries ( 1, &startQuery );
glGenQueries ( 1, &endQuery );

//  . . . 
 
// Начинаем запрос
glQueryCounter ( queryStart, GL_TIMESTAMP );
 
// Команды, время выполнения которых нас интересует
 
glQueryCounter ( queryEnd, GL_TIMESTAMP );
  
// . . .
 
// получаем затраченное время (ожидая готовности результата)
glGetQueryObjectui64v ( query, GL_QUERY_RESULT, &startTime );
glGetQueryObjectui64v ( query, GL_QUERY_RESULT, &endTime );

elapsedTime = endTime - startTime;

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

Обратите также внимание на расширение ARB_sync также позволяющее асинхронно проверять выполнение кода.

Расширение ARB_timer_query вошло в OpenGL 3.3 core.

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

#ifndef	__GL_QUERY__
#define	__GL_QUERY__
#include	"libExt.h"

class Query
{
	GLuint	id;
	GLenum	target;
	
public:
	Query ();
	~Query ();

	void beginQuery ( GLenum theTarget )
	{
		target = theTarget;
		glBeginQuery ( target, id );
	}

	void endQuery ()
	{
		glEndQuery ( target );
	}

	void beginQueryIndexed ( GLenum theTarget, int index )
	{
		target = theTarget;
		glBeginQuery ( target, id );
	}

	void endQueryIndexed ( int index )
	{
		glEndQueryIndexed ( target, index );
	}

	void	timestamp ()
	{
		target = GL_TIMESTAMP;

		glQueryCounter ( target, id );
	}

	bool	 isReady     () const;		// result is ready ?
	GLuint	 getResult   () const;
	GLuint64 getResult64 () const;

	static GLuint	activeQuery ( GLenum theTarget );
	static int	counterBits ( GLenum theTarget );
};

#endif

По этой ссылке можно скачать весь код класса Query.