Introduzione a WebGPU

Per utilizzare Jetpack WebGPU, il tuo progetto deve soddisfare i seguenti requisiti minimi:

  • Livello API minimo: è richiesto Android API 24 (Nougat) o versioni successive.
  • Hardware: per il backend sono preferibili i dispositivi che supportano Vulkan 1.1 o versioni successive.
  • Modalità di compatibilità e supporto di OpenGL ES: l'utilizzo di WebGPU con la modalità di compatibilità è possibile impostando l'opzione standardizzata featureLevel su compatibility durante la richiesta di GPUAdapter.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

Installazione e configurazione

Prerequisiti:

Android Studio: scarica l'ultima versione di Android Studio dal sito web ufficiale e segui le istruzioni riportate nella Guida all'installazione di Android Studio.

Creare un nuovo progetto

Una volta installato Android Studio, segui questi passaggi per configurare il tuo progetto WebGPU:

  1. Avvia un nuovo progetto: apri Android Studio e fai clic su Nuovo progetto.
  2. Seleziona un modello: scegli il modello Attività vuota in Android Studio e fai clic su Avanti.

    La finestra di dialogo Nuovo progetto di Android Studio, che mostra l'elenco integrato di
    attività che Studio creerà per tuo conto.
    Figura 1.Creazione di un nuovo progetto in Android Studio
  3. Configura il progetto:

    • Nome: assegna un nome al progetto (ad es. "JetpackWebGPUSample").
    • Nome pacchetto: verifica che il nome del pacchetto corrisponda allo spazio dei nomi scelto (ad es. com.example.webgpuapp).
    • Lingua: seleziona Kotlin.
    • SDK minimo: seleziona API 24: Android 7.0 (Nougat) o versioni successive, come consigliato per questa libreria.
    • Linguaggio di configurazione della build: ti consigliamo di utilizzare Kotlin DSL (build.gradle.kts) per la gestione moderna delle dipendenze.
    La finestra di dialogo Attività vuota di Android Studio che contiene i campi per
    compilare la nuova attività vuota, ad esempio Nome, Nome pacchetto, Posizione
    di salvataggio e SDK minimo.
    Figura 2.Inizia con un'attività vuota
  4. Fine: fai clic su Fine e attendi che Android Studio sincronizzi i file del progetto.

Aggiungere la libreria Jetpack WebGPU

La libreria androidx.webgpu contiene i file della libreria .so dell'NDK WebGPU e le interfacce del codice gestito.

Puoi aggiornare la versione della libreria aggiornando build.gradle e sincronizzando il progetto con i file Gradle utilizzando il pulsante "Sincronizza progetto" in Android Studio.

Architettura di alto livello

Il rendering WebGPU all'interno di un'applicazione Android viene eseguito su un thread di rendering dedicato per mantenere la reattività della UI.

  • Livello UI: la UI è creata con Jetpack Compose. Una superficie di disegno WebGPU è integrata nella gerarchia di Compose utilizzando AndroidExternalSurface.
  • Logica di rendering: una classe specializzata (ad es. WebGpuRenderer) è responsabile della gestione di tutti gli oggetti WebGPU e del coordinamento del ciclo di rendering.
  • Livello shader: codice shader WGSL archiviato in costanti res o stringa.
Diagramma dell'architettura di alto livello che mostra l'interazione tra il
    thread UI, un thread di rendering dedicato e l'hardware GPU in un'applicazione
    WebGPU per Android.
Figura 3.Architettura di alto livello di WebGPU su Android

Procedura dettagliata: app di esempio

Questa sezione illustra i passaggi essenziali necessari per visualizzare un triangolo colorato sullo schermo, dimostrando il flusso di lavoro principale di WebGPU.

L'attività principale

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            WebGpuSurface()
        }
    }
}

Il componente componibile della superficie esterna

Crea un nuovo file denominato WebgpuSurface.kt. Questo composable racchiude AndroidExternalSurface per fornire un ponte tra Compose e il tuo renderer.

@Composable
fun WebGpuSurface(modifier: Modifier = Modifier) {
    // Create and remember a WebGpuRenderer instance.
    val renderer = remember { WebGpuRenderer() }
    AndroidExternalSurface(
        modifier = modifier.fillMaxSize(),
    ) {
        // This block is called when the surface is created or resized.
        onSurface { surface, width, height ->
            // Run the rendering logic on a background thread.
            withContext(Dispatchers.Default) {
                try {
                    // Initialize the renderer with the surface
                    renderer.init(surface, width, height)
                    // Render a frame.
                    renderer.render() 
                } finally {
                    // Clean up resources when the surface is destroyed.
                    renderer.cleanup()
                }
            }
        }
    }
}

