Premiers pas avec WebGPU

Pour utiliser Jetpack WebGPU, votre projet doit répondre aux exigences minimales suivantes :

  • Niveau d'API minimal : Android API 24 (Nougat) ou version ultérieure est requis.
  • Matériel : les appareils compatibles avec Vulkan 1.1 ou version ultérieure sont préférables pour le backend.
  • Mode Compatibilité et compatibilité avec OpenGL ES : il est possible d'utiliser WebGPU avec le mode Compatibilité en définissant l'option standardisée featureLevel sur compatibility lors de la demande de GPUAdapter.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

Installation et configuration

Prérequis :

Android Studio : téléchargez la dernière version d'Android Studio sur le site Web officiel et suivez les instructions du Guide d'installation d'Android Studio.

Créer un projet

Une fois Android Studio installé, procédez comme suit pour configurer votre projet WebGPU :

  1. Démarrer un nouveau projet : ouvrez Android Studio, puis cliquez sur Nouveau projet.
  2. Sélectionnez un modèle : choisissez le modèle Empty Activity (Activité vide) dans Android Studio, puis cliquez sur Next (Suivant).

    Boîte de dialogue "Nouveau projet" d'Android Studio, affichant la liste intégrée des activités que Studio créera pour vous.
    Figure 1 : Créer un projet dans Android Studio
  3. Configurez votre projet :

    • Nom : donnez un nom à votre projet (par exemple, "JetpackWebGPUSample").
    • Package Name (Nom du package) : vérifiez que le nom du package correspond à l'espace de noms choisi (par exemple, com.example.webgpuapp).
    • Langage : sélectionnez Kotlin.
    • Minimum SDK (SDK minimal) : sélectionnez API 24 : Android 7.0 (Nougat) ou une version ultérieure, comme recommandé pour cette bibliothèque.
    • Langage de configuration de compilation : il est recommandé d'utiliser Kotlin DSL (build.gradle.kts) pour la gestion moderne des dépendances.
    Boîte de dialogue "Empty Activity" (Activité vide) d'Android Studio contenant des champs permettant de renseigner la nouvelle activité vide, tels que "Name" (Nom), "Package Name" (Nom du package), "Save Location" (Emplacement d'enregistrement) et "Minimum SDK" (SDK minimal).
    Figure 2 : commencer avec une activité vide
  4. Terminer : cliquez sur Terminer et attendez qu'Android Studio synchronise les fichiers de votre projet.

Ajouter la bibliothèque Jetpack WebGPU

La bibliothèque androidx.webgpu contient les fichiers de bibliothèque .so du NDK WebGPU ainsi que les interfaces de code géré.

Vous pouvez mettre à jour la version de la bibliothèque en modifiant votre fichier build.gradle et en synchronisant votre projet avec les fichiers Gradle à l'aide du bouton Synchroniser le projet dans Android Studio.

Architecture de haut niveau

Le rendu WebGPU dans une application Android est exécuté sur un thread de rendu dédié pour maintenir la réactivité de l'UI.

  • Couche d'interface utilisateur : l'UI est créée avec Jetpack Compose. Une surface de dessin WebGPU est intégrée à la hiérarchie Compose à l'aide de AndroidExternalSurface.
  • Logique de rendu : classe spécialisée (par exemple, WebGpuRenderer) est responsable de la gestion de tous les objets WebGPU et de la coordination de la boucle de rendu.
  • Couche Shader : code de nuanceur WGSL stocké dans des constantes res ou string.
Schéma d'architecture général montrant l'interaction entre le thread d'UI, un thread de rendu dédié et le matériel GPU dans une application Android WebGPU.
Figure 3 : Architecture de haut niveau de WebGPU sur Android

Procédure détaillée : application exemple

Cette section présente les étapes essentielles requises pour afficher un triangle coloré à l'écran, ce qui illustre le workflow WebGPU de base.

Activité principale

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

Composable de la surface externe

Créez un fichier nommé WebgpuSurface.kt. Ce composable encapsule AndroidExternalSurface pour fournir un pont entre Compose et votre moteur de rendu.

@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()
                }
            }
        }
    }
}

Configurer le moteur de rendu

Créez une classe WebGpuRenderer dans WebGpuRenderer.kt. Cette classe gérera la majeure partie de la communication avec le GPU.

Commencez par définir la structure de la classe et les variables :

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

Initialisation : implémentez ensuite la fonction d'initialisation pour créer l'instance WebGPU et configurer la surface. Cette fonction est appelée par le champ d'application AndroidExternalSurface à l'intérieur du composable de surface externe que nous avons créé précédemment.

