Comienza a usar WebGPU

Para usar Jetpack WebGPU, tu proyecto debe cumplir con los siguientes requisitos mínimos:

  • Minimum API Level: Se requiere la API de Android 24 (Nougat) o una versión posterior.
  • Hardware: Se prefieren los dispositivos que admiten Vulkan 1.1 o versiones posteriores para el backend.
  • Modo de compatibilidad y compatibilidad con OpenGL ES: Para usar WebGPU con el modo de compatibilidad, establece la opción estandarizada featureLevel en compatibility cuando solicites GPUAdapter.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

Instalación y configuración

Requisitos previos:

Android Studio: Descarga la versión más reciente de Android Studio desde el sitio web oficial y sigue las instrucciones que se indican en la Guía de instalación de Android Studio.

Crea un proyecto nuevo

Una vez que se instale Android Studio, sigue estos pasos para configurar tu proyecto de WebGPU:

  1. Start a New Project: Abre Android Studio y haz clic en New Project.
  2. Selecciona una plantilla: Elige la plantilla Empty Activity en Android Studio y haz clic en Next.

    El diálogo New Project de Android Studio, que muestra la lista integrada de actividades que Studio creará en tu nombre.
    Figura 1: Cómo crear un proyecto nuevo en Android Studio
  3. Configura tu proyecto:

    • Nombre: Asigna un nombre a tu proyecto (p.ej., "JetpackWebGPUSample").
    • Package Name: Verifica que el nombre del paquete coincida con el espacio de nombres que elegiste (p.ej., com.example.webgpuapp).
    • Language: Selecciona Kotlin.
    • SDK mínimo: Selecciona API 24: Android 7.0 (Nougat) o una versión posterior, como se recomienda para esta biblioteca.
    • Lenguaje de configuración de compilación: Se recomienda usar DSL de Kotlin (build.gradle.kts) para la administración de dependencias moderna.
    El diálogo Empty Activity de Android Studio que contiene campos para completar la nueva actividad vacía, como Nombre, Nombre del paquete, Ubicación de almacenamiento y SDK mínimo.
    Figura 2: Cómo iniciar con una actividad vacía
  4. Finalizar: Haz clic en Finalizar y espera a que Android Studio sincronice los archivos de tu proyecto.

Agrega la biblioteca de Jetpack de WebGPU

La biblioteca androidx.webgpu contiene los archivos de biblioteca .so del NDK de WebGPU, así como las interfaces de código administrado.

Puedes actualizar la versión de la biblioteca actualizando tu build.gradle y sincronizando tu proyecto con los archivos de Gradle usando el botón "Sync Project" en Android Studio.

Arquitectura de alto nivel

La renderización de WebGPU dentro de una aplicación para Android se ejecuta en un subproceso de renderización dedicado para mantener la capacidad de respuesta de la IU.

  • Capa de IU: La IU se compila con Jetpack Compose. Se integra una superficie de dibujo de WebGPU en la jerarquía de Compose con AndroidExternalSurface.
  • Lógica de renderización: Es una clase especializada (p.ej., WebGpuRenderer) es responsable de administrar todos los objetos de WebGPU y coordinar el bucle de renderización.
  • La capa de sombreador: Código de sombreador de WGSL almacenado en constantes de cadena o res.
Diagrama de arquitectura de alto nivel que muestra la interacción entre el subproceso de IU, un subproceso de renderización dedicado y el hardware de la GPU en una aplicación para Android de WebGPU.
Figura 3.Arquitectura de alto nivel de WebGPU en Android

Paso a paso: app de ejemplo

En esta sección, se explican los pasos esenciales necesarios para renderizar un triángulo de color en la pantalla, lo que demuestra el flujo de trabajo principal de WebGPU.

La actividad principal

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

El elemento componible de superficie externa

Crea un archivo nuevo llamado WebgpuSurface.kt. Este elemento componible encapsula AndroidExternalSurface para proporcionar un puente entre Compose y tu renderizador.

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

Configura el renderizador

Crea una clase WebGpuRenderer en WebGpuRenderer.kt. Esta clase se encargará de la mayor parte del trabajo de comunicación con la GPU.

Primero, define la estructura de la clase y las variables:

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

Inicialización: A continuación, implementa la función init para crear la instancia de WebGPU y configurar la superficie. El alcance de AndroidExternalSurface llama a esta función dentro del elemento componible de la superficie externa que creamos anteriormente.