Configurare il renderer

Crea un corso WebGpuRenderer in WebGpuRenderer.kt. Questa classe gestirà il lavoro pesante di comunicazione con la GPU.

Innanzitutto, definisci la struttura della classe e le variabili:

class WebGpuRenderer() {
    private lateinit var webGpu: WebGpu
    private lateinit var renderPipeline: GPURenderPipeline
}

Inizializzazione:poi, implementa la funzione init per creare l'istanza WebGPU e configurare la superficie. Questa funzione viene chiamata dall'ambito AndroidExternalSurface all'interno del composable della superficie esterna che abbiamo creato in precedenza.

Nota:la funzione init utilizza createWebGpu, un metodo helper (parte di androidx.webgpu.helper) per semplificare la configurazione. Questa utilità crea l'istanza WebGPU, seleziona un adattatore e richiede un dispositivo.

// Inside WebGpuRenderer class
suspend fun init(surface: Surface, width: Int, height: Int) {
    // 1. Create Instance & Device
    webGpu = createWebGpu(surface)
    val device = webGpu.device

    // 2. Setup Pipeline (compile shaders)
    initPipeline(device)

    // 3. Configure the Surface
    webGpu.webgpuSurface.configure(
      GPUSurfaceConfiguration(
        device,
        width,
        height,
        TextureFormat.RGBA8Unorm,
      )
    )
  }

La libreria androidx.webgpu include file JNI e .so, che vengono collegati e gestiti automaticamente dal sistema di build. Il metodo helper createWebGpu si occupa del caricamento di libwebgpu_c_bundled.so in bundle.

Configurazione della pipeline

Ora che abbiamo un dispositivo, dobbiamo dire alla GPU come disegnare il triangolo. A questo scopo, creiamo una "pipeline" che contiene il codice dello shader (scritto in WGSL).

Aggiungi questa funzione helper privata alla classe WebGpuRenderer per compilare gli shader e creare la pipeline di rendering.

// Inside WebGpuRenderer class
private fun initPipeline(device: GPUDevice) {
    val shaderCode = """
        @vertex fn vs_main(@builtin(vertex_index) vertexIndex : u32) ->
        @builtin(position) vec4f {
            const pos = array(vec2f(0.0, 0.5), vec2f(-0.5, -0.5), vec2f(0.5, -0.5));
            return vec4f(pos[vertexIndex], 0, 1);
        }
        @fragment fn fs_main() -> @location(0) vec4f {
            return vec4f(1, 0, 0, 1);
        }
    """

    // Create Shader Module
    val shaderModule = device.createShaderModule(
      GPUShaderModuleDescriptor(shaderSourceWGSL = GPUShaderSourceWGSL(shaderCode))
    )

    // Create Render Pipeline
    renderPipeline = device.createRenderPipeline(
      GPURenderPipelineDescriptor(
        vertex = GPUVertexState(
          shaderModule,
        ), fragment = GPUFragmentState(
          shaderModule, targets = arrayOf(GPUColorTargetState(TextureFormat.RGBA8Unorm))
        ), primitive = GPUPrimitiveState(PrimitiveTopology.TriangleList)
      )
    )
  }

Disegnare una cornice

Ora che la pipeline è pronta, possiamo implementare la funzione di rendering. Questa funzione acquisisce la successiva texture disponibile dallo schermo, registra i comandi di disegno e li invia alla GPU.

Aggiungi questo metodo alla classe WebGpuRenderer:

// Inside WebGpuRenderer class
fun render() {
    if (!::webGpu.isInitialized) {
      return
    }

    val gpu = webGpu

    // 1. Get the next available texture from the screen
    val surfaceTexture = gpu.webgpuSurface.getCurrentTexture()

    // 2. Create a command encoder
    val commandEncoder = gpu.device.createCommandEncoder()

    // 3. Begin a render pass (clearing the screen to blue)
    val renderPass = commandEncoder.beginRenderPass(
      GPURenderPassDescriptor(
        colorAttachments = arrayOf(
          GPURenderPassColorAttachment(
            GPUColor(0.0, 0.0, 0.5, 1.0),
            surfaceTexture.texture.createView(),
            loadOp = LoadOp.Clear,
            storeOp = StoreOp.Store,
          )
        )
      )
    )

    // 4. Draw
    renderPass.setPipeline(renderPipeline)
    renderPass.draw(3) // Draw 3 vertices
    renderPass.end()

    // 5. Submit and Present
    gpu.device.queue.submit(arrayOf(commandEncoder.finish()))
    gpu.webgpuSurface.present()
  }