Remarque : La fonction init utilise createWebGpu, une méthode d'assistance (qui fait partie de androidx.webgpu.helper) pour simplifier la configuration. Cet utilitaire crée l'instance WebGPU, sélectionne un adaptateur et demande un appareil.

// 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 bibliothèque androidx.webgpu inclut des fichiers JNI et .so qui sont automatiquement associés et gérés par le système de compilation. La méthode d'assistance createWebGpu se charge de charger le libwebgpu_c_bundled.so fourni.

Configuration du pipeline

Maintenant que nous avons un appareil, nous devons indiquer au GPU comment dessiner notre triangle. Pour ce faire, nous créons un "pipeline" contenant notre code de nuanceur (écrit en WGSL).

Ajoutez cette fonction d'assistance privée à votre classe WebGpuRenderer pour compiler les nuanceurs et créer le pipeline de rendu.

// 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)
      )
    )
  }

Dessiner un cadre

Maintenant que le pipeline est prêt, nous pouvons implémenter la fonction de rendu. Cette fonction acquiert la prochaine texture disponible à partir de l'écran, enregistre les commandes de dessin et les envoie au GPU.

Ajoutez cette méthode à votre 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()
  }

Nettoyage des ressources

Implémentez la fonction de nettoyage, qui est appelée par WebGpuSurface lorsque la surface est détruite.

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

Résultat affiché

Capture d'écran de l'écran d'un téléphone Android affichant la sortie d'une application WebGPU : un triangle rouge plein centré sur un fond bleu foncé.
Figure 4 : Sortie rendue de l'exemple d'application WebGPU affichant un triangle rouge

Exemple de structure d'application

Il est recommandé de dissocier l'implémentation du rendu de la logique de l'UI, comme dans la structure utilisée par l'application exemple :

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt : point d'entrée de l'application. Il définit le contenu sur le composable WebGpuSurface.
  • WebGpuSurface.kt : définit le composant d'UI à l'aide de [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)). Il gère le champ d'application du cycle de vie Surface, en initialisant le moteur de rendu lorsque la surface est prête et en effectuant le nettoyage lorsqu'elle est détruite.
  • WebGpuRenderer.kt : encapsule toute la logique spécifique à WebGPU (création d'appareil, configuration de pipeline). Il est dissocié de l'UI et ne reçoit que les [Surface](/reference/android/view/Surface.html) et les dimensions dont il a besoin pour dessiner.

Gestion du cycle de vie et des ressources

La gestion du cycle de vie est gérée par le champ d'application de coroutine Kotlin fourni par [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)) dans Jetpack Compose.

  • Création de la surface : initialisez la configuration Device et Surface au début du bloc lambda onSurface. Ce code s'exécute immédiatement lorsque Surface devient disponible.
  • Destruction de la surface : lorsque l'utilisateur quitte l'écran ou que le système détruit le Surface, le bloc lambda est annulé. Un bloc finally est exécuté, appelant renderer.cleanup() pour éviter les fuites de mémoire.
  • Redimensionnement : si les dimensions de la surface changent, AndroidExternalSurface peut redémarrer le bloc ou gérer directement les mises à jour en fonction de la configuration. Le moteur de rendu écrit donc toujours dans un tampon valide.

Débogage et validation

WebGPU dispose de mécanismes conçus pour valider les structures d'entrée et capturer les erreurs d'exécution.

  • Logcat : les erreurs de validation sont affichées dans le Logcat Android.
  • Portées d'erreur : vous pouvez capturer des erreurs spécifiques en encapsulant les commandes GPU dans des blocs [device.pushErrorScope()](/reference/kotlin/androidx/webgpu/GPUDevice#pushErrorScope(kotlin.Int)) et 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")
    } 
}

Conseils pour l'optimisation des performances

Lorsque vous programmez dans WebGPU, tenez compte des points suivants pour éviter les goulots d'étranglement des performances :

  • Évitez la création d'objets par frame : instanciez les pipelines (GPURenderPipeline), liez les mises en page de groupe et les modules de nuanceur une seule fois lors de la configuration de l'application pour maximiser la réutilisation.
  • Optimiser l'utilisation des tampons : mettez à jour le contenu des GPUBuffers existants via GPUQueue.writeBuffer au lieu de créer de nouveaux tampons à chaque frame.
  • Minimisez les changements d'état : regroupez les appels de dessin qui partagent le même pipeline et liez les groupes pour minimiser la surcharge du pilote et améliorer l'efficacité du rendu.