programação introdutória em opencl e aplicações em...

37
Tendências e Técnicas em Realidade Virtual e Aumentada Capítulo 3 Programação introdutória em OpenCL e aplicações em realidade virtual e aumentada César Leonardo Blum Silveira V3D Studios [email protected] Luiz Gonzaga da Silveira Jr V3D Studios [email protected] Abstract OpenCL is an open standard for high-performance computing in heterogeneous com- putational environments composed of multicore CPUs and many-core GPUs. OpenCL makes it possible to write multi-platform code for such devices, without the need to use vendor-specific languages and tools, or to map problems to graphics APIs. Focused on parallelism, programming in OpenCL is based on writing functions that are executed in multiple simultaneous instances. This chapter aims to introduce key concepts and explore the architecture defined by the OpenCL standard, presenting practical examples in the fields of Virtual and Augmented Reality. Resumo OpenCL é um padrão aberto para programação de alto desempenho em ambientes com- putacionais heterogêneos equipados com CPUs multicore e GPUs many-core. OpenCL torna possível a escrita de código multi-plataforma para tais dispositivos, sem a neces- sidade do uso de linguagens e ferramentas específicas de fabricante ou do mapeamento 65

Upload: others

Post on 18-Aug-2020

1 views

Category:

Documents


0 download

TRANSCRIPT

Tendências e Técnicas em Realidade Virtual e Aumentada

Capítulo

3

Programação introdutória em OpenCLe aplicações em realidade virtual e aumentada

César Leonardo Blum SilveiraV3D [email protected]

Luiz Gonzaga da Silveira JrV3D [email protected]

Abstract

OpenCL is an open standard for high-performance computing in heterogeneous com-putational environments composed of multicore CPUs and many-core GPUs. OpenCLmakes it possible to write multi-platform code for such devices, without the need to usevendor-specific languages and tools, or to map problems to graphics APIs. Focused onparallelism, programming in OpenCL is based on writing functions that are executed inmultiple simultaneous instances. This chapter aims to introduce key concepts and explorethe architecture defined by the OpenCL standard, presenting practical examples in thefields of Virtual and Augmented Reality.

Resumo

OpenCL é um padrão aberto para programação de alto desempenho em ambientes com-putacionais heterogêneos equipados com CPUs multicore e GPUs many-core. OpenCLtorna possível a escrita de código multi-plataforma para tais dispositivos, sem a neces-sidade do uso de linguagens e ferramentas específicas de fabricante ou do mapeamento

65

Tendências e Técnicas em Realidade Virtual e Aumentada

de problemas para APIs gráficas. Com foco no paralelismo, a programação em OpenCLbaseia-se na escrita de funções que são executadas em múltiplas instâncias simultâneas.Este capítulo visa introduzir conceitos-chave e explorar a arquitetura definida no padrãoOpenCL, apresentando exemplos práticos nas áreas de Realidade Virtual e Aumentada.

3.1. IntroduçãoOs computadores pessoais, servidores e dispositivos móveis atuais podem ser equipadoscom placas gráfica capazes rodar aplicações gráficas 3-D de alta qualidade visual emtempo-real. Além disso, a cada aproximadamente um ano e meio, são lançadas novasplacas gráficas ainda mais sofisticadas, capazes de processar grandes volumes de dados.A preços acessíveis e oferecendo alto desempenho1, graças, sobretudo, à indústria de jo-gos digitais, estas placas atualmente podem ser utilizadas para a solução de problemas emdiversas áreas do conhecimento que demandam alto poder de processamento, como pro-cessamento de imagens, visão computacional, simulações de processos físicos e químicos,problemas algébricos que manipulam grandes volumes de informação, entre outras.

O foco deste capítulo é mostrar como tirar proveito do poder computacional pro-vido pelas centenas de processadores das placas gráficas modernas. Aplicações que fazemuso da capacidade de processamento paralelo dos dispositivos gráficos geralmente o fa-zem através do mapeamento de problemas para as APIs gráficas OpenGL ou Direct3D,ou através de programas que executam diretamente nos processadores das placas gráficas,denominados shaders. Além disso, fabricantes de placas gráficas fornecem linguagens,APIs e ferramentas de desenvolvimento que permitem o acesso direto à memória e aosprocessadores destas placas; porém, o uso destes recursos de programação torna o có-digo desenvolvido específico para uma determinada arquitetura. Neste contexto, surge opadrão OpenCL (Open Computing Language), criado pela Apple [Apple Inc. 2009] e pa-dronizado pelo Khronos Group [Khronos Group 2010]. OpenCL é a primeira plataformaaberta e livre de royalties para computação de alto desempenho em sistemas heterogê-neos compostos por CPUs, GPUs, arquiteturas tipo Cell e outros processadores paralelos.OpenCL oferece aos desenvolvedores um ambiente uniforme de programação paralelapara escrever códigos portáveis para estes sistemas heterogêneos, a fim de tirarem pro-veito do poder de processamento dos dispositivos gráficos e processadores e usá-los paradiversas aplicações em múltiplas plataformas computacionais.

A especificação OpenCL 1.0 é realizada em três partes: uma linguagem, umacamada de plataforma e um runtime. A especificação da linguagem descreve a sintaxe e aAPI para escrita de código em OpenCL, que executa nos aceleradores suportados, CPUsmulticore e GPUs many-core. A linguagem é baseada na especificação C99 linguagemC [ISO 2005]. A camada de plataforma fornece ao desenvolvedor acesso às rotinas quebuscam o número e os tipos de dispositivos no sistema. Assim, o desenvolvedor podeescolher e inicializar os dispositivos adequados para o processamento. O runtime permite

1Os modelos de topo tem desempenho na ordem de teraflops, fornecidos por centenas de stream proces-sors.

66

Tendências e Técnicas em Realidade Virtual e Aumentada

ao desenvolvedor enfileirar comandos para execução nos dispositivos, sendo também éresponsável por gerenciar os recursos de memória e computação disponíveis.

Na prática, aplicações OpenCL são estruturadas em uma série de camadas, comomostra a Figura 3.1. Kernels correspondem às entidades que são escritas pelo desenvolve-dor na linguagem OpenCL C. A aplicação faz uso da API C do padrão para comunicar-secom a camada de plataforma (Platform Layer), enviando comandos ao runtime, que ge-rencia diversos aspectos da execução.

Hardware (GPU,CPU,...)

Driver

OpenCL runtime

OpenCL Platform Layer

API OpenCL C

Aplicação

Kernels

Figura 3.1. Arquitetura da OpenCL

3.1.1. Uma breve ilustração

A fim de evitar que o leitor tenha que passar por muitos conceitos teóricos antes de teruma ideia de como ocorre a programação em OpenCL na prática, será apresentado aquium exemplo simples de código. O trecho de código a seguir corresponde a um kernel quecalcula a diferença entre os elementos de dois arrays e os armazena em um terceiro:

__kernel void ArrayDiff(__global const int* a,__global const int* b,__global int* c)

{int id = get_global_id(0);c[id] = a[id] - b[id];

}

Por ora, os detalhes a respeito de palavras-chave da linguagem, como __kernele __global serão deixados de lado, pois serão introduzidos mais adiante. O que é

67

Tendências e Técnicas em Realidade Virtual e Aumentada

importante observar no código é que não há um laço para iterar sobre os arrays. EmOpenCL, o código escrito corresponde a uma unidade do resultado desejado. O runtimecria várias instâncias do mesmo código no momento da execução, e cada uma delas recebeum ID que pode ser usado para determinar a porção dos dados que devem ser acessadapor uma instância em particular. Isto viabiliza o processamento paralelo dos dados.

A título de comparação, o código abaixo realizaria a mesma tarefa, porém deforma sequencial (considere que ARRAY_LENGTH é uma constante que contém o númerode elementos nos arrays):

void ArrayDiff(const int* a,const int* b,int* c)

{for (int i = 0; i < ARRAY_LENGTH; ++i){

c[i] = a[i] - b[i];}

}

O uso de OpenCL é, portanto, recomendado para aplicações que realizam opera-ções computacionalmente custosas, porém paralelizáveis por permitirem o cálculo inde-pendente de diferentes porções do resultado.

3.1.2. Organização do texto

O restante do texto está organizado como segue. A Seção 3.2 apresenta um histórico daprogramação em GPUs. A Seção 3.3 introduz os conceitos da arquitetura OpenCL. AsSeções 3.4 e 3.5 discutem alguns aspectos da linguagem OpenCL C e da sua API, respec-tivamente. A Seção 3.6 apresenta passo-a-passo a construção de uma aplicação OpenCLcompleta. As Seções 3.7 e 3.8 ilustram possibilidades de uso de OpenCL em Reali-dade Virtual e Aumentada através de exemplos. A Seção 3.9 discute aspectos relativosà otimização de aplicações com OpenCL. Finalmente, a Seção 3.10 apresenta algumasconsiderações finais.

