WebVR, часть 3: раскрытие потенциала WebAssembly и AssemblyScript

Опубликовано: 2022-03-11

WebAssembly определенно не является заменой JavaScript как языка общения в Интернете и во всем мире.

WebAssembly (сокращенно Wasm) — это двоичный формат инструкций для виртуальной машины на основе стека. Wasm разработан как портативная платформа для компиляции языков высокого уровня, таких как C/C++/Rust, что позволяет развертывать в Интернете клиентские и серверные приложения». –WebAssembly.org

Важно понимать, что WebAssembly — это не язык. WebAssembly похож на файл .exe или, что еще лучше, на файл Java .class. Он скомпилирован веб-разработчиком с другого языка, затем загружен и запущен в вашем браузере.

WebAssembly предоставляет JavaScript все функции, которые мы иногда хотели позаимствовать, но никогда не хотели иметь. Подобно аренде лодки или лошади, WebAssembly позволяет нам путешествовать на другие языки без необходимости делать экстравагантный выбор «языкового образа жизни». Это позволило сети сосредоточиться на важных вещах, таких как предоставление функций и улучшение взаимодействия с пользователем.

Более 20 языков компилируются в WebAssembly: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go и, конечно же, JavaScript.

Если вы помните диаграмму архитектуры нашей симуляции, мы делегировали всю симуляцию nBodySimulator , поэтому он управляет веб-воркером.

Схема архитектуры моделирования
Рисунок 1: Общая архитектура.

Если вы помните из вступительного поста, в nBodySimulator есть функция step() , вызываемая каждые 33 мс. Функция step() делает следующие вещи, пронумерованные на диаграмме выше:

  1. Функция calculateForces() nBodySimulator вызывает this.worker.postMessage() , чтобы начать расчет.
  2. workerWasm.js this.onmessage() получает сообщение.
  3. workerWasm.js синхронно запускает функцию nBodyForces() из nBodyForces.wasm.
  4. workerWasm.js отвечает с помощью this.postMessage() основному потоку с новыми силами.
  5. Основной поток this.worker.onMessage() возвращенные данные и вызовы.
  6. Метод applyForces() для обновления положения тел.
  7. Наконец, визуализатор перерисовывается.

Поток пользовательского интерфейса, рабочий веб-поток
Рисунок 2: Внутри функции симулятора step()

В предыдущем посте мы создали веб-воркер, который упаковывает наши вычисления WASM. Сегодня мы создаем крошечную коробку с надписью «WASM» и перемещаем данные туда и обратно.

Для простоты я выбрал AssemblyScript в качестве языка исходного кода для написания наших вычислений. AssemblyScript — это подмножество TypeScript, который представляет собой типизированный JavaScript, так что вы уже это знаете.

Например, эта функция AssemblyScript вычисляет силу тяжести между двумя телами: :f64 в someVar:f64 помечает переменную someVar как число с плавающей запятой для компилятора. Помните, что этот код компилируется и выполняется в совершенно другой среде выполнения, чем 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 }

Эта функция AssemblyScript принимает (x, y, z, массу) для двух тел и возвращает массив из трех чисел с плавающей запятой, описывающих (x, y, z) вектор силы, который тела применяют друг к другу. Мы не можем вызвать эту функцию из JavaScript, потому что JavaScript не знает, где ее найти. Мы должны «экспортировать» его в JavaScript. Это подводит нас к нашей первой технической задаче.

Импорт и экспорт WebAssembly

В ES6 мы думаем об импорте и экспорте в коде JavaScript и используем такие инструменты, как Rollup или Webpack, для создания кода, который запускается в устаревших браузерах для обработки import и require() . Это создает нисходящее дерево зависимостей и позволяет использовать классные технологии, такие как «сотрясение дерева» и разделение кода.

В WebAssembly импорт и экспорт выполняют другие задачи, чем импорт ES6. WebAssembly импортирует/экспортирует:

  • Предоставьте среду выполнения для модуля WebAssembly (например, функции trace() и abort() ).
  • Импорт и экспорт функций и констант между средами выполнения.

