Erste Schritte mit WebGPU

Damit Sie Jetpack WebGPU verwenden können, muss Ihr Projekt die folgenden Mindestanforderungen erfüllen:

  • Mindest-API-Level: Android-API 24 (Nougat) oder höher ist erforderlich.
  • Hardware: Geräte, die Vulkan 1.1 oder höher unterstützen, werden für das Backend bevorzugt.
  • Kompatibilitätsmodus und OpenGL ES-Unterstützung: Die Verwendung von WebGPU mit dem Kompatibilitätsmodus ist möglich, indem Sie die standardisierte Option featureLevel auf compatibility setzen, wenn Sie GPUAdapter anfordern.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

Installation und Einrichtung

Voraussetzungen:

Android Studio: Laden Sie die aktuelle Version von Android Studio von der offiziellen Website herunter und folgen Sie der Installationsanleitung für Android Studio.

Ein neues Projekt erstellen

Nachdem Sie Android Studio installiert haben, führen Sie die folgenden Schritte aus, um Ihr WebGPU-Projekt einzurichten:

  1. Neues Projekt starten: Öffnen Sie Android Studio und klicken Sie auf Neues Projekt.
  2. Vorlage auswählen: Wählen Sie in Android Studio die Vorlage Empty Activity aus und klicken Sie auf Next.

    Das Dialogfeld „Neues Projekt“ in Android Studio mit der integrierten Liste der Aktivitäten, die Studio für Sie erstellt.
    Abbildung 1.Neues Projekt in Android Studio erstellen
  3. Projekt konfigurieren:

    • Name: Geben Sie Ihrem Projekt einen Namen, z.B. „JetpackWebGPUSample“).
    • Paketname: Prüfen Sie, ob der Paketname mit dem ausgewählten Namespace übereinstimmt (z.B. com.example.webgpuapp).
    • Sprache: Wählen Sie Kotlin aus.
    • Minimum SDK: Wählen Sie API 24: Android 7.0 (Nougat) oder höher aus, wie für diese Bibliothek empfohlen.
    • Build Configuration Language (Sprache für die Build-Konfiguration): Für die moderne Abhängigkeitsverwaltung wird Kotlin DSL (build.gradle.kts) empfohlen.
    Das Dialogfeld „Leere Aktivität“ in Android Studio mit Feldern zum Ausfüllen der neuen leeren Aktivität, z. B. „Name“, „Paketname“, „Speicherort“ und „Mindest-SDK“.
    Abbildung 2.Mit einer leeren Aktivität beginnen
  4. Fertigstellen: Klicken Sie auf Fertigstellen und warten Sie, bis Android Studio Ihre Projektdateien synchronisiert hat.

WebGPU-Jetpack-Bibliothek hinzufügen

Die Bibliothek androidx.webgpu enthält die .so-Bibliotheksdateien des WebGPU NDK sowie die verwalteten Code-Schnittstellen.

Sie können die Bibliotheksversion aktualisieren, indem Sie Ihre build.gradle-Datei aktualisieren und Ihr Projekt mit Gradle-Dateien synchronisieren. Verwenden Sie dazu die Schaltfläche Projekt synchronisieren in Android Studio.

Gesamtarchitektur

Das WebGPU-Rendering in einer Android-Anwendung wird in einem dedizierten Rendering-Thread ausgeführt, um die Reaktionsfähigkeit der Benutzeroberfläche zu gewährleisten.

  • UI-Ebene: Die Benutzeroberfläche wird mit Jetpack Compose erstellt. Eine WebGPU-Zeichenoberfläche wird mit AndroidExternalSurface in die Compose-Hierarchie eingebunden.
  • Rendering-Logik: Eine spezielle Klasse (z.B. WebGpuRenderer) ist für die Verwaltung aller WebGPU-Objekte und die Koordination des Rendering-Loops verantwortlich.
  • Shader-Ebene: WGSL-Shader-Code, der in Ressourcen- oder String-Konstanten gespeichert ist.
Allgemeines Architekturdiagramm, das die Interaktion zwischen dem UI-Thread, einem dedizierten Rendering-Thread und der GPU-Hardware in einer WebGPU-Android-Anwendung zeigt.
Abbildung 3: WebGPU auf Android – Architektur auf hoher Ebene

Schritt-für-Schritt-Anleitung: Beispiel-App

In diesem Abschnitt werden die wichtigsten Schritte beschrieben, die zum Rendern eines farbigen Dreiecks auf dem Bildschirm erforderlich sind. So wird der WebGPU-Kernworkflow veranschaulicht.

Die Hauptaktivität

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

Composable für die äußere Oberfläche

Erstellen Sie eine neue Datei mit dem Namen „WebgpuSurface.kt“. Dieses Composable umschließt das AndroidExternalSurface, um eine Brücke zwischen Compose und Ihrem Renderer zu schaffen.

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

Renderer einrichten

Erstellen Sie einen WebGpuRenderer-Kurs in WebGpuRenderer.kt. Diese Klasse übernimmt die Kommunikation mit der GPU.

Definieren Sie zuerst die Klassenstruktur und die Variablen:

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

Initialisierung:Implementieren Sie als Nächstes die init-Funktion, um die WebGPU-Instanz zu erstellen und die Oberfläche zu konfigurieren. Diese Funktion wird vom AndroidExternalSurface-Bereich innerhalb des zusammensetzbaren Elements für die externe Oberfläche aufgerufen, das wir zuvor erstellt haben.

