WebVR Część 3: Uwolnienie potencjału WebAssembly i AssemblyScript

Opublikowany: 2022-03-11

WebAssembly zdecydowanie nie zastępuje JavaScript jako lingua franca sieci i świata.

WebAssembly (w skrócie Wasm) to format instrukcji binarnych dla maszyny wirtualnej opartej na stosie. Wasm został zaprojektowany jako przenośny cel do kompilacji języków wysokiego poziomu, takich jak C/C++/Rust, umożliwiający wdrażanie w sieci aplikacji klienckich i serwerowych”. –WebAssembly.org

Ważne jest, aby odróżnić, że WebAssembly nie jest językiem. WebAssembly jest jak plik „.exe” — lub nawet lepiej — plik Java „.class”. Jest kompilowany przez programistę internetowego z innego języka, a następnie pobierany i uruchamiany w przeglądarce.

WebAssembly udostępnia JavaScriptowi wszystkie funkcje, które od czasu do czasu chcieliśmy pożyczyć, ale nigdy tak naprawdę nie chcieliśmy posiadać. Podobnie jak wynajem łodzi lub konia, WebAssembly pozwala nam podróżować do innych języków bez konieczności dokonywania ekstrawaganckich wyborów „językowego stylu życia”. Dzięki temu sieć może skoncentrować się na ważnych rzeczach, takich jak dostarczanie funkcji i poprawa doświadczenia użytkownika.

Ponad 20 języków kompiluje się do WebAssembly: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go i oczywiście JavaScript.

Jeśli pamiętasz diagram architektury naszej symulacji, delegowaliśmy całą symulację do nBodySimulator , aby zarządzał pracownikiem sieciowym.

Schemat architektury symulacji
Rysunek 1: Ogólna architektura.

Jeśli pamiętasz z wpisu wprowadzającego, nBodySimulator posiada funkcję step() wywoływaną co 33ms. Funkcja step() robi te rzeczy - ponumerowane na powyższym schemacie:

  1. calculateForces() w nBodySimulator wywołuje this.worker.postMessage() , aby rozpocząć obliczenia.
  2. workerWasm.js this.onmessage() pobiera komunikat.
  3. workerWasm.js synchronicznie uruchamia funkcję nBodyForces.wasm nBodyForces() .
  4. workerWasm.js odpowiada za pomocą this.postMessage() do głównego wątku z nowymi siłami.
  5. Główny wątek this.worker.onMessage() zwrócone dane i wywołania.
  6. ApplyForces applyForces() w nBodySimulator do aktualizacji pozycji ciał.
  7. Na koniec wizualizator odświeży się.

Wątek interfejsu użytkownika, wątek pracownika internetowego
Rysunek 2: Wewnątrz funkcji step() symulatora

W poprzednim poście zbudowaliśmy robota sieciowego, który otacza nasze obliczenia WASM. Dzisiaj budujemy małe pudełko oznaczone „WASM” i przenosimy dane do i na zewnątrz.

Dla uproszczenia wybrałem AssemblyScript jako język kodu źródłowego do pisania naszych obliczeń. AssemblyScript jest podzbiorem TypeScript — który jest wpisanym JavaScript — więc już go znasz.

Na przykład ta funkcja AssemblyScript oblicza grawitację między dwoma ciałami: :f64 w someVar:f64 oznacza zmienną someVar jako zmiennoprzecinkową dla kompilatora. Pamiętaj, że ten kod jest kompilowany i uruchamiany w zupełnie innym środowisku wykonawczym niż JavaScript.

 // AssemblyScript - a TypeScript-like language that compiles to WebAssembly // src/assembly/nBodyForces.ts /** * Given two bodies, calculate the Force of Gravity, * then return as a 3-force vector (x, y, z) * * Sometimes, the force of gravity is: * * Fg = G * mA * mB / r^2 * * Given: * - Fg = Force of gravity * - r = sqrt ( dx + dy + dz) = straight line distance between 3d objects * - G = gravitational constant * - mA, mB = mass of objects * * Today, we're using better-gravity because better-gravity can calculate * force vectors without polar math (sin, cos, tan) * * Fbg = G * mA * mB * dr / r^3 // using dr as a 3-distance vector lets * // us project Fbg as a 3-force vector * * Given: * - Fbg = Force of better gravity * - dr = (dx, dy, dz) // a 3-distance vector * - dx = bodyB.x - bodyA.x * * Force of Better-Gravity: * * - Fbg = (Fx, Fy, Fz) = the change in force applied by gravity each * body's (x,y,z) over this time period * - Fbg = G * mA * mB * dr / r^3 * - dr = (dx, dy, dz) * - Fx = Gmm * dx / r3 * - Fy = Gmm * dy / r3 * - Fz = Gmm * dz / r3 * * From the parameters, return an array [fx, fy, fz] */ function twoBodyForces(xA: f64, yA: f64, zA: f64, mA: f64, xB: f64, yB: f64, zB: f64, mB: f64): f64[] { // Values used in each x,y,z calculation const Gmm: f64 = G * mA * mB const dx: f64 = xB - xA const dy: f64 = yB - yA const dz: f64 = zB - zA const r: f64 = Math.sqrt(dx * dx + dy * dy + dz * dz) const r3: f64 = r * r * r // Return calculated force vector - initialized to zero const ret: f64[] = new Array<f64>(3) // The best not-a-number number is zero. Two bodies in the same x,y,z if (isNaN(r) || r === 0) return ret // Calculate each part of the vector ret[0] = Gmm * dx / r3 ret[1] = Gmm * dy / r3 ret[2] = Gmm * dz / r3 return ret }

