Wprowadzenie do WebGPU

Aby korzystać z Jetpack WebGPU, Twój projekt musi spełniać te minimalne wymagania:

  • Minimalny poziom interfejsu API: wymagany jest Android API 24 (Nougat) lub nowszy.
  • Sprzęt: preferowane są urządzenia obsługujące interfejs Vulkan w wersji 1.1 lub nowszej.
  • Tryb zgodności i obsługa OpenGL ES: korzystanie z WebGPU w trybie zgodności jest możliwe po ustawieniu standardowej opcji featureLevel na compatibility podczas wysyłania żądania GPUAdapter.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

Instalacja i konfiguracja

Wymagania wstępne:

Android Studio: pobierz najnowszą wersję Androida Studio z oficjalnej strony i postępuj zgodnie z instrukcjami podanymi w przewodniku instalacji Androida Studio.

Tworzenie nowego projektu

Po zainstalowaniu Androida Studio wykonaj te czynności, aby skonfigurować projekt WebGPU:

  1. Rozpocznij nowy projekt: otwórz Android Studio i kliknij Nowy projekt.
  2. Wybierz szablon: w Android Studio wybierz szablon Empty Activity i kliknij Next (Dalej).

    Okno dialogowe Nowy projekt w Android Studio z wbudowaną listą aktywności, które Studio utworzy w Twoim imieniu.
    Rysunek 1. Tworzenie nowego projektu w Android Studio
  3. Skonfiguruj projekt:

    • Nazwa: nadaj projektowi nazwę (np. „JetpackWebGPUSample”).
    • Nazwa pakietu: sprawdź, czy nazwa pakietu jest zgodna z wybraną przestrzenią nazw (np. com.example.webgpuapp).
    • Język: wybierz Kotlin.
    • Minimalny pakiet SDK: wybierz API 24: Android 7.0 (Nougat) lub nowszy, zgodnie z zaleceniami dotyczącymi tej biblioteki.
    • Język konfiguracji kompilacji: do nowoczesnego zarządzania zależnościami zalecamy używanie Kotlin DSL (build.gradle.kts).
    Okno pustej aktywności w Android Studio, które zawiera pola do wypełnienia nowej pustej aktywności, takie jak Nazwa, Nazwa pakietu, Lokalizacja zapisu i Minimalny pakiet SDK.
    Rysunek 2. Rozpoczynanie od pustej aktywności
  4. Zakończ: kliknij Zakończ i poczekaj, aż Android Studio zsynchronizuje pliki projektu.

Dodawanie biblioteki Jetpack WebGPU

Biblioteka androidx.webgpu zawiera pliki biblioteki WebGPU NDK .so oraz interfejsy kodu zarządzanego.

Wersję biblioteki możesz zaktualizować, modyfikując plik build.gradle i synchronizując projekt z plikami Gradle za pomocą przycisku „Synchronizuj projekt” w Android Studio.

Architektura wysokiego poziomu

Renderowanie WebGPU w aplikacji na Androida odbywa się w dedykowanym wątku renderowania, aby zachować responsywność interfejsu.

  • Warstwa interfejsu: interfejs jest zbudowany za pomocą Jetpack Compose. Powierzchnia rysowania WebGPU jest zintegrowana z hierarchią Compose za pomocą funkcji AndroidExternalSurface.
  • Logika renderowania: wyspecjalizowana klasa (np. WebGpuRenderer) odpowiada za zarządzanie wszystkimi obiektami WebGPU i koordynowanie pętli renderowania.
  • Warstwa shadera: kod shadera WGSL przechowywany w stałych wartościach res lub string.
Diagram architektury wysokiego poziomu przedstawiający interakcję między wątkiem interfejsu, dedykowanym wątkiem renderowania i sprzętem GPU w aplikacji na Androida WebGPU.
Rysunek 3. Architektura wysokiego poziomu WebGPU na Androidzie

Krok po kroku: przykładowa aplikacja

W tej sekcji znajdziesz podstawowe kroki wymagane do wyrenderowania kolorowego trójkąta na ekranie, co pokazuje podstawowy przepływ pracy WebGPU.

Główne działanie

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

Kompozycja powierzchni zewnętrznej

Utwórz nowy plik o nazwie WebgpuSurface.kt. Ten komponent kompozycyjny otacza AndroidExternalSurface, aby zapewnić połączenie między Compose a renderem.

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

Konfigurowanie renderera

Utwórz zajęcia WebGpuRenderer w WebGpuRenderer.kt. Ta klasa będzie odpowiadać za komunikację z procesorem graficznym.

Najpierw zdefiniuj strukturę klasy i zmienne:

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

Inicjowanie: następnie zaimplementuj funkcję init, aby utworzyć instancję WebGPU i skonfigurować powierzchnię. Ta funkcja jest wywoływana przez zakres AndroidExternalSurface w kompozycyjnym elemencie zewnętrznym, który został utworzony wcześniej.

