اخبار وب

WebGPU در مرورگر: رندر، Compute و شروع با یک مثلث

آشنایی با Adapter، Device، WGSL و Pipeline در WebGPU، تفاوت با WebGL و Compute و نمونه مستقل HTML با مدیریت نبودن پشتیبانی.

1404/12/16 3 دقیقه مطالعه 868 بازدید
WebGPU در مرورگر: رندر، Compute و شروع با یک مثلث

WebGPU یک API وب برای رندر و محاسبه روی GPU است. مرورگر دسترسی کنترل‌شده‌ای فراهم می‌کند؛ این دسترسی به معنی کنترل آزاد و مستقیم سخت‌افزار نیست. WebGPU هم باعث حذف WebGL از مرورگرها نشده است. انتخاب آن به قابلیت مورد نیاز، محیط کاربر و هزینه توسعه بستگی دارد. ادعای قبلی مقاله درباره سرعت ثابت چندبرابری برای تمام پروژه‌ها حذف شده، چون نتیجه به بار کاری و روش پیاده‌سازی وابسته است.

برای شروع، تفاوت کارهای GPU و CPU را روشن کنید. GPU برای مجموعه بزرگی از کارهای مشابه مناسب است، اما انتقال داده و آماده‌سازی فرمان هزینه دارند. یک محاسبه کوچک که سریع روی CPU تمام می‌شود الزاماً از انتقال به GPU سود نمی‌برد. راهنمای WebGPU در MDN مفاهیم API و پشتیبانی محیط‌ها را معرفی می‌کند. برای محصول، وجود قابلیت را در زمان اجرا هم بررسی کنید.

رندر، برنامه‌ای با منابع و فرمان‌هاست

در رندر، داده شکل، shader و تنظیم pipeline با هم نتیجه تصویر را می‌سازند. Shader بخشی از برنامه است که روی GPU اجرا می‌شود. Pipeline مشخص می‌کند مرحله‌های لازم و شکل خروجی چگونه به هم متصل شوند. مرورگر فرمان‌ها را دریافت و برای اجرای مناسب اعتبارسنجی می‌کند. همین ساختار صریح باعث می‌شود برنامه‌نویس درباره منابع و ترتیب عملیات تصمیم‌های بیشتری بگیرد.

برای یک مثلث ساده، می‌توان مختصات را در shader تعریف کرد و vertex buffer نساخت. برای مدل سه‌بعدی واقعی، داده رأس، material، نور و حرکت هر کدام مسئله جدا دارند. شروع از نمونه کوچک کمک می‌کند چرخه درخواست device و رسم را بفهمید. افزودن مدل خارجی قبل از روشن شدن همین چرخه، خطای بارگذاری asset و خطای رندر را با هم مخلوط می‌کند.

زبان shader در WebGPU، WGSL است. کد JavaScript مدیریت منابع و فرمان‌ها را انجام می‌دهد و WGSL محاسبه مرحله‌های shader را تعریف می‌کند. مشخصات WGSL مرجع قواعد زبان است. شباهت بعضی عبارت‌ها به زبان‌های آشنا، دلیل قابل کپی بودن shader از WebGL نیست؛ ترجمه shader باید با قرارداد داده و pipeline هماهنگ شود.

Adapter و Device دو قدم آغازین‌اند

navigator.gpu در محیطی که قابلیت را ارائه می‌کند نقطه شروع است. requestAdapter یک adapter مناسب درخواست می‌کند و ممکن است نتیجه‌ای ندهد. پس از آن requestDevice محیط منطقی کار با منابع را می‌سازد. مرجع requestAdapter و مرجع requestDevice شرایط این درخواست‌ها را توضیح می‌دهند. بررسی صرف نام مرورگر جای بررسی موفقیت این مرحله‌ها را نمی‌گیرد.

قابلیت مورد نیاز محصول باید صریح باشد. اگر برنامه به feature یا limit خاصی وابسته است، آن را هنگام تصمیم درباره device بررسی کنید. دستگاه کاربر، تنظیمات سیستم و محدودیت محیط می‌توانند نتیجه متفاوتی بدهند. کاربر باید در حالت نبودن قابلیت یک راه روشن ببیند؛ صفحه خالی با خطای console تجربه مناسبی برای ابزار عمومی نیست.

WebGPU در Secure Context در دسترس است. برای توسعه، localhost در مرورگرهای مربوط زمینه قابل اعتماد محسوب می‌شود؛ برای استقرار عمومی HTTPS لازم است. باز کردن فایل از دیسک را روش استاندارد اجرا فرض نکنید. در مستند پروژه، راه راه‌اندازی محلی و محدودیت پشتیبانی را بنویسید تا مشکل محیط با مشکل shader اشتباه نشود.

نمونه کامل رسم یک مثلث