Ta funkcja AssemblyScript pobiera (x, y, z, masa) dla dwóch ciał i zwraca tablicę trzech elementów zmiennoprzecinkowych opisujących wektor siły (x, y, z), który ciała stosują względem siebie. Nie możemy wywołać tej funkcji z JavaScript, ponieważ JavaScript nie ma pojęcia, gdzie ją znaleźć. Musimy go „wyeksportować” do JavaScript. To prowadzi nas do naszego pierwszego wyzwania technicznego.

Import i eksport zespołu WebAssembly

W ES6 myślimy o imporcie i eksporcie w kodzie JavaScript i używamy narzędzi takich jak Rollup lub Webpack do tworzenia kodu, który działa w starszych przeglądarkach do obsługi import i require() . Tworzy to odgórne drzewo zależności i umożliwia fajne technologie, takie jak „drżenie drzewa” i dzielenie kodu.

W WebAssembly importy i eksporty wykonują inne zadania niż import ES6. Importy/eksporty WebAssembly:

  • Zapewnij środowisko wykonawcze dla modułu WebAssembly (np. funkcje trace() i abort() ).
  • Importuj i eksportuj funkcje i stałe między środowiskami wykonawczymi.

W poniższym kodzie env.abort i env.trace są częścią środowiska, które musimy dostarczyć do modułu WebAssembly. Funkcje nBodyForces.logI i friends dostarczają komunikaty debugowania do konsoli. Należy zauważyć, że przekazywanie ciągów do/z WebAssembly nie jest trywialne, ponieważ jedynymi typami WebAssembly są liczby i32, i64, f32, f64, z odwołaniami i32 do abstrakcyjnej pamięci liniowej.

Uwaga: te przykłady kodu przełączają się między kodem JavaScript (pracownik sieciowy) a AssemblyScript (kodem WASM).

 // Web Worker JavaScript in workerWasm.js /** * When we instantiate the Wasm module, give it a context to work in: * nBodyForces: {} is a table of functions we can import into AssemblyScript. See top of nBodyForces.ts * env: {} describes the environment sent to the Wasm module as it's instantiated */ const importObj = { nBodyForces: { logI(data) { console.log("Log() - " + data); }, logF(data) { console.log("Log() - " + data); }, }, env: { abort(msg, file, line, column) { // wasm.__getString() is added by assemblyscript's loader: // https://github.com/AssemblyScript/assemblyscript/tree/master/lib/loader console.error("abort: (" + wasm.__getString(msg) + ") at " + wasm.__getString(file) + ":" + line + ":" + column); }, trace(msg, n) { console.log("trace: " + wasm.__getString(msg) + (n ? " " : "") + Array.prototype.slice.call(arguments, 2, 2 + n).join(", ")); } } }

W naszym kodzie AssemblyScript możemy zakończyć import tych funkcji w następujący sposób:

 // nBodyForces.ts declare function logI(data: i32): void declare function logF(data: f64): void

Uwaga : Przerwij i śledź są importowane automatycznie .

Z AssemblyScript możemy wyeksportować nasz interfejs. Oto kilka eksportowanych stałych:

 // src/assembly/nBodyForces.ts // Gravitational constant. Any G could be used in a game. // This value is best for a scientific simulation. export const G: f64 = 6.674e-11; // for sizing and indexing arrays export const bodySize: i32 = 4 export const forceSize: i32 = 3

