WebVR Bagian 3: Membuka Potensi WebAssembly dan AssemblyScript

Diterbitkan: 2022-03-11

WebAssembly jelas bukan pengganti JavaScript sebagai lingua franca web dan dunia.

WebAssembly (disingkat Wasm) adalah format instruksi biner untuk mesin virtual berbasis tumpukan. Wasm dirancang sebagai target portabel untuk kompilasi bahasa tingkat tinggi seperti C/C++/Rust, memungkinkan penerapan di web untuk aplikasi klien dan server.” –WebAssembly.org

Penting untuk membedakan bahwa WebAssembly bukan bahasa. WebAssembly seperti file '.exe' - atau bahkan lebih baik - file Java '.class'. Itu dikompilasi oleh pengembang web dari bahasa lain, kemudian diunduh dan dijalankan di browser Anda.

WebAssembly memberikan JavaScript semua fitur yang terkadang ingin kami pinjam tetapi tidak pernah benar- benar ingin kami miliki. Sama seperti menyewa perahu atau kuda, WebAssembly memungkinkan kita melakukan perjalanan ke bahasa lain tanpa harus membuat pilihan "gaya hidup bahasa" yang berlebihan. Ini membuat web fokus pada hal-hal penting seperti menghadirkan fitur dan meningkatkan pengalaman pengguna.

Lebih dari 20 bahasa dikompilasi ke WebAssembly: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go, dan tentu saja JavaScript.

Jika Anda ingat diagram arsitektur simulasi kami, kami mendelegasikan seluruh simulasi ke nBodySimulator , sehingga ia mengelola pekerja web.

Diagram arsitektur simulasi
Gambar 1: Arsitektur Keseluruhan.

Jika Anda ingat dari posting intro, nBodySimulator memiliki fungsi step() yang dipanggil setiap 33ms. Fungsi step() melakukan hal-hal ini - diberi nomor dalam diagram di atas:

  1. countForces calculateForces() nBodySimulator memanggil this.worker.postMessage() untuk memulai perhitungan.
  2. workerWasm.js this.onmessage() mendapatkan pesan.
  3. workerWasm.js menjalankan fungsi nBodyForces() secara serempak.
  4. workerWasm.js membalas menggunakan this.postMessage() ke utas utama dengan kekuatan baru.
  5. Thread utama this.worker.onMessage() mengatur data dan panggilan yang dikembalikan.
  6. nBodySimulator's applyForces() untuk memperbarui posisi tubuh.
  7. Akhirnya, visualizer mengecat ulang.

Utas UI, utas pekerja web
Gambar 2: Di dalam fungsi step() simulator

Di posting sebelumnya, kami membangun pekerja web yang membungkus perhitungan WASM kami. Hari ini, kami sedang membangun kotak kecil berlabel "WASM" dan memindahkan data masuk dan keluar.

Untuk kesederhanaan, saya memilih AssemblyScript sebagai bahasa kode sumber untuk menulis perhitungan kami. AssemblyScript adalah bagian dari TypeScript - yang merupakan JavaScript yang diketik - jadi Anda sudah mengetahuinya.

Misalnya, fungsi AssemblyScript ini menghitung gravitasi antara dua benda: :f64 di someVar:f64 menandai variabel someVar sebagai float untuk kompiler. Ingat kode ini dikompilasi dan dijalankan dalam runtime yang sama sekali berbeda dari 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 }

Fungsi AssemblyScript ini mengambil (x, y, z, massa) untuk dua benda dan mengembalikan larik tiga pelampung yang menjelaskan vektor gaya (x, y, z) yang diterapkan benda satu sama lain. Kami tidak dapat memanggil fungsi ini dari JavaScript karena JavaScript tidak tahu di mana menemukannya. Kita harus "mengekspor" ke JavaScript. Ini membawa kita ke tantangan teknis pertama kami.

WebAssembly Impor dan Ekspor

Di ES6, kami memikirkan tentang impor dan ekspor dalam kode JavaScript dan menggunakan alat seperti Rollup atau Webpack untuk membuat kode yang berjalan di browser lama untuk menangani import dan require() . Ini menciptakan pohon dependensi top-down dan memungkinkan teknologi keren seperti "tree-shaking" dan code-splitting.

Di WebAssembly, impor dan ekspor menyelesaikan tugas yang berbeda dari impor ES6. WebAssembly impor/ekspor:

  • Menyediakan lingkungan runtime untuk modul WebAssembly (misalnya, fungsi trace() dan abort() ).
  • Impor dan ekspor fungsi dan konstanta antara runtime.

