Introdução ao WebGPU

Para usar o Jetpack WebGPU, seu projeto precisa atender aos seguintes requisitos mínimos:

  • Nível mínimo da API: é necessário usar a API Android 24 (Nougat) ou mais recente.
  • Hardware: dispositivos compatíveis com Vulkan 1.1 ou mais recente são preferíveis para o back-end.
  • Modo de compatibilidade e suporte ao OpenGL ES: é possível usar o WebGPU com o modo de compatibilidade ao definir a opção padronizada featureLevel como compatibility ao solicitar o GPUAdapter.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

Instalação e configuração

Pré-requisitos:

Android Studio: faça o download da versão mais recente do Android Studio no site oficial e siga as instruções do Guia de instalação do Android Studio.

Criar um novo projeto

Depois de instalar o Android Studio, siga estas etapas para configurar seu projeto WebGPU:

  1. Iniciar um novo projeto: abra o Android Studio e clique em Novo projeto.
  2. Selecionar um modelo: escolha o modelo Empty Activity no Android Studio e clique em Next.

    A caixa de diálogo "Novo projeto" do Android Studio, mostrando a lista integrada de atividades que o Studio vai criar para você.
    Figura 1.Criando um novo projeto no Android Studio
  3. Configure seu projeto:

    • Nome: dê um nome ao projeto (por exemplo, "JetpackWebGPUSample").
    • Nome do pacote: verifique se o nome do pacote corresponde ao namespace escolhido (por exemplo, com.example.webgpuapp).
    • Linguagem: selecione Kotlin.
    • Minimum SDK: selecione API 24: Android 7.0 (Nougat) ou uma versão mais recente, conforme recomendado para essa biblioteca.
    • Linguagem de configuração do build: é recomendável usar a DSL do Kotlin (build.gradle.kts) para o gerenciamento moderno de dependências.
    A caixa de diálogo "Empty Activity" do Android Studio, que contém campos para
    preencher a nova atividade vazia, como "Name", "Package Name", "Save
    Location" e "Minimum SDK".
    Figura 2.Como começar com uma atividade vazia
  4. Concluir: clique em Concluir e aguarde até que o Android Studio sincronize os arquivos do projeto.

Adicionar a biblioteca WebGPU Jetpack

A biblioteca androidx.webgpu contém os arquivos de biblioteca .so do NDK WebGPU e as interfaces de código gerenciado.

Para atualizar a versão da biblioteca, atualize o build.gradle e sincronize o projeto com os arquivos do Gradle usando o botão "Sync Project" no Android Studio.

Arquitetura de alto nível

A renderização da WebGPU em um aplicativo Android é executada em uma linha de execução de renderização dedicada para manter a capacidade de resposta da interface.

  • Camada de UI: a interface é criada com o Jetpack Compose. Uma superfície de desenho da WebGPU é integrada à hierarquia do Compose usando AndroidExternalSurface.
  • Lógica de renderização: uma classe especializada (por exemplo, O WebGpuRenderer) é responsável por gerenciar todos os objetos WebGPU e coordenar o loop de renderização.
  • A camada de shader: código de shader WGSL armazenado em constantes res ou string.
Diagrama de arquitetura de alto nível mostrando a interação entre a
    UI Thread, uma Rendering Thread dedicada e o hardware da GPU em um aplicativo
    Android WebGPU.
Figura 3: arquitetura de alto nível da WebGPU no Android

Por etapas: app de exemplo

Esta seção mostra as etapas essenciais necessárias para renderizar um triângulo colorido na tela, demonstrando o fluxo de trabalho principal do WebGPU.

A atividade principal

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

O elemento combinável da superfície externa

Crie um novo arquivo chamado WebgpuSurface.kt. Esse elemento combinável envolve o AndroidExternalSurface para fornecer uma ponte entre o Compose e seu 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()
                }
            }
        }
    }
}

Configurar o renderizador

Crie uma classe WebGpuRenderer em WebGpuRenderer.kt. Essa classe vai lidar com o trabalho pesado de comunicação com a GPU.

Primeiro, defina a estrutura da classe e as variáveis:

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

