Làm quen với WebGPU

Để sử dụng Jetpack WebGPU, dự án của bạn phải đáp ứng các yêu cầu tối thiểu sau:

  • Cấp độ API tối thiểu: Bắt buộc phải là API Android 24 (Nougat) trở lên.
  • Phần cứng: Các thiết bị hỗ trợ Vulkan 1.1 trở lên được ưu tiên cho phần phụ trợ.
  • Chế độ tương thích và khả năng hỗ trợ OpenGL ES: Bạn có thể sử dụng WebGPU với chế độ tương thích bằng cách đặt lựa chọn featureLevel được chuẩn hoá thành compatibility trong khi yêu cầu GPUAdapter.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

Cài đặt và thiết lập

Điều kiện tiên quyết:

Android Studio: Tải phiên bản mới nhất của Android Studio xuống từ trang web chính thức và làm theo hướng dẫn trong Hướng dẫn cài đặt Android Studio.

Tạo dự án mới

Sau khi cài đặt Android Studio, hãy làm theo các bước sau để thiết lập dự án WebGPU:

  1. Bắt đầu một dự án mới: Mở Android Studio rồi nhấp vào Dự án mới.
  2. Chọn một mẫu: Chọn mẫu Empty Activity (Hoạt động trống) trong Android Studio rồi nhấp vào Next (Tiếp theo).

    Hộp thoại Dự án mới của Android Studio, cho thấy danh sách các hoạt động tích hợp mà Studio sẽ tạo thay cho bạn.
    Hình 1.Tạo một dự án mới trong Android Studio
  3. Định cấu hình dự án:

    • Tên: Đặt tên cho dự án của bạn (ví dụ: "JetpackWebGPUSample").
    • Package Name (Tên gói): Xác minh rằng tên gói khớp với không gian tên bạn chọn (ví dụ: com.example.webgpuapp).
    • Ngôn ngữ: Chọn Kotlin.
    • Minimum SDK (SDK tối thiểu): Chọn API 24: Android 7.0 (Nougat) trở lên, theo đề xuất cho thư viện này.
    • Ngôn ngữ cấu hình bản dựng: Bạn nên sử dụng Kotlin DSL (build.gradle.kts) để quản lý các phần phụ thuộc hiện đại.
    Hộp thoại Empty Activity (Hoạt động trống) của Android Studio có chứa các trường để điền thông tin cho hoạt động trống mới, chẳng hạn như Name (Tên), Package Name (Tên gói), Save Location (Vị trí lưu) và Minimum SDK (SDK tối thiểu).
    Hình 2.Bắt đầu bằng một hoạt động trống
  4. Kết thúc: Nhấp vào Kết thúc rồi chờ Android Studio đồng bộ hoá các tệp dự án.

Thêm thư viện WebGPU Jetpack

Thư viện androidx.webgpu chứa các tệp thư viện .so WebGPU NDK cũng như các giao diện mã được quản lý.

Bạn có thể cập nhật phiên bản thư viện bằng cách cập nhật build.gradle và đồng bộ hoá dự án với các tệp gradle bằng nút "Sync Project" (Đồng bộ hoá dự án) trong Android Studio.

Kiến trúc cấp cao

Quá trình kết xuất WebGPU trong một ứng dụng Android được chạy trên một luồng kết xuất chuyên dụng để duy trì khả năng phản hồi của giao diện người dùng.

  • Lớp giao diện người dùng: Giao diện người dùng được tạo bằng Jetpack Compose. Một bề mặt vẽ WebGPU được tích hợp vào hệ phân cấp Compose bằng cách sử dụng AndroidExternalSurface.
  • Logic hiển thị: Một lớp chuyên biệt (ví dụ: WebGpuRenderer) chịu trách nhiệm quản lý tất cả các đối tượng WebGPU và điều phối vòng lặp kết xuất.
  • Lớp chương trình đổ bóng: Mã chương trình đổ bóng WGSL được lưu trữ trong các hằng số res hoặc chuỗi.
Sơ đồ cấu trúc cấp cao cho thấy sự tương tác giữa Luồng giao diện người dùng, Luồng kết xuất chuyên dụng và phần cứng GPU trong một ứng dụng WebGPU Android.
Hình 3.Kiến trúc cấp cao của WebGPU trên Android

Từng bước: ứng dụng mẫu

Phần này trình bày các bước cần thiết để kết xuất một hình tam giác có màu trên màn hình, minh hoạ quy trình làm việc cốt lõi của WebGPU.

Hoạt động chính

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

Thành phần kết hợp bề mặt bên ngoài

Tạo một tệp mới có tên là WebgpuSurface.kt. Thành phần kết hợp này bao bọc AndroidExternalSurface để cung cấp cầu nối giữa Compose và trình kết xuất của bạn.

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

Thiết lập trình kết xuất

Tạo một lớp WebGpuRenderer trong WebGpuRenderer.kt. Lớp này sẽ xử lý phần lớn công việc giao tiếp với GPU.

Trước tiên, hãy xác định cấu trúc lớp và các biến:

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

Khởi chạy: Tiếp theo, hãy triển khai hàm init để tạo thực thể WebGPU và định cấu hình bề mặt. Hàm này được gọi theo phạm vi AndroidExternalSurface bên trong thành phần kết hợp bề mặt bên ngoài mà chúng ta đã tạo trước đó.