Hinweis:Die init-Funktion verwendet createWebGpu, eine Hilfsmethode (Teil von androidx.webgpu.helper), um die Einrichtung zu vereinfachen. Mit diesem Dienstprogramm wird die WebGPU-Instanz erstellt, ein Adapter ausgewählt und ein Gerät angefordert.

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

Die androidx.webgpu-Bibliothek enthält JNI- und .so-Dateien, die automatisch vom Build-System verknüpft und verwaltet werden. Die Hilfsmethode createWebGpu lädt das gebündelte libwebgpu_c_bundled.so.

Pipeline einrichten

Nachdem wir ein Gerät haben, müssen wir der GPU mitteilen, wie das Dreieck gezeichnet werden soll. Dazu erstellen wir eine „Pipeline“, die unseren Shader-Code (in WGSL geschrieben) enthält.

Fügen Sie diese private Hilfsfunktion Ihrer WebGpuRenderer-Klasse hinzu, um die Shader zu kompilieren und die Rendering-Pipeline zu erstellen.

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

Rahmen zeichnen

Nachdem die Pipeline fertig ist, können wir jetzt die Render-Funktion implementieren. Mit dieser Funktion wird die nächste verfügbare Textur vom Bildschirm abgerufen, Zeichenbefehle werden aufgezeichnet und an die GPU gesendet.

Fügen Sie diese Methode in Ihre WebGpuRenderer-Klasse ein:

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

Ressourcenbereinigung

Implementieren Sie die Bereinigungsfunktion, die von WebGpuSurface aufgerufen wird, wenn die Oberfläche zerstört wird.

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

Gerenderte Ausgabe

Ein Screenshot eines Android-Smartphone-Displays, auf dem die Ausgabe einer WebGPU-Anwendung zu sehen ist: ein zentriertes, rotes Dreieck auf einem dunkelblauen Hintergrund.
Abbildung 4.Die gerenderte Ausgabe der WebGPU-Beispielanwendung mit einem roten Dreieck

Beispiel für die Struktur einer App

Es empfiehlt sich, die Rendering-Implementierung von der UI-Logik zu entkoppeln, wie in der Struktur der Beispiel-App:

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt: Der Einstiegspunkt der Anwendung. Damit wird der Inhalt auf die WebGpuSurface-Composable festgelegt.
  • WebGpuSurface.kt: Definiert die UI-Komponente mit [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)). Sie verwaltet den Lebenszyklusbereich von Surface, initialisiert den Renderer, wenn die Oberfläche bereit ist, und bereinigt ihn, wenn er zerstört wird.
  • WebGpuRenderer.kt: Kapselt die gesamte WebGPU-spezifische Logik (Geräteerstellung, Pipeline-Einrichtung). Sie ist von der Benutzeroberfläche entkoppelt und empfängt nur die [Surface](/reference/android/view/Surface.html) und Dimensionen, die zum Zeichnen erforderlich sind.

Lebenszyklus- und Ressourcenverwaltung

Die Lebenszyklusverwaltung wird vom Kotlin-Coroutine-Bereich übernommen, der von [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)) in Jetpack Compose bereitgestellt wird.

  • Oberflächenerstellung: Initialisieren Sie die Device- und Surface-Konfiguration am Anfang des onSurface-Lambda-Blocks. Dieser Code wird sofort ausgeführt, wenn Surface verfügbar ist.
  • Zerstörung der Oberfläche: Wenn der Nutzer die Seite verlässt oder der Surface vom System zerstört wird, wird der Lambda-Block abgebrochen. Ein finally-Block wird ausgeführt und renderer.cleanup() wird aufgerufen, um Speicherlecks zu vermeiden.
  • Größenanpassung: Wenn sich die Abmessungen der Oberfläche ändern, kann AndroidExternalSurface den Block neu starten oder je nach Konfiguration Updates direkt verarbeiten. Der Renderer schreibt also immer in einen gültigen Puffer.

Fehlerbehebung und Validierung

WebGPU bietet Mechanismen zum Validieren von Eingabestrukturen und zum Erfassen von Laufzeitfehlern.

  • Logcat:Validierungsfehler werden im Android-Logcat ausgegeben.
  • Fehlerbereiche:Sie können bestimmte Fehler erfassen, indem Sie GPU-Befehle in [device.pushErrorScope()](/reference/kotlin/androidx/webgpu/GPUDevice#pushErrorScope(kotlin.Int))- und device.popErrorScope()-Blöcke einschließen.
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")
    } 
}

Tipps zur Leistungssteigerung

Beachten Sie beim Programmieren in WebGPU Folgendes, um Leistungsengpässe zu vermeiden:

  • Objekterstellung pro Frame vermeiden: Instanziieren Sie Pipelines (GPURenderPipeline), Bindungsgruppenlayouts und Shadermodule einmal während der Einrichtung der Anwendung, um die Wiederverwendung zu maximieren.
  • Pufferoptimierung: Aktualisieren Sie den Inhalt vorhandener GPUBuffers über GPUQueue.writeBuffer, anstatt für jeden Frame neue Puffer zu erstellen.
  • Statusänderungen minimieren: Gruppieren Sie Zeichenaufrufe, die dieselbe Pipeline und dieselben Bindungsgruppen verwenden, um den Treiber-Overhead zu minimieren und die Rendering-Effizienz zu verbessern.