Z WebGL do WebGPU

François Beaufort
François Beaufort

Jako deweloper WebGL możesz być zarówno zniechęcony, jak i podekscytowany rozpoczęciem korzystania z WebGPU, następcy WebGL, który wprowadza na strony internetowe postęp w zakresie nowoczesnych interfejsów API grafiki.

Warto wiedzieć, że WebGL i WebGPU mają wiele wspólnych podstawowych koncepcji. Oba interfejsy API umożliwiają uruchamianie na GPU małych programów zwanych shaderami. WebGL obsługuje shadery wierzchołków i fragmentów, a WebGPU obsługuje też shadery obliczeniowe. WebGL używa języka OpenGL Shading Language (GLSL), a WebGPU – języka WebGPU Shading Language (WGSL). Chociaż te 2 języki różnią się od siebie, podstawowe koncepcje są w nich w większości takie same.

W tym artykule znajdziesz kilka różnic między WebGL a WebGPU, które pomogą Ci zacząć.

Stan globalny

WebGL ma dużo globalnego stanu. Niektóre ustawienia dotyczą wszystkich operacji renderowania, np. które tekstury i bufory są powiązane. Ten stan globalny ustawiasz, wywołując różne funkcje interfejsu API, i pozostaje on aktywny, dopóki go nie zmienisz. Stan globalny w WebGL jest głównym źródłem błędów, ponieważ łatwo zapomnieć o zmianie ustawienia globalnego. Stan globalny utrudnia też udostępnianie kodu, ponieważ deweloperzy muszą uważać, aby przypadkowo nie zmienić stanu globalnego w sposób, który wpłynie na inne części kodu.

WebGPU to interfejs API bezstanowy, który nie utrzymuje stanu globalnego. Zamiast tego używa koncepcji potoku, aby obejmować cały stan renderowania, który w WebGL był globalny. Potok zawiera informacje o tym, jakiego mieszania, topologii i atrybutów należy użyć. Potoku nie można zmienić. Jeśli chcesz zmienić niektóre ustawienia, musisz utworzyć kolejny potok. WebGPU używa też koderów poleceń do łączenia poleceń w pakiety i wykonywania ich w kolejności, w jakiej zostały zarejestrowane. Jest to przydatne np. w przypadku mapowania cieni, gdy w jednym przebiegu przez obiekty aplikacja może rejestrować wiele strumieni poleceń, po jednym dla każdej mapy cieni światła.

Podsumowując, globalny model stanu WebGL utrudniał tworzenie niezawodnych bibliotek i aplikacji, które można było łączyć ze sobą, a także sprawiał, że były one podatne na błędy. WebGPU znacznie zmniejszył ilość stanu, który deweloperzy musieli śledzić podczas wysyłania poleceń do procesora graficznego.

Nie synchronizuj

W przypadku procesorów graficznych wysyłanie poleceń i synchroniczne oczekiwanie na ich wykonanie jest zwykle nieefektywne, ponieważ może spowodować opróżnienie potoku i powstanie przerw. Jest to szczególnie ważne w przypadku WebGPU i WebGL, które korzystają z architektury wieloprocesowej, w której sterownik GPU działa w innym procesie niż JavaScript.

Na przykład w WebGL wywołanie gl.getError() wymaga synchronicznego IPC z procesu JavaScript do procesu GPU i z powrotem. Może to spowodować „wąskie gardło” po stronie procesora, ponieważ oba procesy komunikują się ze sobą.

Aby uniknąć tych problemów, WebGPU jest całkowicie asynchroniczny. Model błędu i wszystkie inne operacje są wykonywane asynchronicznie. Na przykład podczas tworzenia tekstury operacja od razu wydaje się zakończona sukcesem, nawet jeśli tekstura jest w rzeczywistości błędem. Błąd można wykryć tylko asynchronicznie. Dzięki temu komunikacja między procesami jest płynna, a aplikacje działają niezawodnie.

Programy cieniujące obliczeń

Shadery obliczeniowe to programy, które działają na procesorze graficznym i wykonują obliczenia ogólnego przeznaczenia. Są one dostępne tylko w WebGPU, a nie w WebGL.

W przeciwieństwie do programów cieniujących wierzchołki i fragmenty nie są one ograniczone do przetwarzania grafiki i mogą być używane do wielu różnych zadań, takich jak uczenie maszynowe, symulacje fizyczne i obliczenia naukowe. Shadery obliczeniowe są wykonywane równolegle przez setki, a nawet tysiące wątków, co sprawia, że są bardzo wydajne w przetwarzaniu dużych zbiorów danych. Dowiedz się więcej o obliczeniach na GPU i w tym obszernym artykule o WebGPU.