Lưu ý: Hàm init sử dụng createWebGpu, một phương thức trợ giúp (một phần của androidx.webgpu.helper) để đơn giản hoá quá trình thiết lập. Tiện ích này tạo phiên bản WebGPU, chọn một bộ chuyển đổi và yêu cầu một thiết bị.

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

Thư viện androidx.webgpu bao gồm các tệp JNI và .so, được hệ thống xây dựng tự động liên kết và quản lý. Phương thức trợ giúp createWebGpu sẽ lo việc tải libwebgpu_c_bundled.so theo gói.

Thiết lập quy trình

Giờ đây, khi đã có một thiết bị, chúng ta cần cho GPU biết cách vẽ hình tam giác. Chúng ta thực hiện việc này bằng cách tạo một "quy trình" chứa mã chương trình đổ bóng (được viết bằng WGSL).

Thêm hàm trợ giúp riêng tư này vào lớp WebGpuRenderer để biên dịch các chương trình đổ bóng và tạo quy trình kết xuất.

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

Vẽ một khung hình

Khi quy trình đã sẵn sàng, giờ đây, chúng ta có thể triển khai hàm kết xuất. Hàm này lấy hoạ tiết có sẵn tiếp theo từ màn hình, ghi lại các lệnh vẽ và gửi chúng đến GPU.

Thêm phương thức này vào lớp 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()
  }

Dọn dẹp tài nguyên

Triển khai hàm dọn dẹp. Hàm này được WebGpuSurface gọi khi bề mặt bị huỷ.

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

Kết quả hiển thị

Ảnh chụp màn hình của một màn hình điện thoại Android hiển thị đầu ra của một ứng dụng WebGPU: một hình tam giác màu đỏ đặc nằm ở giữa trên nền màu xanh dương đậm.
Hình 4.Đầu ra được kết xuất của ứng dụng WebGPU mẫu cho thấy một hình tam giác màu đỏ

Cấu trúc ứng dụng mẫu

Bạn nên tách riêng quá trình triển khai kết xuất khỏi logic giao diện người dùng, như trong cấu trúc mà ứng dụng mẫu sử dụng:

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt: Điểm truy cập của ứng dụng. Thao tác này sẽ đặt nội dung thành WebGpuSurface Composable.
  • WebGpuSurface.kt: Xác định thành phần giao diện người dùng bằng [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)). Lớp này quản lý phạm vi vòng đời Surface, khởi chạy trình kết xuất khi bề mặt đã sẵn sàng và dọn dẹp khi bề mặt bị huỷ.
  • WebGpuRenderer.kt: Đóng gói tất cả logic dành riêng cho WebGPU (Tạo thiết bị, thiết lập Pipeline). Thành phần này tách biệt với giao diện người dùng, chỉ nhận [Surface](/reference/android/view/Surface.html) và các phương diện cần thiết để vẽ.

Quản lý vòng đời và tài nguyên

Việc quản lý vòng đời do phạm vi Coroutine Kotlin cung cấp bởi [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)) trong Jetpack Compose xử lý.

  • Tạo vùng hiển thị: Khởi chạy cấu hình DeviceSurface khi bắt đầu khối lambda onSurface. Mã này sẽ chạy ngay lập tức khi Surface có sẵn.
  • Huỷ bỏ bề mặt: Khi người dùng rời khỏi hoặc Surface bị hệ thống huỷ, khối lambda sẽ bị huỷ. Khối finally được thực thi, gọi renderer.cleanup() để ngăn rò rỉ bộ nhớ.
  • Đổi kích thước: Nếu kích thước bề mặt thay đổi, AndroidExternalSurface có thể khởi động lại khối hoặc trực tiếp xử lý các bản cập nhật tuỳ thuộc vào cấu hình, do đó, trình kết xuất luôn ghi vào một vùng đệm hợp lệ.

Gỡ lỗi và xác thực

WebGPU có các cơ chế được thiết kế để xác thực cấu trúc đầu vào và ghi lại các lỗi thời gian chạy.

  • Logcat: Lỗi xác thực được in vào Android Logcat.
  • Phạm vi lỗi: Bạn có thể ghi lại các lỗi cụ thể bằng cách đóng gói các lệnh GPU trong các khối [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")
    } 
}

Mẹo tăng hiệu suất

Khi lập trình trong WebGPU, hãy cân nhắc những điều sau để tránh tình trạng tắc nghẽn hiệu suất:

  • Tránh tạo đối tượng theo khung hình: Tạo bản sao của các quy trình (GPURenderPipeline), liên kết bố cục nhóm và mô-đun chương trình đổ bóng một lần trong quá trình thiết lập ứng dụng để tối đa hoá khả năng sử dụng lại.
  • Tối ưu hoá việc sử dụng vùng đệm: Cập nhật nội dung của GPUBuffers hiện có thông qua GPUQueue.writeBuffer thay vì tạo vùng đệm mới cho mỗi khung hình.
  • Giảm thiểu các thay đổi về trạng thái: Nhóm các lệnh gọi vẽ dùng chung cùng một quy trình và nhóm liên kết để giảm thiểu chi phí của trình điều khiển và cải thiện hiệu quả kết xuất.