WebVR Partea 3: Deblocarea potențialului WebAssembly și AssemblyScript
Publicat: 2022-03-11WebAssembly nu este cu siguranță un înlocuitor pentru JavaScript ca lingua franca a web-ului și a lumii.
WebAssembly (abreviat Wasm) este un format de instrucțiuni binar pentru o mașină virtuală bazată pe stivă. Wasm este conceput ca o țintă portabilă pentru compilarea de limbaje de nivel înalt precum C/C++/Rust, permițând implementarea pe web pentru aplicații client și server.” –WebAssembly.org
Este important să distingem faptul că WebAssembly nu este o limbă. WebAssembly este ca un „.exe” – sau chiar mai bine – un fișier Java „.class”. Este compilat de dezvoltatorul web dintr-o altă limbă, apoi descărcat și rulat în browser.
WebAssembly oferă JavaScript toate caracteristicile pe care am vrut ocazional să le împrumutăm, dar niciodată nu am vrut să le deținem. La fel ca închirierea unei bărci sau a unui cal, WebAssembly ne permite să călătorim în alte limbi fără a fi nevoie să facem alegeri extravagante de „stil de viață lingvistic”. Acest lucru a permis webului să se concentreze pe lucruri importante, cum ar fi furnizarea de funcții și îmbunătățirea experienței utilizatorului.
Mai mult de 20 de limbi se compilează în WebAssembly: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go și, desigur, JavaScript.
Dacă vă amintiți diagrama arhitecturii simulării noastre, am delegat întreaga simulare către nBodySimulator
, astfel încât acesta să gestioneze lucrătorul web.
Dacă vă amintiți din postarea introductivă, nBodySimulator
are o funcție step()
numită la fiecare 33ms. Funcția step()
face aceste lucruri - numerotate în diagrama de mai sus:
-
calculateForces()
de la nBodySimulator apeleazăthis.worker.postMessage()
pentru a începe calculul. - workerWasm.js
this.onmessage()
primește mesajul. - workerWasm.js rulează sincron funcția
nBodyForces()
a lui nBodyForces.wasm. - workerWasm.js răspunde folosind
this.postMessage()
la firul principal cu noile forțe. - Firul principal este
this.worker.onMessage()
datele returnate și apelurile. - AppForces
applyForces()
de la nBodySimulator actualizează pozițiile corpurilor. - În cele din urmă, vizualizatorul se revopsește.
În postarea anterioară, am creat lucrătorul web care încheie calculele noastre WASM. Astăzi, construim cutia minusculă etichetată „WASM” și mutăm date înăuntru și în afara.
Pentru simplitate, am ales AssemblyScript ca limbaj de cod sursă pentru a scrie calculele noastre. AssemblyScript este un subset de TypeScript - care este un JavaScript tastat - așa că îl știți deja.
De exemplu, această funcție AssemblyScript calculează gravitația între două corpuri: :f64
din someVar someVar:f64
marchează variabila someVar ca un float pentru compilator. Amintiți-vă că acest cod este compilat și rulat într-un timp de rulare complet diferit de 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 }
Această funcție AssemblyScript preia (x, y, z, masa) pentru două corpuri și returnează o matrice de trei flotoare care descriu vectorul forță (x, y, z) pe care corpurile se aplică unul altuia. Nu putem apela această funcție din JavaScript, deoarece JavaScript nu are idee unde să o găsească. Trebuie să-l „exportăm” în JavaScript. Acest lucru ne aduce la prima noastră provocare tehnică.
Importuri și exporturi WebAssembly
În ES6, ne gândim la importuri și exporturi în cod JavaScript și folosim instrumente precum Rollup sau Webpack pentru a crea cod care rulează în browsere vechi pentru a gestiona import
și require()
. Acest lucru creează un arbore de dependență de sus în jos și permite tehnologie cool precum „tree-shaking” și divizarea codului.
În WebAssembly, importurile și exporturile îndeplinesc sarcini diferite față de importul ES6. Importuri/exporturi WebAssembly:
- Furnizați un mediu de rulare pentru modulul WebAssembly (de exemplu, funcțiile
trace()
șiabort()
). - Importă și exportă funcții și constante între timpi de execuție.
În codul de mai jos, env.abort
și env.trace
fac parte din mediul pe care trebuie să-l oferim modulului WebAssembly. Funcțiile nBodyForces.logI
și prietenii furnizează mesaje de depanare către consolă. Rețineți că trecerea șirurilor de caractere în/out din WebAssembly este netrivială, deoarece singurele tipuri ale WebAssembly sunt numere i32, i64, f32, f64, cu referințe i32 la o memorie liniară abstractă.
Notă: Aceste exemple de cod comută înainte și înapoi între codul JavaScript (lucratorul web) și AssemblyScript (codul 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(", ")); } } }
În codul nostru AssemblyScript, putem finaliza importul acestor funcții astfel:
// nBodyForces.ts declare function logI(data: i32): void declare function logF(data: f64): void
Notă : Avortul și urmărirea sunt importate automat .
Din AssemblyScript, ne putem exporta interfața. Iată câteva constante exportate:
// 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
Și aici este exportul lui nBodyForces()
pe care îl vom apela din JavaScript. Exportăm tipul Float64Array
în partea de sus a fișierului, astfel încât să putem folosi încărcătorul JavaScript AssemblyScript în worker-ul nostru web pentru a obține datele (vezi mai jos):