نمونه زیر را با نام index.html ذخیره کنید. تمام HTML، CSS، JavaScript و WGSL در همان فایل هستند. به مدل سه‌بعدی، texture یا کتابخانه خارجی نیاز ندارید. یک سرور فایل محلی مانند python -m http.server 8000 را در پوشه فایل راه‌اندازی کنید و صفحه را از localhost باز کنید. مرورگر و دستگاه باید WebGPU را ارائه کنند؛ پیام صفحه حالت نبودن پشتیبانی را توضیح می‌دهد.

<!doctype html>
<html lang="fa" dir="rtl">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>مثلث WebGPU</title>
<style>body{font-family:sans-serif;margin:0;color:#18212b}main{max-width:48rem;margin:2rem auto;padding:1rem;line-height:1.8}canvas{display:block;width:100%;aspect-ratio:2/1;background:#edf1f5}</style>
</head>
<body><main><h1>مثلث آموزشی WebGPU</h1><p id="status" role="status">محیط آماده می‌شود.</p><canvas id="scene" aria-label="مثلث آبی روی زمینه روشن">نمونه مثلث آموزشی</canvas></main>
<script type="module">
const canvas = document.getElementById("scene");
const status = document.getElementById("status");
let active = true;

async function init() {
  if (!navigator.gpu) { status.textContent = "WebGPU در این محیط در دسترس نیست."; return; }
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) { status.textContent = "GPU مناسب در دسترس نیست."; return; }
  const device = await adapter.requestDevice();
  const context = canvas.getContext("webgpu");
  if (!context) { device.destroy(); status.textContent = "سطح رسم آماده نشد."; return; }
  const format = navigator.gpu.getPreferredCanvasFormat();
  context.configure({ device, format, alphaMode: "opaque" });
  device.lost.then(() => { active = false; status.textContent = "ارتباط با GPU از دست رفت. صفحه را دوباره باز کنید."; });
  device.addEventListener("uncapturederror", (event) => {
    active = false; status.textContent = "رسم با خطا روبه‌رو شد."; console.error(event.error);
  });
  const module = device.createShaderModule({ code: `
    @vertex fn vertexMain(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
      let positions = array<vec2f, 3>(vec2f(0.0, 0.7), vec2f(-0.7, -0.7), vec2f(0.7, -0.7));
      return vec4f(positions[index], 0.0, 1.0);
    }
    @fragment fn fragmentMain() -> @location(0) vec4f {
      return vec4f(0.08, 0.35, 0.70, 1.0);
    }
  ` });
  const pipeline = device.createRenderPipeline({
    layout: "auto",
    vertex: { module, entryPoint: "vertexMain" },
    fragment: { module, entryPoint: "fragmentMain", targets: [{ format }] },
    primitive: { topology: "triangle-list" },
  });
  function draw() {
    if (!active) return;
    const ratio = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.max(1, Math.round(canvas.clientWidth * ratio));
    canvas.height = Math.max(1, Math.round(canvas.clientHeight * ratio));
    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass({ colorAttachments: [{
      view: context.getCurrentTexture().createView(),
      clearValue: { r: 0.93, g: 0.95, b: 0.97, a: 1 },
      loadOp: "clear", storeOp: "store",
    }] });
    pass.setPipeline(pipeline); pass.draw(3); pass.end();
    device.queue.submit([encoder.finish()]);
  }
  window.addEventListener("resize", draw);
  draw(); status.textContent = "مثلث آماده است.";
}
init().catch((error) => { active = false; status.textContent = "راه‌اندازی WebGPU موفق نشد."; console.error(error); });
</script></body></html>

تابع init ابتدا پشتیبانی را بررسی می‌کند و سپس adapter و device می‌گیرد. context از canvas به دست می‌آید و با format ترجیحی مرورگر تنظیم می‌شود. مرجع configure توضیح می‌دهد که بافت نمایش چگونه به device وصل می‌شود. canvas در HTML ID مشخص دارد و JavaScript فقط همان عنصر را می‌خواند.

Vertex shader بر اساس vertex_index یکی از سه مختصات را انتخاب می‌کند. Fragment shader رنگ ثابت مثلث را برمی‌گرداند. draw با تعداد سه رأس همین ساختار را استفاده می‌کند؛ بنابراین مثال درباره مدل سه‌بعدی، نورپردازی یا بارگذاری asset ادعایی ندارد. رنگ زمینه در render pass پاک می‌شود و سپس command buffer برای اجرا به queue می‌رود.

اندازه CSS canvas با اندازه واقعی سطح رسم یکسان نیست. نمونه عرض نمایشی را می‌گیرد و با devicePixelRatio اندازه backing surface را تعیین می‌کند. برای محدود کردن مصرف، نسبت را به دو محدود کرده‌ایم؛ این یک انتخاب آموزشی است. در محصول، وضوح تصویر و مصرف منابع را با دستگاه‌های مخاطب و نیاز تصویر هماهنگ کنید، نه فقط با بالاترین عدد ممکن.