Dalam kode di bawah ini, env.abort dan env.trace adalah bagian dari lingkungan yang harus kita sediakan ke modul WebAssembly. Fungsi nBodyForces.logI dan teman-teman memberikan pesan debug ke konsol. Perhatikan bahwa melewatkan string masuk/keluar dari WebAssembly tidak sepele karena satu-satunya tipe WebAssembly adalah nomor i32, i64, f32, f64, dengan referensi i32 ke memori linier abstrak.

Catatan: Contoh kode ini beralih antara kode JavaScript (pekerja web) dan AssemblyScript (kode 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(", ")); } } }

Dalam kode AssemblyScript kami, kami dapat menyelesaikan impor fungsi-fungsi ini seperti:

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

Catatan : Abort dan trace diimpor secara otomatis .

Dari AssemblyScript, kita dapat mengekspor antarmuka kita. Berikut adalah beberapa konstanta yang diekspor:

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

Dan inilah hasil ekspor nBodyForces() yang akan kita panggil dari JavaScript. Kami mengekspor jenis Float64Array di bagian atas file sehingga kami dapat menggunakan pemuat JavaScript AssemblyScript di pekerja web kami untuk mendapatkan data (lihat di bawah):

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

Artefak WebAssembly: .wasm dan .wat

Ketika nBodyForces.ts AssemblyScript kami dikompilasi ke dalam biner nBodyForces.wasm , ada opsi untuk juga membuat versi "teks" yang menjelaskan instruksi dalam biner.

Artefak WebAssembly
Gambar 3: Ingat, AssemblyScript adalah sebuah bahasa. WebAssembly adalah compiler dan runtime.

Di dalam file nBodyForces.wat , kita dapat melihat impor dan ekspor ini:

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

Kami sekarang memiliki biner nBodyForces.wasm dan pekerja web untuk menjalankannya. Bersiaplah untuk ledakan! Dan beberapa manajemen memori!

Untuk menyelesaikan integrasi, kita harus meneruskan array variabel float ke WebAssembly dan mengembalikan array variabel float ke JavaScript.

Dengan JavaScript borjuis yang naif, saya mulai dengan ceroboh meneruskan array berukuran variabel yang mencolok ini masuk dan keluar dari runtime kinerja tinggi lintas platform. Melewati data ke/dari WebAssembly sejauh ini merupakan kesulitan yang paling tidak terduga dalam proyek ini.

Namun, dengan banyak terima kasih atas pekerjaan berat yang dilakukan oleh tim AssemblyScript, kami dapat menggunakan "loader" mereka untuk membantu:

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

Kebutuhan require() berarti kita perlu menggunakan bundler modul seperti Rollup atau Webpack. Untuk proyek ini, saya memilih Rollup karena kesederhanaan dan fleksibilitasnya dan tidak pernah menoleh ke belakang.

Ingat pekerja web kami berjalan di utas terpisah dan pada dasarnya adalah fungsi onmessage() dengan pernyataan switch() .

loader membuat modul wasm kami dengan beberapa fungsi manajemen memori ekstra praktis. __retain() dan __release() mengelola referensi pengumpulan sampah di runtime pekerja __allocArray() menyalin larik parameter kita ke dalam memori modul __getFloat64Array() menyalin larik hasil dari modul wasm ke runtime pekerja

Kita sekarang dapat menyusun array float masuk dan keluar dari nBodyForces() dan menyelesaikan simulasi kita:

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

Dengan semua yang telah kita pelajari, mari tinjau pekerja web dan perjalanan WebAssembly kita. Selamat datang di browser-backend web yang baru. Ini adalah tautan ke kode di GitHub:

  1. DAPATKAN Index.html
  2. main.js
  3. nBodySimulator.js - meneruskan pesan ke pekerja webnya
  4. workerWasm.js - memanggil fungsi WebAssembly
  5. nBodyForces.ts - menghitung dan mengembalikan serangkaian kekuatan
  6. workerWasm.js - meneruskan hasil kembali ke utas utama
  7. nBodySimulator.js - menyelesaikan janji untuk kekuatan
  8. nBodySimulator.js - kemudian menerapkan kekuatan ke tubuh dan memberi tahu visualisator untuk melukis

Dari sini, mari kita mulai pertunjukan dengan membuat nBodyVisualizer.js ! Postingan kami berikutnya membuat visualizer menggunakan Canvas API, dan postingan terakhir ditutup dengan WebVR dan Aframe.

Terkait: WebAssembly/Tutorial Karat: Pemrosesan Audio Pitch-sempurna