شروع کار با WebGPU

برای استفاده از Jetpack WebGPU، پروژه شما باید حداقل شرایط زیر را داشته باشد:

  • حداقل سطح API : اندروید API 24 (نوقا) یا بالاتر مورد نیاز است.
  • سخت‌افزار : دستگاه‌هایی که از Vulkan 1.1+ پشتیبانی می‌کنند برای backend ترجیح داده می‌شوند.
  • حالت سازگاری و پشتیبانی از OpenGL ES : استفاده از WebGPU با حالت سازگاری با تنظیم گزینه استاندارد featureLevel روی compatibility هنگام درخواست GPUAdapter امکان‌پذیر است.
// Example of requesting an adapter with "compatibility" mode enabled:
val adapter = instance.requestAdapter(
  GPURequestAdapterOptions(featureLevel = FeatureLevel.Compatibility))

نصب و راه‌اندازی

پیش‌نیازها:

اندروید استودیو : آخرین نسخه اندروید استودیو را از وب‌سایت رسمی دانلود کنید و دستورالعمل‌های ذکر شده در راهنمای نصب اندروید استودیو را دنبال کنید.

ایجاد یک پروژه جدید

پس از نصب اندروید استودیو، برای راه‌اندازی پروژه WebGPU خود، این مراحل را دنبال کنید:

  1. شروع یک پروژه جدید : اندروید استودیو را باز کنید و روی «پروژه جدید» کلیک کنید.
  2. انتخاب یک الگو : الگوی Empty Activity را در اندروید استودیو انتخاب کرده و روی Next کلیک کنید.

    پنجره‌ی گفتگوی پروژه‌ی جدید اندروید استودیو، فهرست داخلی فعالیت‌هایی را که استودیو از طرف شما ایجاد خواهد کرد، نشان می‌دهد.
    شکل ۱. ایجاد یک پروژه جدید در اندروید استودیو
  3. پروژه خود را پیکربندی کنید :

    • نام : برای پروژه خود یک نام انتخاب کنید (مثلاً "JetpackWebGPUSample").
    • نام بسته : تأیید کنید که نام بسته با فضای نام انتخابی شما مطابقت دارد (مثلاً com.example.webgpuapp).
    • زبان : کاتلین را انتخاب کنید.
    • حداقل SDK : API 24 را انتخاب کنید: اندروید 7.0 (نوقا) یا بالاتر، همانطور که برای این کتابخانه توصیه شده است.
    • زبان پیکربندی ساخت : توصیه می‌شود برای مدیریت وابستگی‌های مدرن از Kotlin DSL (build.gradle.kts) استفاده کنید.
    کادر محاوره‌ای Empty Activity در اندروید استودیو که شامل فیلدهایی برای پر کردن activity خالی جدید، مانند نام، نام بسته، محل ذخیره و حداقل SDK است.
    شکل ۲. شروع با یک اکتیویتی خالی
  4. پایان : روی پایان کلیک کنید و منتظر بمانید تا اندروید استودیو فایل‌های پروژه شما را همگام‌سازی کند.

کتابخانه WebGPU Jetpack را اضافه کنید

کتابخانه androidx.webgpu شامل فایل‌های کتابخانه WebGPU NDK .so و همچنین رابط‌های کد مدیریت‌شده است.

شما می‌توانید با به‌روزرسانی build.gradle و همگام‌سازی پروژه خود با فایل‌های gradle با استفاده از دکمه‌ی «Sync Project» در اندروید استودیو، نسخه‌ی کتابخانه را به‌روزرسانی کنید.

معماری سطح بالا

رندرینگ WebGPU در یک برنامه اندروید بر روی یک رشته رندرینگ اختصاصی اجرا می‌شود تا پاسخگویی رابط کاربری (UI) حفظ شود.

  • لایه رابط کاربری : رابط کاربری با Jetpack Compose ساخته شده است. یک سطح ترسیم WebGPU با استفاده از AndroidExternalSurface در سلسله مراتب Compose ادغام شده است.
  • منطق رندرینگ : یک کلاس تخصصی (مثلاً WebGpuRenderer) مسئول مدیریت تمام اشیاء WebGPU و هماهنگی حلقه رندرینگ است.
  • لایه سایه‌زن : کد سایه‌زن WGSL که در ثابت‌های res یا رشته‌ای ذخیره می‌شود.
نمودار معماری سطح بالا که تعامل بین نخ رابط کاربری، یک نخ رندر اختصاصی و سخت‌افزار پردازنده گرافیکی (GPU) را در یک برنامه WebGPU اندروید نشان می‌دهد.
شکل 3. WebGPU در معماری سطح بالای اندروید

گام به گام: نمونه برنامه

این بخش مراحل ضروری مورد نیاز برای رندر کردن یک مثلث رنگی روی صفحه را بررسی می‌کند و گردش کار اصلی 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()
                }
            }
        }
    }
}

رندر کننده را تنظیم کنید