Przetwarzanie klatek wideo

Przetwarzanie klatek wideo za pomocą JavaScriptu i WebAssembly ma pewne wady: koszt kopiowania danych z pamięci GPU do pamięci CPU oraz ograniczony poziom równoległości, jaki można osiągnąć za pomocą procesów roboczych i wątków CPU. WebGPU nie ma tych ograniczeń, dzięki czemu doskonale nadaje się do przetwarzania klatek wideo ze względu na ścisłą integrację z interfejsem WebCodecs API.

Poniższy fragment kodu pokazuje, jak zaimportować element VideoFrame jako teksturę zewnętrzną w WebGPU i go przetworzyć. Możesz wypróbować tę wersję demonstracyjną.

// Init WebGPU device and pipeline...
// Configure canvas context...
// Feed camera stream to video...

(function render() {
  const videoFrame = new VideoFrame(video);
  applyFilter(videoFrame);
  requestAnimationFrame(render);
})();

function applyFilter(videoFrame) {
  const texture = device.importExternalTexture({ source: videoFrame });
  const bindgroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [{ binding: 0, resource: texture }],
  });
  // Finally, submit commands to GPU
}

Domyślna przenośność aplikacji

WebGPU wymusza wysyłanie żądań do limits. Domyślnie funkcja requestDevice() zwraca obiekt GPUDevice, który może nie odpowiadać możliwościom sprzętowym urządzenia fizycznego, ale raczej rozsądnemu i najniższemu wspólnemu mianownikowi wszystkich procesorów graficznych. Wymagając od deweloperów przesyłania próśb o limity urządzeń, WebGPU zapewnia, że aplikacje będą działać na jak największej liczbie urządzeń.

Obsługa płótna

Po utworzeniu kontekstu WebGL i podaniu atrybutów kontekstu, takich jak alpha, antialias, colorSpace, depth, preserveDrawingBuffer czy stencil, WebGL automatycznie zarządza obszarem rysowania.

Z drugiej strony WebGPU wymaga samodzielnego zarządzania obszarem rysowania. Aby na przykład uzyskać wygładzanie krawędzi w WebGPU, musisz utworzyć teksturę z wieloma próbkami i renderować do niej. Następnie przekształcisz teksturę z wielokrotnym próbkowaniem w zwykłą teksturę i narysujesz ją na płótnie. Ręczne zarządzanie umożliwia wyświetlanie danych wyjściowych na dowolnej liczbie obszarów roboczych z pojedynczego obiektu GPUDevice. WebGL może natomiast utworzyć tylko jeden kontekst na kanwę.

Zapoznaj się z prezentacją WebGPU Multiple Canvases.

Przeglądarki mają obecnie limit liczby obszarów roboczych WebGL na stronie. W momencie pisania tego artykułu Chrome i Safari mogą używać jednocześnie maksymalnie 16 płócien WebGL, a Firefox – nawet 200. Z drugiej strony nie ma ograniczeń co do liczby elementów canvas WebGPU na stronie.

Zrzut ekranu przedstawiający maksymalną liczbę obszarów roboczych WebGL w przeglądarkach Safari, Chrome i Firefox
Maksymalna liczba obszarów roboczych WebGL w przeglądarkach Safari, Chrome i Firefox (od lewej do prawej) – demo

Przydatne komunikaty o błędach

WebGPU udostępnia stos wywołań dla każdej wiadomości zwracanej przez interfejs API. Oznacza to, że możesz szybko sprawdzić, gdzie w kodzie wystąpił błąd, co jest przydatne podczas debugowania i naprawiania błędów.

Komunikaty o błędach WebGPU są nie tylko łatwe do zrozumienia i dają możliwość podejmowania konkretnych działań, ale też zawierają stos wywołań. Komunikaty o błędach zwykle zawierają opis błędu i sugestie dotyczące jego naprawienia.

WebGPU umożliwia też podanie niestandardowego label dla każdego obiektu WebGPU. Ta etykieta jest następnie używana przez przeglądarkę w komunikatach GPUError, ostrzeżeniach konsoli i narzędziach dla programistów.

Od nazw do indeksów