Nota: La función init usa createWebGpu, un método auxiliar (parte de androidx.webgpu.helper) para optimizar la configuración. Esta utilidad crea la instancia de WebGPU, selecciona un adaptador y solicita 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 biblioteca androidx.webgpu incluye archivos JNI y .so que el sistema de compilación vincula y administra automáticamente. El método auxiliar createWebGpu se encarga de cargar el libwebgpu_c_bundled.so incluido.

Configuración de la canalización

Ahora que tenemos un dispositivo, debemos indicarle a la GPU cómo dibujar nuestro triángulo. Para ello, creamos una "canalización" que contiene nuestro código de sombreador (escrito en WGSL).

Agrega esta función auxiliar privada a tu clase WebGpuRenderer para compilar los sombreadores y crear la canalización de renderización.

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

Cómo dibujar un marco

Con la canalización lista, ahora podemos implementar la función de renderización. Esta función adquiere la siguiente textura disponible de la pantalla, registra los comandos de dibujo y los envía a la GPU.

Agrega este método a tu clase 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()
  }

Limpieza de recursos

Implementa la función de limpieza, a la que llama WebGpuSurface cuando se destruye la superficie.

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

Resultado renderizado

Captura de pantalla de la pantalla de un teléfono Android en la que se muestra el resultado de una aplicación de WebGPU: un triángulo rojo sólido centrado sobre un fondo azul oscuro.
Figura 4.El resultado renderizado de la aplicación de muestra de WebGPU que muestra un triángulo rojo

Estructura de la app de ejemplo

Es una buena práctica desacoplar la implementación de la renderización de la lógica de la IU, como en la estructura que usa la app de ejemplo:

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt: Es el punto de entrada de la aplicación. Establece el contenido en el elemento WebGpuSurface componible.
  • WebGpuSurface.kt: Define el componente de la IU con [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)). Administra el alcance del ciclo de vida de Surface, inicializa el renderizador cuando la superficie está lista y realiza la limpieza cuando se destruye.
  • WebGpuRenderer.kt: Encapsula toda la lógica específica de WebGPU (creación de dispositivos, configuración de canalizaciones). Está desacoplado de la IU y solo recibe el [Surface](/reference/android/view/Surface.html) y las dimensiones que necesita para dibujar.

Administración de recursos y ciclo de vida

El alcance de la corrutina de Kotlin que proporciona [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)) dentro de Jetpack Compose controla la administración del ciclo de vida.

  • Creación de superficies: Inicializa la configuración de Device y Surface al comienzo del bloque de la función lambda onSurface. Este código se ejecuta de inmediato cuando Surface está disponible.
  • Destrucción de la superficie: Cuando el usuario abandona la navegación o el sistema destruye el Surface, se cancela el bloque de Lambda. Se ejecuta un bloque finally, que llama a renderer.cleanup() para evitar pérdidas de memoria.
  • Cambio de tamaño: Si cambian las dimensiones de la superficie, AndroidExternalSurface puede reiniciar el bloque o controlar directamente las actualizaciones según la configuración, de modo que el renderizador siempre escriba en un búfer válido.

Depuración y validación

WebGPU tiene mecanismos diseñados para validar estructuras de entrada y capturar errores de tiempo de ejecución.

  • Logcat: Los errores de validación se imprimen en Logcat de Android.
  • Alcances de errores: Puedes capturar errores específicos encapsulando comandos de GPU dentro de bloques [device.pushErrorScope()](/reference/kotlin/androidx/webgpu/GPUDevice#pushErrorScope(kotlin.Int)) y 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")
    } 
}

Sugerencias sobre el rendimiento

Cuando programes en WebGPU, ten en cuenta lo siguiente para evitar cuellos de botella en el rendimiento:

  • Evita la creación de objetos por fotograma: Crea instancias de canalizaciones (GPURenderPipeline), vincula diseños de grupos y módulos de sombreadores una vez durante la configuración de la aplicación para maximizar la reutilización.
  • Optimiza el uso del búfer: Actualiza el contenido de los objetos GPUBuffers existentes a través de GPUQueue.writeBuffer en lugar de crear búferes nuevos en cada fotograma.
  • Minimiza los cambios de estado: Agrupa las llamadas de dibujo que comparten la misma canalización y los mismos grupos de vinculación para minimizar la sobrecarga del controlador y mejorar la eficiencia de la renderización.