WebVR Parte 3: Sbloccare il potenziale di WebAssembly e AssemblyScript

Pubblicato: 2022-03-11

WebAssembly non è sicuramente un sostituto di JavaScript come lingua franca del web e del mondo.

WebAssembly (abbreviato Wasm) è un formato di istruzione binaria per una macchina virtuale basata su stack. Wasm è progettato come destinazione portatile per la compilazione di linguaggi di alto livello come C/C++/Rust, consentendo la distribuzione sul Web per applicazioni client e server". –WebAssembly.org

È importante distinguere che WebAssembly non è un linguaggio. WebAssembly è come un file '.exe' - o anche meglio - un file Java '.class'. Viene compilato dallo sviluppatore web da un'altra lingua, quindi scaricato ed eseguito sul tuo browser.

WebAssembly offre a JavaScript tutte le funzionalità che occasionalmente volevamo prendere in prestito ma non avremmo mai voluto possedere. Proprio come noleggiare una barca o un cavallo, WebAssembly ci consente di viaggiare in altre lingue senza dover fare stravaganti scelte di "stile di vita linguistico". Ciò ha consentito al Web di concentrarsi su cose importanti come la fornitura di funzionalità e il miglioramento dell'esperienza utente.

Più di 20 linguaggi vengono compilati in WebAssembly: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go e, naturalmente, JavaScript.

Se ricordi il diagramma dell'architettura della nostra simulazione, abbiamo delegato l'intera simulazione a nBodySimulator , quindi gestisce il web worker.

Diagramma dell'architettura della simulazione
Figura 1: Architettura generale.

Se ricordi dal post introduttivo, nBodySimulator ha una funzione step() chiamata ogni 33 ms. La funzione step() fa queste cose, numerate nel diagramma sopra:

  1. CalcutForces calculateForces() di nBodySimulator chiama this.worker.postMessage() per avviare il calcolo.
  2. workerWasm.js this.onmessage() ottiene il messaggio.
  3. workerWasm.js esegue in modo sincrono la funzione nBodyForces() di nBodyForces.wasm.
  4. workerWasm.js risponde usando this.postMessage() al thread principale con le nuove forze.
  5. Il thread principale this.worker.onMessage() il marshalling dei dati e delle chiamate restituiti.
  6. applyForces applyForces() di nBodySimulator per aggiornare le posizioni dei corpi.
  7. Infine, il visualizzatore ridipinge.

Thread dell'interfaccia utente, thread di lavoro Web
Figura 2: all'interno della funzione step() del simulatore

Nel post precedente, abbiamo creato il web worker che esegue il wrapping dei nostri calcoli WASM. Oggi stiamo costruendo la minuscola scatola etichettata "WASM" e spostando i dati dentro e fuori.

Per semplicità, ho scelto AssemblyScript come linguaggio del codice sorgente per scrivere i nostri calcoli. AssemblyScript è un sottoinsieme di TypeScript, che è un JavaScript digitato, quindi lo conosci già.

Ad esempio, questa funzione AssemblyScript calcola la gravità tra due corpi: :f64 in someVar:f64 contrassegna la variabile someVar come float per il compilatore. Ricorda che questo codice viene compilato ed eseguito in un runtime completamente diverso da 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 }

Questa funzione AssemblyScript prende (x, y, z, massa) per due corpi e restituisce un array di tre float che descrivono il vettore di forza (x, y, z) che i corpi si applicano l'uno all'altro. Non possiamo chiamare questa funzione da JavaScript perché JavaScript non ha idea di dove trovarla. Dobbiamo "esportarlo" in JavaScript. Questo ci porta alla nostra prima sfida tecnica.

Importazioni ed esportazioni di WebAssembly

In ES6, pensiamo alle importazioni e alle esportazioni nel codice JavaScript e utilizziamo strumenti come Rollup o Webpack per creare codice che viene eseguito in browser legacy per gestire import e require() . Questo crea un albero delle dipendenze dall'alto verso il basso e abilita tecnologie interessanti come "scuotimento degli alberi" e suddivisione del codice.

In WebAssembly, le importazioni e le esportazioni svolgono attività diverse rispetto a un'importazione ES6. Importazioni/esportazioni di WebAssembly:

  • Fornire un ambiente di runtime per il modulo WebAssembly (ad es. funzioni trace() e abort() ).
  • Importa ed esporta funzioni e costanti tra i runtime.