// 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 }
Artefacte WebAssembly: .wasm și .wat
Când AssemblyScript nBodyForces.ts
nostru este compilat într-un binar WebAssembly nBodyForces.wasm
, există o opțiune de a crea și o versiune „text” care descrie instrucțiunile din binar.
În interiorul fișierului nBodyForces.wat
, putem vedea aceste importuri și exporturi:
;; 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 ...
Acum avem binarul nostru nBodyForces.wasm
și un lucrător web pentru al rula. Pregătește-te pentru decolare! Și ceva management al memoriei!
Pentru a finaliza integrarea, trebuie să transmitem o matrice variabilă de floats la WebAssembly și să returnăm o matrice variabilă de floats la JavaScript.
Cu un burghez JavaScript naiv, mi-am propus să trec fără îndoială aceste matrice stridente de dimensiuni variabile într-un timp de rulare de înaltă performanță multiplatformă. Trecerea datelor către/de la WebAssembly a fost, de departe, cea mai neașteptată dificultate în acest proiect.
Cu toate acestea, cu multe mulțumiri pentru munca grea făcută de echipa AssemblyScript, putem folosi „încărcătorul” lor pentru a ajuta:
// workerWasm.js - our web worker /** * AssemblyScript loader adds helpers for moving data to/from AssemblyScript. * Highly recommended */ const loader = require("assemblyscript/lib/loader")
Require require()
înseamnă că trebuie să folosim un pachet de module precum Rollup sau Webpack. Pentru acest proiect, am ales Rollup pentru simplitatea și flexibilitatea sa și nu m-am uitat niciodată înapoi.
Amintiți-vă că lucrătorul nostru web rulează într-un fir separat și este în esență o onmessage()
cu o instrucțiune switch()
.
loader
creează modulul nostru wasm cu câteva funcții suplimentare utile de gestionare a memoriei. __retain()
și __release()
gestionează referințele de colectare a gunoiului în timpul de execuție al lucrătorului __allocArray()
noastră de parametri în memoria modulului wasm __getFloat64Array()
matricea rezultate din modulul wasm în timpul de execuție al lucrătorului
Acum putem organiza matrice float în și în afara nBodyForces()
și finalizam simularea:
// 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 }) } }
Cu tot ce am învățat, haideți să revizuim călătoria noastră web worker și WebAssembly. Bun venit la noul browser-backend al web. Acestea sunt link-uri către codul de pe GitHub:
- GET Index.html
- main.js
- nBodySimulator.js - transmite un mesaj lucrătorului său web
- workerWasm.js - apelează funcția WebAssembly
- nBodyForces.ts - calculează și returnează o serie de forțe
- workerWasm.js - transmite rezultatele înapoi la firul principal
- nBodySimulator.js - rezolvă promisiunea pentru forțe
- nBodySimulator.js - apoi aplică forțele corpurilor și le spune vizualizatorilor să picteze
De aici, să începem spectacolul creând nBodyVisualizer.js
! Următoarea noastră postare creează un vizualizator folosind API-ul Canvas, iar postarea finală se încheie cu WebVR și Aframe.