3.2. Programação em GPUAntes de mais nada, é importante recordar a evolução das tecnologias destes dispositi-vos gráficos e das APIs de suporte à programação. Sabemos que sobretudo a indústriados jogos tem impulsionado de forma decisiva esta evolução, através do desenvolvimentode jogos visual ricos e que exigem cada vez mais enorme poder computacional para se-rem executados em tempo-real. Para entender este caminho retrocedendo no tempo eobservamos que as placas gráficas começaram como simples controladoras de vídeo egradativamente incorporaram funções de aceleração 3-D, bem como modificaram o mo-delo de programação. Neste entremeio, o termo GPU (Graphics Processing Unit) foi

68

Tendências e Técnicas em Realidade Virtual e Aumentada

cunhado para denotar que o dispositivo gráfico tinha se tornado um processador. Estaevolução alavancou inúmeras possibilidades para a computação gráfica, em especial paraa renderização em tempo-real, pois se dispunha de um dispositivo dedicado para paraprocessamento gráfico 3-D.

A evolução dos dispositivos gráficos foi acompanhada por softwares, abstraem osdetalhes de hardware de cada fabricante e oferecem APIs para programação. Os grandesrepresentantes deste seguimento são o padrão aberto OpenGL e a tecnologia Direct3D(parte da DirectX) da Microsoft. Ambas as tecnologias implementam o processo de ren-derização usando como base um pipeline gráfico, que descreve todo o fluxo de dados doprocesso de renderização. As APIs da OpenGL e do Direct3D fornecem um conjuntos defunções para a programação do pipeline gráfico, e ainda provêm mecanismos transparen-tes de acionamento do dispositivo gráfico e comunicação com o sistema computacionalprincipal, Figura 3.2.

Processamento de primitivas

Transformações & Iluminação (T&L)

API VBO

Montagem de primitivas Rasterizador

Texturas ... Fog

Stencil ... Color BufferBlending FRAMEBUFFER

Figura 3.2. Pipeline gráfico da OpenGL

Durante o processo evolutivo, as placas gráficas 3-D tornaram-se passíveis de se-rem programadas diretamente através do pipeline gráfico. Os programas, denominadosshaders, são escritos através de linguagens específicas (GLSL, HLSL, Cg e outras), com-pilados para o hardware gráfico. Desta forma, as GPUs oferecem mecanismos para per-mutar funções fixas por códigos desenvolvidos externamente, porém limitados a hot spotsdefinidos pipeline gráfico, Figura 3.3.

A possibilidade de se programar os processadores das GPUs através de shaders,provocou uma explosão de pesquisas e experimentação no sentido de empregar estes dis-positivos na solução de problemas em diversas outras áreas. Para viabilizar o uso dosprocessadores do dispositivo gráfico bastava codificar o problema e os dados em estru-turas processáveis pelos shaders e representadas pelo pipeline gráfico. Nascia assim aGPGPU (General Purpose computation on GPU), programação em GPU para propósitogeneral.

Recentemente ocorreu uma grande inovação nos dispositivos gráficos, instruções

69

Tendências e Técnicas em Realidade Virtual e Aumentada

Processamento de primitivas

Vertex Shader

API VBO

Montagem de primitivas Rasterizador

Fragment Shader

Stencil ... Color BufferBlending FRAMEBUFFER

Geometry Shader

Stream buffer

Figura 3.3. Pipeline gráfico programável da OpenGL

de processador e hardware de memória foram adicionados a GPU , para dar suporte àslinguagens de programação de propósito geral. Concomitantemente, foram criados ambi-entes de programação para permitir que as GPUs possam ser programadas com linguagensde programação familiares, como C e C++. Esta inovação torna as GPU processadoresmany-core, paralelos, completamente programáveis para aplicações de propósito geral.Nascia assim o termo GPU Computing, cunhado para denotar o uso de GPU para com-putação via uma API e linguagem de programação paralela, sem necessariamente ficarpreso à estrutura do pipeline gráfico, a menos que seja conveniente.

Duas soluções tecnológicas proprietárias dão suporte a GPU Computing, CUDAda NVIDIA e Stream da AMD/ATI e a OpenCL, um padrão aberto com suporte aos dispo-sitivos gráficos de diversos fabricantes, preservando-se a interface de programação. Tantoa NVIDIA [NVIDIA Corporation 2010], quanto a AMD/ATI [ATI 2010] são fontes de ex-celentes materiais sobre OpenCL e também provedoras das implementações da OpenCLpara seus respectivos hardwares. O restante deste capítulo será destinado exclusivamentea OpenCL e por vezes, uma comparação com CUDA e Stream. A sinergia entre OpenCLe OpenGL será abordada com mais detalhes, pois são mais e mais comuns aplicaçõesque utilizam tanto os recursos gráficos para renderização e outras tarefas não-gráficas,como áudio, simulação física, inteligência artificial, sistemas financeiros, diversas áreasda matemática em si e em qualquer problema que demande alto desempenho e seja com-putacionalmente paralelizável.

3.3. A arquitetura OpenCLA arquitetura OpenCL é composta por um conjunto de entidades que definem uma camadade abstração de baixo nível para os dispositivos que suportam o padrão. A arquiteturatambém pode ser vista na forma de quatro modelos: modelo de plataforma, modelo deexecução, modelo de programação e modelo de memória.

As seções a seguir introduzem os conceitos da arquitetura OpenCL. Os modelos de

70

Tendências e Técnicas em Realidade Virtual e Aumentada

plataforma, execução e programação são introduzidos junto à discussão de alguns destesconceitos. O modelo de memória é detalhado em uma seção separada. Os nomes dosconceitos foram mantidos na sua forma original em inglês, deixando o leitor familiarizadocom a terminologia encontrada na literatura.

3.3.1. Devices

Um device OpenCL é qualquer dispositivo computacional acessível pelo runtime OpenCLpara o envio de tarefas a serem executadas. Uma GPU ou uma CPU são consideradas de-vices segundo esta definição. Cada device possui uma ou mais compute units capazesde executar instruções em paralelo. Uma compute unit comporta um ou mais processingelements, que efetivamente realizam o processamento.

O sistema computacional responsável pela agregação, inicialização e submissãode tarefas a diferentes devices OpenCL é denominado host. Host, devices, compute unitse processing elements compõem o modelo de plataforma da arquitetura OpenCL. As enti-dades representadas por retângulos na Figura 3.4 correspondem ao modelo de plataforma.

Device

Memória global e memória constante

Computer Unit

ProcessingElement

Memória privada

Memória local

Computer Unit

ProcessingElement

Memória privada

Memória local

Device

Figura 3.4. Modelos de plataforma e de memória da arquitetura OpenCL.

3.3.2. Kernels

Um kernel (também chamado de kernel function ou compute kernel ) é um conjunto deinstruções que podem ser executadas em um device OpenCL. Kernels são compilados apartir de funções escritas na linguagem OpenCL C (ver Seção 3.4) e constituem a unidadebásica de desenvolvimento em OpenCL.

Durante a execução, podem existir uma ou mais instâncias de um mesmo kernel,

71

Tendências e Técnicas em Realidade Virtual e Aumentada

sendo cada instância denominada um work-item. O padrão OpenCL define em seu modelode programação dois modos de uso para um kernel :

• No modo data parallel, múltiplos work-items operam sobre porções diferentes deum conjunto de dados. Diversos work-items podem ser executados em paralelo,de acordo com o número de compute units e de processing elements do device emuso. Neste modo, work-items são combinados em work-groups.

• No modo task parallel, um único work-item é executado sobre um único processingelement do device, designando uma tarefa sequencial. Este modo é assim denomi-nado porque permite a execução de tarefas distintas em paralelo, de acordo com onúmero de compute units e de processing elements do device em uso.

Um kernel tem seus work-items executados sobre um espaço de índices deno-minado NDRange (N-dimensional range). São suportados espaços de índices de 1, 2 e3 dimensões. Quando o host encaminha a execução de um kernel para um device, eleinforma também o número de dimensões, bem como o tamanho de cada uma destas, doespaço de índices sobre o qual o kernel deve ser executado. O runtime encarrega-se decriar um work-item para cada ponto neste espaço. Em cada dimensão, os índices sãointeiros que variam de 0 até o tamanho da respectiva dimensão menos um. A defini-ção do espaço de índices e a criação de work-items para os pontos do espaço de índicesconstituem o modelo de execução da arquitetura OpenCL.