Nel codice seguente, env.abort e env.trace fanno parte dell'ambiente che dobbiamo fornire al modulo WebAssembly. Le funzioni nBodyForces.logI e friends forniscono messaggi di debug alla console. Si noti che il passaggio di stringhe in/out da WebAssembly non è banale poiché gli unici tipi di WebAssembly sono i32, i64, f32, f64 numeri, con i32 riferimenti a una memoria lineare astratta.

Nota: questi esempi di codice passano avanti e indietro tra codice JavaScript (il web worker) e AssemblyScript (il codice 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(", ")); } } }

Nel nostro codice AssemblyScript, possiamo completare l'importazione di queste funzioni in questo modo:

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

Nota : Interruzione e traccia vengono importati automaticamente .

Da AssemblyScript, possiamo esportare la nostra interfaccia. Ecco alcune costanti esportate:

 // 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

Ed ecco l'esportazione di nBodyForces() che chiameremo da JavaScript. Esportiamo il tipo Float64Array nella parte superiore del file in modo da poter utilizzare il caricatore JavaScript di AssemblyScript nel nostro web worker per ottenere i dati (vedi sotto):

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

Artefatti WebAssembly: .wasm e .wat

Quando il nostro AssemblyScript nBodyForces.ts viene compilato in un binario WebAssembly nBodyForces.wasm , c'è un'opzione per creare anche una versione "testo" che descrive le istruzioni nel binario.

Artefatti di WebAssembly
Figura 3: Ricorda, AssemblyScript è un linguaggio. WebAssembly è un compilatore e un runtime.

All'interno del file nBodyForces.wat , possiamo vedere queste importazioni ed esportazioni:

 ;; 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 ...

Ora abbiamo il nostro binario nBodyForces.wasm e un web worker per eseguirlo. Preparati per il decollo! E un po' di gestione della memoria!

Per completare l'integrazione, dobbiamo passare un array variabile di float a WebAssembly e restituire un array variabile di float a JavaScript.

Con l'ingenuo borghese JavaScript, ho deciso di passare arbitrariamente questi sgargianti array di dimensioni variabili dentro e fuori da un runtime multipiattaforma ad alte prestazioni. Il passaggio dei dati a/da WebAssembly è stata, di gran lunga, la difficoltà più inaspettata in questo progetto.

Tuttavia, con molte grazie per il lavoro pesante svolto dal team di AssemblyScript, possiamo usare il loro "caricatore" per aiutare:

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

require() significa che dobbiamo usare un bundler di moduli come Rollup o Webpack. Per questo progetto, ho scelto Rollup per la sua semplicità e flessibilità e non ho mai guardato indietro.

Ricorda che il nostro web worker viene eseguito in un thread separato ed è essenzialmente una funzione onmessage() con un'istruzione switch() .

loader crea il nostro modulo wasm con alcune utili funzioni di gestione della memoria extra. __retain() e __release() gestiscono i riferimenti della garbage collection nel runtime di lavoro __allocArray() copia il nostro array di parametri nella memoria del modulo wasm __getFloat64Array() copia l'array di risultati dal modulo wasm nel runtime di lavoro

Ora possiamo effettuare il marshalling di array float dentro e fuori nBodyForces() e completare la nostra simulazione:

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

Con tutto ciò che abbiamo imparato, esaminiamo il nostro web worker e il percorso di WebAssembly. Benvenuti nel nuovo browser back-end del web. Questi sono i link al codice su GitHub:

  1. OTTIENI Indice.html
  2. main.js
  3. nBodySimulator.js - passa un messaggio al suo web worker
  4. workerWasm.js - chiama la funzione WebAssembly
  5. nBodyForces.ts - calcola e restituisce un array di forze
  6. workerWasm.js - riporta i risultati al thread principale
  7. nBodySimulator.js - risolve la promessa per le forze
  8. nBodySimulator.js - quindi applica le forze ai corpi e dice ai visualizzatori di dipingere

Da qui, iniziamo lo spettacolo creando nBodyVisualizer.js ! Il nostro prossimo post crea un visualizzatore utilizzando l'API Canvas e il post finale si conclude con WebVR e Aframe.

Correlati: Tutorial WebAssembly/Rust: elaborazione audio perfetta