A oto eksport nBodyForces() , który wywołamy z JavaScript. Eksportujemy typ Float64Array na górze pliku, dzięki czemu możemy użyć modułu ładującego JavaScript AssemblyScript w naszym elemencie roboczym WWW, aby uzyskać dane (patrz poniżej):

 // src/assembly/nBodyForces.ts export const FLOAT64ARRAY_ID = idof<Float64Array>(); ... /** * Given N bodies with mass, in a 3d space, calculate the forces of gravity to be applied to each body. * * This function is exported to JavaScript, so only takes/returns numbers and arrays. * For N bodies, pass and array of 4N values (x,y,z,mass) and expect a 3N array of forces (x,y,z) * Those forces can be applied to the bodies mass to update its position in the simulation. * Calculate the 3-vector each unique pair of bodies applies to each other. * * 0 1 2 3 4 5 * 0 xxxxx * 1 xxxx * 2 xxx * 3 xx * 4 x * 5 * * Sum those forces together into an array of 3-vector x,y,z forces * * Return 0 on success */ export function nBodyForces(arrBodies: Float64Array): Float64Array { // Check inputs const numBodies: i32 = arrBodies.length / bodySize if (arrBodies.length % bodySize !== 0) trace("INVALID nBodyForces parameter. Chaos ensues...") // Create result array. This should be garbage collected later. let arrForces: Float64Array = new Float64Array(numBodies * forceSize) // For all bodies: for (let i: i32 = 0; i < numBodies; i++) { // Given body i: pair with every body[j] where j > i for (let j: i32 = i + 1; j < numBodies; j++) { // Calculate the force the bodies apply to one another const bI: i32 = i * bodySize const bJ: i32 = j * bodySize const f: f64[] = twoBodyForces( arrBodies[bI], arrBodies[bI + 1], arrBodies[bI + 2], arrBodies[bI + 3], // x,y,z,m arrBodies[bJ], arrBodies[bJ + 1], arrBodies[bJ + 2], arrBodies[bJ + 3], // x,y,z,m ) // Add this pair's force on one another to their total forces applied x,y,z const fI: i32 = i * forceSize const fJ: i32 = j * forceSize // body0 arrForces[fI] = arrForces[fI] + f[0] arrForces[fI + 1] = arrForces[fI + 1] + f[1] arrForces[fI + 2] = arrForces[fI + 2] + f[2] // body1 arrForces[fJ] = arrForces[fJ] - f[0] // apply forces in opposite direction arrForces[fJ + 1] = arrForces[fJ + 1] - f[1] arrForces[fJ + 2] = arrForces[fJ + 2] - f[2] } } // For each body, return the sum of forces all other bodies applied to it. // If you would like to debug wasm, you can use trace or the log functions // described in workerWasm when we initialized // Eg trace("nBodyForces returns (b0x, b0y, b0z, b1z): ", 4, arrForces[0], arrForces[1], arrForces[2], arrForces[3]) // x,y,z return arrForces // success }

Artefakty WebAssembly: .wasm i .wat

Kiedy nasz AssemblyScript nBodyForces.ts zostanie skompilowany do pliku binarnego WebAssembly nBodyForces.wasm , istnieje możliwość utworzenia również „tekstowej” wersji opisującej instrukcje w pliku binarnym.

Artefakty WebAssembly
Rysunek 3: Pamiętaj, że AssemblyScript to język. WebAssembly to kompilator i środowisko uruchomieniowe.

Wewnątrz pliku nBodyForces.wat możemy zobaczyć te importy i eksporty:

 ;; This is a comment in nBodyForces.wat (module ;; compiler defined types (type $FUNCSIG$iii (func (param i32 i32) (result i32))) … ;; Expected imports from JavaScript (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64))) ;; Memory section defining data constants like strings (memory $0 1) (data (i32.const 8) "\1e\00\00\00\01\00\00\00\01\00\00\00\1e\00\00\00~\00l\00i\00b\00/\00r\00t\00/\00t\00l\00s\00f\00.\00t\00s\00") ... ;; Our global constants (not yet exported) (global $nBodyForces/FLOAT64ARRAY_ID i32 (i32.const 3)) (global $nBodyForces/G f64 (f64.const 6.674e-11)) (global $nBodyForces/bodySize i32 (i32.const 4)) (global $nBodyForces/forceSize i32 (i32.const 3)) ... ;; Memory management functions we'll use in a minute (export "memory" (memory $0)) (export "__alloc" (func $~lib/rt/tlsf/__alloc)) (export "__retain" (func $~lib/rt/pure/__retain)) (export "__release" (func $~lib/rt/pure/__release)) (export "__collect" (func $~lib/rt/pure/__collect)) (export "__rtti_base" (global $~lib/rt/__rtti_base)) ;; Finally our exported constants and function (export "FLOAT64ARRAY_ID" (global $nBodyForces/FLOAT64ARRAY_ID)) (export "G" (global $nBodyForces/G)) (export "bodySize" (global $nBodyForces/bodySize)) (export "forceSize" (global $nBodyForces/forceSize)) (export "nBodyForces" (func $nBodyForces/nBodyForces)) ;; Implementation details ...