Cada work-item de um kernel é identificado por uma tupla de IDs globais, comum ID para cada dimensão do espaço de índices, e por uma tupla de IDs locais dentrodo work-group. Da mesma forma, cada work-group é identificado por uma tupla de IDs,com um ID pra cada dimensão do espaço de índices. Assim, um work-item pode seridentificado pelos seus IDs globais ou pela combinação dos IDs do seu work-group comseus IDs locais. A Figura 3.5 ilustra a organização de IDs em um espaço de índices deduas dimensões.

Aplicações que realizam processamento paralelo muitas vezes requerem a pre-sença de algum mecanismo de sincronização. OpenCL permite que work-items em ummesmo work-group definam barreiras onde a execução é suspendida até que todos oswork-items atinjam a barreira. No entanto, não existe nenhuma forma de sincronizar asatividades entre diferentes work-groups.

3.3.3. Program objects

Um (program object) é utilizado pelo host para encapsular um programa OpenCL, istoé, o código de um ou mais kernels, na forma de código-fonte ou binária. Program ob-jects também encapsulam uma referência para um context (ver Seção 3.3.6), o últimoexecutável compilado com sucesso e uma lista de devices para os quais o programa foicompilado.

72

Tendências e Técnicas em Realidade Virtual e Aumentada

Figura 3.5. Exemplo de espaço de índices de duas dimensões. Os identificado-res externos correspondem aos identificadores dos work-groups, representadospelas caixas maiores. As caixas menores correspondem aos work-items. Osidentificadores na parte superior das caixas menores correspondem aos IDs glo-bais, enquanto os identificadores na parte inferior correspondem aos IDs locais.

3.3.4. Memory objects

Um memory object permite ao host alocar e acessar a memória global (ver Seção 3.3.7)dos devices OpenCL utilizados pela aplicação.

Existem duas categorias de memory objects: buffer objects (ou simplesmentebuffers) e image objects. Buffers armazenam coleções de tamanho fixo de valores deum determinado tipo de dados, isto é, correspondem a arrays comumente usados na pro-gramação. Image objects armazenam texturas bi- ou tri-dimensionais, frame-buffers ouimagens. Enquanto buffers são sempre suportados por devices OpenCL, estes podem nãooferecer suporte a image objects. É necessário que o host verifique o suporte antes doenvio de um kernel que utiliza image objects para execução.

A principal diferença entre as duas categorias de memory objects reside na formacomo os dados armazenados por estes são acessados. Buffers são acessados por meio deíndices inteiros. Imagens são acessadas através de sampler objects, que possuem parâme-tros relativos à representação binária dos valores dos pixels e métodos de interpolação,entre outros.

3.3.5. Command queues

Uma command queue é a entidade que permite que o host comunique-se com um deviceOpenCL, através do envio de comandos para este. Tais comandos possibilitam o envio dekernels para execução, comunicação com a memória global dos devices e sincronizaçãode tarefas.

Os comandos inseridos em uma command queue sempre são iniciados em ordem,porém o término da sua execução pode ocorrer fora de ordem. No entanto, também épossível fazer com que a execução seja totalmente em ordem, isto é, um comando só

73

Tendências e Técnicas em Realidade Virtual e Aumentada

começará a ser executado quando o comando anterior houver finalizado sua execução.Sempre que um comando é enfileirado, é possível solicitar o retorno de um event objectpara o comando, que permite o aguardo pelo evento do término da execução do comandoantes que a execução prossiga.

3.3.6. Contexts

Um context constitui um ambiente para execução de kernels OpenCL, agregando um oumais devices, memory objects e command queues. A criação e configuração de um con-text estão entre as primeiras tarefas que o host deve realizar em uma aplicação OpenCL.

3.3.7. Modelo de memória

A arquitetura OpenCL define um modelo de memória que divide a memória dos devicesem quatro categorias. Estas categorias determinam a forma como os work-items compar-tilham diferentes regiões de memória:

• Memória global (global memory): compartilhada por todos os work-items de umkernel em execução, sendo permitido o acesso de escrita e leitura. Os work-itemsde um determinado work-group podem perceber mudanças na memória global re-alizadas pelos work-items de outros work-groups (no entanto, como explicado naSeção 3.3.2, não há mecanismo de sincronização entre work-groups, portanto amemória global não é um meio confiável de comunicação entre work-groups dis-tintos).

• Memória local (local memory): compartilhada apenas entre os work-items de ummesmo work-group, sendo permitido o acesso para escrita e leitura.

• Memória privada (private memory): não-compartilhada, restrita a cada work-item,para leitura e escrita.

• Memória constante (constant memory): compartilhada por todos os work-items,mas é permitido somente o acesso para leitura durante a execução de um kernel.

As entidades elípticas na Figura 3.4 correspondem às diferentes categorias de me-mória encontradas em um device da arquitetura OpenCL. Na figura, também pode serobservada a relação do modelo de plataforma com o modelo de memória.

3.3.8. Consistência de memória

O estado visível da memória a um work-item pode não ser o mesmo para outros work-items durante a execução de um kernel, sendo isto dependente de implementação. Porém,as seguintes garantias são feitas quanto à consistência do acesso à memória:

1. Para um único work-item, há consistência de leitura e escrita. Se um work-itemescrever em uma região de memória e a seguir ler a mesma região, o valor lido seráaquele que foi escrito.

74

Tendências e Técnicas em Realidade Virtual e Aumentada

2. As memórias global e local são consistentes entre work-items de um mesmo work-group em uma barreira (ver Seção 3.5.2).

3.4. A linguagem OpenCLA linguagem utilizada para a escrita kernels OpenCL, chamada OpenCL C, é baseada naespecificação C99 da linguagem C, com algumas extensões e um conjunto de restrições.As seções a seguir detalham os aspectos mais relevantes da linguagem OpenCL C.

3.4.1. Tipos de dados

A linguagem OpenCL C oferece tipos de dados divididos em duas categorias: tipos esca-lares e tipos vetoriais. Tipos escalares permitem o armazenamento de informações numé-ricas com diferentes níveis de precisão e em diversos intervalos. Tipos vetoriais permitemo armazenamento de múltiplos dados escalares em uma única estrutura. Além disso, tiposdefinidos através de struct ou union, além de arrays, funções e ponteiros, tambémsão suportados. Há também tipos especiais para imagens (image2d_t e image3d_t),samplers de imagens (sampler_t) e eventos (event_t).

A Tabela 3.1 apresenta os principais tipos escalares da linguagem OpenCL C.Tipos vetoriais são nomeados de acordo com os tipos escalares, sendo sua forma geraltipon. tipo corresponde a um dos tipos listados na Tabela 3.1, enquanto n especificao número de componentes do vetor, o qual pode ser 2, 4, 8 ou 16. Por exemplo, umvetor com 4 componentes do tipo int possui tipo int4. Tipos vetoriais com compo-nentes sem sinal sempre são declarados seguindo a forma abreviada do tipo escalar, istoé, com o prefixo u no início do nome do tipo, ao invés do modificador unsigned. Nãosão suportados vetores de bool, half, size_t e void, nem de tipos definidos pelousuário.

As componentes de uma variável de tipo vetorial são acessadas por meio do ope-rador .. As componentes são identificadas pelos caracteres x, y, z e w, ou por índicesnuméricos de 0 a F (notação hexadecimal). Mais de uma componente pode ser informadaem um único acesso, em qualquer ordem, ou mesmo de forma repetida, para formar ou-tro vetor, contanto que sejam respeitadas as quantidades de componentes permitidas. Osexemplos abaixo ilustram alguns acessos a vetores:

float4 vec1;float4 vec2;float16 vec3;

// Acesso à primeira componente de vec1vec1.x = 1.0f;

// Acesso simultâneo às componentes x e z de vec2vec2.xz = (float2)(2.0f, 3.0f);

// Acesso com componentes misturadas

75

Tendências e Técnicas em Realidade Virtual e Aumentada

Tipo Descriçãobool Booleano; assume os valores true ou false.uchar / char Inteiro de 8 bits sem/com sinal.ushort / short Inteiro de 16 bits sem/com sinal.uint / int Inteiro de 32 bits sem/com sinal.ulong / long Inteiro de 64 bits sem/com sinal.float Ponto-flutuante de 32 bits.half Ponto-flutuante de 16 bits.size_t Equivalente a uint ou ulong, dependendo

da implementação.void Tipo vazio.

Tabela 3.1. Tipos de dados escalares da linguagem OpenCL C. Tipos sem si-nal também podem ser nomeados na forma unsigned [nome do tipo comsinal].

vec1.zw = vec2.yx;

// Acesso com componentes repetidasvec2.yz = vec1.ww;

// Acesso às componentes 0, 5, 10 e 15 de vec3vec3.s05af = vec1;

3.4.2. Qualificadores