В приведенном ниже коде env.abort и env.trace являются частью среды, которую мы должны предоставить модулю WebAssembly. nBodyForces.logI и другие функции предоставляют отладочные сообщения на консоль. Обратите внимание, что передача строк в/из WebAssembly нетривиальна, поскольку единственными типами WebAssembly являются числа i32, i64, f32, f64 со ссылками i32 на абстрактную линейную память.

Примечание. В этих примерах кода выполняется переключение между кодом JavaScript (веб-воркер) и кодом AssemblyScript (код 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(", ")); } } }

В нашем коде на AssemblyScript мы можем завершить импорт этих функций следующим образом:

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

Примечание . Прерывание и трассировка импортируются автоматически .

Из AssemblyScript мы можем экспортировать наш интерфейс. Вот некоторые экспортированные константы:

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

А вот экспорт nBodyForces() , который мы будем вызывать из JavaScript. Мы экспортируем тип Float64Array в начало файла, чтобы мы могли использовать загрузчик JavaScript JavaScript в нашем веб-воркере для получения данных (см. ниже):

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

Артефакты WebAssembly: .wasm и .wat

Когда наш AssemblyScript nBodyForces.ts компилируется в двоичный файл WebAssembly nBodyForces.wasm , есть возможность также создать «текстовую» версию, описывающую инструкции в двоичном файле.

Артефакты WebAssembly
Рисунок 3: Помните, что AssemblyScript — это язык. WebAssembly — это компилятор и среда выполнения.

Внутри файла nBodyForces.wat мы можем видеть эти импорты и экспорты:

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

Теперь у нас есть двоичный файл nBodyForces.wasm и веб-воркер для его запуска. Будьте готовы к взрыву! И немного управления памятью!

Чтобы завершить интеграцию, мы должны передать переменный массив чисел с плавающей запятой в WebAssembly и вернуть переменный массив чисел с плавающей запятой в JavaScript.

С наивным буржуазным JavaScript я намеревался бессмысленно передавать эти безвкусные массивы переменного размера в кроссплатформенную высокопроизводительную среду выполнения и из нее. Передача данных в/из WebAssembly была, безусловно, самой неожиданной трудностью в этом проекте.

Тем не менее, с большой благодарностью за тяжелую работу, проделанную командой AssemblyScript, мы можем использовать их «загрузчик», чтобы помочь:

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

Требование require() означает, что нам нужно использовать сборщик модулей, такой как Rollup или Webpack. Для этого проекта я выбрал Rollup из-за его простоты и гибкости и никогда не оглядывался назад.

Помните, что наш веб-воркер работает в отдельном потоке и, по сути, является onmessage() с оператором switch() .

loader создает наш модуль wasm с некоторыми дополнительными удобными функциями управления памятью. __retain() и __release() управляют ссылками на сборку мусора в рабочей среде __allocArray() копирует наш массив параметров в память модуля wasm. __getFloat64Array() копирует результирующий массив из модуля wasm в рабочую среду.

Теперь мы можем маршалировать массивы с плавающей запятой в nBodyForces() и из них и завершить нашу симуляцию:

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

Со всем, что мы узнали, давайте рассмотрим наш путь к веб-воркеру и WebAssembly. Добро пожаловать в новый веб-браузер. Это ссылки на код на GitHub:

  1. ПОЛУЧИТЬ Index.html
  2. main.js
  3. nBodySimulator.js — передает сообщение своему веб-воркеру
  4. workerWasm.js — вызывает функцию WebAssembly
  5. nBodyForces.ts - вычисляет и возвращает массив сил
  6. workerWasm.js — передает результаты обратно в основной поток
  7. nBodySimulator.js - разрешает обещание сил
  8. nBodySimulator.js — затем применяет силы к телам и сообщает визуализаторам рисовать

Отсюда давайте начнем шоу с создания nBodyVisualizer.js ! В следующем посте создается визуализатор с использованием Canvas API, а последний пост завершается с помощью WebVR и Aframe.

Связанный: WebAssembly/Rust Tutorial: Безупречная обработка звука