איך מתחילים להשתמש ב-WebGPU

כדי להשתמש ב-Jetpack WebGPU, הפרויקט שלכם צריך לעמוד בדרישות המינימום הבאות:

  • רמת ה-API המינימלית: נדרשת רמת API‏ 24 (Nougat) ואילך ב-Android.
  • חומרה: מומלץ להשתמש בקצה העורפי במכשירים שתומכים ב-Vulkan 1.1 ומעלה.
  • מצב תאימות ותמיכה ב-OpenGL ES: אפשר להשתמש ב-WebGPU עם מצב תאימות על ידי הגדרת האפשרות המתוקננת featureLevel לערך compatibility בזמן שליחת הבקשה GPUAdapter.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

התקנה והגדרה

דרישות מוקדמות:

Android Studio: מורידים את הגרסה העדכנית של Android Studio מהאתר הרשמי ופועלים לפי ההוראות שמופיעות במדריך להתקנת Android Studio.

יצירת פרויקט חדש

אחרי שמתקינים את Android Studio, מבצעים את השלבים הבאים כדי להגדיר את פרויקט WebGPU:

  1. מתחילים פרויקט חדש: פותחים את Android Studio ולוחצים על New Project (פרויקט חדש).
  2. בחירת תבנית: בוחרים את התבנית Empty Activity ב-Android Studio ולוחצים על Next (הבא).

    תיבת הדו-שיח 'פרויקט חדש' ב-Android Studio, שמוצגת בה רשימה מובנית של פעילויות ש-Studio ייצור בשבילכם.
    איור 1. יצירת פרויקט חדש ב-Android Studio
  3. הגדרת הפרויקט:

    • שם: נותנים שם לפרויקט (לדוגמה, ‫'JetpackWebGPUSample').
    • שם החבילה: מוודאים ששם החבילה זהה למרחב השמות שבחרתם (לדוגמה, com.example.webgpuapp).
    • שפה: בוחרים באפשרות Kotlin.
    • גרסת ה-SDK המינימלית: בוחרים באפשרות API 24: Android 7.0 (Nougat) או בגרסה מתקדמת יותר, כמומלץ לספרייה הזו.
    • שפת תצורת ה-build: מומלץ להשתמש ב-Kotlin DSL ‏ (build.gradle.kts) לניהול תלויות מודרני.
    תיבת הדו-שיח Empty Activity (פעילות ריקה) ב-Android Studio, שמכילה שדות למילוי הפעילות הריקה החדשה, כמו Name (שם), Package Name (שם החבילה), Save Location (מיקום השמירה) ו-Minimum SDK (גרסת ה-SDK המינימלית).
    איור 2. התחלה עם פעילות ריקה
  4. סיום: לוחצים על סיום ומחכים ש-Android Studio יסנכרן את קובצי הפרויקט.

הוספת ספריית WebGPU Jetpack

  • מוסיפים את מאגר google אל settings.gradle כמו שמתואר במאמר שימוש בספריית Jetpack באפליקציה.
  • מוסיפים את יחסי התלות של הארטיפקטים שאתם צריכים לקובץ build.gradle של האפליקציה או המודול:

הספרייה androidx.webgpu מכילה את קובצי הספרייה WebGPU NDK ‏ .so וגם את הממשקים של הקוד המנוהל.

כדי לעדכן את גרסת הספרייה, צריך לעדכן את קובץ build.gradle ולסנכרן את הפרויקט עם קובצי Gradle באמצעות הלחצן Sync Project (סנכרון הפרויקט) ב-Android Studio.

ארכיטקטורה ברמה גבוהה

רינדור WebGPU באפליקציית Android מופעל בשרשור רינדור ייעודי כדי לשמור על היענות ממשק המשתמש.

  • שכבת ממשק המשתמש: ממשק המשתמש בנוי באמצעות Jetpack פיתוח נייטיב. משטח ציור של WebGPU משולב בהיררכיית הפיתוח הנייטיב באמצעות AndroidExternalSurface.
  • לוגיקת רינדור: מחלקה מיוחדת (למשל, ‫WebGpuRenderer) אחראי לניהול כל אובייקטי WebGPU ולתיאום של לולאת הרינדור.
  • שכבת ה-Shader: קוד Shader של WGSL שמאוחסן ב-res או בקבועי מחרוזת.
תרשים ארכיטקטורה ברמה גבוהה שמראה את האינטראקציה בין ה-UI Thread, ה-Rendering Thread הייעודי וחומרת ה-GPU באפליקציית WebGPU Android.
איור 3. ארכיטקטורה ברמה גבוהה של WebGPU ב-Android

הוראות מפורטות: אפליקציה לדוגמה

בקטע הזה מפורטים השלבים החיוניים שנדרשים כדי לעבד משולש צבעוני על המסך, ומוצג בו תהליך העבודה הבסיסי של WebGPU.

הפעילות הראשית

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

הקומפוזיציה של המשטח החיצוני

יוצרים קובץ חדש בשם WebgpuSurface.kt. רכיב ה-Composable הזה עוטף את AndroidExternalSurface כדי לספק גשר בין Compose לבין רכיב הרינדור.

@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

יוצרים כיתה ב-WebGpuRendererWebGpuRenderer.kt. המחלקה הזו תטפל בעבודה המורכבת של התקשורת עם ה-GPU.

קודם כול, מגדירים את מבנה המחלקה ואת המשתנים:

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

