Para cargas de trabalho em que a computação da GPU é ideal, a migração de scripts do RenderScript para OpenGL ES (GLES) permite que aplicativos programados em Kotlin, Java ou que usem o NDK aproveitem o hardware da GPU. Confira abaixo uma visão geral de alto nível para ajudar você a usar sombreadores de computação do OpenGL ES 3.1 para substituir scripts do RenderScript.
.Inicialização do GLES
Em vez de criar um objeto de contexto do RenderScript, siga as etapas abaixo para criar um contexto fora da tela do GLES usando EGL:
Acesse a exibição padrão
Inicialize a EGL usando a tela padrão, especificando a versão do GLES.
Escolha uma configuração de EGL com um tipo de plataforma de
EGL_PBUFFER_BIT
.Use a tela e a configuração para criar um contexto de EGL.
Crie a plataforma fora da tela com
eglCreatePBufferSurface
. Se o contexto for usado apenas para computação, a plataforma pode ser trivialmente pequena (1 x 1).Crie a linha de execução de renderização e chame
eglMakeCurrent
nela com o contexto de exibição, plataforma e EGL para vincular o contexto de GL à linha de execução.
O app de exemplo demonstra como inicializar o contexto do Vulkan em
GLSLImageProcessor.kt
.
Para saber mais, consulte
EGLSurfaces e OpenGL ES.
Saída de depuração do GLES
A coleta de erros úteis do OpenGL usa uma extensão para ativar a geração de registros de depuração,
que define um callback de saída de depuração. O método para fazer isso pelo SDK,
glDebugMessageCallbackKHR
, nunca foi implementado e gera uma
exceção. O app de exemplo
inclui um wrapper para o callback do código do NDK.
Alocações do GLES
Uma alocação do RenderScript pode ser migrada para uma textura de armazenamento imutável ou um objeto de buffer de armazenamento do sombreador. Para imagens somente leitura, use um Objeto do sampler, que permite a filtragem (links em inglês).
Os recursos do GLES são alocados dentro dele. Para evitar a sobrecarga de cópia
de memória ao interagir com outros componentes do Android, há uma
extensão para Imagens KHR (link em inglês) que permite o compartilhamento
de matrizes 2D de dados de imagem. Essa extensão é necessária para dispositivos Android
na versão 8.0 e mais recentes. A biblioteca
graphics-core
do Android Jetpack
inclui suporte à criação dessas imagens no código gerenciado e ao mapeamento
para um HardwareBuffer
alocado:
val outputBuffers = Array(numberOfOutputImages) {
HardwareBuffer.create(
width, height, HardwareBuffer.RGBA_8888, 1,
HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
)
}
val outputEGLImages = Array(numberOfOutputImages) { i ->
androidx.opengl.EGLExt.eglCreateImageFromHardwareBuffer(
display,
outputBuffers[i]
)!!
}
Infelizmente, isso não cria a textura de armazenamento imutável necessária para
que um sombreador de computação grave diretamente no buffer. O exemplo usa
glCopyTexSubImage2D
para copiar a textura de armazenamento usada
pelo sombreador de computação para o KHR Image
. Se o driver OpenGL oferecer suporte à extensão de
armazenamento de imagens EGL (link em inglês), essa extensão
poderá ser usada para criar uma textura de armazenamento imutável compartilhada para evitar a cópia.
Conversão para sombreadores de computação do GLSL
Os scripts do RenderScript são convertidos em sombreadores de computação do GLSL.
Criar um sombreador de computação do GLSL
No OpenGL ES,os sombreadores de computação são gravados na Linguagem de sombreamento do OpenGL (GLSL).
Adaptação de scripts globais
Com base nas características dos scripts globais, é possível usar uniformes ou objetos de buffer uniformes para globais que não são modificados no sombreador:
- Buffer uniforme (link em inglês): é recomendado para scripts globais com alterações frequentes e maiores que o limite da constante de push.
Para globais que são modificados no sombreador, use uma textura de armazenamento imutável ou um objeto de buffer de armazenamento do sombreador (links em inglês).
Executar cálculos
Sombreadores de computação não fazem parte do pipeline de gráficos. Eles são de uso geral e projetados para calcular jobs com paralelização. Isso permite que você tenha mais controle sobre como eles são executados, mas também significa que você precisa entender um pouco mais sobre como o job é carregado em paralelo.
Criar e inicializar o programa de computação
Criar e inicializar o programa de computação é muito parecido com trabalhar com qualquer outro sombreador do GLES.
Crie o programa e o sombreador de computação associado a ele.
Anexe a origem, compile o sombreador e verifique os resultados da compilação.
Anexe o sombreador, vincule e use o programa.
Crie, inicialize e vincule qualquer uniforme.
Iniciar um cálculo
Os sombreadores de computação operam em um espaço abstrato 1D, 2D ou 3D em uma série de grupos de trabalho, que são definidos no código-fonte do sombreador e representam o tamanho mínimo de invocação, bem como a geometria do sombreador. O sombreador abaixo funciona em uma imagem 2D e define os grupos de trabalho em duas dimensões:
private const val WORKGROUP_SIZE_X = 8
private const val WORKGROUP_SIZE_Y = 8
private const val ROTATION_MATRIX_SHADER =
"""#version 310 es
layout (local_size_x = $WORKGROUP_SIZE_X, local_size_y = $WORKGROUP_SIZE_Y, local_size_z = 1) in;
Os grupos de trabalho podem compartilhar memória, definida por GL_MAX_COMPUTE_SHARED_MEMORY_SIZE
,
que tem pelo menos 32 KB e pode usar memoryBarrierShared()
para fornecer
acesso à memória coerente.
Definir o tamanho do grupo de trabalho
Mesmo que o espaço do problema funcione bem com tamanhos de grupo de trabalho de 1, é importante definir um tamanho adequado de grupo de trabalho para carregar em paralelo o sombreador de computação. Se o tamanho for muito pequeno, o driver da GPU não poderá carregar sua computação em paralelo o suficiente, por exemplo. O ideal é que esses tamanhos sejam ajustados pela GPU, embora padrões razoáveis funcionem bem o suficiente nos dispositivos atuais, como o tamanho do grupo de trabalho de 8x8 no snippet do sombreador.
Há uma GL_MAX_COMPUTE_WORK_GROUP_COUNT
, mas ela é substancial. Precisa ser
pelo menos 65.535 nos três eixos, de acordo com a especificação.
Enviar o sombreador
A etapa final para executar cálculos é enviar o sombreador usando uma
das funções de envio, como
glDispatchCompute
. A função de envio é responsável
por definir o número de grupos de trabalho para cada eixo:
GLES31.glDispatchCompute(
roundUp(inputImage.width, WORKGROUP_SIZE_X),
roundUp(inputImage.height, WORKGROUP_SIZE_Y),
1 // Z workgroup size. 1 == only one z level, which indicates a 2D kernel
)
Para retornar o valor, primeiro aguarde a operação de computação terminar usando uma barreira de memória:
GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)
Para encadear vários kernels,
(por exemplo, para migrar o código usando ScriptGroup
), crie e envie
vários programas e sincronize o acesso deles à saída com barreiras de
memória.
O app de exemplo demonstra duas tarefas de computação:
- Rotação HUE: uma tarefa de computação com um único sombreador de computação. Consulte
GLSLImageProcessor::rotateHue
para conferir o exemplo de código. - Desfoque: uma tarefa de computação mais complexa que executa dois sombreadores
de computação em sequência. Consulte
GLSLImageProcessor::blur
para conferir o exemplo de código.
Saiba mais sobre as barreiras de memória em Garantir a visibilidade assim como Variáveis compartilhadas ,