Pulizia delle risorse

Implementa la funzione di pulizia, chiamata da WebGpuSurface quando la superficie viene eliminata.

// Inside WebGpuRenderer class
fun cleanup() {
    if (::webGpu.isInitialized) {
      webGpu.close()
    }
  }

Output visualizzato

Screenshot della schermata di uno smartphone Android che mostra l'output di un'applicazione WebGPU: un triangolo rosso pieno centrato su uno sfondo blu scuro.
Figura 4. L'output sottoposto a rendering dell'applicazione WebGPU di esempio che mostra un triangolo rosso

Struttura dell'app di esempio

È buona norma separare l'implementazione del rendering dalla logica dell'interfaccia utente, come nella struttura utilizzata dall'app di esempio:

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt: il punto di accesso dell'applicazione. Imposta i contenuti su WebGpuSurface Componibile.
  • WebGpuSurface.kt: definisce il componente UI utilizzando [AndroidExternalSurface](/reference/kotlin/androidx/compose/foundation/package-summary#AndroidExternalSurface(androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.unit.IntSize,androidx.compose.foundation.AndroidExternalSurfaceZOrder,kotlin.Boolean,kotlin.Function1)). Gestisce l'ambito del ciclo di vita Surface, inizializzando il renderer quando la superficie è pronta ed eseguendo la pulizia quando viene distrutta.
  • WebGpuRenderer.kt: incapsula tutta la logica specifica di WebGPU (creazione di dispositivi, configurazione della pipeline). È disaccoppiato dalla UI e riceve solo le [Surface](/reference/android/view/Surface.html) e le dimensioni necessarie per il disegno.

Gestione del ciclo di vita e delle risorse

La gestione del ciclo di vita viene gestita dall'ambito delle coroutine Kotlin fornito da [AndroidExternalSurface](/reference/kotlin/androidx/compose/foundation/package-summary#AndroidExternalSurface(androidx.compose.ui.Modifier,kotlin.Boolean,androidx.compose.ui.unit.IntSize,androidx.compose.foundation.AndroidExternalSurfaceZOrder,kotlin.Boolean,kotlin.Function1)) all'interno di Jetpack Compose.

  • Creazione della superficie: inizializza la configurazione di Device e Surface all'inizio del blocco lambda onSurface. Questo codice viene eseguito immediatamente quando Surface diventa disponibile.
  • Distruzione della superficie: quando l'utente esce o il Surface viene distrutto dal sistema, il blocco lambda viene annullato. Viene eseguito un blocco finally, che chiama renderer.cleanup() per evitare perdite di memoria.
  • Ridimensionamento: se le dimensioni della superficie cambiano, AndroidExternalSurface potrebbe riavviare il blocco o gestire direttamente gli aggiornamenti a seconda della configurazione, in modo che il renderer scriva sempre in un buffer valido.

Debug e convalida

WebGPU dispone di meccanismi progettati per convalidare le strutture di input e rilevare gli errori di runtime.

  • Logcat:gli errori di convalida vengono stampati in Android Logcat.
  • Ambiti di errore: puoi acquisire errori specifici racchiudendo i comandi della GPU all'interno dei blocchi [device.pushErrorScope()](/reference/kotlin/androidx/webgpu/GPUDevice#pushErrorScope(kotlin.Int)) e device.popErrorScope().
device.pushErrorScope(ErrorFilter.Validation)
// ... potentially incorrect code ...
device.popErrorScope { status, type, message ->
    if (status == PopErrorScopeStatus.Success && type != ErrorType.NoError) {
        Log.e("WebGPU", "Validation Error: $message")
    } 
}

Suggerimenti per il rendimento

Quando programmi in WebGPU, considera quanto segue per evitare colli di bottiglia delle prestazioni:

  • Evita la creazione di oggetti per frame: crea istanze di pipeline (GPURenderPipeline), associa i layout dei gruppi e i moduli shader una sola volta durante la configurazione dell'applicazione per massimizzare il riutilizzo.
  • Ottimizza l'utilizzo del buffer: aggiorna i contenuti di GPUBuffers esistenti tramite GPUQueue.writeBuffer anziché creare nuovi buffer per ogni frame.
  • Ridurre al minimo le modifiche dello stato: raggruppa le chiamate di disegno che condividono la stessa pipeline e associa i gruppi per ridurre al minimo l'overhead del driver e migliorare l'efficienza del rendering.