A linguagem OpenCL C oferece um único qualificador de função, __kernel, cujo pro-pósito é indicar que a função qualificada é um kernel. Dentro de um código OpenCL, umachamada a uma função definida com o qualificador __kernel comporta-se de formasimilar a qualquer outra chamada. Deve-se atentar, no entanto, a chamadas a funções__kernel que tenham no seu código variáveis declaradas com o qualificador __local(ver texto a seguir), pois o comportamento é definido pela implementação.

Os qualificadores de espaço de endereçamento da linguagem OpenCL C, __global,__local, __constant e __private2 indicam o tipo de região de memória ondeuma variável ou buffer está alocado, de acordo com o modelo de memória descrito naseção 3.3.7. As seguintes regras se aplicam ao uso de qualificadores de espaço de ende-reçamento:

1. Variáveis declaradas sem um qualificador de espaço de endereçamento são automa-ticamente tratadas como __private.

2Estes qualificadores podem também ser usados sem o prefixo __.

76

Tendências e Técnicas em Realidade Virtual e Aumentada

2. Em um kernel, argumentos declarados como ponteiros devem ser __global, __localou __constant. O uso de um destes qualificadores é obrigatório para tais argu-mentos.

3. Um ponteiro de um espaço de endereçamento só pode ser atribuído a outro ponteirodo mesmo espaço de endereçamento. Por exemplo, tentar atribuir o valor de umponteiro __local a outro declarado com o qualificador __global constitui umaoperação ilegal.

4. Variáveis cujo escopo seja o programa inteiro devem ser declaradas com o qualifi-cador __constant.

A seguir estão ilustradas algumas declarações de variáveis com os qualificadoresde espaço de endereçamento apresentados:

// Array de inteiros alocado na memória global__global int* arr;

// Vetor com 4 componentes ponto-flutuante alocado// na memória local__local float4 vec;

// Inteiro de 64 bits alocado na memória privadalong n;

3.4.3. Operadores

A linguagem OpenCL C oferece um conjunto de operadores que trabalham tanto comtipos escalares quanto vetoriais, permitindo a construção de expressões aritméticas, ló-gicas e relacionais com tais tipos. A Tabela 3.2 apresenta os principais operadores dalinguagem, divididos de acordo com a sua categoria.

A seguir são listadas algumas observações importantes sobre o uso dos operadoresda linguagem OpenCL C:

1. Uma operação entre um vetor e um escalar converte o escalar para o tipo do vetor3

e aplica a operação sobre cada componente do vetor. Por exemplo, uma soma entreum vetor e um escalar resultará em um vetor cujas componentes foram acrescidasda quantidade escalar. Da mesma forma, os operadores unários retornam resultadosvetoriais onde a operação foi aplicada individualmente sobre cada componente dovetor.

3O escalar deve possuir o mesmo número de bits ou menos que o tipo das componentes do vetor; casocontrário, a tentativa de conversão para um tipo menor resultará em um erro de compilação.

77

Tendências e Técnicas em Realidade Virtual e Aumentada

Categoria OperadoresOperadores aritméticos binários +, -, *, /, %Operadores aritméticos unários +, -, ++, -Operadores relacionais >, >=, <, <=, ==, !=Operadores lógicos binários &&, ||Operadores lógicos unários !Operadores bit-a-bit &, |, ˆ, , <<, >>Operadores de atribuição =,

+=, -=, *=, /=, %=,&=, |=, ^=, <<=, >>=

Tabela 3.2. Principais operadores da linguagem OpenCL C.

2. Operações entre vetores requerem que estes sejam do mesmo tamanho, isto é, con-tenham o mesmo número de componentes. Caso contrário, ocorrerá um erro decompilação. Tais operações são aplicadas componente-a-componente sobre o ve-tor, por isso a necessidade de estes possuírem o mesmo número de componentes.

3. Operações lógicas e relacionais com pelo menos um operando vetorial retornamum vetor do mesmo tipo deste, porém sempre com sinal. Cada componente podeconter o valor 0 para um resultado falso e -1 (todos os bits configurados em 1) paraum resultado verdadeiro.

4. Os operadores aritméticos %, ++, -, operadores lógicos e operadores bit-a-bit nãosuportam tipos ponto-flutuante.

3.4.4. Restrições

A linguagem OpenCL C estende a linguagem da especificação C99 com alguns tiposnovos e outros elementos, mas também acrescenta uma série de restrições. A lista aseguir enumera algumas das principais destas restrições:

1. Ponteiros para função não são permitidos.

2. Argumentos para kernels não podem ser ponteiros para ponteiros.

3. Variáveis do tipo image2d_t e image3d_t podem ser declaradas apenas comoargumentos para funções.

4. Não são permitidos arrays nem ponteiros para variáveis do tipo sampler_t. Va-riáveis deste tipo não podem ser usadas como variáveis locais em uma função,retornadas por uma função nem passadas como argumento para funções chama-das por um kernel (exceto as funções da API OpenCL). Finalmente, uma variávelsampler_t passada como argumento para um kernel não pode ser modificada.

78

Tendências e Técnicas em Realidade Virtual e Aumentada

5. Funções e macros com número variável de argumentos não são suportados.

6. Os qualificadores extern, static e auto não são suportados.

7. Não há suporte a recursão.

8. A escrita em ponteiros ou arrays com tipos de tamanho inferior a 32 bits (inclusiveos os tipos vetoriais char2 e uchar2) não é permitida.

9. Não são permitidos argumentos do tipo event_t para kernels.

10. Os elementos de uma struct ou union devem pertencer ao mesmo espaço deendereçamento, de acordo com os qualificadores discutidos em 3.4.1.

3.5. A API OpenCLAlém da linguagem OpenCL C, o padrão OpenCL define uma API para uso em con-junto com estas linguagem, provendo diversas funções que podem ser usadas pelos ker-nels durante a computação das suas tarefas. Estas funções permitem a a identificação dework-items e work-groups, a realização de operações matemáticas diversas (livrando oprogramador da tarefa de implementá-las), sincronização, e ainda outras funcionalidades.

Esta fora do escopo deste material realizar uma listagem exaustiva de toda a APIOpenCL. Caso esteja interessado em ter acesso à todas as funções disponíveis, o leitordeve referir-se ao documento da especificação OpenCL 1.0 [Khronos Group 2009]. Asseções a seguir apresentam as funções de identificação e sincronização da API OpenCL.

3.5.1. Identificação

Funções de identificação permitem que cada work-item adquira conhecimento sobre a sualocalização no espaço de índices sobre o qual o kernel é executado.

3.5.1.1. get_global_id

size_t get_global_id(uint dimindx)

Retorna o índice global do work-item no espaço de índices, na dimensão identifi-cada por dimindx.

3.5.1.2. get_local_id

size_t get_local_id(uint dimindx)

79

Tendências e Técnicas em Realidade Virtual e Aumentada

Retorna o índice local do work-item no seu work-group, na dimensão identificadapor dimindx.

3.5.1.3. get_group_id

size_t get_group_id(uint dimindx)

Retorna o índice do work-group ao qual o work-item pertence, na dimensão iden-tificada por dimindx.

3.5.1.4. get_global_size

size_t get_global_size(uint dimindx)

Retorna o número total de work-items na dimensão identificada por dimindx.

3.5.1.5. get_local_size

size_t get_local_size(uint dimindx)

Retorna o número de work-items em um work-group na dimensão identificadapor dimindx.

3.5.1.6. get_num_groups

size_ get_num_groups(uint dimindx)

Retorna o número de work-groups na dimensão identificada por dimindx.

3.5.1.7. get_work_dim

uint get_work_dim()

80

Tendências e Técnicas em Realidade Virtual e Aumentada

Retorna o número de dimensões do espaço de índices sobre o qual o kernel estásendo executado.

3.5.2. Sincronização

A API de sincronização permite a especificação de pontos de sincronização que garantema consistência de memória, devendo ser atingidos por todos os work-items em um work-group antes que a execução prossiga.

3.5.2.1. barrier

void barrier(cl_mem_fence_flags flags)

Cria uma barreira de sincronização. A execução só continuará após todos os work-items pertencentes ao work-group do work-item que atingiu a barreiram também a atin-jam. Do contrário, o work-item, ou os work-items, que chegarem à barreira terão suaexecução bloqueada indefinidamente, e o host não será notificado do término da execu-ção do kernel em momento algum.

Uma barreira permite a criação de uma memory fence, que garante que todasas escritas na memória anteriores à barreira terão sido realmente efetuadas. O parâmetroflags permite controlar a memory fence. Seu valor pode ser CLK_LOCAL_MEM_FENCE,garantindo a consistência da memória local, CLK_GLOBAL_MEM_FENCE, garantindo aconsistência da memória global, ou a combinação de ambos os valores através do opera-dor | (or bit-a-bit).