W WebGL wiele elementów jest połączonych za pomocą nazw. Możesz na przykład zadeklarować w GLSL zmienną typu uniform o nazwie myUniform i uzyskać jej lokalizację za pomocą funkcji gl.getUniformLocation(program, 'myUniform'). Jest to przydatne, ponieważ jeśli wpiszesz nazwę zmiennej uniform w nieprawidłowy sposób, pojawi się błąd.

Z drugiej strony w WebGPU wszystko jest połączone za pomocą przesunięcia bajtowego lub indeksu (często nazywanego lokalizacją). Twoim obowiązkiem jest dbanie o to, aby lokalizacje kodu w WGSL i JavaScript były zsynchronizowane.

Generowanie mipmap

W WebGL możesz utworzyć poziom 0 mip mapy tekstury, a następnie wywołać funkcję gl.generateMipmap(). WebGL wygeneruje wtedy wszystkie pozostałe poziomy mip.

W WebGPU musisz samodzielnie wygenerować mapy mip. Nie ma wbudowanej funkcji, która by to umożliwiała. Więcej informacji o tej decyzji znajdziesz w specyfikacji. Możesz użyć przydatnych bibliotek, takich jak webgpu-utils, do generowania map mip lub dowiedzieć się, jak to zrobić samodzielnie.

Bufory i tekstury pamięci masowej

Bufory jednolite są obsługiwane zarówno przez WebGL, jak i WebGPU i umożliwiają przekazywanie do shaderów stałych parametrów o ograniczonym rozmiarze. Bufory pamięci, które wyglądają podobnie do buforów jednolitych, są obsługiwane tylko przez WebGPU i są bardziej wydajne i elastyczne niż bufory jednolite.

  • Bufory pamięci przechowujące dane przekazywane do shaderów mogą być znacznie większe niż bufory jednolite. Specyfikacja mówi, że powiązania buforów jednolitych mogą mieć rozmiar do 64 KB (patrz maxUniformBufferBindingSize), ale maksymalny rozmiar powiązania bufora pamięci w WebGPU wynosi co najmniej 128 MB (patrz maxStorageBufferBindingSize).

  • Bufory pamięci są zapisywalne i obsługują niektóre operacje niepodzielne, a bufory jednolite są tylko do odczytu. Umożliwia to wdrażanie nowych klas algorytmów.

  • Powiązania buforów pamięci obsługują tablice o rozmiarze określonym w czasie działania, co zapewnia większą elastyczność algorytmów, natomiast rozmiary tablic buforów jednolitych muszą być podane w cieniowaniu.

Tekstury pamięci są obsługiwane tylko w WebGPU i są dla tekstur tym, czym bufory pamięci są dla buforów jednolitych. Są bardziej elastyczne niż zwykłe tekstury, ponieważ obsługują zapisy z dostępem losowym (a w przyszłości także odczyty).

Zmiany w buforze i teksturze

W WebGL możesz utworzyć bufor lub teksturę, a potem w dowolnym momencie zmienić ich rozmiar, np. za pomocą funkcji gl.bufferData()gl.texImage2D().

W WebGPU bufory i tekstury są niezmienne. Oznacza to, że po utworzeniu nie można zmienić ich rozmiaru, sposobu użycia ani formatu. Możesz tylko zmienić ich zawartość.

Różnice w konwencjach dotyczących spacji

W WebGL zakres Z przestrzeni przycinania wynosi od -1 do 1. W WebGPU zakres przestrzeni przycinania Z wynosi od 0 do 1. Oznacza to, że obiekty o wartości z równej 0 znajdują się najbliżej kamery, a obiekty o wartości z równej 1 są najdalej.

Ilustracja przedstawiająca zakresy przestrzeni wycinania Z w WebGL i WebGPU.
Zakresy przestrzeni przycinania Z w WebGL i WebGPU.

WebGL korzysta z konwencji OpenGL, w której oś Y jest skierowana w górę, a oś Z – w stronę użytkownika. WebGPU korzysta z konwencji Metal, w której oś Y jest skierowana w dół, a oś Z – na zewnątrz ekranu. Pamiętaj, że w przypadku współrzędnych bufora ramki, widocznego obszaru oraz fragmentu/piksela oś Y jest skierowana w dół. W przestrzeni klipu kierunek osi Y jest nadal skierowany w górę, tak jak w WebGL.

Podziękowania

Dziękujemy Corentinowi Wallezowi, Greggowi Tavaresowi, Stephenowi White’owi, Kenowi Russellowi i Rachel Andrew za sprawdzenie tego artykułu.

Polecam też stronę WebGPUFundamentals.org, na której znajdziesz szczegółowe informacje o różnicach między WebGPU a WebGL.