Mamy teraz nasz plik binarny nBodyForces.wasm i robota sieciowego do jego uruchomienia. Przygotuj się na start! I trochę zarządzania pamięcią!

Aby zakończyć integrację, musimy przekazać zmienną tablicę pływaków do WebAssembly i zwrócić zmienną tablicę zmiennych pływaków do JavaScript.

Z naiwnym mieszczańskim JavaScriptem, postanowiłem beztrosko przekazywać te krzykliwe tablice o zmiennych rozmiarach do i z międzyplatformowego, wysokowydajnego środowiska wykonawczego. Przekazywanie danych do/z WebAssembly było zdecydowanie najbardziej nieoczekiwaną trudnością w tym projekcie.

Jednak z wielkim podziękowaniem za ciężkie podnoszenie wykonane przez zespół AssemblyScript, możemy użyć ich „ładowacza”, aby pomóc:

 // workerWasm.js - our web worker /** * AssemblyScript loader adds helpers for moving data to/from AssemblyScript. * Highly recommended */ const loader = require("assemblyscript/lib/loader")

require() oznacza, że ​​musimy użyć pakietu modułów, takiego jak Rollup lub Webpack. Do tego projektu wybrałem Rollup ze względu na jego prostotę i elastyczność i nigdy nie oglądałem się za siebie.

Pamiętaj, że nasz pracownik sieciowy działa w osobnym wątku i jest zasadniczo funkcją onmessage() z instrukcją switch() .

loader tworzy nasz moduł wasm z dodatkowymi przydatnymi funkcjami zarządzania pamięcią. __retain() i __release() zarządzają odwołaniami do wyrzucania elementów bezużytecznych w środowisku __allocArray() kopiuje naszą tablicę parametrów do pamięci modułu __getFloat64Array() kopiuje tablicę wyników z modułu wasm do środowiska wykonawczego procesu roboczego

Możemy teraz organizować tablice zmiennoprzecinkowe do i z nBodyForces() i dokończyć naszą symulację:

 // workerWasm.js /** * Web workers listen for messages from the main thread. */ this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. wasm = loader.instantiate(msg.wasmModule, importObj) // Throws // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return // Message: Given array of floats describing a system of bodies (x,y,x,mass), // calculate the Grav forces to be applied to each body case 'nBodyForces': if (!wasm) throw new Error('wasm not initialized') // Copy msg.arrBodies array into the wasm instance, increase GC count const dataRef = wasm.__retain(wasm.__allocArray(wasm.FLOAT64ARRAY_ID, msg.arrBodies)); // Do the calculations in this thread synchronously const resultRef = wasm.nBodyForces(dataRef); // Copy result array from the wasm instance to our javascript runtime const arrForces = wasm.__getFloat64Array(resultRef); // Decrease the GC count on dataRef from __retain() here, // and GC count from new Float64Array in wasm module wasm.__release(dataRef); wasm.__release(resultRef); // Message results back to main thread. // see nBodySimulation.js this.worker.onmessage return this.postMessage({ purpose: 'nBodyForces', arrForces }) } }

Mając wszystko, czego się nauczyliśmy, przyjrzyjmy się naszemu pracownikowi sieciowemu i przygodzie WebAssembly. Witamy w nowym zapleczu przeglądarki internetowej. Oto linki do kodu na GitHub:

  1. POBIERZ Index.html
  2. main.js
  3. nBodySimulator.js - przekazuje wiadomość do swojego webworkera
  4. workerWasm.js — wywołuje funkcję WebAssembly
  5. nBodyForces.ts - oblicza i zwraca tablicę sił
  6. workerWasm.js - przekazuje wyniki z powrotem do głównego wątku
  7. nBodySimulator.js - rozwiązuje obietnicę sił
  8. nBodySimulator.js - następnie przykłada siły do ​​ciał i nakazuje wizualizatorom malowanie

Stąd zacznijmy pokaz od stworzenia nBodyVisualizer.js ! Nasz następny post tworzy wizualizator za pomocą interfejsu API Canvas, a ostatni post zawiera WebVR i Aframe.

Powiązane: WebAssembly/Rust Tutorial: Doskonałe przetwarzanie dźwięku