3.6. OpenCL na práticaUma vez discutidos os principais conceitos referentes à arquitetura e ao padrão OpenCL,é interessante ilustrar o seu uso de forma prática, através da apresentação de um exemplosimples.

As seções a seguir demonstram a construção de uma aplicação OpenCL simples,para o cálculo da diferença entre os elementos de dois arrays. O kernel utilizado é omesmo apresentado na Seção 3.1.1.

A execução do kernel é bastante simples. Após obter o seu ID global no espaço deíndices, este é utilizado para endereçar a posição correspondente nos arrays de entrada esaída. A atribuição de IDs aos work-items é feita pelo runtime, cabendo ao desenvolvedorinformar apenas o número de work-items desejados através da especificação das dimen-sões do espaço de índices. Os work-items, então, acessam as regiões de memória que lhesinteressam de acordo com seus IDs. Estes work-items executam de forma paralela, deacordo com a quantidade de compute units e processing elements do device em uso.

Os trechos de código das seções seguintes ilustram em sequência todos os passos

81

Tendências e Técnicas em Realidade Virtual e Aumentada

necessários para a criação de uma aplicação OpenCL simples. Se inseridos em um arquivode código-fonte, com os devidos cuidados de ligação e localização de cabeçalhos, estecódigo gera um executável funcional em plataformas que suportam o padrão OpenCL.

As explicações para o propósito e os argumentos de cada função estão incluídasnos comentários junto aos trechos de código. Detalhes de menor relevância para a dis-cussão são deixados de lado, sendo solicitado que o leitor busque mais informações naespecificação do padrão OpenCL, onde há uma listagem exaustiva de todas as funções epossíveis valores para seus argumentos.

3.6.1. Inclusão de cabeçalhos e definições

A inclusão do cabeçalho contendo as declarações de funções e outros símbolos da OpenCLé diferente de acordo com o sistema operacional. Em sistemas Mac OS X, o cabeçalho éincluído da seguinte maneira:

#include <OpenCL/opencl.h>

Nos demais sistemas, da seguinte forma:

#include <CL/opencl.h>

Neste exemplo, também é incluído o cabeçalho padrão para entrada e saída padrãona linguagem C++, e é definida uma macro para controlar o tamanho dos arrays:

#include <iostream>

#define ARRAY_LENGTH 32

3.6.2. Declaração de variáveis

Os conceitos apresentados na Seção 3.3 possuem, em sua maior parte, tipos de dados as-sociados a si no momento da programação. Variáveis destes tipos são declaradas pelo hostpara serem utilizadas pelo runtime. O código abaixo apresenta as declarações necessáriaspara este exemplo:

int main(int argc, char** argv){

// Identificador de devicecl_device_id deviceId;

// Context OpenCLcl_context context;

// Command queue

82

Tendências e Técnicas em Realidade Virtual e Aumentada

cl_command_queue queue;

// Program objectcl_program program;

// Kernelcl_kernel kernel;

// Event objectcl_event event;

// Memory objects (para buffers)cl_mem bufA;cl_mem bufB;cl_mem bufC;

Além das variáveis relacionadas ao ambiente OpenCL, também são declaradasvariáveis que o host utiliza para o armazenamento de dados de entrada e saída para okernel, para a determinação das dimensões do espaço de índices e para o armazenamentode códigos de erro:

// Arrays no lado hostint* hostA;int* hostB;int* hostC;

// Array de um único elemento contendo o número de// work-items que serão criados na única dimensão (0)// usada neste exemplosize_t globalSize[1] = { ARRAY_LENGTH };

// Variável para armazenamento de códigos de erroint err;

3.6.3. Código-fonte do kernel

O código-fonte do kernel é armazenado em uma string embutida no código-fonte do pro-grama executado pelo host. Uma possível alternativa seria realizar a carga do código-fonte a partir de um arquivo texto durante a execução. Esta código-fonte é passado para acamada de plataforma para ser compilado para a placa gráfica usada usada para o proces-samento.

const char* source ="__kernel void ArrayDiff( \

__global const int* a, \__global const int* b, \__global int* c) \

{ \

83

Tendências e Técnicas em Realidade Virtual e Aumentada

int id = get_global_id(0); \c[id] = a[id] - b[id]; \

}";

3.6.4. Inicialização do context

Para tornar possível o acesso a devices OpenCL, é necessário que o host primeiro inicia-lize e configure um context. Nesta discussão, a inicialização do context OpenCL não serefere somente à alocação do context em si, mas também à criação do ambiente mínimonecessário para a execução de kernels OpenCL. No exemplo, é criado um context paraum único device, uma GPU, com uma única command queue:

// A função clGetDeviceIDs permite a obtenção de// um ou mais identificadores para devices do// host que suportem o padrão OpenCL.// Aqui, é obtido um ID para uma única GPU.err = clGetDeviceIDs(

// (cl_platform_id) Ver especificação.NULL,// (cl_device_type) Tipo de device desejado.// Aqui, uma GPU. Outras opções comuns são// CL_DEVICE_TYPE_CPU, para uma CPU, e// CL_DEVICE_TYPE_ALL, para obter todos os// devices disponíveis.CL_DEVICE_TYPE_GPU,// (cl_uint) Número de devices desejados.1,// (cl_device_id*) Ponteiro para o local onde a// lista de IDs de devices deverá ser armazenada.&deviceId,// (cl_uint*) Retorna o número de devices// encontrados.NULL

);

if (err != CL_SUCCESS) {std::cerr << "Falha ao obter ID para o device.\n";exit(EXIT_FAILURE);

}

// A função clCreateContext permite a criação de um// context OpenCL.context = clCreateContext(

// (const cl_context_properties*) Ver especificação.0,// (cl_uint) Número de devices no context.1,// (const cl_device_id*) Lista de IDs de devices// para o context.

84

Tendências e Técnicas em Realidade Virtual e Aumentada

&deviceId,// Ponteiro para uma função de notificação chamada// sempre que houver um erro no context. Ver// especificação para detalhes sobre a assinatura da// função.NULL,// (void*) Ponteiro para dados arbitrários passados// para a função apontada pelo argumento anterior.NULL,// (cl_int*) Retorna código de erro.&err

);

if (!context || err != CL_SUCCESS) {std::cerr << "Falha ao criar context.\n";exit(EXIT_FAILURE);

}

// A função clCreateCommandQueue cria uma command queue// para um determinado device em um context.queue = clCreateCommandQueue(

// (cl_context) Context.context,// (cl_device_id) Identificador do device.deviceId,// (cl_command_queue_properties) Propriedades da command// queue. Ver especificação.0,// (cl_int*) Retorna código de erro.&err

);

if (!queue || err != CL_SUCCESS) {std::cerr << "Falha ao criar command queue.\n";exit(EXIT_FAILURE);

}

3.6.5. Compilação do kernel

A compilação do kernel ocorre em três passos:

1. Criação de um program object no context.

2. Compilação do programa para os devices do context.

3. Criação de um kernel a partir do programa compilado.

O código a seguir demonstra a realização destes passos:

85

Tendências e Técnicas em Realidade Virtual e Aumentada

// A função clCreateProgramWithSource cria um program// object a partir do código-fonte de um kernel,// estando este código-fonte representado em um array// de strings.program = clCreateProgramWithSource(

// (cl_context) Context.context,// (cl_uint) Número de strings no array do// código-fonte.1,// (const char**) Array de strings do código-fonte.&source,// (const size_t*) Array de tamanhos de string para// cada string do array do código-fonte.NULL,// (cl_int*) Retorna código de erro.&err

);

if (!program || err != CL_SUCCESS) {std::cerr << "Falha ao criar program object.\n";exit(EXIT_FAILURE);

}

// A função clBuildProgram compila o código-fonte de um// program object para um ou mais devices do// context.err = clBuildProgram(

// (cl_program) Program object.program,// (cl_uint) Número de devices para o qual o// programa deve ser compilado. 0 indica que o// programa deve ser compilado para todos os// devices do context.0,// (const cl_device_id*) Lista de devices para// o qual o programa deve ser compilado. NULL indica// que o programa deve ser compilado para todos os// devices do context.NULL,// (const char*) Opções para o compilador OpenCL.NULL,// Ponteiro para função que será chamada após a// compilação. Ver especificação para mais detalhes.NULL,// (void*) Ponteiro para dados arbitrários passados// para a função apontada pelo argumento anterior.NULL

);

// Caso a compilação tenha falhado, imprime as mensagens

86

Tendências e Técnicas em Realidade Virtual e Aumentada

// de erro do compiladorif (err != CL_SUCCESS) {

size_t len;char info[2048];

// A função clGetProgramBuildInfo permite a obtenção// de dados sobre a compilação de um programa para um// determinado device.clGetProgramBuildInfo(

