Migrar scripts para o OpenGL ES 3.1

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:

  1. Acesse a exibição padrão

  2. Inicialize a EGL usando a tela padrão, especificando a versão do GLES.

  3. Escolha uma configuração de EGL com um tipo de plataforma de EGL_PBUFFER_BIT.

  4. Use a tela e a configuração para criar um contexto de EGL.

  5. 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).

  6. 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 (link em inglês) 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 programados na OpenGL Shading Language (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.

  1. Crie o programa e o sombreador de computação associado a ele.

  2. Anexe a origem, compile o sombreador e verifique os resultados da compilação.

  3. Anexe o sombreador, vincule e use o programa.

  4. 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 (link em inglês) 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.

Para saber mais sobre as barreiras de memória, consulte Como garantir a visibilidade e Variáveis compartilhadas.