אתחול: לאחר מכן, מטמיעים את פונקציית האתחול כדי ליצור את מופע WebGPU ולהגדיר את המשטח. הפונקציה הזו נקראת על ידי היקף AndroidExternalSurface בתוך רכיב ה-Composable של הממשק החיצוני שיצרנו קודם.

הערה: הפונקציה init משתמשת ב-createWebGpu, שיטת עזר (חלק מ-androidx.webgpu.helper) כדי לייעל את ההגדרה. כלי השירות הזה יוצר את מופע WebGPU, בוחר מתאם ומבקש מכשיר.

// 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 כוללת קבצים מסוג JNI ו-‎ .so, שמקושרים ומנוהלים באופן אוטומטי על ידי מערכת ה-build. שיטת העזר createWebGpu מטפלת בטעינה של חבילת libwebgpu_c_bundled.so.

הגדרת צינור עיבוד הנתונים

עכשיו, אחרי שיש לנו מכשיר, אנחנו צריכים להגיד ל-GPU איך לצייר את המשולש. אנחנו עושים את זה על ידי יצירת 'צינור' שמכיל את קוד ה-shader שלנו (שנכתב ב-WGSL).

מוסיפים את פונקציית העזר הפרטית הזו למחלקה WebGpuRenderer כדי לקמפל את ה-shaders וליצור את צינור העיבוד.

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

ציור מסגרת

אחרי שהצינור מוכן, אפשר להטמיע את פונקציית הרינדור. הפונקציה הזו מקבלת את הטקסטורה הזמינה הבאה מהמסך, מתעדת פקודות ציור ושולחת אותן ל-GPU.

מוסיפים את השיטה הזו לכיתה 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()
  }

ניקוי משאבים

מטמיעים את פונקציית הניקוי, שמופעלת על ידי WebGpuSurface כשהמשטח נהרס.

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

פלט שעבר רינדור

צילום מסך של מסך טלפון Android שמוצגת בו הפלט של אפליקציית WebGPU: משולש אדום מלא במרכז על רקע כחול כהה.
איור 4. הפלט שעבר רינדור של אפליקציית WebGPU לדוגמה, שבו מוצג משולש אדום

דוגמה למבנה של אפליקציה

מומלץ להפריד את ההטמעה של הרינדור מהלוגיקה של ממשק המשתמש, כמו במבנה שבו נעשה שימוש באפליקציה לדוגמה:

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt: נקודת הכניסה לאפליקציה. הוא מגדיר את התוכן כWebGpuSurface ניתן להרכבה.
  • WebGpuSurface.kt: הגדרת רכיב ממשק המשתמש באמצעות [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)). הוא מנהל את היקף מחזור החיים של Surface, ומפעיל את רכיב ה-Renderer כשהמשטח מוכן ומנקה אותו כשהוא נהרס.
  • WebGpuRenderer.kt: מכיל את כל הלוגיקה הספציפית ל-WebGPU (יצירת מכשיר, הגדרת צינור). הוא מופרד מממשק המשתמש ומקבל רק את [Surface](/reference/android/view/Surface.html) והממדים שהוא צריך כדי לצייר.

ניהול מחזור החיים והמשאבים

ניהול מחזור החיים מתבצע על ידי היקף שגרות ההמשך של Kotlin שסופק על ידי [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)) ב-Jetpack Compose.

  • יצירת משטח: מפעילים את ההגדרה של Device ושל Surface בתחילת בלוק ה-lambda של onSurface. הקוד הזה מופעל באופן מיידי כשהרכיב Surface הופך לזמין.
  • השמדה של Surface: כשהמשתמש יוצא מהדף או כשבלוק ה-lambda של Surface מושמד על ידי המערכת, הפעולה מבוטלת. מתבצעת חסימה של finally, ומופעלת קריאה ל-renderer.cleanup() כדי למנוע דליפות זיכרון.
  • שינוי גודל: אם המידות של משטח התצוגה משתנות, יכול להיות ש-AndroidExternalSurface יפעיל מחדש את הבלוק או יטפל ישירות בעדכונים, בהתאם להגדרה, כך שהרכיב לעיבוד תמיד יכתוב למאגר תקף.

ניפוי באגים ואימות

ל-WebGPU יש מנגנונים שנועדו לאמת את מבני הקלט ולתעד שגיאות בזמן ריצה.

  • Logcat: שגיאות האימות מודפסות ב-Android Logcat.
  • היקפי שגיאה: אפשר לתעד שגיאות ספציפיות על ידי הוספת פקודות GPU בתוך בלוקים של [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")
    } 
}

טיפים לשיפור הביצועים

כשמבצעים תכנות ב-WebGPU, חשוב להביא בחשבון את הנקודות הבאות כדי להימנע מנקודות צוואר בקבוק בביצועים:

  • מומלץ להימנע מיצירת אובייקטים לכל פריים: כדאי ליצור מופעים של צינורות עיבוד נתונים (GPURenderPipeline), פריסות של קבוצות כבילה ומודולים של Shader פעם אחת במהלך הגדרת האפליקציה כדי למקסם את השימוש החוזר.
  • אופטימיזציה של השימוש ב-Buffer: עדכון התוכן של GPUBuffers קיים באמצעות GPUQueue.writeBuffer במקום ליצור מאגרי Buffer חדשים בכל פריים.
  • צמצום שינויי המצב: קיבוץ קריאות לציור שמשתפות את אותו צינור וקשירת קבוצות כדי לצמצם את התקורה של מנהל ההתקן ולשפר את יעילות העיבוד.