یک کلاس WebGpuRenderer در WebGpuRenderer.kt ایجاد کنید. این کلاس وظیفه سنگین ارتباط با GPU را بر عهده خواهد داشت.

ابتدا، ساختار کلاس و متغیرها را تعریف کنید:

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

مقداردهی اولیه: در مرحله بعد، تابع init را برای ایجاد نمونه WebGPU و پیکربندی سطح پیاده‌سازی کنید. این تابع توسط دامنه AndroidExternalSurface درون سطح خارجی قابل ترکیب که قبلاً ایجاد کردیم، فراخوانی می‌شود.

نکته: تابع 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 است که به طور خودکار توسط سیستم ساخت لینک و مدیریت می‌شوند. متد کمکی createWebGpu وظیفه بارگذاری فایل libwebgpu_c_bundled.so را بر عهده دارد.

راه اندازی خط لوله

حالا که یک دستگاه داریم، باید به GPU بگوییم که چگونه مثلث ما را رسم کند. ما این کار را با ایجاد یک "خط لوله" که شامل کد سایه‌زن ما (نوشته شده در WGSL) است، انجام می‌دهیم.

این تابع کمکی خصوصی را به کلاس WebGpuRenderer خود اضافه کنید تا shader ها را کامپایل کرده و خط لوله رندر را ایجاد کند.

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

خروجی رندر شده

تصویری از صفحه نمایش یک گوشی اندروید که خروجی یک برنامه WebGPU را نشان می‌دهد: یک مثلث قرمز توپر که در مرکز یک پس‌زمینه آبی تیره قرار دارد.
شکل ۴. خروجی رندر شده از برنامه WebGPU نمونه که یک مثلث قرمز را نشان می‌دهد

ساختار برنامه نمونه

این یک روش خوب است که پیاده‌سازی رندر خود را از منطق رابط کاربری جدا کنید، همانطور که در ساختار استفاده شده توسط برنامه نمونه مشاهده می‌کنید:

app/src/main/
├── java/com/example/app/
│   ├── MainActivity.kt       // Entry point
│   ├── WebGpuSurface.kt      // Composable Surface
│   └── WebGpuRenderer.kt     // Pure WebGPU logic
  • MainActivity.kt : نقطه ورود برنامه. این فایل محتوا را روی WebGpuSurface Composable تنظیم می‌کند.
  • 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 را مدیریت می‌کند، رندرکننده را هنگامی که سطح آماده است، مقداردهی اولیه می‌کند و هنگامی که سطح از بین می‌رود، آن را پاکسازی می‌کند.
  • WebGpuRenderer.kt : تمام منطق خاص WebGPU (ایجاد دستگاه، تنظیم Pipeline) را کپسوله‌سازی می‌کند. این فایل از رابط کاربری جدا شده و فقط [Surface](/reference/android/view/Surface.html) و ابعادی را که برای ترسیم نیاز دارد، دریافت می‌کند.

مدیریت چرخه عمر و منابع

مدیریت چرخه حیات توسط دامنه‌ی کوروتین کاتلین که توسط [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 را در ابتدای بلوک لامبدا onSurface مقداردهی اولیه کنید. این کد بلافاصله پس از در دسترس قرار گرفتن Surface اجرا می‌شود.
  • تخریب سطح : وقتی کاربر از صفحه خارج می‌شود یا Surface توسط سیستم تخریب می‌شود، بلوک لامبدا لغو می‌شود. یک بلوک finally اجرا می‌شود و renderer.cleanup() را برای جلوگیری از نشت حافظه فراخوانی می‌کند.
  • تغییر اندازه : اگر ابعاد سطح تغییر کند، AndroidExternalSurface ممکن است بلوک را مجدداً راه‌اندازی کند یا بسته به پیکربندی، به‌روزرسانی‌ها را مستقیماً مدیریت کند، بنابراین رندرکننده همیشه در یک بافر معتبر می‌نویسد.

اشکال‌زدایی و اعتبارسنجی

WebGPU مکانیزم‌هایی دارد که برای اعتبارسنجی ساختارهای ورودی و ثبت خطاهای زمان اجرا طراحی شده‌اند.

  • Logcat: خطاهای اعتبارسنجی در 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 ) را نمونه‌سازی کنید، طرح‌بندی‌های گروهی و ماژول‌های شیدر را متصل کنید تا حداکثر استفاده مجدد را داشته باشید.
  • بهینه‌سازی مصرف بافر : به جای ایجاد بافرهای جدید برای هر فریم، محتویات GPUBuffers موجود را از طریق GPUQueue.writeBuffer به‌روزرسانی کنید.
  • به حداقل رساندن تغییرات وضعیت : فراخوانی‌های ترسیم که از یک خط لوله مشترک استفاده می‌کنند را گروه‌بندی کنید و گروه‌ها را به هم متصل کنید تا سربار درایور به حداقل برسد و کارایی رندر بهبود یابد.