// (cl_program) Program object.program,// (cl_device_id) ID do device para o qual se// deseja obter dados a respeito da compilação.deviceId,// (cl_program_build_info) Nome do parâmetro que// indica a informação desejada sobre a compilação.// Ver especificação para outros parâmetros.CL_PROGRAM_BUILD_LOG,// (size_t) Tamanho do array onde os dados// retornados serão armazenados.sizeof(info),// (void*) Array onde os dados serão armazenados.info,// (size_t*) Retorna o tamanho em bytes dos dados// obtidos.&len

);

std::cerr << info << std::endl;exit(EXIT_FAILURE);

}

// A função clCreateKernel cria um kernel a partir// de um program object que passou pela etapa de// compilação.kernel = clCreateKernel(

// (cl_program) Program object, após compilação.program,// (const char*) Nome da função __kernel declarada// no código-fonte do programa."ArrayAbsDiff",// Retorna código de erro.&err

);

if (!kernel || err != CL_SUCCESS) {std::cerr << "Falha ao criar kernel.\n";exit(EXIT_FAILURE);

}

87

Tendências e Técnicas em Realidade Virtual e Aumentada

3.6.6. Criação dos arrays no host

Neste exemplo, os arrays de entrada são criados pelo host e inicializados com valoresaleatórios de -50 a +50. Também é criado um array onde são armazenados os resultadosda execução do kernel :

// Cria os arrays no host, para armazenamento dos// dados de entrada e saída do kernel.hostA = new int[ARRAY_LENGTH];hostB = new int[ARRAY_LENGTH];hostC = new int[ARRAY_LENGTH];

// Inicializa os arrays hostA e hostB com valores// aleatórios entre -50 e +50.for (int i = 0; i < ARRAY_LENGTH; ++i) {

hostA[i] = rand() % 101 - 50;hostB[i] = rand() % 101 - 50;

}

3.6.7. Criação dos buffers OpenCL

Após a criação do context e da obtenção dos dados de entrada, estes devem ser transferi-dos para a memória do device que será usado para a computação (neste caso, a GPU). Noentanto, é necessário que antes sejam alocados memory objects do tamanho adequado nocontext, que permitem a transferência dos dados para a memória do device:

// A função clCreateBuffer permite a criação de// buffers em um determinado context. Estes buffers// permitem a posterior entrada e saída de dados// para a memória global dos devices do context.bufA = clCreateBuffer(

// (cl_context) Context.context,// (cl_mem_flags) Flags de especificação de// informações sobre a alocação e o uso da// memória associada ao buffer. A flag// CL_MEM_READ_ONLY cria um buffer somente-leitura.// Outra flags comum é CL_MEM_READ_WRITE, para// a criação de buffers que permitem leitura e// escrita da memória. Ver especificação para demais// flags permitidas.CL_MEM_READ_ONLY,// (size_t) Tamanho em bytes do memory object// a ser alocado.ARRAY_LENGTH * sizeof(int),// (void*) Ponteiro para dados de inicialização// do buffer.NULL,// Retorna código de erro.

88

Tendências e Técnicas em Realidade Virtual e Aumentada

&err);

if (!bufA || err != CL_SUCCESS){

std::cerr << "Falha ao criar buffer bufA.\n";exit(EXIT_FAILURE);

}

bufB = clCreateBuffer(context,CL_MEM_READ_ONLY,ARRAY_LENGTH * sizeof(int),NULL,&err

);

if (!bufA || err != CL_SUCCESS){

std::cerr << "Falha ao criar buffer bufB.\n";exit(EXIT_FAILURE);

}

bufC = clCreateBuffer(context,CL_MEM_READ_WRITE,ARRAY_LENGTH * sizeof(int),NULL,&err

);

if (!bufA || err != CL_SUCCESS){

std::cerr << "Falha ao criar buffer bufC.\n";exit(EXIT_FAILURE);

}

3.6.8. Escrita dos buffers de entrada

Após a criação dos memory objects, é possível transferir os dados para o device. Isto éfeito através do envio de comandos de escrita de buffers para a sua command queue:

// A função clEnqueueWriteBuffer insere em uma command// queue um comando para escrita de dados do// host na memória do device associado à// command queue, por meio de um memory object.err = clEnqueueWriteBuffer(

// (cl_queue) Command queue.queue,// (cl_mem) Memory object.

89

Tendências e Técnicas em Realidade Virtual e Aumentada

bufA,// (cl_bool) Determina se o host deve// bloquear até o término da escrita dos dados.CL_TRUE,// (size_t) Offset a partir do qual os dados// devem ser transferidos.0,// (size_t) Comprimento em bytes dos dados a// serem transferidos.ARRAY_LENGTH * sizeof(int),// (const void*) Ponteiro para a região de memória// do host onde os dados se localizam.hostA,// (cl_uint) Número de eventos na lista de eventos// que devem ser aguardados antes do início da// transferência dos dados.0,// (const cl_event*) Lista de eventos que devem ser// aguardados antes do início da transferência dos// dados.NULL,// (cl_event*) Retorna um event object para// aguardar pelo término da transferência dos dados,// caso a chamada seja não-bloqueante.NULL);

err |= clEnqueueWriteBuffer(queue,bufB,CL_TRUE,0,ARRAY_LENGTH * sizeof(int),hostB,0,NULL,NULL

);

if (err != CL_SUCCESS) {std::cerr << "Falha ao transferir dados para o device.\n";exit(EXIT_FAILURE);

}

É interessante observar que os memory objects em si, no momento da sua criação,estão associados ao context, e não a um device específico. A associação com um deviceocorre no momento do enfileiramento de um comando de escrita (ou leitura), dado quecada command queue é específica de um único device.

90

Tendências e Técnicas em Realidade Virtual e Aumentada

3.6.9. Configuração dos argumentos do kernel

Antes do envio do kernel para execução, é necessário que seu argumentos sejam configu-rados com os valores adequados. A configuração dos argumentos se dá pela informaçãodas regiões de memória do host onde seus valores se encontram.

// A função clSetKernelArg é utilizada para configurar os// argumentos para um kernel.err = clSetKernelArg(

// (cl_kernel) Kernel.kernel,// (cl_uint) Posição (índice) do argumento, de acordo// com a ordem em que os argumentos foram declarados// no código-fonte OpenCL C.0,// (size_t) Tamanho dos dados apontados pelo argumento// seguinte.sizeof(cl_mem),// (const void*) Dados a serem passados como argumento.&bufA);

err |= clSetKernelArg(kernel, 1, sizeof(cl_mem), &bufB);err |= clSetKernelArg(kernel, 2, sizeof(cl_mem), &bufC);

if (err != CL_SUCCESS) {std::cerr << "Falha ao configurar argumentos do kernel.\n";exit(EXIT_FAILURE);

}

3.6.10. Envio do kernel para execução

Após a configuração dos argumentos, o kernel pode ser enviado para execução no device.É importante observar que a chamada que enfileira o comando de execução do kernel ésempre não-bloqueante. O término da execução deve ser aguardado a partir do eventoretornado pelo enfileiramento.

// A função clEnqueueNDRangeKernel insere em uma// command queue um kernel para execução em um// device. A função permite também a// especificação das dimensões do espaço de índices// sobre o qual o kernel será executado.err = clEnqueueNDRangeKernel(

// (cl_queue) Command queue.queue,// (cl_kernel) Kernel.kernel,// (cl_uint) Número de dimensões do espaço de// índices.1,// (const size_t*) Ver especificação.

91

Tendências e Técnicas em Realidade Virtual e Aumentada

NULL,// (const size_t*) Array de tamanhos das dimensões// globais do espaço de índices.globalSize,// (const size_t*) Array de tamanhos das dimensões// locais do espaço de índices (dimensões dos// work-groups). Quando NULL, o runtime determina// o tamanho das dimensões.NULL,// (cl_uint) Número de eventos na lista de eventos// que devem ser aguardados antes do início da// execução do kernel.0,// (const cl_event*) Lista de eventos que devem ser// aguardados antes do início da execução do kernel.NULL,// (cl_event*) Retorna um event object para// aguardar pelo término da execução do kernel.&event

);

if (err != CL_SUCCESS) {std::cerr << "Falha ao enviar kernel para execução.\n";exit(EXIT_FAILURE);

}

Quando o array de tamanhos de dimensões locais é especificado pelo desenvolve-dor, é necessário que a divisão das dimensões globais pelas dimensões locais seja exata,sem resto. Caso contrário, um erro ocorrerá e o kernel não será enviado para execução.

3.6.11. Sincronização: espera pelo término da execução do kernel

