WebVR Bölüm 3: WebAssembly ve AssemblyScript'in Potansiyelinin Kilidini Açma

Yayınlanan: 2022-03-11

WebAssembly, kesinlikle web'in ve dünyanın ortak lingua franca'sı olarak JavaScript'in yerini tutmaz .

WebAssembly (kısaltılmış Wasm), yığın tabanlı bir sanal makine için ikili bir talimat biçimidir. Wasm, C/C++/Rust gibi yüksek seviyeli dillerin derlenmesi için taşınabilir bir hedef olarak tasarlanmıştır ve istemci ve sunucu uygulamaları için web üzerinde dağıtıma olanak tanır.” –WebAssembly.org

WebAssembly'nin bir dil olmadığını ayırt etmek önemlidir. WebAssembly bir '.exe' gibidir - hatta daha iyisi - bir Java '.class' dosyasıdır. Web geliştiricisi tarafından başka bir dilden derlenir, ardından indirilir ve tarayıcınızda çalıştırılır.

WebAssembly, JavaScript'e ara sıra ödünç almak istediğimiz ancak gerçekten sahip olmak istemediğimiz tüm özellikleri veriyor. Bir tekne veya at kiralamaya çok benzeyen WebAssembly, abartılı “dil yaşam tarzı” seçimleri yapmak zorunda kalmadan diğer dillere seyahat etmemizi sağlar. Bu, web'in özellikler sunma ve kullanıcı deneyimini iyileştirme gibi önemli şeylere odaklanmasını sağladı.

20'den fazla dil WebAssembly için derlenir: Rust, C/C++, C#/.Net, Java, Python, Elixir, Go ve tabii ki JavaScript.

Simülasyonumuzun mimari diyagramını hatırlarsanız, tüm simülasyonu nBodySimulator , böylece web işçisini yönetir.

Simülasyonun mimari diyagramı
Şekil 1: Genel Mimari.

Giriş yazısından hatırlarsanız, nBodySimulator her 33ms'de bir step() işlevine sahiptir. step() işlevi şunları yapar - yukarıdaki şemada numaralandırılmıştır:

  1. nBodySimulator'ın hesapForces() işlevi, hesaplamayı başlatmak için this.worker.postMessage() calculateForces() .
  2. WorkWasm.js this.onmessage() mesajı alır.
  3. workWasm.js, nBodyForces.wasm'ın nBodyForces() işlevini eşzamanlı olarak çalıştırır.
  4. workWasm.js, this.postMessage() işlevini kullanarak yeni güçlerle ana iş parçacığına yanıt verir.
  5. Ana iş parçacığının this.worker.onMessage() , döndürülen verileri ve çağrıları sıralar.
  6. nBodySimulator'ın applyForces() , gövdelerin konumlarını güncellemek için.
  7. Son olarak, görselleştirici yeniden boyar.

UI iş parçacığı, web çalışanı iş parçacığı
Şekil 2: Simülatörün step() fonksiyonunun içinde

Önceki gönderide, WASM hesaplamalarımızı saran web işçisini oluşturduk. Bugün, "WASM" etiketli küçük kutuyu inşa ediyor ve verileri içeri ve dışarı taşıyoruz.

Basit olması için, hesaplamalarımızı yazmak için kaynak kod dili olarak AssemblyScript'i seçtim. AssemblyScript, TypeScript'in bir alt kümesidir - ki bu da JavaScript'tir - zaten biliyorsunuzdur.

Örneğin, bu AssemblyScript işlevi iki gövde arasındaki yerçekimini hesaplar :f64 içindeki someVar:f64 , someVar değişkenini derleyici için bir kayan nokta olarak işaretler. Bu kodun derlendiğini ve JavaScript'ten tamamen farklı bir çalışma zamanında çalıştırıldığını unutmayın.

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

Bu AssemblyScript işlevi, iki gövde için (x, y, z, kütle) değerini alır ve gövdelerin birbirine uyguladığı (x, y, z) kuvvet vektörünü açıklayan üç kayan noktadan oluşan bir dizi döndürür. JavaScript'in onu nerede bulacağına dair hiçbir fikri olmadığı için bu işlevi JavaScript'ten çağıramayız. JavaScript'e “dışa aktarmalıyız”. Bu bizi ilk teknik mücadelemize getiriyor.

WebAssembly İçe ve Dışa Aktarma

ES6'da, JavaScript kodundaki içe ve dışa aktarmaları düşünüyoruz ve import ve require() işlemlerini gerçekleştirmek için eski tarayıcılarda çalışan kod oluşturmak için Rollup veya Webpack gibi araçları kullanıyoruz. Bu, yukarıdan aşağıya bir bağımlılık ağacı oluşturur ve "ağaç sallama" ve kod bölme gibi harika teknolojileri mümkün kılar.

WebAssembly'de içe aktarma ve dışa aktarma işlemleri, bir ES6 içe aktarma işleminden farklı görevleri gerçekleştirir. WebAssembly içe/dışa aktarır:

  • WebAssembly modülü için bir çalışma zamanı ortamı sağlayın (örneğin, trace() ve abort() işlevleri).
  • Çalışma zamanları arasında işlevleri ve sabitleri içe ve dışa aktarın.