این نمونه یک فریم می‌کشد و هنگام resize دوباره رسم می‌کند. حلقه animation ندارد و حرکت ایجاد نمی‌کند. اگر requestAnimationFrame اضافه می‌کنید، توقف هنگام بی‌نیازی به رندر و آزاد کردن منابع را هم در نظر بگیرید. یک صفحه ثابت نباید تنها به دلیل داشتن canvas، GPU را بی‌وقفه درگیر نگه دارد. طراحی نرخ رندر بخشی از مصرف انرژی و پاسخ‌گویی محصول است.

خطا و از دست رفتن Device را مدیریت کنید

ممکن است device پس از راه‌اندازی از دست برود. نمونه با device.lost وضعیت را دریافت می‌کند و رسم بعدی را متوقف می‌کند. مرجع GPUDevice.lost به این وضعیت مربوط است. در محصولی که کار طولانی کاربر را نگه می‌دارد، برنامه بازیابی باید از داده کاربر جدا باشد؛ از دست رفتن GPU نباید به معنی از دست رفتن سند ویرایش‌شده شود.

خطای shader یا تنظیم pipeline هم باید در عیب‌یابی از نبودن پشتیبانی جدا شود. یک پیام عمومی برای کاربر لازم است، اما log توسعه باید محل مسئله را روشن کند. هیچ ادعایی درباره موفقیت اجرا روی همه دستگاه‌ها از یک نمونه کد به دست نمی‌آید. روش بررسی قابلیت، fallback و ثبت خطا باید برای محیط‌های هدف پروژه تکمیل شوند.

Compute کاربردی جدا از رسم دارد

WebGPU امکان Compute Pipeline را برای کارهای محاسباتی فراهم می‌کند. این قابلیت با draw یک شکل روی canvas یکسان نیست. داده ورودی، تقسیم کار و خواندن نتیجه از bufferها باید طراحی شوند. مشخصات WebGPU مرجع مدل منابع و pipeline است. الگوریتمی که برای CPU نوشته شده، لزوماً با انتقال مستقیم به shader تقسیم کار مناسبی پیدا نمی‌کند.

برای محاسبه، حجم و شکل داده مهم‌اند. اگر نتیجه باید مدام به CPU برگردد، هزینه انتقال و همگام‌سازی وارد مسیر می‌شود. اگر داده چند مرحله روی GPU می‌ماند، الگوی دیگری دارید. به جای وعده عمومی چندبرابر سرعت، مسیر داده را رسم کنید و نیاز واقعی را مشخص کنید. مسئله هم باید قابلیت تقسیم مناسب داشته باشد؛ همه منطق برنامه از parallel بودن سود نمی‌برد.

کتابخانه رندر یا API خام؟

کار مستقیم با WebGPU برای شناخت مدل و ساخت ابزار تخصصی مفید است، ولی محصول سه‌بعدی کامل به مدیریت صحنه، material، asset و تعامل نیاز دارد. کتابخانه مناسب بخشی از این کار را انجام می‌دهد. قابلیت و محدودیت renderer همان نسخه کتابخانه را بخوانید؛ وجود یک گزینه WebGPU به معنی همسان بودن تمام رفتارهای rendererهای قدیمی و جدید نیست.

برای تیم طراحی و توسعه، خروجی واقعی مدل را هم بررسی کنید. مدل بسیار سنگین یا textureهای بزرگ تنها به خاطر API جدید سبک نمی‌شوند. تعداد drawها، اندازه داده و کیفیت مناسب نمایش باید با نیاز کاربر هماهنگ شوند. مقاله شروع رندر سه‌بعدی با Blender به مسیر تولید asset مربوط است و Performance Budget به محدودیت‌های تحویل آن در وب.

پشتیبانی محصول را با راه ادامه تعریف کنید

اگر WebGPU هسته قابلیت است، پیش از ورود کاربر به کار طولانی، آمادگی محیط را مشخص کنید. اگر صرفاً نمایش تزئینی است، متن و عمل اصلی صفحه باید بدون آن قابل استفاده بماند. fallback می‌تواند نمایش ساده‌تر، تصویر از پیش آماده یا توضیح محدودیت باشد؛ انتخاب آن باید با وظیفه واقعی هماهنگ باشد. در نمونه آموزشی فقط پیام متنی داریم و asset تصویری اضافه نشده است.

قدم بعدی، انتخاب یک صحنه یا محاسبه کوچک با ورودی مشخص است. قرارداد منابع، معیار مورد نیاز و حالت نبودن GPU را بنویسید. سپس تصمیم بگیرید API خام لازم دارید یا renderer یک کتابخانه کافی است. این تصمیم از شناخت جریان داده و تجربه کاربر می‌آید، نه از عنوان‌هایی درباره جایگزینی کامل WebGL یا تضمین سرعت ثابت.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!