Sincronização no host consiste no aguardo pelo término de um ou mais comandos sub-metidos a uma command queue, ou mesmo de todos estes. Aqui, o evento retornado pelocomando de enfileiramento do kernel é usado para aguardar o término da execução domesmo:

// A função clWaitForEvents aguarda o término de todos// os eventos em um array de eventos.// Aqui, como há apenas um evento, o endereço do "array"// corresponde ao próprio endereço da variável event.clWaitForEvents(

// Número de eventos no array.1,// Array de eventos.&event

);

92

Tendências e Técnicas em Realidade Virtual e Aumentada

3.6.12. Leitura dos resultados

A leitura de dados da memória do device é efetuada de forma praticamente idêntica àescrita de dados, porém com a invocação de uma função diferente:

// A função clEnqueueReadBuffer insere em uma command// queue um comando para leitura de dados de um// device para a memória do host. Os// argumentos são os mesmos de clEnqueueWriteBuffer,// porém o conteúdo do array apontado pelo 6o. argumento// é sobrescrito com os dados lidos do device.// Aqui, o terceiro argumento é CL_FALSE, portanto// a chamada é não-bloqueante.err = clEnqueueReadBuffer(

queue,bufC,CL_FALSE,0,ARRAY_LENGTH * sizeof(int),hostC,0,NULL,NULL

);

if (err != CL_SUCCESS) {std::cerr << "Falha ao ler resultados.\n";exit(EXIT_FAILURE);

}

3.6.13. Sincronização: espera pelo término da leitura dos resultados

A leitura dos resultados foi feita de forma não-bloqueante na seção anterior para ilustraruma forma de sincronização alternativa ao uso de clWaitForEvents:

// A função clFinish bloqueia a execução no host// até o término da execução de todos os comandos na command// queue de comandos passada como argumento. Neste caso, o// o único comando na command queue é o comando de leitura do// buffer que contém os resultados da computação.clFinish(queue);

3.6.14. Apresentação dos resultados

O código a seguir imprime os dados de entrada gerados, bem como da saída calculada,para verificação dos resultados da execução:

// Imprime os arrays gerados e os resultados na tela#define PRINT_ARRAY(name, array) \

93

Tendências e Técnicas em Realidade Virtual e Aumentada

std::cout << name << ":"; \for (int i = 0; i < ARRAY_LENGTH; ++i) { \

std::cout << " " << array[i]; \} \std::cout << "\n";

PRINT_ARRAY("A", hostA)PRINT_ARRAY("B", hostB)PRINT_ARRAY("C", hostC)

3.6.15. Liberação de recursos

Após a execução do kernel, os recursos alocados devem ser liberados através de funçõesespeciais disponibilizadas pela OpenCL. Existe uma função de liberação para cada tipode objeto utilizado:

clReleaseMemObject(bufA);clReleaseMemObject(bufB);clReleaseMemObject(bufC);clReleaseKernel(kernel);clReleaseProgram(program);clReleaseCommandQueue(queue);clReleaseContext(context);

Finalmente, os demais recursos são liberados pela aplicação e esta é encerrada:

delete hostA;delete hostB;delete hostC;

return 0;}

3.7. Aplicações em Realidade VirtualEm Realidade Virtual, um ambiente computacional imaginário ou simulando o mundoreal é o cenário no qual usuários podem interagir com objetos, controlá-los ou simples-mente visualizá-los em um tela de computador ou utilizando sistemas estereoscópicos,que permitem a imersão naquele mundo virtual. Para tornar o ambiente atrativo, o sistemadeve ser visualmente rico, dinâmico e ser processado em tempo-real. Neste contexto, aexigência de placa gráfica é inevitável para a execução e o desenvolvimento destes aplica-tivos. Como descrito na Seção 3.2, para a programação de aplicativos gráficos, a escolhapor APIs gráficas como OpenGL e Direct3D é o caminho natural para a realização datarefa.4.

4APIs que fornecem um nível de abstração de acima destas, como OpenSceneGraph, OpenSG e outrasauxiliam no desenvolvimento, porém ainda utilizam as APIs OpenGL ou Direct3D como base.

94

Tendências e Técnicas em Realidade Virtual e Aumentada

A possibilidade de interação entre OpenCL e OpenGL permite que além da rende-rização em tempo-real, outros algoritmos paralelizáveis tirem proveito do poder de pro-cessamento do dispositivo gráfico. Para ilustrar esta sinergia entre OpenCL e OpenGL, émostrado que object buffers OpenGL podem ser mapeados no espaço de endereçamentode objetos OpenCL, permitindo que um kernel OpenCL leia dados escritos por meio daAPI OpenGL. O contrário também é possível, isto é, kernels OpenCL podem escreverdados que são consumidos pelo código OpenGL.

O exemplo a seguir demonstra como explorar a interoperabilidade entre OpenGL eOpenCL, modificando-se dinamicamente um vertex buffer através de um kernel OpenCL.Neste exemplo, os vértices de uma malha (mesh ), são armazenados em um VBO, sendosuas posições modificadas via OpenCL. Objetivamente, os seguintes passos devem serexecutados:

1. Criar um VBO (vazio):

// Cria o buffer objectglGenBuffers(1, vbo);glBindBuffer(GL_ARRAY_BUFFER, *vbo);// Inicia o buffer object com o tamanho total da malha (vazia)unsigned int size = mesh_width * mesh_height * 4 * sizeof(float);glBufferData(GL_ARRAY_BUFFER, size, 0, GL_DYNAMIC_DRAW);

2. Criar um memory object de um VBO na memória globa, para que a OpenCL tenhaacesso ao VBO:

// cria buffer OpenCL do VBOvbo_cl = clCreateFromGLBuffer(

cxGPUContext, CL_MEM_WRITE_ONLY, *vbo, NULL);

3. Obter o VBO para escrita a partir da OpenCL;

// mapeia o buffer object para escrita pela OpenCLciErrNum |= clEnqueueAcquireGLObjects(

cqCommandQueue, 1, &vbo_cl, 0,0,0);

4. Executar o seguinte kernel para modificar as posições dos vértices:

__kernel void seno(__global float4* pos,unsigned int width,unsigned int height,float time)

{unsigned int x = get_global_id(0);unsigned int y = get_global_id(1);

// Calcula coordenadas u,v.float u = x / (float) width;

95

Tendências e Técnicas em Realidade Virtual e Aumentada

float v = y / (float) height;u = u*2.0f - 1.0f;v = v*2.0f - 1.0f;

// Calcular um padrão de onda senoidalfloat freq = 4.0f;float w = sin(u*freq + time) * cos(v*freq + time) * 0.5f;

// Escreve vértice de saídapos[y*width+x] = (float4)(u, w, v, 1.0f);

}

5. Liberar o VBO para retornar a posse para OpenGL:

// liberar o buffer object para a OpenGLciErrNum |= clEnqueueReleaseGLObjects(

cqCommandQueue, 1, &vbo_cl, 0,0,0);

6. Renderizar o resultado usando OpenGL:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glBindBuffer(GL_ARRAY_BUFFER, vbo);glVertexPointer(4, GL_FLOAT, 0, 0);glEnableClientState(GL_VERTEX_ARRAY);glColor3f(1.0, 0.0, 0.0);glDrawArrays(GL_POINTS, 0, mesh_width * mesh_height);glDisableClientState(GL_VERTEX_ARRAY);

A Figura 3.6 mostra telas da aplicação em execução, destacando as diferentesformas da malha, obtidas pelo deslocamento dos vértices.

Figura 3.6. Malha renderizada em OpenGL, com forma modificada via OpenCL.

96

Tendências e Técnicas em Realidade Virtual e Aumentada

Em termos de aplicações, pode-se ainda enumerar algumas que poderiam fazerempregar OpenGL e OpenCL em conjunto: simulação de fluidos, sistemas de partículas,manipulação de texturas, visualização de imagens, entre outras. Em todos estes casos,OpenGL é responsável pela renderização (ou simplesmente visualização, no caso de ima-gens), enquanto a simulação é realizada por meio de kernels OpenCL.

Resultados semelhantes aos do exemplo apresentado podem ser obtidos através daOpenGL e do uso shaders. OpenCL pode ser mais interessante para a solução de deter-minados problemas, enquanto shaders para outros. O mecanismo de gerenciamento deshaders na GPU fica completamente a cargo da API gráfica. Por outro lado, OpenCL podeser mais flexível e oferecer mais recursos do que shaders. Para desenvolvedores de aplica-ções genéricas, empregar shaders torna necessário estudar mais uma linguagem (GLSL,HLSL, Cg, etc.), com uma curva de aprendizagem mais longa, dada a necessidade de seconhecer o pipeline gráfico. A falta de comunicação entre processos pode ser limitante nouso de shaders. OpenCL oferece um modelo de memória mais flexível e com capacidadede sincronização entre work-items; no entanto, exige um conhecimento mais detalhadodo modelo de programação, gerenciamento de memória e controle de concorrência.

