Esegui la migrazione degli script a OpenGL ES 3.1

Per i carichi di lavoro in cui l'elaborazione GPU è ideale, la migrazione degli script RenderScript a OpenID ES (GLES) consente alle applicazioni scritte in Kotlin, Java o che utilizzano NDK di sfruttare l'hardware GPU. Segue una panoramica generale per aiutarti a utilizzare gli mesh di computing OpenGL ES 3.1 per sostituire gli script RenderScript.

Inizializzazione GLES

Anziché creare un oggetto di contesto RenderScript, segui questi passaggi per creare un contesto GLES fuori schermo utilizzando EGL:

  1. Ottieni il display predefinito

  2. Inizializza EGL utilizzando la visualizzazione predefinita, specificando la versione GLES.

  3. Scegli una configurazione EGL con un tipo di piattaforma EGL_PBUFFER_BIT.

  4. Utilizza gli elementi di visualizzazione e configurazione per creare un contesto EGL.

  5. Crea la superficie fuori schermo con eglCreatePBufferSurface. Se il contesto verrà utilizzato solo per il calcolo, la superficie può essere di dimensioni banali (1 x 1).

  6. Crea il thread di rendering e chiama eglMakeCurrent nel thread di rendering con il contesto di visualizzazione, superficie e EGL per associare il contesto GL al thread.

L'app di esempio mostra come inizializzare il contesto GLES in GLSLImageProcessor.kt. Per scoprire di più, consulta EGLSurfaces e OpenGL ES.

Output debug GLES

Per ricevere errori utili da OpenGL, utilizza un'estensione per attivare il logging del debug che imposta un callback dell'output di debug. Il metodo per eseguire questa operazione dall'SDK, glDebugMessageCallbackKHR, non è mai stato implementato e genera un'eccezione. L'app di esempio include un wrapper per il callback dal codice NDK.

Assegnazioni GLES

È possibile eseguire la migrazione di un'allocazione di RenderScript a una texture di archiviazione non modificabile o a un oggetto Shader Storage buffer. Per le immagini di sola lettura, puoi utilizzare un oggetto Sampler, che consente l'applicazione di filtri.

Le risorse GLES sono allocate all'interno di GLES. Per evitare l'overhead di copia durante l'interazione con altri componenti Android, è disponibile un'estensione per KHR Images che consente la condivisione di array 2D di dati immagine. Questa estensione è necessaria per i dispositivi Android a partire da Android 8.0. La libreria graphics-core Android Jetpack include il supporto per la creazione di queste immagini all'interno di codice gestito e la mappatura alle HardwareBuffer allocati:

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]
    )!!
}

Sfortunatamente, ciò non crea la texture di archiviazione immutabile necessaria per uno scheduler di computing per scrivere direttamente nel buffer. L'esempio utilizza glCopyTexSubImage2D per copiare la texture di archiviazione utilizzata da Compute Shaper in KHR Image. Se il driver OpenGL supporta l'estensione EGL Image Storage, questa estensione può essere utilizzata per creare una texture di archiviazione immutabile condivisa per evitare la copia.

Conversione in mesh di computing GLSL

Gli script RenderScript vengono convertiti in mesh di computing GLSL.

Scrivi uno Shader di computing GLSL

In OpenGL ES,gli handle di calcolo sono scritti in GLSL (OpenGL Shading Language).

Adattamento degli script globali

A seconda delle caratteristiche degli script globali, puoi utilizzare uniformi o oggetti buffer uniformi per elementi globali che non vengono modificati all'interno dello strumento:

  • Buffer uniforme: consigliato per elementi globali di script modificati di frequente di dimensioni superiori al limite della costante push.

Per le globali che vengono modificate all'interno dello mesh, puoi utilizzare una texture di archiviazione immutabile o un oggetto Shader Storage buffer.

Esegui calcoli

Gli Shader di calcolo non fanno parte della pipeline grafica, ma sono generici e progettati per calcolare job a elevata parallelizzazione. Ciò ti consente di avere un maggiore controllo sul modo in cui vengono eseguiti, ma significa anche che devi comprendere un po' di più su come il job viene parallelizzato.

Crea e inizializza il programma di computing

La creazione e l'inizializzazione del programma di computing hanno molto in comune con qualsiasi altro meshr GLES.

  1. Crea il programma e il prober di computing associato.

  2. Collega il codice sorgente dello mesh, compila lo strumento (e controlla i risultati della compilazione).

  3. Allega lo Shader, collega il programma e utilizzalo.

  4. Crea, inizializza e associa eventuali uniformi.

Avvia un calcolo

Gli handle di calcolo operano all'interno di uno spazio astratto 1D, 2D o 3D su una serie di gruppi di lavoro definiti all'interno del codice sorgente dello mesh e che rappresentano le dimensioni minime delle chiamate e la geometria dello Shader. Il seguente meshr funziona su un'immagine 2D e definisce i gruppi di lavoro in due dimensioni:

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;

I gruppi di lavoro possono condividere la memoria, definita da GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, che ha una dimensione di almeno 32 kB e può utilizzare memoryBarrierShared() per fornire un accesso coerente alla memoria.

Definisci le dimensioni del gruppo di lavoro

Anche se lo spazio dei problemi funziona bene con gruppi di lavoro di dimensioni pari a 1, è importante impostare una dimensione appropriata per il gruppo di lavoro per caricare in contemporanea lo Shader di computing. Ad esempio, se le dimensioni sono troppo ridotte, il driver GPU potrebbe non caricare in contemporanea il calcolo in modo sufficiente. Idealmente, queste dimensioni dovrebbero essere ottimizzate per ogni GPU, anche se i valori predefiniti ragionevoli funzionano abbastanza bene sui dispositivi attuali, ad esempio una dimensione del gruppo di lavoro di 8 x 8 nello snippet dello mesh.

Il valore GL_MAX_COMPUTE_WORK_GROUP_COUNT è elevato; in base alle specifiche, deve essere almeno 65535 in tutti e tre gli assi.

Invia lo ombreggiatore

Il passaggio finale nell'esecuzione dei calcoli consiste nell'inviare lo meshr utilizzando una delle funzioni di invio, ad esempio glDispatchCompute. La funzione di invio è responsabile di impostare il numero di gruppi di lavoro per ogni asse:

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
)

Per restituire il valore, attendi prima il completamento dell'operazione di calcolo utilizzando una barriera di memoria:

GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

Per concatenare più kernel (ad esempio per la migrazione del codice utilizzando ScriptGroup), crea ed esegui più programmi e sincronizza il loro accesso all'output mediante barriere di memoria.

L'app di esempio mostra due attività di computing:

  • Rotazione HUE: un'attività di calcolo con un singolo mesh di computing. Vedi GLSLImageProcessor::rotateHue per l'esempio di codice.
  • Sfocatura: un'attività di calcolo più complessa che esegue in sequenza due Shader di calcolo. Visita la pagina GLSLImageProcessor::blur per l'esempio di codice.

Per scoprire di più sulle barriere di memoria, consulta Assicurare la visibilità e Variabili condivise.