Uwaga: funkcja init używa createWebGpu, metody pomocniczej (części androidx.webgpu.helper), aby uprościć konfigurację. To narzędzie tworzy instancję WebGPU, wybiera adapter i wysyła żądanie urządzenia.

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

androidx.webgpu Biblioteka zawiera pliki JNI i .so, które są automatycznie łączone i zarządzane przez system kompilacji. Metoda pomocnicza createWebGpu odpowiada za wczytywanie pakietu libwebgpu_c_bundled.so.

Konfiguracja potoku

Skoro mamy już urządzenie, musimy poinformować procesor graficzny, jak narysować trójkąt. W tym celu tworzymy „potok” zawierający kod shadera (napisany w języku WGSL).

Dodaj tę prywatną funkcję pomocniczą do klasy WebGpuRenderer, aby skompilować shadery i utworzyć potok renderowania.

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

Rysowanie ramki

Gdy potok będzie gotowy, możemy wdrożyć funkcję renderowania. Ta funkcja pobiera następną dostępną teksturę z ekranu, rejestruje polecenia rysowania i przesyła je do procesora graficznego.

Dodaj tę metodę do klasy 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()
  }

Czyszczenie zasobów

Zaimplementuj funkcję czyszczenia, która jest wywoływana przez WebGpuSurface, gdy powierzchnia zostanie zniszczona.

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

Wyrenderowane dane wyjściowe

Zrzut ekranu telefonu z Androidem przedstawiający wynik działania aplikacji WebGPU: czerwony trójkąt na środku ciemnoniebieskiego tła.
Rysunek 4. Wyrenderowany wynik przykładowej aplikacji WebGPU przedstawiający czerwony trójkąt

Struktura przykładowej aplikacji

Warto oddzielić implementację renderowania od logiki interfejsu, tak jak w strukturze używanej przez przykładową aplikację:

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt: punkt wejścia aplikacji. Ustawia treść na WebGpuSurface Composable.
  • WebGpuSurface.kt: definiuje komponent interfejsu za pomocą [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)). Zarządza Surfacezakresem cyklu życia, inicjując moduł renderujący, gdy powierzchnia jest gotowa, i czyszcząc go, gdy zostanie zniszczony.
  • WebGpuRenderer.kt: zawiera całą logikę związaną z WebGPU (tworzenie urządzenia, konfiguracja potoku). Jest on odłączony od interfejsu i otrzymuje tylko [Surface](/reference/android/view/Surface.html) i wymiary potrzebne do rysowania.

Zarządzanie cyklem życia i zasobami

Zarządzanie cyklem życia jest obsługiwane przez zakres Kotlin Coroutine udostępniany przez [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)) w Jetpack Compose.

  • Tworzenie powierzchni: zainicjuj konfigurację DeviceSurface na początku bloku lambda onSurface. Ten kod zostanie uruchomiony natychmiast po udostępnieniu Surface.
  • Zniszczenie powierzchni: gdy użytkownik przejdzie na inną stronę lub Surface zostanie zniszczony przez system, blok lambda zostanie anulowany. Wykonuje się blok finally, wywołując renderer.cleanup(), aby zapobiec wyciekom pamięci.
  • Zmiana rozmiaru: jeśli zmienią się wymiary powierzchni, AndroidExternalSurface może ponownie uruchomić blok lub bezpośrednio obsłużyć aktualizacje w zależności od konfiguracji, dzięki czemu moduł renderujący zawsze zapisuje dane w prawidłowym buforze.

Debugowanie i weryfikacja

WebGPU ma mechanizmy zaprojektowane do weryfikowania struktur wejściowych i wykrywania błędów w czasie działania.

  • Logcat: błędy weryfikacji są drukowane w Android Logcat.
  • Zakresy błędów: możesz przechwytywać konkretne błędy, umieszczając polecenia GPU w blokach [device.pushErrorScope()](/reference/kotlin/androidx/webgpu/GPUDevice#pushErrorScope(kotlin.Int))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")
    } 
}

Wskazówki dotyczące skuteczności

Podczas programowania w WebGPU pamiętaj o tych kwestiach, aby uniknąć wąskich gardeł wydajności:

  • Unikaj tworzenia obiektów w każdej klatce: instancje potoków (GPURenderPipeline), układy grup wiązań i moduły cieniowania twórz tylko raz podczas konfiguracji aplikacji, aby zmaksymalizować ich ponowne wykorzystanie.
  • Optymalizacja wykorzystania bufora: aktualizuj zawartość istniejących buforów za pomocą funkcji GPUQueue.writeBuffer zamiast tworzyć nowe bufory w każdej klatce.GPUBuffers
  • Minimalizuj zmiany stanu: grupuj wywołania rysowania, które korzystają z tego samego potoku i grup wiązań, aby zminimalizować obciążenie sterownika i zwiększyć wydajność renderowania.