A escalabilidade na instanciação dos shaders está limitada ao dispositivo gráficode saída, enquanto OpenCL limita-se apenas à capacidade da placa gráfica. No entanto,alguns acessos ao hardware só podem ser realizados por meio de shaders, como, porexemplo, mipmapping e cubemaps. No que concerne a depuração, shaders tem evoluído,enquanto a OpenCL ainda carece de ferramentas, apesar de permitir a execução de kernelsem devices emulados e suportar breakpoints.

3.8. Aplicações em Realidade AumentadaAplicações de Realidade Aumentada dependem de bons algoritmos de visão computaci-onal para a correta detecção dos pontos de interesse na cena, como marcadores. Além deuma boa capacidade de detecção, tais algoritmos devem possuir bom desempenho com-putacional, para proporcionar uma experiência adequada e contínua ao usuário.

Muitos algoritmos de visão computacional baseiam-se na informação de bordas(grandes variações de intensidade entre pixels adjacentes) das imagens para a detecção depontos ou características de interesse. O operador de Laplace é utilizado para a detecçãodas bordas de uma imagem, tendo a vantagem de seus resultados serem invariantes arotação e de permitir implementações eficientes, uma vez que sua aplicação se dá deforma independente sobre cada ponto da imagem. Computações deste tipo são adequadasao modelo de paralelismo oferecido pela arquitetura OpenCL.

O operador de Laplace é aplicado sobre uma imagem realizando-se a convoluçãodesta com a máscara apresentada na Figura 3.7(a). O resultado é uma imagem com valorespróximos de zero nas regiões mais homogêneas, e com valores muito baixos ou muitoaltos onde há bordas. As Figuras 3.7(b) e 3.7(c) apresentam uma imagem de entrada e oresultado da aplicação do operador sobre esta, respectivamente.

97

Tendências e Técnicas em Realidade Virtual e Aumentada

(a)

(b) (c)

Figura 3.7. (a) Operador de Laplace. (b) Imagem original. (c) Resultado da con-volução, escalado para o intervalo de valores de 0 a 255.

O kernel OpenCL a seguir implementa o operador de Laplace:

__kernel void Laplacian(__global const int* image,__global int* laplacian)

{int x = get_global_id(0);int y = get_global_id(1);int width = get_global_size(0);int height = get_global_size(1);

if (x > 0 && x < width - 1 &&y > 0 && y < height - 1)

{laplacian[y * width + x] =

-4 * image[y * width + x] +image[(y - 1) * width + x] +image[(y + 1) * width + x] +image[y * width + (x - 1)] +image[y * width + x + 1];

98

Tendências e Técnicas em Realidade Virtual e Aumentada

}}

Assume-se que a imagem de entrada está representada em escala de cinza, tendoapenas um canal. O argumento image contém um ponteiro para uma região de memóriaglobal com a imagem de entrada. Os pixels estão dispostos em ordem row-major, isto é,com uma linha seguida da outra. O resultado do operador de Laplace é armazenado naregião de memória global apontada pela argumento laplacian.

O kernel apresentado executa sobre um espaço de índices de duas dimensões, con-forme pode ser observado pelas chamadas a get_global_id e get_global_size,sendo estas dimensões correspondentes às dimensões da imagem de entrada. Os work-items designados para o contorno da imagem não realizam trabalho algum, pois não pos-suem uma vizinhança completa.

Conforme visto na Seção 3.3.2, os work-items de um kernel executam em para-lelo, de acordo com a capacidade dos device em uso. Kernels como o apresentado tempotencial para obter grandes vantagens com o uso de GPUs modernas, dada a grandequantidade de processadores com que estas são equipadas, e dado o número de cálculosque devem ser feitos para uma imagem inteira. Acelerando passos simples como este,algoritmos de visão computacional podem dedicar mais esforço à detecção de pontos deinteresse em si, o que muitas vezes requer heurísticas complexas para uma operação ro-busta.

3.9. Aspectos de otimizações através da OpenCLA otimização do desempenho de aplicações através do uso de OpenCL pode ser classifi-cada em dois níveis:

1. Otimização da solução do problema.

2. Otimização para uma arquitetura específica.

No primeiro nível, a otimização consiste na paralelização das tarefas necessáriaspara encontrar a solução do problema. É importante que o desenvolvedor saiba identificarpontos de paralelismo e atividades que podem ser executadas simultaneamente. Alémdisso, é igualmente importante saber determinar os pontos de sincronização necessáriospara a consistência dos dados. Esta fora do escopo deste trabalho tratar sobre a identifi-cação de atividades paralelizáveis.

O segundo nível de otimização consiste na escrita de kernels que tomem proveitoda forma como a arquitetura OpenCL é mapeada para um device (ou uma família dedevices específicos). Em GPUs, o acesso aos diferentes espaços de endereçamento damemória possui tempos de latência variados, e alguns espaços possuem caches associ-adas, enquanto outros não. É o caso, por exemplo, das GPUs da arquitetura CUDA da

99

Tendências e Técnicas em Realidade Virtual e Aumentada

NVIDIA. Nestas GPUs, a latência de acesso à memória global é grande quando com-parada com a latência de acesso à memória local. Além disso, a memória local possuicaches em tais GPUs [NVIDIA Corporation 2009]. Cabe ao desenvolvedor buscar a do-cumentação disponibilizada pelo fabricante de cada dispositivo a respeito da otimizaçãode kernels OpenCL para as suas arquiteturas.

3.10. Considerações finaisA OpenCL possibilita a utilização do poder de processamento de CPUs e GPUs para pro-gramação de alto desempenho em ambientes computacionais heterogêneos. Por ser umpadrão livre de royalties e aberto, tem se mostrado, na sua curta existência, ser uma grandealternativa às linguagens e ferramentas específicas de fabricante. Usando OpenCL, o de-senvolvedor não mais necessita mapear soluções para APIs gráficas para utilizar GPUs,sendo o foco da programação na implementação de funções OpenCL escritas em C, quesão executadas em múltiplas instâncias simultâneas em diferentes processadores.

Este material procurou introduzir os conceitos-chave e explorar a arquitetura defi-nida no padrão OpenCL, apresentando exemplos práticos, com códigos e o funcionamentoda plataforma, comparando com código sequencial e APIs gráficas. Espera-se que o lei-tor, hobista e profissional de áreas que demandam paralelismo na solução de problemas,possa fazer uso dos conhecimentos e códigos deste material para resolver seus proble-mas de forma mais rápida e eficiente, bem como formar opinião a respeitos de problemasque não aquirem ganho de desempenho ao utilizar GPUs. Para programadores OpenGL,apresentou-se uma breve introdução sobre a sinergia entre OpenGL e OpenCL, que po-dem e devem conviver juntas para prover soluções mais robustas, principalmente ondehouver o envolvimento de renderização.

Para manter um canal de comunicação entre os leitores e os autores, a V3DStudios disponibiliza todo este material, código-fonte e dicas de programação no sitehttp://labs.v3d.com.br. Além disso, este site está sendo preparado para proverinformações, código aberto e listas de discussão para os interessados no tema de proces-samento de alto desempenho usando processadores multicore e many-core em conjunto.

Referências[Apple Inc. 2009] Apple Inc. (2009). OpenCL Programming Guide for Mac OS

X. http://developer.apple.com/mac/library/documentation/Performance/Conceptual/OpenCL_MacProgGuide/Introduction/Introduction.html.

[ATI 2010] ATI (2010). OpenCL: The Open Standard for Parallel Program-ming of GPUs and Multi-core CPUs. http://ati.amd.com/technology/streamcomputing/opencl.html.

[ISO 2005] ISO (2005). ISO/IEC 9899:1999 - C language specification. http://www.open-std.org/JTC1/SC22/wg14/www/docs/n1124.pdf.

100

Tendências e Técnicas em Realidade Virtual e Aumentada

[Khronos Group 2009] Khronos Group (2009). The OpenCL Specification. http://www.khronos.org/registry/cl/specs/opencl-1.0.48.pdf.

[Khronos Group 2010] Khronos Group (2010). OpenCL - The open standard for pa-rallel programming of heterogeneous systems. http://www.khronos.org/opencl/.

[NVIDIA Corporation 2009] NVIDIA Corporation (2009). NVIDIA OpenCLBest Practices Guide. http://www.nvidia.com/content/cudazone/CUDABrowser/downloads/papers/NVIDIA_OpenCL_BestPracticesGuide.pdf.

[NVIDIA Corporation 2010] NVIDIA Corporation (2010). OpenCL: Developer Zone.http://developer.nvidia.com/object/opencl.html.

101