Inicialização:em seguida, implemente a função init para criar a instância WebGPU e configurar a superfície. Essa função é chamada pelo escopo AndroidExternalSurface dentro do elemento combinável de superfície externa que criamos anteriormente.

Observação:a função init usa createWebGpu, um método auxiliar (parte de androidx.webgpu.helper) para simplificar a configuração. Essa utilidade cria a instância do WebGPU, seleciona um adaptador e solicita um 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,
      )
    )
  }

A biblioteca androidx.webgpu inclui arquivos JNI e .so, que são vinculados e gerenciados automaticamente pelo sistema de build. O método auxiliar createWebGpu carrega o libwebgpu_c_bundled.so agrupado.

Configuração do pipeline

Agora que temos um dispositivo, precisamos informar à GPU como desenhar o triângulo. Para isso, criamos um "pipeline" que contém nosso código de shader (escrito em WGSL).

Adicione esta função auxiliar particular à classe WebGpuRenderer para compilar os shaders e criar o pipeline de renderização.

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

Desenhar um frame

Com o pipeline pronto, podemos implementar a função de renderização. Essa função adquire a próxima textura disponível da tela, grava comandos de desenho e os envia para a GPU.

Adicione este método à 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()
  }

Limpeza de recursos

Implemente a função de limpeza, que é chamada pelo WebGpuSurface quando a superfície é destruída.

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

Saída renderizada

Captura de tela da tela de um smartphone Android mostrando a saída de um
    aplicativo WebGPU: um triângulo vermelho sólido centralizado em um fundo azul
    escuro.
Figura 4.A saída renderizada do aplicativo de amostra da WebGPU mostrando um triângulo vermelho

Estrutura do app de exemplo

É recomendável separar a implementação de renderização da lógica da interface do usuário, como na estrutura usada pelo app de exemplo:

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt: o ponto de entrada do aplicativo. Ele define o conteúdo como o elemento combinável WebGpuSurface.
  • WebGpuSurface.kt: define o componente de UI usando [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)). Ele gerencia o escopo do ciclo de vida do Surface, inicializando o renderizador quando a superfície está pronta e limpando quando ela é destruída.
  • WebGpuRenderer.kt: encapsula toda a lógica específica da WebGPU (criação de dispositivos, configuração de pipelines). Ele é desacoplado da interface, recebendo apenas o [Surface](/reference/android/view/Surface.html) e as dimensões necessárias para renderizar.

Gerenciamento de recursos e ciclo de vida

O gerenciamento do ciclo de vida é processado pelo escopo de corrotina do Kotlin fornecido por [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)) no Jetpack Compose.

  • Criação de superfície: inicialize a configuração Device e Surface no início do bloco lambda onSurface. Esse código é executado imediatamente quando o Surface fica disponível.
  • Destruição da superfície: quando o usuário sai da navegação ou o Surface é destruído pelo sistema, o bloco lambda é cancelado. Um bloco finally é executado, chamando renderer.cleanup() para evitar vazamentos de memória.
  • Redimensionamento: se as dimensões da superfície mudarem, o AndroidExternalSurface poderá reiniciar o bloco ou processar atualizações diretamente, dependendo da configuração. Assim, o renderizador sempre grava em um buffer válido.

Depuração e validação

A WebGPU tem mecanismos projetados para validar estruturas de entrada e capturar erros de tempo de execução.

  • Logcat:os erros de validação são impressos no Logcat do Android.
  • Escopos de erro:é possível capturar erros específicos encapsulando comandos de GPU em blocos [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")
    } 
}

Dicas de desempenho

Ao programar em WebGPU, considere o seguinte para evitar gargalos de performance:

  • Evite a criação de objetos por frame: instancie pipelines (GPURenderPipeline), vincule layouts de grupos e módulos de shader uma vez durante a configuração do aplicativo para maximizar a reutilização.
  • Otimizar o uso do buffer: atualize o conteúdo dos GPUBuffers atuais usando GPUQueue.writeBuffer em vez de criar novos buffers a cada frame.
  • Minimizar mudanças de estado: agrupe chamadas de desenho que compartilham o mesmo pipeline e vincule grupos para minimizar a sobrecarga do driver e melhorar a eficiência da renderização.