Aşağıdaki kodda env.abort ve env.trace , WebAssembly modülüne sağlamamız gereken ortamın bir parçasıdır. nBodyForces.logI ve arkadaşlar işlevleri, konsola hata ayıklama mesajları sağlar. WebAssembly'nin tek türleri i32, i64, f32, f64 sayıları ve soyut bir doğrusal belleğe i32 referansları olduğu için WebAssembly'den içeri/dışarı dize geçirmenin önemsiz olduğunu unutmayın.

Not: Bu kod örnekleri, JavaScript kodu (web çalışanı) ve AssemblyScript (WASM kodu) arasında geçiş yapmaktadır.

 // 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 kodumuzda, bu işlevlerin içe aktarımını şu şekilde tamamlayabiliriz:

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

Not : Durdurma ve izleme otomatik olarak içe aktarılır .

AssemblyScript'ten arayüzümüzü dışa aktarabiliriz. İşte bazı dışa aktarılan sabitler:

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

Ve işte JavaScript'ten çağıracağımız nBodyForces() 'in dışa aktarımı. Verileri almak için web çalışanımızda AssemblyScript'in JavaScript yükleyicisini kullanabilmemiz için dosyanın en üstünde Float64Array türünü dışa aktarırız (aşağıya bakın):

 // 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 Eserleri: .wasm ve .wat

AssemblyScript nBodyForces.ts bir WebAssembly nBodyForces.wasm ikili dosyasında derlendiğinde, ikili dosyadaki talimatları açıklayan bir "metin" sürümü oluşturma seçeneği de vardır.

Web Montaj Eserleri
Şekil 3: Unutmayın, AssemblyScript bir dildir. WebAssembly bir derleyici ve çalışma zamanıdır.

nBodyForces.wat dosyasının içinde şu ithalat ve ihracatları görebiliriz:

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

Artık nBodyForces.wasm ikili dosyamız ve onu çalıştıracak bir web çalışanımız var. Patlamaya hazır olun! Ve biraz bellek yönetimi!

Entegrasyonu tamamlamak için, WebAssembly'ye değişken bir float dizisini iletmeliyiz ve değişken bir float dizisini JavaScript'e döndürmeliyiz.

Saf JavaScript burjuvası ile, bu şatafatlı değişken boyutlu dizileri, platformlar arası yüksek performanslı bir çalışma zamanının içine ve dışına nedensizce geçirmek için yola çıktım. WebAssembly'ye/WebAssembly'den veri iletmek, bu projedeki açık ara en beklenmedik zorluktu.

Bununla birlikte, AssemblyScript ekibi tarafından yapılan ağır kaldırma için çok teşekkürler, yardımcı olması için onların "yükleyicisini" kullanabiliriz:

 // 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() , Rollup veya Webpack gibi bir modül paketleyici kullanmamız gerektiği anlamına gelir. Bu proje için sadeliği ve esnekliği nedeniyle Rollup'u seçtim ve bir daha arkama bakmadım.

Web çalışanımızın ayrı bir iş parçacığında çalıştığını ve esasen bir switch() deyimiyle bir onmessage() işlevi olduğunu unutmayın.

loader , bazı ekstra kullanışlı bellek yönetimi işlevleriyle wasm modülümüzü oluşturur. __retain() ve __release() , çalışan çalışma zamanında çöp toplama referanslarını __allocArray() , parametre dizimizi wasm modülünün belleğine kopyalar __getFloat64Array() , wasm modülünden çalışan çalışma zamanına sonuç dizisini kopyalar

Artık yüzer dizileri nBodyForces() içinde ve dışında sıralayabilir ve simülasyonumuzu tamamlayabiliriz:

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

Tüm öğrendiklerimizle birlikte web çalışanımızı ve WebAssembly yolculuğumuzu gözden geçirelim. Web'in yeni tarayıcı arka ucuna hoş geldiniz. Bunlar GitHub'daki kodun bağlantıları:

  1. GET Index.html
  2. ana.js
  3. nBodySimulator.js - web çalışanına bir mesaj iletir
  4. workWasm.js - WebAssembly işlevini çağırır
  5. nBodyForces.ts - bir dizi kuvveti hesaplar ve döndürür
  6. workWasm.js - sonuçları ana iş parçacığına geri iletir
  7. nBodySimulator.js - kuvvetler için verilen sözü çözer
  8. nBodySimulator.js - ardından kuvvetleri gövdelere uygular ve görselleştiricilere boyamalarını söyler

Buradan, nBodyVisualizer.js oluşturarak gösteriye başlayalım! Sonraki gönderimiz Canvas API kullanarak bir görselleştirici oluşturuyor ve son gönderi WebVR ve Aframe ile tamamlanıyor.

İlgili: WebAssembly/Pas Eğitimi: Mükemmel Ses İşleme