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
featureLevelencompatibilitycuando solicitesGPUAdapter.
// 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:
- Start a New Project: Abre Android Studio y haz clic en New Project.
Selecciona una plantilla: Elige la plantilla Empty Activity en Android Studio y haz clic en Next.
Figura 1: Cómo crear un proyecto nuevo en Android Studio 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.
Figura 2: Cómo iniciar con una actividad vacía Finalizar: Haz clic en Finalizar y espera a que Android Studio sincronice los archivos de tu proyecto.
Agrega la biblioteca de Jetpack de WebGPU
- Agrega el repositorio de
googleasettings.gradlecomo se describe en Cómo usar una biblioteca de Jetpack en tu app. - Agrega las dependencias de los artefactos que necesitas en el archivo build.gradle de tu app o módulo:
- Nota: Consulta webgpu | Jetpack | Android Developers para conocer la versión más reciente de la biblioteca.
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.
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
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
WebGpuSurfacecomponible. - 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 deSurface, 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
DeviceySurfaceal comienzo del bloque de la función lambdaonSurface. Este código se ejecuta de inmediato cuandoSurfaceestá 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 bloquefinally, que llama arenderer.cleanup()para evitar pérdidas de memoria. - Cambio de tamaño: Si cambian las dimensiones de la superficie,
AndroidExternalSurfacepuede 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
GPUBuffersexistentes a través deGPUQueue.writeBufferen 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.