View
217
Download
0
Category
Preview:
Citation preview
UNIVERSIDADE REGIONAL DE BLUMENAU
CENTRO DE CIÊNCIAS EXATAS E NATURAIS
CURSO DE CIÊNCIA DA COMPUTAÇÃO – BACHARELADO
RENDERIZADOR 3D PARA APLICAÇÕES GRÁFICAS
UTILIZANDO VULKAN
DANIEL STRECK
BLUMENAU
2017
DANIEL STRECK
RENDERIZADOR 3D PARA APLICAÇÕES GRÁFICAS
UTILIZANDO VULKAN
Trabalho de Conclusão de Curso apresentado
ao curso de graduação em Ciência da
Computação do Centro de Ciências Exatas e
Naturais da Universidade Regional de
Blumenau como requisito parcial para a
obtenção do grau de Bacharel em Ciência da
Computação.
Prof. Dalton Solano dos Reis, Mestre - Orientador
BLUMENAU
2017
RENDERIZADOR 3D PARA APLICAÇÕES GRÁFICAS
UTILIZANDO VULKAN
Por
DANIEL STRECK
Trabalho de Conclusão de Curso aprovado
para obtenção dos créditos na disciplina de
Trabalho de Conclusão de Curso II pela banca
examinadora formada por:
______________________________________________________
Presidente: Prof. Dalton Solano dos Reis, M. Sc. – Orientador, FURB
______________________________________________________
Membro: Prof. Mauro Marcelo Mattos, Doutor – FURB
______________________________________________________
Membro: Prof. Aurélio Faustino Hoppe, M.Sc. – FURB
Blumenau, 12 de dezembro de 2017
AGRADECIMENTOS
Aos meus professores, que me guiaram na busca por conhecimento.
À minha família, que sempre me apoiou e incentivou.
À minha namorada, por ter sido paciente e me incentivado.
E ao meu orientador, por ter acreditado na ideia proposta por este trabalho.
Você tem poder sobre sua mente – não eventos
exteriores. Perceba isso, e você encontrará
força.
Marco Aurélio
RESUMO
Neste trabalho é apresentada a implementação de uma biblioteca renderizadora 3D utilizando
as APIs Vulkan e OpenGL. O enfoque do trabalho está no estudo exploratório da API Vulkan
e seu funcionamento e da comparação de performance e de detalhes de implementação entre
as APIs gráficas. Desenvolveu-se uma biblioteca de renderização 3D com um módulo
utilizando Vulkan e outro OpenGL, ambos implementando uma mesma interface genérica
para garantir que os mesmos testes possam ser realizados de forma similar nas duas APIs. O
desenvolvimento da biblioteca é apresentado em detalhes, contendo informações referentes à
funcionalidade do Vulkan e OpenGL, com demonstrações do código implementado onde se
aplica. Para captação de métricas de performance, três cenários de testes utilizando cada API
foram desenvolvidos para efeito de comparação. Ao final conclui-se que a partir dos dados
obtidos nos testes desenvolvidos, a utilização da API Vulkan resultou em melhor desempenho
nos cenários empregados. Por fim são apresentadas sugestões de extensão deste trabalho.
Palavras chave: Computação gráfica. Vulkan. OpenGL.
ABSTRACT
This paper presents a 3D renderer implementation utilizing the OpenGL and Vulkan APIs.
The main focus of the paper is the exploratory study of the Vulkan API and the performance
and implementation details comparison between the two graphics APIs. A 3D rendering
library with a Vulkan and an OpenGL module was developed, both implementing the same
base generic interface to guarantee the same tests can be applied in a similar manner in both
APIs. The library’s development is shown in detail, containing information about Vulkan and
OpenGL’s implementation details, and where applicable, source code is provided. Three test
scenarios were developed to acquire performance metrics using each API to effectively
realize the comparison between the two. At the end, it can be concluded from the performance
tests that the Vulkan API is more performant than OpenGL on the presented scenarios. At last
extension topics for this work are proposed.
Key words: Computer graphics. Vulkan. OpenGL.
LISTA DE FIGURAS
Figura 1 – Estágios do pipeline gráfico .................................................................................... 15
Figura 2 – Captura de tela da aplicação ProtoStar.................................................................... 20
Figura 3 – Processo de criação da árvore de renderização (Render tree) ................................. 22
Figura 4 – Processo de renderização da Render tree................................................................ 23
Figura 5 – Captura de tela do Vulkan Tutorial ......................................................................... 25
Figura 6 – Diagrama de atividade (Vulkan) ............................................................................. 27
Figura 7 - Diagrama de classes (Vulkan) ................................................................................. 29
Figura 8 – Fluxo principal da biblioteca ................................................................................... 30
Figura 9 – Configuração do diretório de Include do Vulkan SDK ........................................... 45
Figura 10 – Configuração do diretório de Lib do Vulkan SDK ............................................... 46
Figura 11 – Estrutura do projeto no Visual Studio ................................................................... 47
Figura 12 – Captura de tela da cena 1 ...................................................................................... 51
Figura 13 – Captura de tela da cena 2 ...................................................................................... 51
Figura 14 – Captura de tela da cena 3 ...................................................................................... 52
Figura 15 – Gráfico de quadros por segundos .......................................................................... 53
Figura 16 – Gráfico com a relação de frame time (ms) entre as APIs ...................................... 54
Figura 17 – Gráfico com a relação de utilização de mémoria RAM entre as APIs.................. 55
Figura 18 – Gráfico com a relação de utilização de CPU entre as APIs .................................. 55
Figura 19 – Gráfico comparativo de escalabilidade de FPS entre as APIs .............................. 56
Figura 19 – Representação gráfica da arquiterua com Vulkan ................................................. 60
LISTA DE QUADROS
Quadro 1 – inicialização de instância Vulkan .......................................................................... 32
Quadro 2 – Busca de um dispositivo físico (RendererVk.cpp:259) ......................................... 33
Quadro 3 – Atribuição das queues e dispositivo lógico (RendererVk.cpp:283) ...................... 35
Quadro 4 – Configuração do swap chain (RendererVk.cpp:354) ............................................ 36
Quadro 5 – Configuração do render pass (RendererVk.cpp:501)............................................ 37
Quadro 6 – Configuração de um VkPipeline (RendererVk.cpp:632) ...................................... 38
Quadro 7 – Conversão de shader GLSL para SPIR-V (compile shaders.bat) .......................... 38
Quadro 8 – Configuração dos semáforos (RendererVk.cpp:632) ............................................ 39
Quadro 9 – Inicialização do GLFW para OpenGL (RendererGL.cpp:30) ............................... 40
Quadro 10 – Inicialização do OpenGL (RendererGL.cpp:52) ................................................. 40
Quadro 11 – Pseudocódigo de gravação dos command buffers ............................................... 43
Quadro 12 – Pseudocódigo de uma função de desenho com Vulkan ..................................... 44
Quadro 13 – Pseudocódigo de uma função de desenho com OpenGL ................................... 45
Quadro 14 – Definição classe Cena1 ...................................................................................... 48
Quadro 15 – Classe VulkanShader ........................................................................................... 49
Quadro 16 – Comparativo entre os trabalhos correlatos .......................................................... 50
LISTA DE TABELAS
Tabela 1 – Tempo (milissegundos) de um frame na CPU em relação à quantidade de objetos
............................................................................................................................... 24
Tabela 2 – Tempo (milissegundos) de um frame na GPU em relação à quantidade de objetos
............................................................................................................................... 24
LISTA DE ABREVIATURAS E SIGLAS
API – Application Programming Interface
CPU – Central Processing Unit
FPS – Frames Per Second
GLEW – OpenGL Extension Wrangler Library
GPU – Graphics Processing Unit
GLSL – GL Shading Language
OpenGL – Open Graphics Library
SDK – Software Development Kit
SPIR-V – Standard Portable Intermediate Representation – V
SUMÁRIO
1 INTRODUÇÃO .................................................................................................................. 13
1.1 OBJETIVOS ...................................................................................................................... 14
1.2 ESTRUTURA.................................................................................................................... 14
2 FUNDAMENTAÇÃO TEÓRICA .................................................................................... 15
2.1 FUNDAMENTAÇÃO EM COMPUTAÇÃO GRÁFICA ................................................ 15
2.2 OPENGL E OPENGL MATHEMATICS (GLM) ............................................................ 17
2.3 VULKAN .......................................................................................................................... 17
2.4 TRABALHOS CORRELATOS ........................................................................................ 19
2.4.1 UNREAL ENGINE ......................................................................................................... 19
2.4.2 Vulkan based render toolkit ............................................................................................ 21
2.4.3 Vulkan tutorial ................................................................................................................ 24
3 DESENVOLVIMENTO DA BIBLIOTECA ................................................................... 26
3.1 REQUISITOS .................................................................................................................... 26
3.2 ESPECIFICAÇÃO ............................................................................................................ 26
3.2.1 Diagrama de atividades ................................................................................................... 26
3.2.2 Diagrama de classes ........................................................................................................ 28
3.3 IMPLEMENTAÇÃO ........................................................................................................ 30
3.3.1 Técnicas e ferramentas utilizadas.................................................................................... 30
3.3.2 OPERACIONALIDADE DA IMPLEMENTAÇÃO ...................................................... 45
3.4 ANÁLISE DOS RESULTADOS ...................................................................................... 49
3.4.1 COMPARAÇÃO ENTRE OS TRABALHOS CORRELATOS .................................... 49
3.4.2 RESULTADOS DOS CENÁRIOS DE TESTES ........................................................... 50
3.4.3 Desempenho .................................................................................................................... 52
4 CONCLUSÕES .................................................................................................................. 57
4.1 EXTENSÕES .................................................................................................................... 58
REFERÊNCIAS ..................................................................................................................... 59
ANEXO A – REPRESENTAÇÃO GRÁFICA DA ARQUITETURA COM VULKAN . 60
13
1 INTRODUÇÃO
Pode-se afirmar que para criar a ilusão de imagem em movimento em vídeos ou
aplicações gráficas, a taxa de quadros para que uma aplicação possa ser denominada de
tempo-real começa a partir dos 15 quadros por segundo. Portanto, há necessidade de garantir
alta performance para que aplicações interativas se tornem agradáveis para os usuários
(AKENINE-MÖLLER; HAINES; HOFFMAN, 2008, p.1). Para atingir altos níveis de
performance, APIs são comumente empregadas para programação de aplicações que utilizam
o hardware com recursos de computação gráfica.
Uma das entidades que mantém tais APIs é o grupo Khronos, que tem como objetivo
criar e manter especificações abertas de APIs para computação paralela, computação gráfica,
mídias dinâmicas, visão computacional e processamento de sensores em uma gama variada de
plataformas (KHRONOS, 2016c). Algumas das especificações mantidas pelo grupo
começaram a evoluir de maneira separada, tais como o OpenGL (uma API gráfica utilizada
em plataformas desktop e consoles) e o OpenGL ES (OpenGL for Embedded Systems) para
mobile. Devido a isso, a iniciativa do OpenGL começou a se fragmentar e evoluir
independentemente, o que causou falta de conformidade e compatibilidade entre aplicações
do OpenGL ES e OpenGL (KHRONOS, 2016c).
Devido a esta fragmentação, desenvolvedores começaram a apontar outras
inconsistências e possíveis melhorias para a API. Isto resultou em uma iniciativa sem
precedentes a partir de desenvolvedores proeminentes das indústrias de desenvolvimento de
jogos digitais, tanto de hardware quanto software, para especificação do futuro do OpenGL,
que veio a se tornar o Vulkan (KHRONOS, 2016c).
Algumas das vantagens propostas pela especificação do Vulkan, em relação às APIs
anteriores, são a uniformidade entre plataformas, suporte ao envio de comandos assíncronos
da CPU para GPU e a disponibilização explicita de suas funções com pouco overhead. Estas
mudanças transferem maior responsabilidade para o desenvolvedor, para utilizá-las de forma
que atenda melhor cada aplicação em particular. Através dessas funções, possibilita-se que a
API se comporte de maneira extremamente previsível (SAMSUNG, 2016).
Diante do exposto, desenvolveu-se um estudo exploratório da API Vulkan, realizando
a implementação de uma biblioteca para renderização de cenas 3D e análise da performance
obtida em comparação com a API OpenGL.
14
1.1 OBJETIVOS
O objetivo deste trabalho é realizar um estudo exploratório da API Vulkan.
Os objetivos específicos são:
a) desenvolver uma biblioteca para renderização 3D utilizando Vulkan;
b) realizar uma comparação da biblioteca desenvolvida com Vulkan a uma análoga
desenvolvida com OpenGL;
c) analisar a performance atingida nos testes propostos.
1.2 ESTRUTURA
O trabalho está organizado em quatro capítulos. O primeiro capítulo contém a
introdução, os objetivos e a estrutura. No segundo capítulo, está presente a fundamentação
teórica necessária para a compreensão do objeto de estudo deste trabalho. O desenvolvimento
do trabalho é demonstrado no terceiro capítulo, onde é apresentado um diagrama de
atividades e o digrama de classe e são apresentadas as técnicas e detalhes de implementação.
O quarto capítulo contempla a conclusão do trabalho e sugestões para trabalhos futuros.
15
2 FUNDAMENTAÇÃO TEÓRICA
Este capítulo aborda assuntos relevantes para a compreensão do objeto de estudo deste
trabalho. A seção 2.1 trata de assuntos relacionados à fundamentação em Computação
Gráfica, como uma visão geral do pipeline gráfico moderno e conceitos como occlusion
culling e materiais. A seção 2.2 contempla a API OpenGL e a biblioteca OpenGL
Mathematics (GLM). Na seção 2.3 são apresentados conceitos sobre a API Vulkan, tais como
uma descrição geral sobre a mesma e suas características como command queues, SPIR-V,
validation layers e swap chain. Na seção 2.4 são apresentados os trabalhos correlatos.
2.1 FUNDAMENTAÇÃO EM COMPUTAÇÃO GRÁFICA
Para se gerar imagens através da Computação Gráfica, faz-se necessária a utilização de
dispositivos de hardware com recurso do pipeline gráfico, geralmente GPUs. Tais dispositivos
podem receber comandos para executar diversas operações computacionais, que podem ter
como resultado imagens rasterizadas ou simplesmente produtos computacionais.
Um pipeline gráfico de renderização tem como produto final uma imagem
bidimensional de forma rasterizada. Para tal, diversos estágios são empregados (Figura 1),
alguns são fixos e outros são programáveis ou configuráveis.
Figura 1 – Estágios do pipeline gráfico
Fonte: Akenine-Möller, Haines e Hoffman (2008).
O pipeline é ativado com um draw call, que, de acordo com Akenine-Möller, Haines e
Hoffman (2008, p.31), é uma chamada para a API gráfica para desenhar um grupo de
primitivas, causando a execução do pipeline gráfico. Após o comando de execução do
pipeline, o primeiro estágio a ser executado é o estágio de geometria. Ele recebe as primitivas
de renderização (pontos, linhas e triângulos) enviados a partir da aplicação e é responsável
pelas operações que ocorrem a cada vértice e polígono (AKENINE-MÖLLER; HAINES;
HOFFMAN, 2008). Durante este estágio, ocorrem algumas etapas que podem ser
16
consideradas estágios separados ou não, que dependendo da implementação do hardware,
podem ocorrer de forma paralela.
Primeiramente ocorre a etapa de transformações geométricas, na qual as primitivas
enviadas pela aplicação são transformadas para espaço global e espaço de câmera sintética
respectivamente. Em seguida, de acordo com Akenine-Möller, Haines e Hoffman (2008, p.17)
na etapa de vertex shading são executadas equações de shading que tem o propósito de definir
o aspecto visual de um objeto através de atributos disponíveis por vértice, como sua
localização no espaço universal, vetor normal, cores ou informações personalizadas. O
produto dessa etapa é posteriormente utilizado com entrada para o estágio de rasterização.
Após o shading, a próxima etapa é a de projeção, na qual o volume de visão da câmera
sintética é transformado em projeção ortográfica ou perspectiva. Em seguida ocorre a etapa de
clipping. Nesta etapa são descartados os objetos primitivos que não estão inteira ou
parcialmente dentro do volume de visão da câmera sintética, e portanto não precisam ser
desenhados. Os objetos que estão parcialmente dentro do volume de visão passam pela
operação de clipping, na qual novos vértices precisam ser determinados para as partes de
objetos que estão parcialmente no volume de visão. A última etapa do estágio de geometria é
o mapeamento para espaço de tela, na qual as primitivas, ainda representados em coordenadas
tridimensionais, são convertidos para espaço de tela em duas dimensões.
O próximo estágio do pipeline gráfico é o de rasterização, que tem por finalidade
converter os dados providos pelo estágio de geometria para pixels e determinar seus
respectivos valores de cor (AKENINE-MÖLLER; HAINES; HOFFMAN, 2008).
O estágio de rasterização é constituído também por diversas etapas que são: triangle
setup, uma etapa não-programável, na qual dados que posteriormente serão utilizados para
realizar o processo de triangle traversal, são gerados e interpolados a partir dos dados
providos pelo estágio de geometria. Após, no estágio de triangle traversal, os pixels que
possuem seu centro contido por um triângulo, são marcados e um fragment é criado para eles.
Em seguida ocorre o estágio de pixel shading. Neste estágio programável, que recebe
como entrada dos dados das etapas anteriores interpolados, são executadas equações de
shading nos fragments, que produzirão valores de cor para os pixels que serão encaminhados
para etapas posteriores do pipeline. Nesta etapa são aplicadas técnicas como mapeamento de
textura em um objeto gráfico.
A última etapa do estágio de rasterização é denominada merging e nela os fragmentos
providos pela etapa anterior são combinados com os pixels presentes no color buffer (um
array retangular de valores de cor dos pixels a serem desenhados na tela). Nesta etapa ocorre
17
também a resolução de visibilidade. Para tal, geralmente se emprega o Z-Buffer (ou depth
buffer), que é um array com as mesmas dimensões do color buffer e guarda o valor de
coordenada Z de pixel em relação à câmera sintética.
O conceito de material é comumente empregado em aplicações gráficas que, de acordo
com Akenine-Möller, Haines e Hoffman (2008, p.104) tem a seguinte definição: a aparência
de um objeto gráfico é representada por agregar materiais a modelos na cena. Cada material é
associado com conjunto de shaders, texturas e outras propriedades para simular a interação de
luz com objetos. Outra técnica muito empregada em renderizadores gráficos é o occlusion
culling, definido por Akenine-Möller, Haines e Hoffman (2008, p.671) como uma técnica de
otimização utilizada para não renderizar objetos em uma cena que estão obstruídos por outros.
2.2 OPENGL E OPENGL MATHEMATICS (GLM)
OpenGL é uma API para o desenvolvimento de aplicações gráficas introduzida
originalmente em 1992, tornando-se a API gráfica com especificação aberta mais utilizada
para uma alta gama de aplicações gráficas (KHRONOS, 2016b). Em sua concepção, o
OpenGL é agnóstico em relação à plataforma e linguagem de programação.
Em sua especificação, OpenGL segue um modelo de estados globais, no qual qualquer
objeto (frame buffer, handle de texturas) pode ser adquirido por uma aplicação em tempo de
execução (KHRONOS, 2016b). O que torna a API mais acessível para desenvolvedores, uma
vez que funções providas pela API, como transformações geométricas por exemplo, não
precisam ser necessariamente implementadas.
OpenGL Mathematics é uma biblioteca matemática em forma header de C++ para
softwares gráficos baseados no OpenGL Shading Language (GLSL) (G-TRUC CREATION,
2016). A biblioteca está inclusa no SDK do Vulkan. São fornecidas nela, funções matemáticas
referentes a quaternions, transformações geométricas, matemática vetorial, entre outros.
2.3 VULKAN
Vulkan é uma API para desenvolvimento de aplicações gráficas aceleradas por
hardware mantida pelo grupo Khronos. Sua concepção se deu pela necessidade de uma API
gráfica que atendesse melhor o ecossistema moderno do mercado de aplicações gráficas.
Especificado em conjunto com os líderes da indústria da computação gráfica, para ser o novo
padrão de API gráficas com especificação aberta (KHRONOS, 2016c).
Em sua concepção, o Vulkan é mais explícito do que seu antecessor (OpenGL). Nele,
o desenvolvedor possui maior responsabilidade na utilização dos recursos de hardware, ao
18
invés de haver certos comandos abstraídos nos drivers ou em comandos fixos disponíveis pela
API. Como resultado, o Vulkan pode ser utilizado de forma que se adapta melhor a aplicação,
possivelmente assim, rendendo maior desempenho (KHRONOS, 2016c).
Alguns benefícios da especificação do Vulkan são comentados de acordo com Khronos
(2016c): introdução do conceito de command queue – fila de command buffers para serem
enviados para o dispositivo físico (GPU). A criação das command queues pode ser feita de
forma paralela e assíncrona utilizando-se diversas threads, o que otimiza a utilização da CPU.
Outra vantagem proposta em relação ao OpenGL, é que especificação do Vulkan permite que
aplicações sejam desenvolvidas para múltiplas plataformas sem que haja necessidade de
alterações específicas para cada uma.
A SPIR-V é uma linguagem intermediária desenvolvida pelo grupo Khronos para
representação nativa de shaders gráficos e kernels computacionais. Permite que shaders
possam ser compilados para formato binário antes da execução, o que cria alguns benefícios
como: depuração antes do tempo de execução, maior otimização e proteção de propriedade
intelectual. Os programas (shaders) podem ser escritos utilizando a mesma linguagem
empregue no OpenGL, o GLSL, mas pode-se também fazer uso do SDK para desenvolver um
compilador para linguagens de terceiros, que ao final precisam estar no formato binário do
SPIR-V (KHRONOS, 2016a).
O conceito de validation layers foi introduzido no SDK do Vulkan para auxiliar no
processo de depuração e verificação de conformidade com a especificação no
desenvolvimento de aplicações que utilizam o mesmo (KHRONOS, 2016c). As validation
layers podem ser empregadas durante o desenvolvimento de uma aplicação para reportar a
utilização de forma incorreta da API Vulkan. Fornece também recursos como árvore de
chamadas de funções da API para análise e depuração. Uma das vantagens propostas por essa
ferramenta é que ela pode ser desabilitada completamente da aplicação quando ela estiver
com o desenvolvimento concluído. Removendo assim qualquer tipo de overhead que poderia
ser causado pela API realizando validações internas para verificar a conformidade e validade
das chamadas de função.
Para apresentar os resultados de renderização para uma superfície ou tela utilizando
Vulkan faz-se necessária a utilização do objeto swap chain (KHRONOS, 2016e). Este objeto,
fornecido através de uma extensão, é uma abstração de um array com imagens associadas a
uma tela pronta para apresentação. A aplicação renderiza uma imagem que resulta em um
objeto VkImage, o qual é adicionado à fila do swap chain para futuramente ser apresentada
19
pelo presentation engine, que é responsável por ordenar quais imagens podem ser adquiridas
pela aplicação.
Na API Vulkan existem dois tipos de recursos para representar dados arbitrários na
memória da GPU: VkBuffer e VkImage. VkBuffers são containers para dados de forma
linear que podem ser usados de diversas formas – estruturas de dados, arrays “crus” e até
informações sobre imagens. VkImages, em contrapartida, são estruturadas, possuem tipo e
possuem dados sobre formato. Podem ser multidimensionais para formar arrays, e suportam
operações avançadas de leitura e escrita de dados (SELLERS; KESSENICH, 2016).
2.4 TRABALHOS CORRELATOS
Serão introduzidos um produto, um trabalho acadêmico e uma ferramenta educacional
que implementam o objeto de estudo a ser explorado por este trabalho. O primeiro é o motor
para aplicações multimídia com foco em jogos digitais Unreal Engine (EPIC GAMES,
2016b). O segundo é um trabalho que realiza um estudo exploratório de um renderizador 3D
de alta performance, o Vulkan Based Render Toolkit (MAINUŠ, 2016). O terceiro é um guia
em formato de tutorial para utilização da API Vulkan (OVERVOORDE, 2016).
2.4.1 UNREAL ENGINE
A Unreal Engine é um conjunto de ferramentas para desenvolvimento de aplicações
multimídia com alta fidelidade gráfica e alta performance mantidas pela EPIC Games (EPIC
GAMES, 2016c). Foi criado em 1998 e foi originalmente apresentado ao público com o jogo
digital Unreal (BLESZINSKI, 2010) e a partir da versão 4, a utilização do motor se tornou
gratuita e com código aberto, sendo cobrados apenas 5% da receita a partir de 3 mil dólares
(EPIC GAMES, 2016c).
A EPIC Games faz parte do grupo Khronos e participou ativamente na especificação
da API Vulkan. Portanto, foi uma das primeiras engines a implementar o seu renderer
backend utilizando Vulkan (SAMSUNG, 2016). A implementação da API Vulkan foi
introduzida de forma experimental na versão 4.12 da Unreal Engine, permitindo assim a
implementação de aplicações multimídia com suporte à Vulkan (EPIC GAMES, 2016b).
Em conjunto com a Samsung, a Epic Games desenvolveu uma aplicação de
demonstração para o Samsung Galaxy S7 chamada ProtoStar (Figura 2), utilizando Vulkan,
com o objetivo de demonstrar o potencial da API em plataformas mobile. Para atingir altos
níveis de fidelidade gráfica, foram implementadas diversas técnicas de computação gráfica
como (EPIC GAMES, 2016a):
20
a) reflexos planares dinâmicos (reflexos de alta qualidade para objetos dinâmicos);
b) simulação de partículas na GPU;
c) Temporal Anti-Aliasing (TAA);
d) compressão de texturas de alta qualidade;
e) Chromatic aberration;
f) refração dinâmica de luz para plataformas mobile;
g) suporte a cenas com milhares de objetos dinâmicos.
Figura 2 – Captura de tela da aplicação ProtoStar
Fonte: Epic Games (2016c).
Um dos recursos disponíveis chama-se command buffers, que é um objeto utilizado
para gravar comandos que podem subsequentemente ser enviados para a fila de um
dispositivo para execução (KHRONOS, 2016c). Para manter a taxa de quadros aceitável no
dispositivo Samsung Galaxy S7, a equipe da Unreal Engine empregou algumas técnicas,
como a utilização de somente 3 grandes command buffers, sendo utilizado somente um por
frame e alternando-se entre eles utilizando a técnica de Round Robin.
Akenine-Möller, Haines e Hoffman (2008, p.711) define GPU instancing como o
conceito de desenhar um objeto várias vezes com somente um draw call (comando de
desenho). Foram apontadas também algumas das vantagens da reutilização dos command
buffers, como em um caso de otimização simples, o GPU instancing, e em um caso de uso
ótimo, a renderização estereoscópica para Realidade Virtual (RV) (ARM, 2016).
De acordo com Khronos Group (2016d), semáforos são utilizados para coordenar
operações internas com filas externas a uma fila de comandos. Outra técnica de sincronização
é feita com a utilização de fences. Objetos deste tipo podem ser utilizadas pelo dispositivo
21
hospedeiro para determinar a completude da execução de uma fila de comandos. Para realizar
a sincronização e ordenação do envio dos comandos para a GPU, se utilizou semáforos para
comandos na GPU, e fences para a CPU, principalmente para verificação se um comando
arbitrário já foi completado pela GPU. Em conclusão, os autores afirmam ter atingido níveis
de fidelidade gráfica e performance no mesmo nível de consoles de sétima geração
(SAMSUNG, 2016).
2.4.2 Vulkan based render toolkit
Este trabalho tem como objetivo realizar um estudo exploratório da API Vulkan e
demonstrar novas funções disponíveis na mesma. Para tal, o autor desenvolveu em C++ um
render toolkit e uma aplicação para análise de performance (MAINUŠ 2016).
O render toolkit é formado por um módulo que trata do ciclo de vida da aplicação e o
gerenciamento de eventos (denominado core) e um módulo para renderização. De acordo
com Mainuš (2016), o módulo de renderização cria o objeto compositor, que é responsável
por criar os render passes e por percorrer a árvore de objetos gráficos presentes no grafo de
cena de uma cena, para fazer uma triagem e determinar quais objetos atendem algumas
restrições especificas para serem ordenadas na árvore. A ordenação é feita levando em
consideração objetos com contextos de renderização similares, ou seja, com características
como material e malhas iguais. Este processo está ilustrado na Figura 3.
22
Figura 3 – Processo de criação da árvore de renderização (Render tree)
Fonte: Mainuš (2016).
Após o processo ilustrado na Figura 3 a render tree é delegada para o render
worker que, por sua vez, divide as tarefas de renderização de maneira assíncrona através de
diversas threads. Inicialmente cada render component tem os dados de inicialização
preparados. Em seguida, os atributos referentes a um render component são enviados para a
memória da GPU. Na próxima etapa as informações de malha referentes aos materiais que
serão utilizados pelos objetos, são atribuídas ao pipeline para poderem ser reutilizados caso
uma malha seja repetida (GPU instancing). Por último, dados referentes à cena são agregados
ao pipeline. Este processo está ilustrado na Figura 4 (MAINUŠ, 2016).
23
Figura 4 – Processo de renderização da Render tree
Fonte: Mainuš (2016).
Para verificação de funcionalidades, o autor criou uma aplicação utilizando o render
toolkit e realizou testes divididos em 5 níveis de otimização, sendo eles:
a) no primeiro nível não é realizado nenhum tipo de otimização;
b) no segundo nível os trabalhos de renderização foram paralelizados com 5 threads;
c) no terceiro nível adicionou-se a técnica de utilização de staging buffers;
d) no quarto nível incorpora-se ao segundo nível o recurso de memory pools para pré-
alocar e reutilizar os recursos;
e) o quinto nível implementa todas as otimizações propostas anteriormente, mas faz
com que ocorram o mínimo possível de trocas de estados do pipeline.
Nas Tabelas 1 e 2 pode-se observar respectivamente o tempo de cada frame na CPU e
GPU em relação à quantidade de objetos presentes na cena.
24
Tabela 1 – Tempo (milissegundos) de um frame na CPU em relação à quantidade de objetos
1k 2k 3k 4k 5k 6k 7k
Level 0 6.2 9.1 14.8 16.7
Level 1 4.9 6.2 11.6 13.3 13.1 16.7
Level 2 6.5 7.4 27.9 49.7 46.9 42.0 37.0
Level 3 6.7 7.6 8.2 13.2 16.3 18.0 19.9
Level 4 8.8 8.7 11.9 16.9 19.8 22.9 25.3 Fonte: Mainuš (2016).
Tabela 2 – Tempo (milissegundos) de um frame na GPU em relação à quantidade de objetos
1k 2k 3k 4k 5k 6k 7k
Level 0 48.3 61.0 68.1 66.3
Level 1 49.4 61.2 67.8 64.3 63.7 63.2
Level 2 5.7 7.7 9.7 10.5 11.7 13.1 14.0
Level 3 5.5 7.5 9.3 10.0 10.6 11.0 11.9
Level 4 5.2 7.0 8.6 9.4 10.0 10.8 11.4 Fonte: Mainuš (2016).
2.4.3 Vulkan tutorial
Este trabalho é uma ferramenta educacional em formato de tutorial. Foi elaborado por
Alexander Overvoorde em 2016 com o intuito de ensinar o básico de utilização da API
Vulkan (OVERVOORDE, 2016). O trabalho é disponibilizado através de sua homepage e em
formato de E-Book, nos quais encontra-se um guia com os passos necessários para utilização
da API Vulkan. Além da parte textual explicando o funcionamento da API, é fornecido
também o código fonte dos exemplos utilizados. O tutorial está dividido em capítulos, os
quais estão elencados a seguir:
a) introduction;
b) overview;
c) development environment;
d) drawing a triangle;
e) vertex buffers;
f) uniform buffers;
g) texture mapping;
h) depth buffering;
i) loading models.
No capítulo de introdução o autor introduz os objetivos do tutorial, a API Vulkan e o
público alvo para o qual o tutorial foi elaborado. No capítulo seguinte, denominado Overview,
o autor descreve as origens da API Vulkan e em seguida elenca os passos necessários para
renderização de uma imagem utilizando a API. Em seguida, no capítulo de Development
environment, o autor descreve as bibliotecas auxiliares utilizadas e como instalá-las, além da
25
configuração dos ambientes de desenvolvimento para utilização do código fonte provido. Em
sequência, no capítulo intitulado Drawing a triangle, o autor descreve todos os passos e
técnicas de implementação necessárias para desenhar um triangulo e mostrá-lo na tela em
Vulkan. No capítulo seguinte, Vertex buffers, é descrito como é possível generalizar dados
referentes aos vértices para desenho de objetos gráficos e como fazer o gerenciamento de
memória necessário para tal.
No capítulo Uniform buffers, o autor descreve como utilizar os resource descriptors
para enviar variáveis globais para os shaders. No capítulo seguinte intitulado Texture
mapping, o autor entra em detalhes de como utilizar a técnica de mapeamento de texturas em
malhas 3D. No penúltimo capítulo, intitulado Depth buffering, o autor aponta como introduzir
o elemento de profundidade no eixo Z do espaço cartesiano, na qual se faz uso do depth buffer
para guardar tais informações e criar impressão de terceira dimensão. Por último, no capítulo
Loading models, o autor introduz o carregamento de malhas 3D utilizando a biblioteca
Tinyobj, o qual está ilustrado na captura de tela da Figura 5. Após a última etapa do tutorial se
tem uma aplicação que renderiza uma malha 3D no formato OBJ com mapeamento de textura
e modelo de iluminação Blinn-Phong com Vulkan.
Figura 5 – Captura de tela do Vulkan Tutorial
Fonte: elaborado pelo autor.
26
3 DESENVOLVIMENTO DA BIBLIOTECA
Neste capítulo são demonstradas as etapas do desenvolvimento da biblioteca. Na seção
3.1 são apresentados os requisitos funcionais e não funcionais da biblioteca. A seção 3.2
demonstra a especificação da biblioteca. A seção 3.3 apresenta de forma explicativa a
implementação da biblioteca de renderização. Por fim, a seção 3.4 apresenta os resultados dos
testes.
3.1 REQUISITOS
A biblioteca desenvolvida deve permitir:
a) renderizar cenas compostas por diversos objetos gráficos em 3D (Requisito
Funcional - RF);
b) ser implementada utilizando o pipeline gráfico programável para aplicação de
shaders (RF);
c) utilizar a API Vulkan (Requisito Não Funcional - RNF);
d) ser implementada utilizando a linguagem C++ (RNF);
e) utilizar a biblioteca OpenGL Mathematics (GLM) para funções matemáticas
(RNF).
3.2 ESPECIFICAÇÃO
A biblioteca foi especificada utilizando-se da Unified Modeling Language (UML),
utilizando as ferramentas Enterprise Architect e Microsoft Visual Studio
Community 2015.
3.2.1 Diagrama de atividades
Na Figura 6 pode-se observar um diagrama de atividades ilustrando o processo de
renderização de uma cena, primeiramente com a inicialização e criação de contexto da API
Vulkan e objetos necessários para renderização que serão utilizados posteriormente.
27
Figura 6 – Diagrama de atividade (Vulkan)
Fonte: elaborado pelo autor.
Após a criação de uma cena e a confecção dos objetos gráficos com suas malhas e
texturas se inicia o loop principal da aplicação. Nele, primeiramente ocorre a atualização dos
UniformBuffer dos objetos gráficos, os quais contém informações sobre transformações
geométricas e posição da fonte de luz em espaço global, e a gravação de command buffers de
desenho. Em sequência a função drawFrame, a qual busca uma imagem válida para ser
28
desenhada e envia a fila de comandos é chamada e invoca o processo de renderização na
GPU. Este processo ocorre até que o evento para fechamento de janela seja recebido.
3.2.2 Diagrama de classes
Na Figura 7 está presente o diagrama de classe apresentando a modelagem do
renderizador utilizando Vulkan. A principal classe a ser notada é a RendererVk a qual
entende a classe abstrata Renderer e serve como principal gerenciador do processo de
renderização. A classe Scene tem como propósito conter os objetos gráficos (DrawableObj)
que serão renderizados na cena. Tais objetos gráficos na implementação com Vulkan são
representados pela classe VulkanDrawableObj os quais estendem a classe abstrata
DrawableObj. Os objetos do tipo VulkanDrawableObj possuem um ponteiro para as classes
VulkanMesh e VulkanMaterial as quais representam o aspecto visual do objeto. Objetos do
tipo VulkanMesh possuem uma lista dos vértices e índices dos mesmos que compõem uma
malha 3D. Já os objetos do tipo VulkanMaterial representam o conceito de material e por
sua vez, contém referência para objetos do tipo VulkanShader e VulkanTexture. A classe
VulkanDevice serve como abstração do dispositivo que será utilizado para renderização,
contendo o dispositivo lógico, físico e filas de comandos que o pertencem. A classe
VulkanSwapChain funciona como abstração do swap chain do Vulkan, contendo os objetos e
funções necessárias para manipulação da mesma. Além das classes apresentadas, outras
classes implementadas no trabalho serão apresentadas com maior detalhamento na seção 3.3.
30
3.3 IMPLEMENTAÇÃO
Neste capítulo são mostradas as técnicas, ferramentas e a operacionalidade da
implementação. A seção 3.3.1 apresenta o detalhamento das ferramentas e as técnicas
utilizadas. A seção 3.3.2 demonstra o processo operacional da biblioteca.
3.3.1 Técnicas e ferramentas utilizadas
A IDE Microsoft Visual Studio Community 2015 foi utilizada para o desenvolvimento
da biblioteca, e a mesma foi desenvolvida na linguagem C++. Foram utilizadas as seguintes
tecnologias no desenvolvimento da biblioteca:
a) Vulkan SDK: SDK com o header fornecendo a implementação do Vulkan;
b) GLEW: para carregamento da API OpenGL;
c) GLFW: gerenciador de janelas multi-plataforma;
d) GLM: biblioteca para operações matemáticas;
e) Tinyobj: biblioteca para carregamento de malhas 3D no formato OBJ;
f) STP image: biblioteca para carregamento de imagens.
A seguir tem-se o detalhamento das técnicas utilizadas para implementação da
biblioteca de renderização. Na Figura 8 pode-se observar o fluxo principal de execução da
biblioteca.
Figura 8 – Fluxo principal da biblioteca
Fonte: elaborado pelo autor.
O detalhamento das técnicas de implementação está separado em seções e será
apresentado o código fonte. Nota-se que a ênfase por um maior detalhamento para os detalhes
de implementação foi dada para a API Vulkan, por este ser um trabalho exploratório dessa
API. A próxima seção descreve o processo de inicialização das APIs gráficas.
3.3.1.1 INICIALIZAÇÃO DAS APIS GRÁFICAS
Esta seção compreende a etapa de inicialização das APIs gráficas e seus respectivos
objetos que se fazem necessários para a utilização das mesmas. Primeiramente serão
apresentadas as etapas de inicialização da API Vulkan, e em sequência as da API OpenGL.
A seguir estão descritos os passos necessários para inicializar a API Vulkan e preparar
o contexto com os objetos necessários para realizar o processo de renderização de uma cena.
31
No ANEXO A é possível se ter uma visualização gráfica geral dos objetos que serão tratados
a seguir.
Para o desenvolvimento com Vulkan utilizou-se uma versão modificada e adaptada de
Overvoorde (2016). Os tipos de objetos que devem ser inicializados em ordem estão
elencados a seguir:
a) instância, physical device;
b) logical device, queue families;
c) window surface, swap chain;
d) image views, frame buffers;
e) render passes;
f) graphics pipeline;
g) command pool, command buffers;
h) presentation.
Para se utilizar a API Vulkan faz-se necessária a criação de um objeto vkInstance, o
qual possui como propósito servir de instância operante da API e separar o estado de outras
aplicações executando Vulkan. A partir dele é possível realizar operações subsequentes com a
biblioteca, como elencar os dispositivos com os quais a API pode se comunicar e verificar
quais extensions e layers estão disponíveis no dispositivo.
Em sua criação através da função vkCreateInstance (Quadro 1) deve ser
especificado um struct de configuração do tipo VkInstanceCreateInfo que descreve
informações sobre a instância a ser inicializada como quais extensions serão utilizadas, o
nome da aplicação e sua versão. Todas as funções de criação de objetos em Vulkan seguem
esse padrão, com structs de configuração e especificação do objeto a ser instanciado.
32
Quadro 1 – inicialização de instância Vulkan 1. void RendererVk::initVulkan() 2. { 3. if (enableValidationLayers && !VulkanHelper::checkValidationLayerSupport()) 4. throw std::runtime_error("validation layers error"); 5. 6. VkApplicationInfo applicationInfo = 7. { 8. VK_STRUCTURE_TYPE_APPLICATION_INFO, // VkStructureType sType 9. nullptr, // const void *pNext 10. "vkFurb", // const char *pApplicationName 11. VK_MAKE_VERSION(1, 0, 0), // uint32_t applicationVersion 12. "No Engine", // const char *pEngineName 13. VK_MAKE_VERSION(1, 0, 0), // uint32_t engineVersion 14. VK_API_VERSION_1_0 // uint32_t apiVersion 15. }; 16. VkInstanceCreateInfo createInfo = {}; 17. createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; 18. createInfo.pApplicationInfo = &applicationInfo; 19. auto extensions = getRequiredExtensions(); 20. createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size()); 21. createInfo.ppEnabledExtensionNames = extensions.data(); 22. // inicializar com ValidationLayers para receber mensagens de depuracao 23. if (enableValidationLayers) 24. { 25. createInfo.enabledLayerCount= static_cast<uint32_t>(validationLayers.size()); 26. createInfo.ppEnabledLayerNames = validationLayers.data(); 27. } 28. else 29. { 30. createInfo.enabledLayerCount = 0; 31. } 32. if (vkCreateInstance(&createInfo, nullptr, &_vkInstance) != VK_SUCCESS) 33. throw std::runtime_error("ERRO AO INICIALIZAR VkInstance"); 34. 35. if (enableValidationLayers) 36. setupDebugCallback(); 37. }
Fonte: elaborado pelo autor.
Após a inicialização de instância do Vulkan, a função vkEnumeratePhysicalDevices
pode ser utilizada para buscar dispositivos físicos (como GPUs) que são compatíveis com
Vulkan. Primeiramente chama-se a função com o terceiro argumento com valor nullptr,
para retornar o número de dispositivos físicos, então chama-se novamente a função com um
vetor para receber os objetos concretos. Após buscar os dispositivos físicos disponíveis que
suportam Vulkan, pode-se elencar quais dispositivos serão os mais adequados para a
utilização. Para tal, as funções vkGetPhysicalDeviceProperties e
vkGetPhysicalDeviceFeatures podem ser chamadas para verificação de aspectos como o
layout de memória do dispositivo ou se o mesmo suporta recursos como geometry shaders,
entre outros. Após escolhido o dispositivo, deve-se instanciar um (ou mais) objeto(s) do tipo
vkPhysicalDevice, o qual é responsável por gerenciar o acesso e a verificação de recursos
disponíveis através de dispositivos físicos. O processo descrito para adquirir um
33
vkPhysicalDevice pode ser visualizado no código C++ no Quadro 2, onde se abstraiu o
código de Overvoorde (2016) na função getPhysicalDevice.
Quadro 2 – Busca de um dispositivo físico (RendererVk.cpp:259) 1. bool RendererVk::getPhysicalDevice() 2. { 3. uint32_t deviceCount = 0; 4. vkEnumeratePhysicalDevices(_vkInstance, &deviceCount, nullptr); 5. 6. if (deviceCount == 0) 7. throw std::runtime_error("failed to find GPUs with Vulkan support!"); 8. 9. std::vector<VkPhysicalDevice> devices(deviceCount); 10. vkEnumeratePhysicalDevices(_vkInstance, &deviceCount, devices.data()); 11. 12. for (const auto& device : devices) { 13. if (isDeviceSuitable(device)) { 14. vkPhysicalDevice = device; 15. break; 16. } 17. } 18. 19. if (vkPhysicalDevice == VK_NULL_HANDLE) 20. throw std::runtime_error("failed to find a suitable GPU!"); 21. }
Fonte: adaptado de Overvoorde (2016).
Para exibir imagens em uma janela utilizando Vulkan, faz-se necessário utilizar
alguma de suas extensões Window System Integration (WSI), pois como o Vulkan é
agnóstico de plataforma, não suporta em seu núcleo a integração com janelas de sistemas
operacionais específicos. Então deve-se se obter um objeto do tipo VkSurfaceKHR que atua
como “superfície de renderização” na qual as imagens geradas irão ser apresentadas. Para tal
pode-se chamar a função vkCreateWin32SurfaceKHR do Vulkan que recebe como
argumento um struct de configuração, ou pode-se utilizar uma função
(glfwCreateWindowSurface) provida pelo gerenciador de janelas GLFW, a qual não necessita
de um struct de configuração e é multi-plataforma. A utilização de um VkSurfaceKHR é
opcional, pois pode-se utilizar a API Vulkan para renderizar imagens sem exibi-las em uma
janela.
A partir de um objeto do tipo VkPhysicalDevice, pode-se instanciar uma ou mais
instâncias de objetos do tipo VkDevice, conforme necessário de acordo com a aplicação
sendo desenvolvida, este processo pode ser visualizado no Quadro 3. Objetos desse tipo
atuam como dispositivo lógico, realizando a conexão entre a aplicação e o dispositivo sendo
utilizado.
A maioria dos recursos e objetos necessários para efetuar operações subsequentes com
a API necessitam como argumento em sua função de criação uma referência a um VkDevice.
34
Tais recursos incluem a criação de objetos como: VkQueues, VkImage, VkBuffer,
VkFramebuffer, VkRenderPass, VkPipeline, VkCommandBuffer, entre outros, os quais
serão introduzidos a seguir.
A comunicação entre o dispositivo físico e a aplicação se dá através de operações que
são gravadas em command buffers e enviadas para VkQueues de famílias específicas. Tais
famílias são denominadas queue families e são filas de comandos com finalidades específicas,
por exemplo, uma fila somente para renderização, outra somente para utilizar a GPU para
computação (sem renderização) ou ainda outra somente para a apresentação de imagens após
a renderização (present). A verificação de quais filas gráficas estão disponíveis no dispositivo
físico se dá através da função findQueueFamilies, como pode ser averiguado no Quadro 3,
onde se abstraiu o código de Overvoorde (2016) na função getLogicalDevice.
35
Quadro 3 – Atribuição das queues e dispositivo lógico (RendererVk.cpp:283) 1. bool RendererVk::getLogicalDevice() 2. { 3. QueueFamilyIndices indices =
VulkanHelper::findQueueFamilies(vkPhysicalDevice, _vkSurfaceKHR); 4. 5. std::vector<VkDeviceQueueCreateInfo> queueCreateInfos; 6. std::set<int> uniqueQueueFamilies = { indices.graphicsFamily,
indices.presentFamily }; 7. 8. float queuePriority = 1.0f; 9. for (int queueFamily : uniqueQueueFamilies) { 10. VkDeviceQueueCreateInfo queueCreateInfo = {}; 11. queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; 12. queueCreateInfo.queueFamilyIndex = queueFamily; 13. queueCreateInfo.queueCount = 1; 14. queueCreateInfo.pQueuePriorities = &queuePriority; 15. queueCreateInfos.push_back(queueCreateInfo); 16. } 17. 18. VkPhysicalDeviceFeatures deviceFeatures = {}; 19. deviceFeatures.samplerAnisotropy = VK_TRUE; 20. 21. VkDeviceCreateInfo createInfo = {}; 22. createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; 23. createInfo.queueCreateInfoCount =
static_cast<uint32_t>(queueCreateInfos.size()); 24. createInfo.pQueueCreateInfos = queueCreateInfos.data(); 25. createInfo.pEnabledFeatures = &deviceFeatures; 26. createInfo.enabledExtensionCount =
static_cast<uint32_t>(deviceExtensions.size()); 27. createInfo.ppEnabledExtensionNames = deviceExtensions.data(); 28. 29. if (vkCreateDevice(vkPhysicalDevice, &createInfo, nullptr, &vkDevice) !=
VK_SUCCESS) { 30. throw std::runtime_error("failed to create logical device!"); 31. } 32. 33. vkGetDeviceQueue(vkDevice, indices.graphicsFamily, 0, &graphicsQueue); 34. vkGetDeviceQueue(vkDevice, indices.presentFamily, 0, &presentQueue); 35. }
Fonte: adaptado de Overvoorde (2016).
A aquisição de imagens que servirão de container para imagens renderizadas e
posteriormente apresentação na tela, é feita através de um objeto do tipo swap chain. O
swap chain é constituído basicamente por uma fila de imagens (VkImage). Pode ser concebido
como contendo o front e back buffer (se estiver operando em modo double buffering), mas
não é necessariamente análogo pois contém mais informações.
Para efetuar a renderização deve-se buscar uma imagem no swap chain, renderizar
nela e depois devolvê-la para o swap chain e para a fila de imagens que podem ser adquiridas
para apresentação na tela. Não há necessidade de criar ou alocar memória para as imagens do
swap chain, pois é um dos poucos objetos em que o Vulkan realiza o gerenciamento de
memória automático implícito. Portanto, como a alocação de memória e feita pela API, ela
não fornece acesso direto às imagens (VkImage) contidas nela. Para acessar o conteúdo das
36
imagens faz-se necessária a criação de objetos VkImageView que atuam como uma forma de
“visão” para a imagem, com permissão somente de leitura, que serão posteriormente
utilizados nos frame buffer. A criação de tal objeto se dá pela especificação de um struct de
configuração contendo informações como a quantidade de imagens que serão utilizadas, a
superfície de renderização e suas qualidades e as filas de comandos que serão utilizadas
(Quadro 4).
Nota-se que em OpenGL o conceito de swap chain não existia e o gerenciamento de
frame buffers era feito de forma implícita pela API, e era necessário somente chamar a função
glSwapBuffers para trocar as imagens do front e back buffers.
Quadro 4 – Configuração do swap chain (RendererVk.cpp:354) 1. ... 2. VkSwapchainCreateInfoKHR createInfo = {}; 3. createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; 4. createInfo.surface = _vkSurfaceKHR; 5. createInfo.minImageCount = imageCount; 6. createInfo.imageFormat = surfaceFormat.format; 7. createInfo.imageColorSpace = surfaceFormat.colorSpace; 8. createInfo.imageExtent = extent; 9. createInfo.imageArrayLayers = 1; 10. createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; 11. 12. QueueFamilyIndices indices = VulkanHelper::findQueueFamilies(vkPhysicalDevice,
_vkSurfaceKHR); 13. uint32_t queueFamilyIndices[] = {indices.graphicsFamily, indices.presentFamily }; 14. 15. if (indices.graphicsFamily != indices.presentFamily) { 16. createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; 17. createInfo.queueFamilyIndexCount = 2; 18. createInfo.pQueueFamilyIndices = queueFamilyIndices; 19. } 20. else { 21. createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; 22. } 23. 24. createInfo.preTransform = swapChainSupport.capabilities.currentTransform; 25. createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; 26. createInfo.presentMode = presentMode; 27. createInfo.clipped = VK_TRUE; 28. createInfo.oldSwapchain = VK_NULL_HANDLE; 29. 30. if (vkCreateSwapchainKHR(vkDevice, &createInfo, nullptr, &_swapChain.swapChain) !=
VK_SUCCESS) 31. throw std::runtime_error("failed to create swap chain!"); 32. ...
Fonte: adaptado de Overvoorde (2016).
Outro objeto necessário para realizar uma operação de renderização em Vulkan, é o
VkRenderPass, o qual descreve um render pass. Um render pass em Vulkan consiste de
alguns attachments, imagens artefatos de renderização, como depth, color e stencil. Nele é
descrita a maneira que estas imagens serão utilizadas durante o processo de renderização. No
caso da biblioteca desenvolvida, existe somente um sub pass com os attachments de color e
depth, que definem a cor e profundidade de um fragment em uma imagem. Para rotinas
37
gráficas mais complexas pode-se utilizar de mais de um sub pass como por exemplo efetuar
um desfoque, em que se teria uma imagem da cena renderizada em um sub pass (como é feito
nesta biblioteca) e em outro se aplicaria o desfoque com o produto do render pass anterior,
combinando o resultado de acordo com uma função de blending.
Os attachments do render pass devem ser manipulados por objetos do tipo
VkFramebuffer, os quais contém as VkImageViews dos mesmos. Para cada imagem no swap
chain faz-se necessário um VkFramebuffer equivalente. O processo de especificação de um
render pass pode ser visualizado no código em C++ no Quadro 5, onde se abstraiu o código
de criação de render pass de Overvoorde (2016) para uma função da classe RendererVk.
Quadro 5 – Configuração do render pass (RendererVk.cpp:501) 1. ... 2. std::array<VkAttachmentDescription, 2> attachments = { colorAttachment,
depthAttachment }; 3. VkRenderPassCreateInfo renderPassInfo = {}; 4. renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; 5. renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size()); 6. renderPassInfo.pAttachments = attachments.data(); 7. renderPassInfo.subpassCount = 1; 8. renderPassInfo.pSubpasses = &subpass; 9. renderPassInfo.dependencyCount = 1; 10. renderPassInfo.pDependencies = &dependency; 11. 12. if (vkCreateRenderPass(vkDevice, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) 13. throw std::runtime_error("failed to create render pass!"); 14. ...
Fonte: adaptado de Overvoorde (2016).
A configuração da maneira na qual o pipeline gráfico será utilizado para renderização é
definida em outro objeto necessário no processo de renderização, o VkPipeline (Quadro 6).
Nota-se que o VkPipeline é criado e atribuído antes do processo de renderização ser iniciado,
portanto se algum aspecto (alteração de shader por exemplo) precisar ser alterado em tempo
de execução ele deve ser recriado. Somente alguns aspectos podem sofrer alteração sem
necessidade de reinicialização de um VkPipeline, como o tamanho de viewport e o line
width.
Os aspectos que devem ser definidos no VkPipeline são:
a) os shaders que serão utilizados;
b) o formato dos vértices que serão submetidos (se haverá descrição de informações
relativas a cor ou vetor normal, por exemplo);
c) a descrição da viewport (em que região do frame buffer a imagem será renderizada
(posição mínima, até posição máxima));
d) a descrição do rasterizer (descreve a forma que os vértices fornecidos serão
transformados em fragments que irão alimentar o estágio de fragment shader do
38
pipeline);
e) se haverá utilização de Multisample Anti-Aliasing;
f) o tipo de color blending que será utilizado;
g) o pipeline layout;
h) o render pass;
i) se haverá subpasses;
j) se sua criação vai ser baseada em um VkPipeline existente (base pipeline);
k) e se os fragments podem ser descartados no depth test.
Quadro 6 – Configuração de um VkPipeline (RendererVk.cpp:632) 1. ... 2. VkGraphicsPipelineCreateInfo pipelineInfo = {}; 3. pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; 4. pipelineInfo.stageCount = 2; 5. pipelineInfo.pStages = shaderStages; 6. pipelineInfo.pVertexInputState = &vertexInputInfo; 7. pipelineInfo.pInputAssemblyState = &inputAssembly; 8. pipelineInfo.pViewportState = &viewportState; 9. pipelineInfo.pRasterizationState = &rasterizer; 10. pipelineInfo.pMultisampleState = &multisampling; 11. pipelineInfo.pColorBlendState = &colorBlending; 12. pipelineInfo.layout = pipelineLayout; 13. pipelineInfo.renderPass = renderPass; 14. pipelineInfo.subpass = 0; 15. pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; 16. pipelineInfo.pDepthStencilState = &depthStencil; 17. ...
Fonte: adaptado de Overvoorde (2016).
Na utilização de shaders em Vulkan faz-se necessário compilar o código em GLSL
para o formato binários SPIR-V. O SDK fornecido pela LunarG possui um executável que
realiza esta conversão, o qual foi utilizado neste trabalho como se pode ver no Quadro 7.
Quadro 7 – Conversão de shader GLSL para SPIR-V (compile shaders.bat)
1. C:/VulkanSDK/1.0.42.1/Bin32/glslangValidator.exe -V shader_base.vert
2. C:/VulkanSDK/1.0.42.1/Bin32/glslangValidator.exe -V shader_base.frag
Fonte: elaborado pelo autor.
Além da compilação para formato binário do shaders, para poderem ser utilizados no
VkPipeline faz-se necessário criar objetos do tipo VkShaderModule, que atuam como a
representação em Vulkan de um shader. Porém, os VkShaderModule somente representam os
programas shader de forma separada e isolada do resto de pipeline, portanto, para poderem
ser utilizados deve-se criar objetos do tipo VkPipelineShaderStageCreateInfo para
especificar em qual estágio do pipeline gráfico o shader deve ser utilizado (vertex ou
fragment por exemplo) e o ponto de entrada do programa (geralmente “main”). Os
39
VkPipelineShaderStageCreateInfo devem ser atribuídos à propriedade “pStages” do
struct de configuração do VkPipeline, como pode ser visto no Quadro 6.
O envio de informações para os shaders é feito através da utilização de descriptors.
Através deles, os shaders conseguem acessar recursos arbitrários, como uniform buffers
contendo transformações geométricas no estágio de vertex ou texture samplers no estágio de
fragment, por exemplo. Os descriptors são representados por objetos do tipo
VkDescriptorSet, os quais são alocados e sua memória gerenciada por um objeto do tipo
VkDescriptorPool. Cada binding de um shader deve ser descrito em um objeto do tipo
VkDescriptorSetLayout.
Comandos em Vulkan não são chamadas de função como eram em OpenGL. Para
enviar comandos para a GPU em Vulkan faz-se necessária a utilização de command buffers.
Eles são constituídos por uma fila de comandos que serão enviados para uma GPU somente
quando a função de vkQueueSubmit for chamada. Command buffers são alocados a partir de
command pools, as quais gerenciam a memória dos mesmos.
A última etapa de inicialização antes de se poder renderizar algo, é instanciar
semáforos do pipeline para sincronização entre operações desencadeados por command
buffers. Semáforos em Vulkan são representados por objetos do tipo VkSemaphore, e na
implementação deste trabalho foram utilizados 2 semáforos: um que é liberado quando uma
imagem está disponível para ser buscada no swap chain, e outro para sinalizar quando o
processo de renderização foi concluído e uma imagem está pronta para ser apresentada. O
código de inicialização dos semáforos pode ser visto no Quadro 8, onde se abstraiu o código
de criação de semáforos de Overvoorde (2016) para uma função da classe RendererVk.
Quadro 8 – Configuração dos semáforos (RendererVk.cpp:632) 1. bool RendererVk::createSemaphores() 2. { 3. VkSemaphoreCreateInfo semaphore_create_info; 4. semaphore_create_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; 5. semaphore_create_info.pNext = nullptr; 6. semaphore_create_info.flags = 0; 7. 8. if ((vkCreateSemaphore(vkDevice, &semaphore_create_info, nullptr,
&_imageAvailableSemaphore) != VK_SUCCESS) || 9. (vkCreateSemaphore(vkDevice, &semaphore_create_info, nullptr,
&_renderingFinishedSemaphore) != VK_SUCCESS)) { 10. std::cout << "Could not create semaphores!" << std::endl; 11. return false; 12. } 13. 14. return true; 15. }
Fonte: adaptado de Overvoorde (2016).
40
Após a realização de todos os passos até aqui citados e a inicialização de todos os
objetos necessários para utilização da API Vulkan, pode-se realizar o carregamento de cenas
para depois efetuar a renderização. Tais processos serão explicados nas seções 3.3.1.2 e
3.3.1.3.
A inicialização do OpenGL se faz com muito menos complexidades se comparada com
Vulkan. Primeiramente foi necessário inicializar o gerenciador de janelas GLFW definindo a
versão do OpenGL que será utilizada (neste trabalho versão 4.5) e definindo o contexto para a
janela criada (Quadro 9).
Quadro 9 – Inicialização do GLFW para OpenGL (RendererGL.cpp:30) 1. void RendererGL::initGLFW() 2. { 3. glfwInit(); 4. 5. glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); 6. glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5); 7. glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 8. glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 9. 10. glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); 11. 12. _glfwWindow = glfwCreateWindow(WIDTH, HEIGHT, "OpenGL", nullptr, nullptr); 13. glfwMakeContextCurrent(_glfwWindow); 14. }
Fonte: elaborado pelo autor.
A inicialização do OpenGL em si é realizada através do GLEW chamando a função
glewInit para inicialização da API. Além disso fez-se necessário definir alguns estados da
API como a função de blending que será utilizada, habilitar o depth test e habilitar o culling
de faces (Quadro 10).
Quadro 10 – Inicialização do OpenGL (RendererGL.cpp:52) 1. void RendererGL::initOpenGL() 2. { 3. glEnable(GL_BLEND); 4. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 5. 6. glEnable(GL_DEPTH_TEST); 7. glDepthFunc(GL_LESS); 8. 9. glEnable(GL_CULL_FACE); 10. 11. glewExperimental = GL_TRUE; 12. glewInit(); 13. }
Fonte: elaborado pelo autor.
Após a realização dos passos citados pode-se iniciar o processo de carregamento de
cenas e posteriormente sua renderização em OpenGL. Na seção seguinte será demonstrado o
processo de carregamento de cenas.
41
3.3.1.2 CARREGAMENTO DE CENAS
Nesta seção será descrito como ocorre o carregamento de objetos que compõe as cenas
nesta biblioteca. Primeiramente serão apresentadas as etapas do carregamento em Vulkan e na
sequência em OpenGL. O processo de carregamento das cenas consiste basicamente de:
a) carregar as malhas dos objetos gráficos;
b) carregar as texturas e shaders que serão utilizadas nos materiais.
Após a inicialização dos objetos citados até o momento, a estrutura para renderizar
cenas em Vulkan está pronta. As malhas em Vulkan são representadas por objetos do tipo
VkBuffer contendo os dados referentes aos vértices especificados. Possuem também um
VkBuffer contendo os índices de tais vértices para renderização indexada. Para cada
VkBuffer utilizado precisa-se de um objeto do tipo VkDeviceMemory para manter-se os
dados dos buffers em memória acessível pela CPU.
Utilizou-se da biblioteca Tinyobj para carregar arquivos de malhas 3D no formato OBJ
para a biblioteca. Para tal a função LoadObj do Tinyobj é utilizada para ler o arquivo e
retornar listas com posição de vértices, vetor normal e coordenadas de textura (UV). Nota-se
que nas coordenadas de textura deve-se inverter o valor de y (ou V), pois em Vulkan o valor
de y em coordenadas de tela tem sinal inverso em relação ao OpenGL.
As texturas em Vulkan foram representadas por objetos do tipo VkImage, os quais
contêm as informações de como a imagem deve ser tratada pela API. Utilizou-se a biblioteca
STP image para realizar o carregamento de imagens, as quais são carregadas arrays de bytes
que irão popular um VkBuffer. Após o carregamento a criação de um VkBuffer deve-se
converter o VkBuffer em um VkImage, o que pode ser feito com o comando
vkCmdCopyBufferToImage. Após a criação do VkImage deve-se criar um objeto do tipo
VkImageView, pois a implementação de Vulkan não permite que VkImages sejam acessadas
diretamente. Para que texturas possam ser acessadas por shaders deve-se criar objetos
samplers para que as texturas sejam acessada através deles. Tais objetos controlam aspectos
como filtros e transformações nas imagens, como por exemplo, anisotropia e o modo de
repetição da imagem. A representação desse tipo de objeto em Vulkan faz-se com objetos do
tipo VkSampler os quais são criados com o comando vkCreateSampler, e como todos os
outros objetos em Vulkan, requer um struct de configuração para sua concepção.
A representação de malhas 3D em OpenGL é feita através de objetos Vertex Buffer
Object (VBO), Vertex Array Object (VAO) e Element Buffer Object (EBO). O objeto VBO é criado
a partir da lista vértices carregados utilizando a biblioteca Tinyobj e é utilizado para enviar os
42
dados referentes aos vértices de uma malha para a memória da GPU. O VAO é responsável por
descrever o estado referente a vértices ou outros atributos que servirão de input para o shader
utilizado para desenhar um objeto gráfico. No caso deste trabalho, descreve como acessar as
coordenadas de um vértice, as coordenadas de textura, as coordenadas de vetor normal e a
cor. Para realizar a renderização indexada faz-se a utilização de um objeto do tipo EBO.
Texturas na implementação com OpenGL também são carregadas utilizando a
biblioteca STP Image. A mesma carrega as imagens do disco para formato de array de bytes,
os quais são fornecidos para a função do OpenGL glTexImage2D a qual gera a textura na
memória de vídeo.
O carregamento de shaders é feito lendo os arquivos que contém seus programas e
enviando seu conteúdo para ser compilado pelo OpenGL através da função
glCompileShader.
3.3.1.3 RENDERIZAÇÃO
Nas seções a seguir serão discutidas as etapas necessárias para renderizar uma imagem
após a configuração de cena. Primeiramente serão mostradas as etapas e detalhes de
implementação em Vulkan e em sequência as do OpenGL.
O processo de renderização em Vulkan ocorre somente após a realização das etapas de
inicialização descritas nas seções anteriores. As etapas que são necessárias para renderização
de uma imagem estão listados a seguir:
a) gravação de command buffers;
b) adquirir uma imagem válida do swap chain;
c) enviar os command buffers posteriormente gravados para a fila gráfica;
d) mostrar a imagem na tela com a chamada de função vkQueuePresentKHR.
A primeira etapa para desenhar uma imagem na tela em Vulkan é gravar os command
buffers de desenho. Para cada command buffer utilizado (no caso deste trabalho são 3, pois a
técnica de triple-buffering está sendo empregada), deve-se inicializar a gravação através da
chamada função vkBeginCommandBuffer passando o command buffer alvo da gravação
como argumento. Em seguida deve-se atribuir o render pass elaborado anteriormente, para tal
chama-se a função vkCmdBeginRenderPass com um objeto um struct de configuração
especificando o VkRenderPass que será utilizado. Na sequência, para cada objeto gráfico a
ser renderizado na cena, deve-se realizar as seguintes operações:
43
a) atribuir o descriptor set correspondente aos atributos que serão enviados ao shader
que será utilizado para desenhar tal objeto;
b) atribuir o vertex buffer contendo os vértices da malha 3D;
c) atribuir o index buffer;
d) atribuir o VkPipeline;
e) enviar as push constants (opcional, no caso deste trabalho é utilizado para enviar o
uniform buffer para o shader);
f) executar o comando de desenho indexado (vkCmdDrawIndexed).
Após a determinação dos comandos para cada objeto, deve-se sinalizar que não haverá
mais comandos referentes ao render pass com a função vkCmdEndRenderPass. Em seguida,
para finalizar a gravação em um command buffer, chama-se a função vkEndCommandBuffer.
O processo de gravação de command buffers é demonstrado no pseudocódigo do Quadro 11.
Quadro 11 – Pseudocódigo de gravação dos command buffers 1. for (size_t i = 0; i < commandBuffers.size(); i++) 2. { 3. (...)
4. vkBeginCommandBuffer(commandBuffers[i], &beginInfo);
5. (...)
6. vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, (...));
7. for (const auto &obj : scene->getSceneGraph())
8. {
9. vkCmdBindDescriptorSets(commandBuffers[i], (...));
10. (...)
11. vkCmdBindVertexBuffers(commandBuffers[i], (...));
12. vkCmdBindIndexBuffer(commandBuffers[i], (...));
13. vkCmdBindPipeline(commandBuffers[i], (...));
14. vkCmdPushConstants(commandBuffers[i], (...));
15. vkCmdDrawIndexed(commandBuffers[i], (...));
16. }
17. vkCmdEndRenderPass(commandBuffers[i]);
18. vkEndCommandBuffer(commandBuffers[i]);
19. }
Fonte: elaborado pelo autor.
Com os command buffers devidamente gravados, pode-se iniciar os procedimentos
finais para renderizar uma imagem e apresentá-la na tela. Primeiramente, faz-se necessário
adquirir uma imagem válida para utilização do swap chain. Para tal, chama-se a função
vkAcquireNextImageKHR, a qual retorna o índice de uma imagem do swap chain que pode
ser utilizada para renderização. Em seguida, os comandos de desenho gravados anteriormente
são enviados para a fila gráfica da GPU com o comando vkQueueSubmit. Após todas as
etapas citadas até este ponto, pode-se chamar a função vkQueuePresentKHR com referência
para a fila de apresentação da GPU e struct de configuração contendo os semáforos de
renderização e o swap chain que será utilizado. Com estes passos executados, a imagem será
44
renderizada na tela. Os passos descritos anteriormente são demonstrados no pseudocódigo do
Quadro 12.
Quadro 12 – Pseudocódigo de uma função de desenho com Vulkan 1. void drawFrame() 2. { 3. // adquirir uma imagem no swapchain para poder desenhar
4. VkResult result = vkAcquireNextImageKHR((...));
5. (...)
6. // verificar se a imagem adquirida é valida
7. if (result == VK_ERROR_OUT_OF_DATE_KHR) {
8. recreateSwapChain();
9. }
10. (...)
11. // enviar os command buffers para a fila gráfica
12. vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
13. (...)
14. // mostrar a imagem na tela
15. vkQueuePresentKHR(presentQueue, &presentInfo);
16. }
Fonte: elaborado pelo autor.
Após a realização dos passos citados anteriormente realizados, tem-se uma imagem
renderizada e seu conteúdo exibido em uma janela. Isso conclui a etapa de renderização em
Vulkan. Na sequência serão apresentados os passos para renderização em OpenGL.
O processo de renderização em OpenGL possui as seguintes etapas:
a) invocar o comando de clear screen para limpar os buffers;
b) desenhar na janela;
c) swap buffer, para alternar entre o front e back buffer.
Na etapa de clear screen, executa-se o comando glClear que limpa o conteúdo do
frame buffer ativo. Após esta etapa, para cada objeto a ser desenhado deve-se executar os
seguintes passos:
a) atribuir um shader ao estado do OpenGL através da chamada de função
glUseProgram;
b) enviar a matriz transformação para o shader através de chamada de função
glUniformMatrix4fv;
c) atribuir a textura ao estado do OpenGL com a função glBindTexture;
d) atribuir o VAO do objeto com a função glBindVertexArray;
e) desenhar o objeto com a função glDrawElements.
O último passo para que os objetos desenhados apareçam na janela, é chamar a função
glfwSwapBuffers do GLFW, para que frame buffer apropriado seja exibido. O pseudocódigo
do processo descrito anteriormente pode ser visualizado no Quadro 13.
45
Quadro 13 – Pseudocódigo de uma função de desenho com OpenGL 1. void drawFrame() 2. { 3. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 4. 5. for (auto obj : scene->getSceneGraph()) 6. { 7. glUseProgram(this->Program); 8. 9. glUniformMatrix4fv((...)); 10. 11. glActiveTexture(GL_TEXTURE0); 12. (...) 13. glBindTexture(GL_TEXTURE_2D, textureId); 14. 15. glBindVertexArray(this->VAO); 16. // Draw mesh 17. glDrawElements(GL_TRIANGLES, this->indicesSize, GL_UNSIGNED_INT, 0); 18. (...) 19. } 20. glfwSwapBuffers(_glfwWindow); 21. }
Fonte: elaborado pelo autor.
3.3.2 OPERACIONALIDADE DA IMPLEMENTAÇÃO
Para utilização da API Vulkan, faz-se necessária a instalação de seu SDK
disponibilizado pela LunarG e uma placa de vídeo que suporte a API. Além disso, nas
propriedades do projeto no Visual Studio faz-se necessário apontar o caminho da pasta
Include do SDK do Vulkan nas configurações de C/C++ (Configuration Properties, C/C++,
General), como ilustrado na Figura 9. Deve-se também configurar o caminho da pasta Lib do
SDK do Vulkan nas configurações do Linker (Configuration Properties, Linker, General),
como ilustrado na Figura 10. As outras bibliotecas estão inclusas na pasta da solução do
projeto, portanto não precisam ser incluídas manualmente. Nota-se que a biblioteca foi
desenvolvida e testada somente na plataforma Microsoft Windows 10.
Figura 9 – Configuração do diretório de Include do Vulkan SDK
Fonte: elaborado pelo autor.
46
Figura 10 – Configuração do diretório de Lib do Vulkan SDK
Fonte: elaborado pelo autor.
Para efetuar o estudo comparativo entre as APIs, se desenvolveu uma biblioteca para
renderização de cenas com objetos compostos por malhas 3D simples, mapeamento de textura
e modelo de iluminação Blinn-Phong nos objetos.
A biblioteca foi separada em três projetos do Visual Studio, o primeiro sendo uma
biblioteca estática e interface base para as subsequentes implementações do renderizador para
cada API (commonBase). O segundo contendo a implementação que realiza a abstração da API
utilizando OpenGL (glFurb), outro contendo uma implementação análoga mas utilizando
Vulkan (vkFurb), como ilustrado na Figura 11.
47
Figura 11 – Estrutura do projeto no Visual Studio
Fonte: elaborado pelo autor.
No projeto commonBase se encontra a implementação genérica que serve como base
para implementação do renderizador em OpenGL e Vulkan respectivamente. As derivações
da classe abstrata Renderer atuam como gerenciador principal e ponto de entrada do
processo de renderização. Nelas ocorre a inicialização das APIs gráficas e do gerenciador de
janela. Além disso, também mantém-se nela uma referência para uma cena contendo os
objetos gráficos que serão renderizados pela biblioteca.
Uma cena é especificada na classe Scene a qual contém uma coleção de objetos do
tipo DrawableObj, uma Camera e um objeto do tipo Light. No Quadro 14 é possível verificar
a implementação da cena 1 como exemplo. A classe DrawableObj é uma classe abstrata da
qual as classes dos tipos OGLDrawableObj e VulkanDrawableObj devem implementar. As
classes desse tipo representam um objeto gráfico em uma cena e contém a seguintes
informações que definem seu aspecto visual: malha 3D, material e matriz de transformação.
Ainda nessa classe é definido o método update o qual realiza a atualização do uniform buffer
do objeto, que contém as transformações geométricas, matriz de projeção e view da câmera e
posição da luz que serão enviadas ao shader para serem consumidas posteriormente no estado
apropriado do pipeline gráfico.
48
Além da lista de objetos que serão renderizados, contém também um objeto do tipo
Camera, o qual contém as matrizes de projeção e view que definem o aspecto visual da cena.
O objeto Light contém somente a posição que a luz virtual vai estar situada em espaço
global.
Quadro 14 – Definição classe Cena1 1. #include "../VulkanHeader.h" 2. 3. class Cena1 : public Scene 4. { 5. public: 6. Cena1() 7. { 8. name = "Cena1"; 9. // criação dos assets que serão utilizdados 10. VulkanTexture *texture(new VulkanTexture("../textures/bronze.jpg")); 11. VulkanMaterial* material(new VulkanMaterial); 12. VulkanShader* shader(new VulkanShader("../shaders/vert.spv",
"../shaders/frag.spv")); 13. VulkanMesh* mesh(new VulkanMesh("../models/buddha.obj")); 14. // atribuir o shader ao material 15. material->setShader(shader); 16. // criação do objeto gráfico 17. DrawableObj* obj1(new VulkanDrawableObj); 18. // configuração do objeto gráifco 19. PtrDownCast(obj1)->material = material; 20. PtrDownCast(obj1)->getMaterial()->setTexture(texture); 21. shader->updateDescriptorSet(PtrDownCast(obj1)->uniformBuffer, texture); 22. PtrDownCast(obj1)->setMesh(mesh); 23. PtrDownCast(obj1)->setPosition(glm::vec3(0)); 24. PtrDownCast(obj1)->setScale(glm::vec3(1)); 25. // adicionar o objeto gráfico à cena 26. this->AddObject(obj1); 27. // configurar luz e camera da cena 28. this->light = Light(glm::vec4(0, 5, 10, 0)); 29. this->camera = new Camera(glm::vec3(0, 0.5f, 1), glm::vec3(0.0f, .1f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f)); 30. } 31. };
Fonte: elaborado pelo autor.
As classes derivadas de Mesh representam uma malha 3D e contém informações
referentes aos vértices e índices que compões a mesma. Em sua implementação na API
Vulkan, além de conter uma lista de vértices e uma lista de índices, possui objetos dos tipos
VkBuffers e VkDeviceMemory para representar os vértices e índices na memória da GPU. Já
na implementação em OpenGL, existem os atributos VAO, VBO e EBO que são handles do
OpenGL para objetos do tipo Vertex Array Object, Vertex Buffer Object e Element Buffer
Object.
As classes OGLMaterial e VulkanMaterial representam o conceito de material de um
objeto gráfico. Na implementação deste trabalho sua implementação é constituída apenas de
uma referência para um objeto do tipo Shader e um objeto do tipo Texture.
49
Na implementação com Vulkan a classe VulkanShader representa um shader
programável com programas dos estágios de vertex e fragment sendo utilizados. Para
representação de um shader foi definido que ele deve conter um objeto do tipo VkPipeline e
seus descriptors, como demonstrado no Quadro 15.
Quadro 15 – Classe VulkanShader 1. ... 2. class VulkanShader 3. { 4. public: 5. VulkanShader(std::string vertPath, std::string fragPath); 6. ~VulkanShader(); 7. 8. void prepare(); 9. 10. void createDescriptorPool(); 11. void createDescriptorSet(); 12. void updateDescriptorSet(VkBuffer uniformBuffer,VulkanTexture* vulkanTexture); 13. void createDescriptorSetLayout(); 14. void recreateGraphicsPipeline(); 15. 16. VkDescriptorPool getDescriptorPool() { return descriptorPool; } 17. const VkDescriptorSet* getDescriptorSet() { return &descriptorSet; } 18. 19. VkDescriptorSet descriptorSet; 20. VkDescriptorPool descriptorPool; 21. VkDescriptorSetLayout descriptorSetLayout; 22. 23. VkPipeline graphicsPipeline; 24. };
Fonte: elaborado pelo autor.
3.4 ANÁLISE DOS RESULTADOS
Nesta seção serão apresentados os cenários de testes desenvolvidos e os resultados
estatísticos oriundos dos mesmos. Na próxima seção será apresentado um quadro comparativo
entre os trabalhos correlatos e na seção seguinte serão apresentados os resultados dos cenários
de teste.
3.4.1 COMPARAÇÃO ENTRE OS TRABALHOS CORRELATOS
Conforme análise apresentada (Quadro 14), todos os trabalhos correlatos foram
realizados utilizando a API Vulkan, mas apenas Epic Games (2016a) e Mainuš (2016)
implementam a técnica de reutilização de command buffers previamente construídos para
envio de comandos através da API para a GPU.
50
Quadro 16 – Comparativo entre os trabalhos correlatos
Características Epic Games
(2016a)
Mainuš (2016) Overvoorde
(2016)
VkFurb (2017)
suporte à Vulkan sim sim sim sim
reutilização de
command buffers
sim sim não não
suporte à materiais sim sim não sim
occlusion culling sim não não não
multi-threaded
command
submission
sim sim não não
Fonte: elaborado pelo autor.
Todos os trabalhos correlatos, com exceção de Overvoorde (2016), implementam o
conceito de material, o qual é implementado de forma básica no trabalho aqui apresentado. A
Unreal Engine implementa a técnica de otimização occlusion culling, mas Vulkan Based
Render Toolkit e Vulkan Tutorial não, o qual também não é implementado neste trabalho.
Observa-se também que Epic Games (2016a) e Mainuš (2016) utilizam técnicas de
programação concorrente para a criação de command buffers (Multi-threaded command
submission), enquanto Overvoorde (2016) e este trabalho não.
3.4.2 RESULTADOS DOS CENÁRIOS DE TESTES
Foram desenvolvidas três cenas de testes com configurações variáveis para verificação
e comparação de performance entre as APIs gráficas. As configurações das cenas de testes
estão listadas a seguir:
a) cena 1: 1 objeto, constituído por 543.652 vértices, 1.087.716 triângulos (Stanford
Happy Buddha), como ilustrado na Figura 12;
b) cena 2: 289 objetos iguais, constituídos por 35.947 vértices, 69.451 triângulos
(Stanford Bunny), como ilustrado na Figura 13;
c) cena 3: 120 objetos mistos; sendo 100 Stanford Bunny, 10 Blender Monkey (507
vértices, 968 triângulos), 10 Stanford Happy Buddha, como ilustrado na Figura 14.
51
Figura 12 – Captura de tela da cena 1
Fonte: elaborado pelo autor.
Figura 13 – Captura de tela da cena 2
Fonte: elaborado pelo autor.
52
Figura 14 – Captura de tela da cena 3
Fonte: elaborado pelo autor.
3.4.3 Desempenho
A comparação de performance foi feita utilizando as seguintes métricas de
performance:
a) quadros por segundo;
b) frame time (tempo em milissegundos entre o frame anterior e o atual);
c) consumo de memória;
d) utilização da CPU.
Para obtenção de dados estatísticos, se desenvolveu uma aplicação que executa a
biblioteca com as cenas de teste por 20 segundos, e calcula as médias das métricas
mencionadas anteriormente em relação ao tempo. As cenas de teste foram executadas em
computador de uso pessoal possuindo as seguintes configurações:
a) processador Intel I5 6600K com frequência de 4,4GHz por core;
b) placa de vídeo Nvidia Geforce GTX 1060 6GB;
c) 8 Gb de memória RAM DDR4 com frequência de 3400 MHz.
53
Nas próximas seções serão apresentados os resultados dos testes comparativos da
execução das cenas de testes.
3.4.3.1 Quadros por segundo
Na Figura 15 é apresentado um gráfico com a comparação entre, a média da
quantidade de quadros por segundos obtidos em cada cena e cada API gráfica. Na primeira
cena de testes a diferença no número de quadros por segundo em favor ao Vulkan foi de
aproximadamente 8%. Na segunda cena, Vulkan teve em média aproximadamente 19% a
mais de quadros por segundos. Na última cena, Vulkan apresentou em média
aproximadamente 10% quadros por segundo a mais que OpenGL. Pode-se inferir que
utilizando Vulkan obteve-se em média aproximadamente 12% a mais de renderização de
quadros por segundo.
Figura 15 – Gráfico de quadros por segundos
Fonte: elaborado pelo autor.
54
3.4.3.2 Frame time
Na Figura 16, é apresentado o gráfico de comparação entre a média dos frame times
em milissegundos capturados nas cenas entre as APIs gráficas. Na primeira cena de testes, o
tempo de renderização de um frame em média foi aproximadamente 13% mais lento. Na
segunda cena, Vulkan levou, em média, aproximadamente 21% menos tempo para renderizar
um frame. Na última cena, Vulkan renderizou um frame em média aproximadamente 23%
mais rápido que OpenGL. Conclui-se que o tempo para renderização de um frame em
OpenGL foi de aproximadamente 19% mais lento que com Vulkan.
Figura 16 – Gráfico com a relação de frame time (ms) entre as APIs
Fonte: elaborado pelo autor.
3.4.3.3 Consumo de memória
A Figura 17 ilustra o gráfico de comparação entre a utilização de memória RAM
média das cenas de teste para cada API gráfica. Na primeira cena de testes Vulkan consumiu
em média aproximadamente 53% de memória RAM a menos que OpenGL. Na segunda cena,
houve pouca diferença no consumo, sendo que Vulkan consumiu somente 2% de memória
RAM a menos que OpenGL. Na última cena, a diferença no consumo de memória RAM foi
de aproximadamente 11%. Pode-se concluir que em média, quando utilizando OpenGL,
houve um consumo aproximadamente 22% maior de memória RAM.
55
Figura 17 – Gráfico com a relação de utilização de mémoria RAM entre as APIs
Fonte: elaborado pelo autor.
3.4.3.4 Utilização da CPU
Na Figura 18 pode-se observar o gráfico comparativo de utilização da CPU (em
porcentagem) entre as cenas de teste e as APIs gráficas. É possível constatar que nas 3 cenas
de testes um padrão se repetiu. A API Vulkan utilizou aproximadamente 98% a menos poder
de processamento em relação API OpenGL.
Figura 18 – Gráfico com a relação de utilização de CPU entre as APIs
Fonte: elaborado pelo autor.
3.4.3.5 Escalabilidade de FPS entre APIs
Na Figura 19 pode-se visualizar um gráfico representando a escalabilidade de FPS
entre as APIs. Foram realizados 20 testes com o mesmo tipo de objeto. A cada teste se
56
adicionou mais 50 objetos na cena, totalizando no último teste, 1000 objetos. Pode-se
averiguar que nas configurações testadas, o comportamento das APIs em relação a FPS foi
similar.
Figura 19 – Gráfico comparativo de escalabilidade de FPS entre as APIs
Fonte: elaborado pelo autor.
57
4 CONCLUSÕES
A proposta do trabalho era implementar uma biblioteca de renderização 3D utilizando
as APIs Vulkan e OpenGL e realizar uma comparação estatística de performance entre as
duas, tal objetivo foi alcançado. Originalmente foram propostos dois requisitos funcionais a
mais (implementar as técnicas de occlusion culling e multi-threaded command submission) do
que foram atendidos, porém, ficou claro durante o desenvolvimento do trabalho que devido à
complexidade da API Vulkan, tais requisitos estavam fora do escopo para realização deste
primeiro estudo exploratório da API.
Nota-se que maior ênfase foi colocada na parte de implementação utilizando a API
Vulkan, pois um dos objetivos do trabalho era realizar um estudo exploratório da mesma
devido à natureza recente do objeto de estudo. Além disso, durante o desenvolvimento do
trabalho preocupou-se em deixar as interfaces das implementações com as duas APIs o mais
similar possível, para conseguir gerar dados estatísticos válidos para comparação. Através das
cenas de teste desenvolvidas, foi possível verificar que a versão da biblioteca com Vulkan
obteve um melhor desempenho em relação a implementação com OpenGL.
Nota-se que o desenvolvimento utilizando a API Vulkan apresentou grande dificuldade
para compreensão de seus vários objetos de etapas. A natureza da API é muito explicita, e ao
contrário do OpenGL, cada estágio do pipeline da GPU precisa ser explicitamente inicializado
e a forma que o mesmo será utilizado precisa ser explicitamente definida antes da execução
do processo de renderização. Portanto além de ser necessário possuir uma boa compreensão
dos elementos que compõe a API, faz-se necessário possuir conhecimento avançado em
computação gráfica e de arquitetura de placas de vídeo modernas para utilização da API
Vulkan.
Outra dificuldade apresentada foi com as dependências circulares em C++, onde duas
unidades de compilação precisam referenciar uma a outra. Por exemplo, em um caso em que
duas classes arbitrárias possuem referência uma a outra, e que se faça necessário incluir o
header de uma classe A antes de uma classe B e o header da classe B antes de A. Tal tarefa
prova-se temporalmente impossível. Para resolver tal problema utilizou-se da técnica de
forward declaration, na qual se declara a classe necessária antes da classe na qual ela será
referenciada porém sem a sua definição, a qual será inferida pelo compilador posteriormente.
58
4.1 EXTENSÕES
Para trabalhos futuros, são sugeridas as seguintes extensões:
a) utilizar programação concorrente (multi-threaded command submission) para
criação de command buffers em Vulkan;
b) implementar outras rotinas gráficas para realização de mais testes comparativos
(Occlusion Culling, shadow mapping, deferred rendering);
c) implementar uma biblioteca com Vulkan para as plataformas Android ou iOS;
d) implementar técnicas de otimização como ordenação de cena por material para
minimização de trocas de estado da API Vulkan;
e) implementar outros cenários de testes.
59
REFERÊNCIAS
AKENINE-MÖLLER, Tomas; HAINES, Eric; HOFFMAN, Naty. Real-Time Rendering. 3.
ed. Natick: A K Peters/CRC Press, 2008. 1045 p.
ARM. Vulkan on Mobile. San Francisco: Vulkan On Mobile, 2016. Color. Disponível em:
<http://malideveloper.arm.com/downloads/Presentations/GDC 2016/Sponsored/Vulkan on
Mobile with Unreal Engine 4 Case Study.pdf>. Acesso em: 10 set. 2016.
BLESZINSKI, Cliff. History of the Unreal Engine. 2010. Disponível em:
<http://www.ign.com/articles/2010/02/23/history-of-the-unreal-engine>. Acesso em: 02 set.
2016.
EPIC GAMES: Protostar: Pushing Mobile Graphics with UE4 & Vulkan API | Feature
Highlight | Unreal Engine. 2016a. Disponível em:
<https://www.youtube.com/watch?v=lIdNoSB69PI>. Acesso em: 10 set. 2016.
______. Vulkan: How to use Vulkan in Unreal Engine 4. 2016b. Disponível em:
<https://wiki.unrealengine.com/Vulkan>. Acesso em: 03 set. 2016.
______. What Is Unreal Engine 4? 2016c. Disponível em:
<https://www.unrealengine.com/what-is-unreal-engine-4>. Acesso em: 01 set. 2016.
G-TRUC CREATION. OpenGL Mathematics, 2016. Disponível em: <http://glm.g-
truc.net/0.9.7/index.html>. Acesso em: 04 set. 2016.
KHRONOS GROUP. An Introduction to SPIR-V, 2016a. Disponível em:
<https://www.khronos.org/registry/spir-v/papers/WhitePaper.pdf>. Acesso em: 01 nov. 2016.
______. OpenGL Overview, 2016b. Disponível em: <https://www.opengl.org/about/>.
Acesso em: 06 nov. 2016.
______. The Khronos Group Inc, 2016c. Disponível em: <https://www.khronos.org/>.
Acesso em: 12 set. 2016.
______. Vulkan 1.0.25 - A Specification, 2016d. Disponível em:
<https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html>. Acesso em: 01 set.
2016.
______. Vulkan 1.0.32 - A Specification (with KHR extensions), 2016e. Disponível em:
<https://www.khronos.org/registry/vulkan/specs/1.0-
wsi_extensions/xhtml/vkspec.html#_wsi_swapchain>. Acesso em: 05 nov. 2016.
MAINUŠ, Matěj. Vulkan based render toolkit. In: EXCEL@FIT, 2016, Brno. Excel@FIT
2016. Brno: Brno University Of Technology, 2016. p. 1 - 5. Disponível em:
<http://excel.fit.vutbr.cz/submissions/2016/040/40.pdf>. Acesso em: 01 set. 2016.
SAMSUNG (Org.). SDC 2016 Session: Developing Console Games with Vulkan and
Unreal. Disponível em: <https://www.youtube.com/watch?v=NyGsMr2tcks>. Acesso em: 01
set. 2016.
SELLERS, Graham; KESSENICH, John. Vulkan Programming Guide: The Official Guide
to Learning Vulkan. Boston: Addison-wesley Professional, 2016. 480 p.
SINGH, Parminder. Learning Vulkan: Discover how to build impressive 3D graphics with
the next-generation graphics API-Vulkan. Birmingham: Packt Publishing Ltd, 2016. 466 p.
OVERVOORDE, Alexander. Vulkan Tutorial. 2016. Disponível em: <https://vulkan-
tutorial.com/>. Acesso em: 01 ago. 2017.
60
ANEXO A – Representação gráfica da arquitetura com Vulkan
Na Figura 20 é possível observar um diagrama que ilustra os principais objetos e suas
relações na API Vulkan. Pode-se ter uma visão geral da arquitetura do Vulkan e de quais
objetos interagem, e suas dependências.
Figura 20 – Representação gráfica da arquiterua com Vulkan
Fonte: Singh (2016).
Recommended